feat(test-runner): small changes to Reporter api (#7709)

- `TestResult.startTime`
- `Suite.location` is optional now
- `Test.status()` renamed to `Test.outcome()` to differentiate against a
  `Test.expectedStatus` and `TestResult.status` of the different type.
This commit is contained in:
Dmitry Gozman 2021-07-18 17:40:59 -07:00 committed by GitHub
parent e5c7941b49
commit 66ea613c4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 49 additions and 30 deletions

View file

@ -265,6 +265,7 @@ export class Dispatcher {
worker.on('testBegin', (params: TestBeginPayload) => { worker.on('testBegin', (params: TestBeginPayload) => {
const { test, result: testRun } = this._testById.get(params.testId)!; const { test, result: testRun } = this._testById.get(params.testId)!;
testRun.workerIndex = params.workerIndex; testRun.workerIndex = params.workerIndex;
testRun.startTime = new Date(params.startWallTime);
this._reportTestBegin(test); this._reportTestBegin(test);
}); });
worker.on('testEnd', (params: TestEndPayload) => { worker.on('testEnd', (params: TestEndPayload) => {

View file

@ -31,7 +31,8 @@ export type WorkerInitParams = {
export type TestBeginPayload = { export type TestBeginPayload = {
testId: string; testId: string;
workerIndex: number, startWallTime: number; // milliseconds since unix epoch
workerIndex: number;
}; };
export type TestEndPayload = { export type TestEndPayload = {

View file

@ -112,7 +112,7 @@ export class Loader {
try { try {
const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file)); const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file));
suite._requireFile = file; suite._requireFile = file;
suite.location.file = file; suite.location = { file, line: 0, column: 0 };
setCurrentlyLoadingFileSuite(suite); setCurrentlyLoadingFileSuite(suite);
await this._requireOrImport(file); await this._requireOrImport(file);
this._fileSuites.set(file, suite); this._fileSuites.set(file, suite);

View file

@ -87,7 +87,7 @@ export class BaseReporter implements Reporter {
const flaky: Test[] = []; const flaky: Test[] = [];
this.suite.allTests().forEach(test => { this.suite.allTests().forEach(test => {
switch (test.status()) { switch (test.outcome()) {
case 'skipped': ++skipped; break; case 'skipped': ++skipped; break;
case 'expected': ++expected; break; case 'expected': ++expected; break;
case 'unexpected': unexpected.push(test); break; case 'unexpected': unexpected.push(test); break;

View file

@ -35,7 +35,7 @@ class DotReporter extends BaseReporter {
process.stdout.write(colors.gray('×')); process.stdout.write(colors.gray('×'));
return; return;
} }
switch (test.status()) { switch (test.outcome()) {
case 'expected': process.stdout.write(colors.green('·')); break; case 'expected': process.stdout.write(colors.green('·')); break;
case 'unexpected': process.stdout.write(colors.red(test.results[test.results.length - 1].status === 'timedOut' ? 'T' : 'F')); break; case 'unexpected': process.stdout.write(colors.red(test.results[test.results.length - 1].status === 'timedOut' ? 'T' : 'F')); break;
case 'flaky': process.stdout.write(colors.yellow('±')); break; case 'flaky': process.stdout.write(colors.yellow('±')); break;

View file

@ -127,21 +127,24 @@ class JSONReporter implements Reporter {
const result: JSONReportSuite[] = []; const result: JSONReportSuite[] = [];
for (const projectSuite of suites) { for (const projectSuite of suites) {
for (const fileSuite of projectSuite.suites) { for (const fileSuite of projectSuite.suites) {
if (!fileSuites.has(fileSuite.location.file)) { const file = fileSuite.location!.file;
if (!fileSuites.has(file)) {
const serialized = this._serializeSuite(fileSuite); const serialized = this._serializeSuite(fileSuite);
if (serialized) { if (serialized) {
fileSuites.set(fileSuite.location.file, serialized); fileSuites.set(file, serialized);
result.push(serialized); result.push(serialized);
} }
} else { } else {
this._mergeTestsFromSuite(fileSuites.get(fileSuite.location.file)!, fileSuite); this._mergeTestsFromSuite(fileSuites.get(file)!, fileSuite);
} }
} }
} }
return result; return result;
} }
private _relativeLocation(location: Location): Location { private _relativeLocation(location: Location | undefined): Location {
if (!location)
return { file: '', line: 0, column: 0 };
return { return {
file: toPosixPath(path.relative(this.config.rootDir, location.file)), file: toPosixPath(path.relative(this.config.rootDir, location.file)),
line: location.line, line: location.line,
@ -149,7 +152,7 @@ class JSONReporter implements Reporter {
}; };
} }
private _locationMatches(s: JSONReportSuite | JSONReportSpec, location: Location) { private _locationMatches(s: JSONReportSuite | JSONReportSpec, location: Location | undefined) {
const relative = this._relativeLocation(location); const relative = this._relativeLocation(location);
return s.file === relative.file && s.line === relative.line && s.column === relative.column; return s.file === relative.file && s.line === relative.line && s.column === relative.column;
} }
@ -205,7 +208,7 @@ class JSONReporter implements Reporter {
expectedStatus: test.expectedStatus, expectedStatus: test.expectedStatus,
projectName: test.titlePath()[1], projectName: test.titlePath()[1],
results: test.results.map(r => this._serializeTestResult(r)), results: test.results.map(r => this._serializeTestResult(r)),
status: test.status(), status: test.outcome(),
}; };
} }

View file

@ -87,7 +87,7 @@ class JUnitReporter implements Reporter {
suite.allTests().forEach(test => { suite.allTests().forEach(test => {
++tests; ++tests;
if (test.status() === 'skipped') if (test.outcome() === 'skipped')
++skipped; ++skipped;
if (!test.ok()) if (!test.ok())
++failures; ++failures;
@ -102,7 +102,7 @@ class JUnitReporter implements Reporter {
const entry: XMLEntry = { const entry: XMLEntry = {
name: 'testsuite', name: 'testsuite',
attributes: { attributes: {
name: path.relative(this.config.rootDir, suite.location.file), name: suite.location ? path.relative(this.config.rootDir, suite.location.file) : '',
timestamp: this.timestamp, timestamp: this.timestamp,
hostname: '', hostname: '',
tests, tests,
@ -130,7 +130,7 @@ class JUnitReporter implements Reporter {
}; };
entries.push(entry); entries.push(entry);
if (test.status() === 'skipped') { if (test.outcome() === 'skipped') {
entry.children.push({ name: 'skipped'}); entry.children.push({ name: 'skipped'});
return; return;
} }

View file

@ -295,7 +295,7 @@ function filterByFocusedLine(suite: Suite, focusedTestFileLines: FilePatternFilt
re.lastIndex = 0; re.lastIndex = 0;
return re.test(testFileName) && (line === testLine || line === null); return re.test(testFileName) && (line === testLine || line === null);
}); });
const suiteFilter = (suite: Suite) => testFileLineMatches(suite.location.file, suite.location.line); const suiteFilter = (suite: Suite) => !!suite.location && testFileLineMatches(suite.location.file, suite.location.line);
const testFilter = (test: Test) => testFileLineMatches(test.location.file, test.location.line); const testFilter = (test: Test) => testFileLineMatches(test.location.file, test.location.line);
return filterSuite(suite, suiteFilter, testFilter); return filterSuite(suite, suiteFilter, testFilter);
} }
@ -406,6 +406,8 @@ function getClashingTestsPerSuite(rootSuite: Suite): Map<string, Test[]> {
} }
function buildItemLocation(rootDir: string, testOrSuite: Suite | Test) { function buildItemLocation(rootDir: string, testOrSuite: Suite | Test) {
if (!testOrSuite.location)
return '';
return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`; return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`;
} }

View file

@ -21,7 +21,6 @@ import { Annotations, Location } from './types';
class Base { class Base {
title: string; title: string;
location: Location = { file: '', line: 0, column: 0 };
parent?: Suite; parent?: Suite;
_only = false; _only = false;
@ -48,6 +47,7 @@ export type Modifier = {
export class Suite extends Base implements reporterTypes.Suite { export class Suite extends Base implements reporterTypes.Suite {
suites: Suite[] = []; suites: Suite[] = [];
tests: Test[] = []; tests: Test[] = [];
location?: Location;
_fixtureOverrides: any = {}; _fixtureOverrides: any = {};
_entries: (Suite | Test)[] = []; _entries: (Suite | Test)[] = [];
_hooks: { _hooks: {
@ -116,6 +116,7 @@ export class Suite extends Base implements reporterTypes.Suite {
export class Test extends Base implements reporterTypes.Test { export class Test extends Base implements reporterTypes.Test {
fn: Function; fn: Function;
results: reporterTypes.TestResult[] = []; results: reporterTypes.TestResult[] = [];
location: Location;
expectedStatus: reporterTypes.TestStatus = 'passed'; expectedStatus: reporterTypes.TestStatus = 'passed';
timeout = 0; timeout = 0;
@ -131,14 +132,15 @@ export class Test extends Base implements reporterTypes.Test {
_repeatEachIndex = 0; _repeatEachIndex = 0;
_projectIndex = 0; _projectIndex = 0;
constructor(title: string, fn: Function, ordinalInFile: number, testType: TestTypeImpl) { constructor(title: string, fn: Function, ordinalInFile: number, testType: TestTypeImpl, location: Location) {
super(title); super(title);
this.fn = fn; this.fn = fn;
this._ordinalInFile = ordinalInFile; this._ordinalInFile = ordinalInFile;
this._testType = testType; this._testType = testType;
this.location = location;
} }
status(): 'skipped' | 'expected' | 'unexpected' | 'flaky' { outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' {
if (!this.results.length) if (!this.results.length)
return 'skipped'; return 'skipped';
if (this.results.length === 1 && this.expectedStatus === this.results[0].status) if (this.results.length === 1 && this.expectedStatus === this.results[0].status)
@ -158,14 +160,13 @@ export class Test extends Base implements reporterTypes.Test {
} }
ok(): boolean { ok(): boolean {
const status = this.status(); const status = this.outcome();
return status === 'expected' || status === 'flaky' || status === 'skipped'; return status === 'expected' || status === 'flaky' || status === 'skipped';
} }
_clone(): Test { _clone(): Test {
const test = new Test(this.title, this.fn, this._ordinalInFile, this._testType); const test = new Test(this.title, this.fn, this._ordinalInFile, this._testType, this.location);
test._only = this._only; test._only = this._only;
test.location = this.location;
test._requireFile = this._requireFile; test._requireFile = this._requireFile;
return test; return test;
} }
@ -175,6 +176,7 @@ export class Test extends Base implements reporterTypes.Test {
retry: this.results.length, retry: this.results.length,
workerIndex: 0, workerIndex: 0,
duration: 0, duration: 0,
startTime: new Date(),
stdout: [], stdout: [],
stderr: [], stderr: [],
attachments: [], attachments: [],

View file

@ -62,9 +62,8 @@ export class TestTypeImpl {
const ordinalInFile = countByFile.get(suite._requireFile) || 0; const ordinalInFile = countByFile.get(suite._requireFile) || 0;
countByFile.set(suite._requireFile, ordinalInFile + 1); countByFile.set(suite._requireFile, ordinalInFile + 1);
const test = new Test(title, fn, ordinalInFile, this); const test = new Test(title, fn, ordinalInFile, this, location);
test._requireFile = suite._requireFile; test._requireFile = suite._requireFile;
test.location = location;
suite._addTest(test); suite._addTest(test);
if (type === 'only') if (type === 'only')

View file

@ -199,6 +199,7 @@ export class WorkerRunner extends EventEmitter {
return; return;
const startTime = monotonicTime(); const startTime = monotonicTime();
const startWallTime = Date.now();
let deadlineRunner: DeadlineRunner<any> | undefined; let deadlineRunner: DeadlineRunner<any> | undefined;
const testId = test._id; const testId = test._id;
@ -293,7 +294,7 @@ export class WorkerRunner extends EventEmitter {
return testInfo.timeout ? startTime + testInfo.timeout : undefined; return testInfo.timeout ? startTime + testInfo.timeout : undefined;
}; };
this.emit('testBegin', buildTestBeginPayload(testId, testInfo)); this.emit('testBegin', buildTestBeginPayload(testId, testInfo, startWallTime));
if (testInfo.expectedStatus === 'skipped') { if (testInfo.expectedStatus === 'skipped') {
testInfo.status = 'skipped'; testInfo.status = 'skipped';
@ -461,10 +462,11 @@ export class WorkerRunner extends EventEmitter {
} }
} }
function buildTestBeginPayload(testId: string, testInfo: TestInfo): TestBeginPayload { function buildTestBeginPayload(testId: string, testInfo: TestInfo, startWallTime: number): TestBeginPayload {
return { return {
testId, testId,
workerIndex: testInfo.workerIndex workerIndex: testInfo.workerIndex,
startWallTime,
}; };
} }

View file

@ -35,8 +35,10 @@ test('should work with custom reporter', async ({ runInlineTest }) => {
onStdErr() { onStdErr() {
console.log('\\n%%reporter-stderr%%'); console.log('\\n%%reporter-stderr%%');
} }
onTestEnd(test) { onTestEnd(test, result) {
console.log('\\n%%reporter-testend-' + test.title + '-' + test.titlePath()[1] + '%%'); console.log('\\n%%reporter-testend-' + test.title + '-' + test.titlePath()[1] + '%%');
if (!result.startTime)
console.log('\\n%%error-no-start-time');
} }
onTimeout() { onTimeout() {
console.log('\\n%%reporter-timeout%%'); console.log('\\n%%reporter-timeout%%');

View file

@ -64,7 +64,7 @@ export interface Suite {
/** /**
* Location where the suite is defined. * Location where the suite is defined.
*/ */
location: Location; location?: Location;
/** /**
* Child suites. * Child suites.
@ -142,9 +142,11 @@ export interface Test {
results: TestResult[]; results: TestResult[];
/** /**
* Overall test status. * Testing outcome for this test. Note that outcome does not directly match to the status:
* - Test that is expected to fail and actually fails is 'expected'.
* - Test that passes on a second retry is 'flaky'.
*/ */
status(): 'skipped' | 'expected' | 'unexpected' | 'flaky'; outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky';
/** /**
* Whether the test is considered running fine. * Whether the test is considered running fine.
@ -165,7 +167,12 @@ export interface TestResult {
/** /**
* Index of the worker where the test was run. * Index of the worker where the test was run.
*/ */
workerIndex: number, workerIndex: number;
/**
* Test run start time.
*/
startTime: Date;
/** /**
* Running time in milliseconds. * Running time in milliseconds.