feat(test runner): show the number of fatal errors at the end (#15975)

This commit is contained in:
Dmitry Gozman 2022-07-28 14:46:21 -07:00 committed by GitHub
parent 6f47f0f22a
commit 62e4e80599
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 108 additions and 33 deletions

View File

@ -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,
}; };
} }

View File

@ -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');

View File

@ -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`);

View File

@ -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');

View File

@ -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 {
} }

View File

@ -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() {

View File

@ -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'));
}); });

View File

@ -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');
});