From 09a9f106d6fd3094e7da0c54bf6bae1ce7b01047 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Sat, 18 May 2024 10:54:06 +0200 Subject: [PATCH] Improve Cause Rendering (Vitest) (#2747) Co-authored-by: Tim --- .changeset/gorgeous-students-jog.md | 6 ++++ .changeset/soft-moons-raise.md | 5 ++++ .changeset/tough-buses-brush.md | 5 ++++ packages/effect/src/internal/cause.ts | 21 +++++++++---- packages/effect/src/internal/fiberRuntime.ts | 2 +- packages/effect/src/internal/runtime.ts | 5 ++++ .../test/Effect/cause-rendering.test.ts | 17 +++++++++++ packages/vitest/src/index.ts | 30 +++++++++++++++---- 8 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 .changeset/gorgeous-students-jog.md create mode 100644 .changeset/soft-moons-raise.md create mode 100644 .changeset/tough-buses-brush.md diff --git a/.changeset/gorgeous-students-jog.md b/.changeset/gorgeous-students-jog.md new file mode 100644 index 0000000000..6f6a94b074 --- /dev/null +++ b/.changeset/gorgeous-students-jog.md @@ -0,0 +1,6 @@ +--- +"effect": minor +"@effect/vitest": minor +--- + +Improve causal rendering in vitest by rethrowing pretty errors diff --git a/.changeset/soft-moons-raise.md b/.changeset/soft-moons-raise.md new file mode 100644 index 0000000000..f5d565c1ed --- /dev/null +++ b/.changeset/soft-moons-raise.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Throw plain error object derived from fiber failure diff --git a/.changeset/tough-buses-brush.md b/.changeset/tough-buses-brush.md new file mode 100644 index 0000000000..eaf28d5156 --- /dev/null +++ b/.changeset/tough-buses-brush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Consider Generator.next a cutpoint diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index c87a5d56a2..d4714d8841 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -977,12 +977,25 @@ export const pretty = (cause: Cause.Cause): string => { } class PrettyError extends globalThis.Error implements Cause.PrettyError { + span: undefined | Span = undefined constructor(originalError: unknown) { const prevLimit = Error.stackTraceLimit Error.stackTraceLimit = 0 - super(prettyErrorMessage(originalError), { cause: originalError }) + super(prettyErrorMessage(originalError)) Error.stackTraceLimit = prevLimit + this.name = originalError instanceof Error ? originalError.name : "Error" + if (typeof originalError === "object" && originalError !== null) { + if (spanSymbol in originalError) { + this.span = originalError[spanSymbol] as Span + } + Object.keys(originalError).forEach((key) => { + if (!(key in this)) { + // @ts-expect-error + this[key] = originalError[key] + } + }) + } this.stack = prettyErrorStack( this.message, originalError instanceof Error && originalError.stack @@ -992,10 +1005,6 @@ class PrettyError extends globalThis.Error implements Cause.PrettyError { ) } - get span(): Span | undefined { - return hasProperty(this.cause, spanSymbol) ? this.cause[spanSymbol] as Span : undefined - } - toJSON() { const out: any = { message: this.message, stack: this.stack } if (this.span) { @@ -1048,7 +1057,7 @@ const prettyErrorStack = (message: string, stack: string, span?: Span | undefine const lines = stack.split("\n") for (let i = 1; i < lines.length; i++) { - if (lines[i].includes("effect_cutpoint")) { + if (lines[i].includes("effect_cutpoint") || lines[i].includes("Generator.next")) { break } out.push( diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index da3a020d5d..f61565e808 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -1335,7 +1335,7 @@ export class FiberRuntime implements Fiber.RuntimeFi internalCause.sequential(internalCause.die(e), internalCause.interrupt(FiberId.none)) ) } else { - cur = core.exitFailCause(internalCause.die(e)) + cur = core.die(e) } } } diff --git a/packages/effect/src/internal/runtime.ts b/packages/effect/src/internal/runtime.ts index 7062821460..bf93d5b7eb 100644 --- a/packages/effect/src/internal/runtime.ts +++ b/packages/effect/src/internal/runtime.ts @@ -179,6 +179,10 @@ class FiberFailureImpl extends Error implements Runtime.FiberFailure { } this.name = `(FiberFailure) ${this.name}` + + if (this.message === undefined || this.message.length === 0) { + this.message = "An error has occurred" + } } toJSON(): unknown { @@ -187,6 +191,7 @@ class FiberFailureImpl extends Error implements Runtime.FiberFailure { cause: this[FiberFailureCauseId].toJSON() } } + toString(): string { return "(FiberFailure) " + (this.stack ?? this.message) } diff --git a/packages/effect/test/Effect/cause-rendering.test.ts b/packages/effect/test/Effect/cause-rendering.test.ts index 3ea1495f93..b003d2c4cd 100644 --- a/packages/effect/test/Effect/cause-rendering.test.ts +++ b/packages/effect/test/Effect/cause-rendering.test.ts @@ -117,4 +117,21 @@ describe("Effect", () => { assert.include(error.stack, "span-123") assert.include(error.stack, "cause-rendering.test.ts:110") })) + + it.effect("includes span name in stack", () => + Effect.gen(function*() { + const fn = Effect.functionWithSpan({ + options: (n) => ({ name: `fn-${n}` }), + body: (a: number) => + Effect.sync(() => { + assert.strictEqual(a, 2) + }) + }) + const cause = yield* fn(0).pipe( + Effect.sandbox, + Effect.flip + ) + const prettyErrors = Cause.prettyErrors(cause) + assert.include(prettyErrors[0].stack ?? "", "at fn-0 ") + })) }) diff --git a/packages/vitest/src/index.ts b/packages/vitest/src/index.ts index 562ba6f4e8..dea0a8c96f 100644 --- a/packages/vitest/src/index.ts +++ b/packages/vitest/src/index.ts @@ -2,9 +2,11 @@ * @since 1.0.0 */ import type { Tester, TesterContext } from "@vitest/expect" +import * as Cause from "effect/Cause" import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import * as Equal from "effect/Equal" +import * as Exit from "effect/Exit" import { pipe } from "effect/Function" import * as Layer from "effect/Layer" import * as Logger from "effect/Logger" @@ -16,6 +18,22 @@ import * as Utils from "effect/Utils" import type { TestAPI } from "vitest" import * as V from "vitest" +const runTest = (effect: Effect.Effect) => + Effect.gen(function*() { + const exit: Exit.Exit = yield* Effect.exit(effect) + if (Exit.isSuccess(exit)) { + return () => {} + } else { + const errors = Cause.prettyErrors(exit.cause) + for (let i = 1; i < errors.length; i++) { + yield* Effect.logError(errors[i]) + } + return () => { + throw errors[0] + } + } + }).pipe(Effect.runPromise).then((f) => f()) + /** * @since 1.0.0 */ @@ -58,7 +76,7 @@ export const effect = (() => { pipe( Effect.suspend(() => self(c)), Effect.provide(TestEnv), - Effect.runPromise + runTest ), timeout ) @@ -74,7 +92,7 @@ export const effect = (() => { pipe( Effect.suspend(() => self(c)), Effect.provide(TestEnv), - Effect.runPromise + runTest ), timeout ), @@ -89,7 +107,7 @@ export const effect = (() => { pipe( Effect.suspend(() => self(c)), Effect.provide(TestEnv), - Effect.runPromise + runTest ), timeout ) @@ -109,7 +127,7 @@ export const live = ( (c) => pipe( Effect.suspend(() => self(c)), - Effect.runPromise + runTest ), timeout ) @@ -150,7 +168,7 @@ export const scoped = ( Effect.suspend(() => self(c)), Effect.scoped, Effect.provide(TestEnv), - Effect.runPromise + runTest ), timeout ) @@ -169,7 +187,7 @@ export const scopedLive = ( pipe( Effect.suspend(() => self(c)), Effect.scoped, - Effect.runPromise + runTest ), timeout )