mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(test runner): show the number of fatal errors at the end (#15975)
This commit is contained in:
parent
6f47f0f22a
commit
62e4e80599
@ -42,6 +42,7 @@ type TestSummary = {
|
|||||||
unexpected: TestCase[];
|
unexpected: TestCase[];
|
||||||
flaky: TestCase[];
|
flaky: TestCase[];
|
||||||
failuresToPrint: TestCase[];
|
failuresToPrint: TestCase[];
|
||||||
|
fatalErrors: TestError[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export class BaseReporter implements Reporter {
|
export class BaseReporter implements Reporter {
|
||||||
@ -54,6 +55,7 @@ export class BaseReporter implements Reporter {
|
|||||||
private monotonicStartTime: number = 0;
|
private monotonicStartTime: number = 0;
|
||||||
private _omitFailures: boolean;
|
private _omitFailures: boolean;
|
||||||
private readonly _ttyWidthForTest: number;
|
private readonly _ttyWidthForTest: number;
|
||||||
|
private _fatalErrors: TestError[] = [];
|
||||||
|
|
||||||
constructor(options: { omitFailures?: boolean } = {}) {
|
constructor(options: { omitFailures?: boolean } = {}) {
|
||||||
this._omitFailures = options.omitFailures || false;
|
this._omitFailures = options.omitFailures || false;
|
||||||
@ -96,7 +98,7 @@ export class BaseReporter implements Reporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onError(error: TestError) {
|
onError(error: TestError) {
|
||||||
console.log('\n' + formatError(this.config, error, colors.enabled).message);
|
this._fatalErrors.push(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onEnd(result: FullResult) {
|
async onEnd(result: FullResult) {
|
||||||
@ -116,7 +118,7 @@ export class BaseReporter implements Reporter {
|
|||||||
protected generateStartingMessage() {
|
protected generateStartingMessage() {
|
||||||
const jobs = Math.min(this.config.workers, this.config._testGroupsCount);
|
const jobs = Math.min(this.config.workers, this.config._testGroupsCount);
|
||||||
const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : '';
|
const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : '';
|
||||||
return `\nRunning ${this.totalTestCount} test${this.totalTestCount > 1 ? 's' : ''} using ${jobs} worker${jobs > 1 ? 's' : ''}${shardDetails}`;
|
return `\nRunning ${this.totalTestCount} test${this.totalTestCount !== 1 ? 's' : ''} using ${jobs} worker${jobs !== 1 ? 's' : ''}${shardDetails}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getSlowTests(): [string, number][] {
|
protected getSlowTests(): [string, number][] {
|
||||||
@ -129,8 +131,10 @@ export class BaseReporter implements Reporter {
|
|||||||
return fileDurations.filter(([,duration]) => duration > threshold).slice(0, count);
|
return fileDurations.filter(([,duration]) => duration > threshold).slice(0, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected generateSummaryMessage({ skipped, expected, unexpected, flaky }: TestSummary) {
|
protected generateSummaryMessage({ skipped, expected, unexpected, flaky, fatalErrors }: TestSummary) {
|
||||||
const tokens: string[] = [];
|
const tokens: string[] = [];
|
||||||
|
if (fatalErrors.length)
|
||||||
|
tokens.push(colors.red(` ${fatalErrors.length} fatal ${fatalErrors.length === 1 ? 'error' : 'errors'}`));
|
||||||
if (unexpected.length) {
|
if (unexpected.length) {
|
||||||
tokens.push(colors.red(` ${unexpected.length} failed`));
|
tokens.push(colors.red(` ${unexpected.length} failed`));
|
||||||
for (const test of unexpected)
|
for (const test of unexpected)
|
||||||
@ -179,7 +183,8 @@ export class BaseReporter implements Reporter {
|
|||||||
skippedWithError,
|
skippedWithError,
|
||||||
unexpected,
|
unexpected,
|
||||||
flaky,
|
flaky,
|
||||||
failuresToPrint
|
failuresToPrint,
|
||||||
|
fatalErrors: this._fatalErrors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
import { BaseReporter } from './base';
|
import { BaseReporter, formatError } from './base';
|
||||||
import type { FullResult, TestCase, TestResult, FullConfig, Suite } from '../../types/testReporter';
|
import type { FullResult, TestCase, TestResult, FullConfig, Suite, TestError } from '../../types/testReporter';
|
||||||
|
|
||||||
class DotReporter extends BaseReporter {
|
class DotReporter extends BaseReporter {
|
||||||
private _counter = 0;
|
private _counter = 0;
|
||||||
@ -64,6 +64,12 @@ class DotReporter extends BaseReporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onError(error: TestError): void {
|
||||||
|
super.onError(error);
|
||||||
|
console.log('\n' + formatError(this.config, error, colors.enabled).message);
|
||||||
|
this._counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
override async onEnd(result: FullResult) {
|
override async onEnd(result: FullResult) {
|
||||||
await super.onEnd(result);
|
await super.onEnd(result);
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
import { BaseReporter, formatFailure, formatTestTitle } from './base';
|
import { BaseReporter, formatError, formatFailure, formatTestTitle } from './base';
|
||||||
import type { FullConfig, TestCase, Suite, TestResult, FullResult, TestStep } from '../../types/testReporter';
|
import type { FullConfig, TestCase, Suite, TestResult, FullResult, TestStep, TestError } from '../../types/testReporter';
|
||||||
|
|
||||||
class LineReporter extends BaseReporter {
|
class LineReporter extends BaseReporter {
|
||||||
private _current = 0;
|
private _current = 0;
|
||||||
@ -100,6 +100,16 @@ class LineReporter extends BaseReporter {
|
|||||||
process.stdout.write(`\u001B[1A\u001B[2K${prefix + this.fitToScreen(title, prefix)}\n`);
|
process.stdout.write(`\u001B[1A\u001B[2K${prefix + this.fitToScreen(title, prefix)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onError(error: TestError): void {
|
||||||
|
super.onError(error);
|
||||||
|
|
||||||
|
const message = formatError(this.config, error, colors.enabled).message + '\n\n';
|
||||||
|
if (!process.env.PW_TEST_DEBUG_REPORTERS)
|
||||||
|
process.stdout.write(`\u001B[1A\u001B[2K`);
|
||||||
|
process.stdout.write(message);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
override async onEnd(result: FullResult) {
|
override async onEnd(result: FullResult) {
|
||||||
if (!process.env.PW_TEST_DEBUG_REPORTERS)
|
if (!process.env.PW_TEST_DEBUG_REPORTERS)
|
||||||
process.stdout.write(`\u001B[1A\u001B[2K`);
|
process.stdout.write(`\u001B[1A\u001B[2K`);
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { colors, ms as milliseconds } from 'playwright-core/lib/utilsBundle';
|
import { colors, ms as milliseconds } from 'playwright-core/lib/utilsBundle';
|
||||||
import { BaseReporter, formatTestTitle, stripAnsiEscapes } from './base';
|
import { BaseReporter, formatError, formatTestTitle, stripAnsiEscapes } from './base';
|
||||||
import type { FullConfig, FullResult, Suite, TestCase, TestResult, TestStep } from '../../types/testReporter';
|
import type { FullConfig, FullResult, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
|
||||||
|
|
||||||
// Allow it in the Visual Studio Code Terminal and the new Windows Terminal
|
// Allow it in the Visual Studio Code Terminal and the new Windows Terminal
|
||||||
const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION;
|
const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION;
|
||||||
@ -47,11 +47,8 @@ class ListReporter extends BaseReporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTestBegin(test: TestCase, result: TestResult) {
|
onTestBegin(test: TestCase, result: TestResult) {
|
||||||
if (this._liveTerminal && this._needNewLine) {
|
if (this._liveTerminal)
|
||||||
this._needNewLine = false;
|
this._maybeWriteNewLine();
|
||||||
process.stdout.write('\n');
|
|
||||||
this._lastRow++;
|
|
||||||
}
|
|
||||||
this._resultIndex.set(result, this._resultIndex.size + 1);
|
this._resultIndex.set(result, this._resultIndex.size + 1);
|
||||||
this._testRows.set(test, this._lastRow++);
|
this._testRows.set(test, this._lastRow++);
|
||||||
if (this._liveTerminal) {
|
if (this._liveTerminal) {
|
||||||
@ -87,15 +84,26 @@ class ListReporter extends BaseReporter {
|
|||||||
this._updateTestLine(test, colors.dim(formatTestTitle(this.config, test, step.parent)) + this._retrySuffix(result), this._testPrefix(result, ''));
|
this._updateTestLine(test, colors.dim(formatTestTitle(this.config, test, step.parent)) + this._retrySuffix(result), this._testPrefix(result, ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dumpToStdio(test: TestCase | undefined, chunk: string | Buffer, stream: NodeJS.WriteStream) {
|
private _maybeWriteNewLine() {
|
||||||
if (this.config.quiet)
|
if (this._needNewLine) {
|
||||||
return;
|
this._needNewLine = false;
|
||||||
const text = chunk.toString('utf-8');
|
process.stdout.write('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateLineCountAndNewLineFlagForOutput(text: string) {
|
||||||
this._needNewLine = text[text.length - 1] !== '\n';
|
this._needNewLine = text[text.length - 1] !== '\n';
|
||||||
if (this._liveTerminal) {
|
if (this._liveTerminal) {
|
||||||
const newLineCount = text.split('\n').length - 1;
|
const newLineCount = text.split('\n').length - 1;
|
||||||
this._lastRow += newLineCount;
|
this._lastRow += newLineCount;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dumpToStdio(test: TestCase | undefined, chunk: string | Buffer, stream: NodeJS.WriteStream) {
|
||||||
|
if (this.config.quiet)
|
||||||
|
return;
|
||||||
|
const text = chunk.toString('utf-8');
|
||||||
|
this._updateLineCountAndNewLineFlagForOutput(text);
|
||||||
stream.write(chunk);
|
stream.write(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,10 +132,7 @@ class ListReporter extends BaseReporter {
|
|||||||
if (this._liveTerminal) {
|
if (this._liveTerminal) {
|
||||||
this._updateTestLine(test, text, prefix);
|
this._updateTestLine(test, text, prefix);
|
||||||
} else {
|
} else {
|
||||||
if (this._needNewLine) {
|
this._maybeWriteNewLine();
|
||||||
this._needNewLine = false;
|
|
||||||
process.stdout.write('\n');
|
|
||||||
}
|
|
||||||
process.stdout.write(prefix + text);
|
process.stdout.write(prefix + text);
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
}
|
}
|
||||||
@ -170,6 +175,14 @@ class ListReporter extends BaseReporter {
|
|||||||
process.stdout.write(testRow + ' : ' + prefix + line + '\n');
|
process.stdout.write(testRow + ' : ' + prefix + line + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onError(error: TestError): void {
|
||||||
|
super.onError(error);
|
||||||
|
this._maybeWriteNewLine();
|
||||||
|
const message = formatError(this.config, error, colors.enabled).message + '\n';
|
||||||
|
this._updateLineCountAndNewLineFlagForOutput(message);
|
||||||
|
process.stdout.write(message);
|
||||||
|
}
|
||||||
|
|
||||||
override async onEnd(result: FullResult) {
|
override async onEnd(result: FullResult) {
|
||||||
await super.onEnd(result);
|
await super.onEnd(result);
|
||||||
process.stdout.write('\n');
|
process.stdout.write('\n');
|
||||||
|
@ -99,7 +99,9 @@ async function gracefullyCloseAndExit() {
|
|||||||
await stopProfiling(workerIndex);
|
await stopProfiling(workerIndex);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try {
|
try {
|
||||||
const payload: TeardownErrorsPayload = { fatalErrors: [serializeError(e)] };
|
const error = serializeError(e);
|
||||||
|
workerRunner.appendWorkerTeardownDiagnostics(error);
|
||||||
|
const payload: TeardownErrorsPayload = { fatalErrors: [error] };
|
||||||
process.send!({ method: 'teardownErrors', params: payload });
|
process.send!({ method: 'teardownErrors', params: payload });
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
|
@ -86,15 +86,13 @@ export class WorkerRunner extends EventEmitter {
|
|||||||
await this._loadIfNeeded();
|
await this._loadIfNeeded();
|
||||||
await this._teardownScopes();
|
await this._teardownScopes();
|
||||||
if (this._fatalErrors.length) {
|
if (this._fatalErrors.length) {
|
||||||
const diagnostics = this._createWorkerTeardownDiagnostics();
|
this.appendWorkerTeardownDiagnostics(this._fatalErrors[this._fatalErrors.length - 1]);
|
||||||
if (diagnostics)
|
|
||||||
this._fatalErrors.unshift(diagnostics);
|
|
||||||
const payload: TeardownErrorsPayload = { fatalErrors: this._fatalErrors };
|
const payload: TeardownErrorsPayload = { fatalErrors: this._fatalErrors };
|
||||||
this.emit('teardownErrors', payload);
|
this.emit('teardownErrors', payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createWorkerTeardownDiagnostics(): TestError | undefined {
|
appendWorkerTeardownDiagnostics(error: TestError) {
|
||||||
if (!this._lastRunningTests.length)
|
if (!this._lastRunningTests.length)
|
||||||
return;
|
return;
|
||||||
const count = this._totalRunningTests === 1 ? '1 test' : `${this._totalRunningTests} tests`;
|
const count = this._totalRunningTests === 1 ? '1 test' : `${this._totalRunningTests} tests`;
|
||||||
@ -102,10 +100,23 @@ export class WorkerRunner extends EventEmitter {
|
|||||||
if (this._lastRunningTests.length < this._totalRunningTests)
|
if (this._lastRunningTests.length < this._totalRunningTests)
|
||||||
lastMessage = `, last ${this._lastRunningTests.length} tests were`;
|
lastMessage = `, last ${this._lastRunningTests.length} tests were`;
|
||||||
const message = [
|
const message = [
|
||||||
colors.red(`Worker teardown error. This worker ran ${count}${lastMessage}:`),
|
'',
|
||||||
|
'',
|
||||||
|
colors.red(`Failed worker ran ${count}${lastMessage}:`),
|
||||||
...this._lastRunningTests.map(testInfo => formatTestTitle(testInfo._test, testInfo.project.name)),
|
...this._lastRunningTests.map(testInfo => formatTestTitle(testInfo._test, testInfo.project.name)),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
return { message };
|
if (error.message) {
|
||||||
|
if (error.stack) {
|
||||||
|
let index = error.stack.indexOf(error.message);
|
||||||
|
if (index !== -1) {
|
||||||
|
index += error.message.length;
|
||||||
|
error.stack = error.stack.substring(0, index) + message + error.stack.substring(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error.message += message;
|
||||||
|
} else if (error.value) {
|
||||||
|
error.value += message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _teardownScopes() {
|
private async _teardownScopes() {
|
||||||
|
@ -517,7 +517,9 @@ test('should report worker fixture teardown with debug info', async ({ runInline
|
|||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.passed).toBe(20);
|
expect(result.passed).toBe(20);
|
||||||
expect(stripAnsi(result.output)).toContain([
|
expect(stripAnsi(result.output)).toContain([
|
||||||
'Worker teardown error. This worker ran 20 tests, last 10 tests were:',
|
'Worker teardown timeout of 1000ms exceeded while tearing down "fixture".',
|
||||||
|
'',
|
||||||
|
'Failed worker ran 20 tests, last 10 tests were:',
|
||||||
'a.spec.ts:12:9 › good10',
|
'a.spec.ts:12:9 › good10',
|
||||||
'a.spec.ts:12:9 › good11',
|
'a.spec.ts:12:9 › good11',
|
||||||
'a.spec.ts:12:9 › good12',
|
'a.spec.ts:12:9 › good12',
|
||||||
@ -528,8 +530,6 @@ test('should report worker fixture teardown with debug info', async ({ runInline
|
|||||||
'a.spec.ts:12:9 › good17',
|
'a.spec.ts:12:9 › good17',
|
||||||
'a.spec.ts:12:9 › good18',
|
'a.spec.ts:12:9 › good18',
|
||||||
'a.spec.ts:12:9 › good19',
|
'a.spec.ts:12:9 › good19',
|
||||||
'',
|
|
||||||
'Worker teardown timeout of 1000ms exceeded while tearing down "fixture".',
|
|
||||||
].join('\n'));
|
].join('\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -290,3 +290,31 @@ test('should not crash on undefined body with manual attachments', async ({ runI
|
|||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should report fatal errors at the end', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.spec.ts': `
|
||||||
|
const test = pwt.test.extend({
|
||||||
|
fixture: [async ({ }, use) => {
|
||||||
|
await use();
|
||||||
|
throw new Error('oh my!');
|
||||||
|
}, { scope: 'worker' }],
|
||||||
|
});
|
||||||
|
test('good', async ({ fixture }) => {
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'b.spec.ts': `
|
||||||
|
const test = pwt.test.extend({
|
||||||
|
fixture: [async ({ }, use) => {
|
||||||
|
await use();
|
||||||
|
throw new Error('oh my!');
|
||||||
|
}, { scope: 'worker' }],
|
||||||
|
});
|
||||||
|
test('good', async ({ fixture }) => {
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { reporter: 'list' });
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(2);
|
||||||
|
expect(stripAnsi(result.output)).toContain('2 fatal errors');
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user