feat(pwt): serialize Error.cause as multiple errors from Worker process
This commit is contained in:
parent
d07f6cfc5c
commit
55a0a7d892
|
|
@ -62,12 +62,18 @@ export function filteredStackTrace(rawStack: RawStack): StackFrame[] {
|
||||||
return frames;
|
return frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeError(error: Error | any): TestInfoError {
|
export function serializeError(error: Error | any): TestInfoError[] {
|
||||||
if (error instanceof Error)
|
if (error instanceof Error) {
|
||||||
return filterStackTrace(error);
|
const errors = [filterStackTrace(error)];
|
||||||
return {
|
while (error.cause) {
|
||||||
|
error = error.cause;
|
||||||
|
errors.push(filterStackTrace(error));
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
return [{
|
||||||
value: util.inspect(error)
|
value: util.inspect(error)
|
||||||
};
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Matcher = (value: string) => boolean;
|
export type Matcher = (value: string) => boolean;
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
|
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
|
||||||
(result.error as any)[stepSymbol] = step;
|
(result.error as any)[stepSymbol] = step;
|
||||||
const error = serializeError(result.error);
|
const error = serializeError(result.error)[0];
|
||||||
if (data.boxedStack)
|
if (data.boxedStack)
|
||||||
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
|
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
|
||||||
step.error = error;
|
step.error = error;
|
||||||
|
|
@ -333,9 +333,9 @@ export class TestInfoImpl implements TestInfo {
|
||||||
const serialized = serializeError(error);
|
const serialized = serializeError(error);
|
||||||
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
|
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
|
||||||
if (step && step.boxedStack)
|
if (step && step.boxedStack)
|
||||||
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
|
serialized[0].stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
|
||||||
this.errors.push(serialized);
|
this.errors.push(...serialized);
|
||||||
this._tracing.appendForError(serialized);
|
this._tracing.appendForError(...serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _runAsStage(stage: TestStage, cb: () => Promise<any>) {
|
async _runAsStage(stage: TestStage, cb: () => Promise<any>) {
|
||||||
|
|
|
||||||
|
|
@ -219,14 +219,16 @@ export class TestTracing {
|
||||||
this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
|
this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
|
||||||
}
|
}
|
||||||
|
|
||||||
appendForError(error: TestInfoError) {
|
appendForError(...errors: TestInfoError[]) {
|
||||||
const rawStack = error.stack?.split('\n') || [];
|
for (const error of errors) {
|
||||||
const stack = rawStack ? filteredStackTrace(rawStack) : [];
|
const rawStack = error.stack?.split('\n') || [];
|
||||||
this._appendTraceEvent({
|
const stack = rawStack ? filteredStackTrace(rawStack) : [];
|
||||||
type: 'error',
|
this._appendTraceEvent({
|
||||||
message: error.message || String(error.value),
|
type: 'error',
|
||||||
stack,
|
message: error.message || String(error.value),
|
||||||
});
|
stack,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendStdioToTrace(type: 'stdout' | 'stderr', chunk: string | Buffer) {
|
appendStdioToTrace(type: 'stdout' | 'stderr', chunk: string | Buffer) {
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
|
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
|
||||||
this._fatalErrors.push(...fakeTestInfo.errors);
|
this._fatalErrors.push(...fakeTestInfo.errors);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._fatalErrors.push(serializeError(e));
|
this._fatalErrors.push(...serializeError(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._fatalErrors.length) {
|
if (this._fatalErrors.length) {
|
||||||
|
|
@ -153,7 +153,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
// No current test - fatal error.
|
// No current test - fatal error.
|
||||||
if (!this._currentTest) {
|
if (!this._currentTest) {
|
||||||
if (!this._fatalErrors.length)
|
if (!this._fatalErrors.length)
|
||||||
this._fatalErrors.push(serializeError(error));
|
this._fatalErrors.push(...serializeError(error));
|
||||||
void this._stop();
|
void this._stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +224,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
// In theory, we should run above code without any errors.
|
// In theory, we should run above code without any errors.
|
||||||
// However, in the case we screwed up, or loadTestFile failed in the worker
|
// However, in the case we screwed up, or loadTestFile failed in the worker
|
||||||
// but not in the runner, let's do a fatal error.
|
// but not in the runner, let's do a fatal error.
|
||||||
this._fatalErrors.push(serializeError(e));
|
this._fatalErrors.push(...serializeError(e));
|
||||||
void this._stop();
|
void this._stop();
|
||||||
} finally {
|
} finally {
|
||||||
const donePayload: DonePayload = {
|
const donePayload: DonePayload = {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import { test, expect } from './playwright-test-fixtures';
|
import { test, expect } from './playwright-test-fixtures';
|
||||||
import * as path from 'path';
|
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.describe(`${useIntermediateMergeReport ? 'merged' : 'created'}`, () => {
|
||||||
test.use({ useIntermediateMergeReport });
|
test.use({ useIntermediateMergeReport });
|
||||||
|
|
||||||
|
|
@ -118,6 +118,39 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||||
expect(output).toContain(`a.spec.ts:5:13`);
|
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 }) => {
|
test('should print codeframe from a helper', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'helper.ts': `
|
'helper.ts': `
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue