Skip to content

Commit 67b738c

Browse files
authored
⚡️ Delay computation of Error stack when no cause (#4472)
**💥 Breaking change** Up-to-now fast-check was always sealing the stack attached to errors as they popped. With error with cause capabilities we started to draft the required links to pass the instance of Error up to the end. We can thus safely delay the computation of the stack. As delaying might have unwanted side-effects (maybe our instance of Error would be touched), we prefer not to make this change as part of a minor release. Over all the breaking is justified by the change we do on the API of the failures. We dropped the previously precomputed errors and only keep the raw instance of errors. Users now have to manipulate it to extract the relevant error if needed. First piece for #4416
1 parent 21fc626 commit 67b738c

20 files changed

+745
-88
lines changed

packages/fast-check/src/check/property/AsyncProperty.generic.ts

+3-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
noUndefinedAsContext,
1212
UndefinedContextPlaceholder,
1313
} from '../../arbitrary/_internals/helpers/NoUndefinedAsContext';
14-
import { Error, String } from '../../utils/globals';
14+
import { Error } from '../../utils/globals';
1515

1616
/**
1717
* Type of legal hook function that can be used to call `beforeEach` or `afterEach`
@@ -120,18 +120,12 @@ export class AsyncProperty<Ts> implements IAsyncPropertyWithHooks<Ts> {
120120
const output = await this.predicate(v);
121121
return output === undefined || output === true
122122
? null
123-
: {
124-
error: new Error('Property failed by returning false'),
125-
errorMessage: 'Error: Property failed by returning false',
126-
};
123+
: { error: new Error('Property failed by returning false') };
127124
} catch (err) {
128125
// precondition failure considered as success for the first version
129126
if (PreconditionFailure.isFailure(err)) return err;
130127
// exception as PropertyFailure in case of real failure
131-
if (err instanceof Error && err.stack) {
132-
return { error: err, errorMessage: err.stack }; // stack includes the message
133-
}
134-
return { error: err, errorMessage: String(err) };
128+
return { error: err };
135129
}
136130
}
137131

packages/fast-check/src/check/property/IRawProperty.ts

-5
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ export type PropertyFailure = {
1717
* @remarks Since 3.0.0
1818
*/
1919
error: unknown;
20-
/**
21-
* The error message extracted from the error
22-
* @remarks Since 3.0.0
23-
*/
24-
errorMessage: string;
2520
};
2621

2722
/**

packages/fast-check/src/check/property/Property.generic.ts

+3-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
noUndefinedAsContext,
1212
UndefinedContextPlaceholder,
1313
} from '../../arbitrary/_internals/helpers/NoUndefinedAsContext';
14-
import { Error, String } from '../../utils/globals';
14+
import { Error } from '../../utils/globals';
1515

1616
/**
1717
* Type of legal hook function that can be used to call `beforeEach` or `afterEach`
@@ -136,18 +136,12 @@ export class Property<Ts> implements IProperty<Ts>, IPropertyWithHooks<Ts> {
136136
const output = this.predicate(v);
137137
return output === undefined || output === true
138138
? null
139-
: {
140-
error: new Error('Property failed by returning false'),
141-
errorMessage: 'Error: Property failed by returning false',
142-
};
139+
: { error: new Error('Property failed by returning false') };
143140
} catch (err) {
144141
// precondition failure considered as success for the first version
145142
if (PreconditionFailure.isFailure(err)) return err;
146143
// exception as PropertyFailure in case of real failure
147-
if (err instanceof Error && err.stack) {
148-
return { error: err, errorMessage: err.stack }; // stack includes the message
149-
}
150-
return { error: err, errorMessage: String(err) };
144+
return { error: err };
151145
}
152146
}
153147

packages/fast-check/src/check/property/TimeoutProperty.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ const timeoutAfter = (timeMs: number, setTimeoutSafe: typeof setTimeout, clearTi
1010
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
1111
const promise = new Promise<PropertyFailure>((resolve) => {
1212
timeoutHandle = setTimeoutSafe(() => {
13-
resolve({
14-
error: new Error(`Property timeout: exceeded limit of ${timeMs} milliseconds`),
15-
errorMessage: `Property timeout: exceeded limit of ${timeMs} milliseconds`,
16-
});
13+
resolve({ error: new Error(`Property timeout: exceeded limit of ${timeMs} milliseconds`) });
1714
}, timeMs);
1815
});
1916
return {

packages/fast-check/src/check/runner/reporter/RunDetails.ts

-9
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export interface RunDetailsFailureProperty<Ts> extends RunDetailsCommon<Ts> {
3030
interrupted: boolean;
3131
counterexample: Ts;
3232
counterexamplePath: string;
33-
error: string;
3433
errorInstance: unknown;
3534
}
3635

@@ -48,7 +47,6 @@ export interface RunDetailsFailureTooManySkips<Ts> extends RunDetailsCommon<Ts>
4847
interrupted: false;
4948
counterexample: null;
5049
counterexamplePath: null;
51-
error: null;
5250
errorInstance: null;
5351
}
5452

@@ -66,7 +64,6 @@ export interface RunDetailsFailureInterrupted<Ts> extends RunDetailsCommon<Ts> {
6664
interrupted: true;
6765
counterexample: null;
6866
counterexamplePath: null;
69-
error: null;
7067
errorInstance: null;
7168
}
7269

@@ -83,7 +80,6 @@ export interface RunDetailsSuccess<Ts> extends RunDetailsCommon<Ts> {
8380
interrupted: boolean;
8481
counterexample: null;
8582
counterexamplePath: null;
86-
error: null;
8783
errorInstance: null;
8884
}
8985

@@ -138,11 +134,6 @@ export interface RunDetailsCommon<Ts> {
138134
* @remarks Since 0.0.7
139135
*/
140136
counterexample: Ts | null;
141-
/**
142-
* In case of failure: it contains the reason of the failure
143-
* @remarks Since 0.0.7
144-
*/
145-
error: string | null;
146137
/**
147138
* In case of failure: it contains the error that has been thrown if any
148139
* @remarks Since 3.0.0

packages/fast-check/src/check/runner/reporter/RunExecution.ts

-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,6 @@ export class RunExecution<Ts> {
120120
// Rq: Same as this.value
121121
// => this.failure !== undefined
122122
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
123-
error: this.failure!.errorMessage,
124-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
125123
errorInstance: this.failure!.error,
126124
failures: this.extractFailures(),
127125
executionSummary: this.rootExecutionTrees,

packages/fast-check/src/check/runner/utils/RunDetailsFormatter.ts

+40-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Error, safePush, safeReplace } from '../../../utils/globals';
1+
import { Error, safeErrorToString, safePush, safeReplace, safeToString, String } from '../../../utils/globals';
22
import { stringify, possiblyAsyncStringify } from '../../../utils/stringify';
33
import { VerbosityLevel } from '../configuration/VerbosityLevel';
44
import { ExecutionStatus } from '../reporter/ExecutionStatus';
@@ -79,10 +79,48 @@ function preFormatTooManySkipped<Ts>(out: RunDetailsFailureTooManySkips<Ts>, str
7979
return { message, details, hints };
8080
}
8181

82+
/** @internal */
83+
function prettyError(errorInstance: unknown) {
84+
// Print the Error message and its associated stacktrace
85+
if (errorInstance instanceof Error && errorInstance.stack !== undefined) {
86+
return errorInstance.stack; // stack includes the message
87+
}
88+
89+
// First fallback: String(.)
90+
try {
91+
return String(errorInstance);
92+
} catch (_err) {
93+
// no-op
94+
}
95+
96+
// Second fallback: Error::toString()
97+
if (errorInstance instanceof Error) {
98+
try {
99+
return safeErrorToString(errorInstance);
100+
} catch (_err) {
101+
// no-op
102+
}
103+
}
104+
105+
// Third fallback: Object::toString()
106+
if (errorInstance !== null && typeof errorInstance === 'object') {
107+
try {
108+
return safeToString(errorInstance);
109+
} catch (_err) {
110+
// no-op
111+
}
112+
}
113+
114+
// Final fallback: Hardcoded string
115+
return 'Failed to serialize errorInstance';
116+
}
117+
82118
/** @internal */
83119
function preFormatFailure<Ts>(out: RunDetailsFailureProperty<Ts>, stringifyOne: (value: Ts) => string) {
84120
const noErrorInMessage = out.runConfiguration.errorWithCause;
85-
const messageErrorPart = noErrorInMessage ? '' : `\nGot ${safeReplace(out.error, /^Error: /, 'error: ')}`;
121+
const messageErrorPart = noErrorInMessage
122+
? ''
123+
: `\nGot ${safeReplace(prettyError(out.errorInstance), /^Error: /, 'error: ')}`;
86124
const message = `Property failed after ${out.numRuns} tests\n{ seed: ${out.seed}, path: "${
87125
out.counterexamplePath
88126
}", endOnFailure: true }\nCounterexample: ${stringifyOne(out.counterexample)}\nShrunk ${

packages/fast-check/src/utils/globals.ts

+7
Original file line numberDiff line numberDiff line change
@@ -538,3 +538,10 @@ export function safeHasOwnProperty(instance: unknown, v: PropertyKey): boolean {
538538
export function safeToString(instance: unknown): string {
539539
return safeApply(untouchedToString, instance, []);
540540
}
541+
542+
// Error
543+
544+
const untouchedErrorToString = Error.prototype.toString;
545+
export function safeErrorToString(instance: Error): string {
546+
return safeApply(untouchedErrorToString, instance, []);
547+
}

packages/fast-check/test/e2e/AsyncScheduler.spec.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe(`AsyncScheduler (seed: ${seed})`, () => {
5151
);
5252
// Node <16: Cannot read property 'toLowerCase' of undefined
5353
// Node >=16: TypeError: Cannot read properties of undefined (reading 'toLowerCase')
54-
expect(out.error).toContain(`'toLowerCase'`);
54+
expect((out.errorInstance as Error).message).toContain(`'toLowerCase'`);
5555
});
5656

5757
it('should detect race conditions leading to infinite loops', async () => {
@@ -175,10 +175,7 @@ describe(`AsyncScheduler (seed: ${seed})`, () => {
175175
expect(outRetry.failed).toBe(true);
176176
expect(outRetry.numRuns).toBe(1);
177177

178-
const cleanError = (error: string) => {
179-
return error.replace(/AsyncScheduler\.spec\.ts:\d+:\d+/g, 'AsyncScheduler.spec.ts:*:*');
180-
};
181-
expect(cleanError(outRetry.error!)).toBe(cleanError(out.error!));
178+
expect(outRetry.errorInstance).toStrictEqual(out.errorInstance);
182179
expect(String(outRetry.counterexample![0])).toBe(String(out.counterexample![0]));
183180
});
184181
});

0 commit comments

Comments
 (0)