Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: patch and filter seeded run stacktraces #3229

Merged
merged 5 commits into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 64 additions & 4 deletions test/support/seeded-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,13 @@ class TestGenerator<
*
* @param method The method name to call.
* @param args The arguments to call it with.
* @param extraStackFrames Additional stack frames to add into the stacktrace.
* @param repetitions The number of times to call it.
*/
private callAndVerify<TMethodName extends MethodOf<TModule>>(
method: TMethodName,
args: Parameters<TModule[TMethodName]>,
extraStackFrames: () => string[],
repetitions: number = 1
): void {
this.setup();
Expand All @@ -148,8 +150,12 @@ class TestGenerator<
}

for (let i = 0; i < repetitions; i++) {
const value = callable(...args);
expect(value).toMatchSnapshot();
try {
const value = callable(...args);
expect(value).toMatchSnapshot();
} catch (error: unknown) {
throw patchExtraStackFrames(error, extraStackFrames);
}
}
}

Expand Down Expand Up @@ -182,10 +188,12 @@ class TestGenerator<
*/
itRepeated(method: NoArgsMethodOf<TModule>, repetitions: number): this {
this.expectNotTested(method);
const extraStackFrames = collectExtraStackFrames();
vi_it(method, () =>
this.callAndVerify(
method,
[] as unknown as Parameters<TModule[NoArgsMethodOf<TModule>]>,
extraStackFrames,
repetitions
)
);
Expand Down Expand Up @@ -233,7 +241,8 @@ class TestGenerator<
const tester: MethodTester<TModule[TMethodName]> = {
it(name: string, ...args: Parameters<TModule[TMethodName]>) {
expectVariantNotTested(name);
vi_it(name, () => callAndVerify(method, args));
const extraStackFrames = collectExtraStackFrames(name.length + 7);
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
vi_it(name, () => callAndVerify(method, args, extraStackFrames));
return tester;
},
itRepeated(
Expand All @@ -242,7 +251,10 @@ class TestGenerator<
...args: Parameters<TModule[TMethodName]>
) {
expectVariantNotTested(name);
vi_it(name, () => callAndVerify(method, args, repetitions));
const extraStackFrames = collectExtraStackFrames(name.length + 7);
vi_it(name, () =>
callAndVerify(method, args, extraStackFrames, repetitions)
);
return tester;
},
};
Expand Down Expand Up @@ -288,6 +300,54 @@ class TestGenerator<
}
}

/**
* Lazily collects the current call stack with an additional offset.
*
* Vitest's stacktraces only contain the stacktrace from inside `it(name, () => { here })`.
* This method collects the location where the `it` block is created instead of executed.
* The stack frames can then later be added to the error stack to provide a more accurate location.
*
* @param extraOffset The additional offset to add to the column numbers to account for the name of the test.
*/
function collectExtraStackFrames(extraOffset: number = 0): () => string[] {
const stack = new Error('collect').stack;
if (stack == null) {
return () => [];
}

return () =>
stack
.split('\n')
.map((e) => e.replaceAll('\\', '/'))
.filter((e) => e.includes('/test/')) // exclude node_modules
.filter((e) => !e.includes('/test/support/')) // exclude this file
.map((e) =>
e.replace(/:(\d+)$/, (_, column: string) => `:${+column + extraOffset}`)
);
}

/**
* Modifies the error stack to include the given additional stack frames.
*
* @param error The error to modify.
* @param extraStackFrames The additional stack frames to add after this file.
*/
function patchExtraStackFrames(
error: unknown,
extraStackFrames: () => string[]
): unknown {
if (error instanceof Error && error.stack != null) {
const stack = error.stack.split('\n');
const index = stack.findLastIndex((e) =>
e.replaceAll('\\', '/').includes('/test/support/')
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
);
stack.splice(index + 1, 0, ...extraStackFrames());
error.stack = stack.join('\n');
}

return error;
}

/**
* Simple interface for a test generator for a given method.
*/
Expand Down
10 changes: 10 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ export default defineConfig({
seed: VITEST_SEQUENCE_SEED,
shuffle: true,
},
onStackTrace(_, { file }) {
if (
file.includes('/src/internal/locale-proxy') ||
file.includes('/test/support/')
) {
return false;
}

return true;
},
typecheck: {
enabled: true,
include: ['test/**/*.spec-d.ts'],
Expand Down