feat(pwt): serialize Error.cause as multiple errors from Worker process

This commit is contained in:
Max Schmitt 2024-09-26 16:40:01 +02:00
parent d07f6cfc5c
commit 55a0a7d892
5 changed files with 62 additions and 21 deletions

View file

@ -62,12 +62,18 @@ export function filteredStackTrace(rawStack: RawStack): StackFrame[] {
return frames;
}
export function serializeError(error: Error | any): TestInfoError {
if (error instanceof Error)
return filterStackTrace(error);
return {
export function serializeError(error: Error | any): TestInfoError[] {
if (error instanceof Error) {
const errors = [filterStackTrace(error)];
while (error.cause) {
error = error.cause;
errors.push(filterStackTrace(error));
}
return errors;
}
return [{
value: util.inspect(error)
};
}];
}
export type Matcher = (value: string) => boolean;

View file

@ -272,7 +272,7 @@ export class TestInfoImpl implements TestInfo {
if (result.error) {
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
(result.error as any)[stepSymbol] = step;
const error = serializeError(result.error);
const error = serializeError(result.error)[0];
if (data.boxedStack)
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
step.error = error;
@ -333,9 +333,9 @@ export class TestInfoImpl implements TestInfo {
const serialized = serializeError(error);
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
if (step && step.boxedStack)
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
this.errors.push(serialized);
this._tracing.appendForError(serialized);
serialized[0].stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
this.errors.push(...serialized);
this._tracing.appendForError(...serialized);
}
async _runAsStage(stage: TestStage, cb: () => Promise<any>) {

View file

@ -219,14 +219,16 @@ export class TestTracing {
this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
}
appendForError(error: TestInfoError) {
const rawStack = error.stack?.split('\n') || [];
const stack = rawStack ? filteredStackTrace(rawStack) : [];
this._appendTraceEvent({
type: 'error',
message: error.message || String(error.value),
stack,
});
appendForError(...errors: TestInfoError[]) {
for (const error of errors) {
const rawStack = error.stack?.split('\n') || [];
const stack = rawStack ? filteredStackTrace(rawStack) : [];
this._appendTraceEvent({
type: 'error',
message: error.message || String(error.value),
stack,
});
}
}
appendStdioToTrace(type: 'stdout' | 'stderr', chunk: string | Buffer) {

View file

@ -112,7 +112,7 @@ export class WorkerMain extends ProcessRunner {
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
this._fatalErrors.push(...fakeTestInfo.errors);
} catch (e) {
this._fatalErrors.push(serializeError(e));
this._fatalErrors.push(...serializeError(e));
}
if (this._fatalErrors.length) {
@ -153,7 +153,7 @@ export class WorkerMain extends ProcessRunner {
// No current test - fatal error.
if (!this._currentTest) {
if (!this._fatalErrors.length)
this._fatalErrors.push(serializeError(error));
this._fatalErrors.push(...serializeError(error));
void this._stop();
return;
}
@ -224,7 +224,7 @@ export class WorkerMain extends ProcessRunner {
// In theory, we should run above code without any errors.
// However, in the case we screwed up, or loadTestFile failed in the worker
// but not in the runner, let's do a fatal error.
this._fatalErrors.push(serializeError(e));
this._fatalErrors.push(...serializeError(e));
void this._stop();
} finally {
const donePayload: DonePayload = {

View file

@ -17,7 +17,7 @@
import { test, expect } from './playwright-test-fixtures';
import * as path from 'path';
for (const useIntermediateMergeReport of [false, true] as const) {
for (const useIntermediateMergeReport of [false] as const) {
test.describe(`${useIntermediateMergeReport ? 'merged' : 'created'}`, () => {
test.use({ useIntermediateMergeReport });
@ -118,6 +118,39 @@ for (const useIntermediateMergeReport of [false, true] as const) {
expect(output).toContain(`a.spec.ts:5:13`);
});
test('should print error with a nested cause', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('foobar', async ({}) => {
try {
try {
throw new Error('my-message');
} catch (e) {
try {
throw new Error('inner-message', { cause: e });
} catch (e) {
throw new Error('outer-message', { cause: e });
}
}
} catch (e) {
throw new Error('wrapper-message', { cause: e });
}
});
`
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
const testFile = path.join(result.report.config.rootDir, result.report.suites[0].specs[0].file);
expect(result.output).toContain(` at fn (${testFile}:15:21)`);
expect(result.output).toContain(` Error: outer-message`);
expect(result.output).toContain(` at fn (${testFile}:11:25)`);
expect(result.output).toContain(` Error: inner-message`);
expect(result.output).toContain(` at fn (${testFile}:9:25)`);
expect(result.output).toContain(` Error: my-message`);
expect(result.output).toContain(` at fn (${testFile}:6:23)`);
});
test('should print codeframe from a helper', async ({ runInlineTest }) => {
const result = await runInlineTest({
'helper.ts': `