fix(testrunner): report suite-level errors (#3661)

This commit is contained in:
Pavel Feldman 2020-08-27 11:41:53 -07:00 committed by GitHub
parent 2b7d79d7fa
commit 6a0f587fae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 73 additions and 37 deletions

View File

@ -43,7 +43,7 @@ export class Runner {
this._suite = suite; this._suite = suite;
for (const suite of this._suite.suites) { for (const suite of this._suite.suites) {
suite.findTest(test => { suite.findTest(test => {
this._testById.set(`${test._ordinal}@${suite.file}::[${suite._configurationString}]`, { test, result: test._appendResult() }); this._testById.set(test._id, { test, result: test._appendResult() });
}); });
} }
@ -58,12 +58,12 @@ export class Runner {
_filesSortedByWorkerHash(): TestRunnerEntry[] { _filesSortedByWorkerHash(): TestRunnerEntry[] {
const result: TestRunnerEntry[] = []; const result: TestRunnerEntry[] = [];
for (const suite of this._suite.suites) { for (const suite of this._suite.suites) {
const ordinals: number[] = []; const ids: string[] = [];
suite.findTest(test => ordinals.push(test._ordinal) && false); suite.findTest(test => ids.push(test._id) && false);
if (!ordinals.length) if (!ids.length)
continue; continue;
result.push({ result.push({
ordinals, ids,
file: suite.file, file: suite.file,
configuration: suite.configuration, configuration: suite.configuration,
configurationString: suite._configurationString, configurationString: suite._configurationString,
@ -98,7 +98,7 @@ export class Runner {
await Promise.all(jobs); await Promise.all(jobs);
} }
async _runJob(worker, entry) { async _runJob(worker: Worker, entry: TestRunnerEntry) {
worker.run(entry); worker.run(entry);
let doneCallback; let doneCallback;
const result = new Promise(f => doneCallback = f); const result = new Promise(f => doneCallback = f);
@ -112,8 +112,16 @@ export class Runner {
// When worker encounters error, we will restart it. // When worker encounters error, we will restart it.
this._restartWorker(worker); this._restartWorker(worker);
// In case of fatal error, we are done with the entry. // In case of fatal error without test id, we are done with the entry.
if (params.fatalError) { if (params.fatalError && !params.failedTestId) {
// Report all the tests are failing with this error.
for (const id of entry.ids) {
const { test, result } = this._testById.get(id);
this._reporter.onTestBegin(test);
result.status = 'failed';
result.error = params.fatalError;
this._reporter.onTestEnd(test, result);
}
doneCallback(); doneCallback();
return; return;
} }
@ -123,11 +131,11 @@ export class Runner {
const pair = this._testById.get(params.failedTestId); const pair = this._testById.get(params.failedTestId);
if (pair.test.results.length < this._config.retries + 1) { if (pair.test.results.length < this._config.retries + 1) {
pair.result = pair.test._appendResult(); pair.result = pair.test._appendResult();
remaining.unshift(pair.test._ordinal); remaining.unshift(pair.test._id);
} }
} }
if (remaining.length) if (remaining.length)
this._queue.unshift({ ...entry, ordinals: remaining }); this._queue.unshift({ ...entry, ids: remaining });
// This job is over, we just scheduled another one. // This job is over, we just scheduled another one.
doneCallback(); doneCallback();

View File

@ -27,7 +27,7 @@ export class Test {
fn: Function; fn: Function;
results: TestResult[] = []; results: TestResult[] = [];
_ordinal: number; _id: string;
_overriddenFn: Function; _overriddenFn: Function;
_startTime: number; _startTime: number;
@ -157,7 +157,7 @@ export class Suite {
let ordinal = 0; let ordinal = 0;
this.findTest((test: Test) => { this.findTest((test: Test) => {
// All tests are identified with their ordinals. // All tests are identified with their ordinals.
test._ordinal = ordinal++; test._id = `${ordinal++}@${this.file}::[${this._configurationString}]`;
}); });
} }

View File

@ -57,7 +57,6 @@ export class TestCollector {
const revertBabelRequire = spec(suite, file, this._config.timeout); const revertBabelRequire = spec(suite, file, this._config.timeout);
require(file); require(file);
revertBabelRequire(); revertBabelRequire();
suite._renumber();
const workerGeneratorConfigurations = new Map(); const workerGeneratorConfigurations = new Map();
@ -104,6 +103,7 @@ export class TestCollector {
clone.title = path.basename(file) + (hash.length ? `::[${hash}]` : '') + ' ' + (i ? ` #repeat-${i}#` : ''); clone.title = path.basename(file) + (hash.length ? `::[${hash}]` : '') + ' ' + (i ? ` #repeat-${i}#` : '');
clone.configuration = configuration; clone.configuration = configuration;
clone._configurationString = configurationString + `#repeat-${i}#`; clone._configurationString = configurationString + `#repeat-${i}#`;
clone._renumber();
} }
} }
} }
@ -122,7 +122,6 @@ export class TestCollector {
continue; continue;
const testCopy = test._clone(); const testCopy = test._clone();
testCopy.only = test.only; testCopy.only = test.only;
testCopy._ordinal = test._ordinal;
copy._addTest(testCopy); copy._addTest(testCopy);
} }
} }

View File

@ -26,7 +26,7 @@ export const fixturePool = new FixturePool();
export type TestRunnerEntry = { export type TestRunnerEntry = {
file: string; file: string;
ordinals: number[]; ids: string[];
configurationString: string; configurationString: string;
configuration: Configuration; configuration: Configuration;
hash: string; hash: string;
@ -43,9 +43,8 @@ function chunkToParams(chunk: Buffer | string): { text?: string, buffer?: strin
export class TestRunner extends EventEmitter { export class TestRunner extends EventEmitter {
private _failedTestId: string | undefined; private _failedTestId: string | undefined;
private _fatalError: any | undefined; private _fatalError: any | undefined;
private _file: any; private _ids: Set<string>;
private _ordinals: Set<number>; private _remaining: Set<string>;
private _remaining: Set<number>;
private _trialRun: any; private _trialRun: any;
private _configuredFile: any; private _configuredFile: any;
private _parsedGeneratorConfiguration: any = {}; private _parsedGeneratorConfiguration: any = {};
@ -55,12 +54,15 @@ export class TestRunner extends EventEmitter {
private _stdOutBuffer: (string | Buffer)[] = []; private _stdOutBuffer: (string | Buffer)[] = [];
private _stdErrBuffer: (string | Buffer)[] = []; private _stdErrBuffer: (string | Buffer)[] = [];
private _testResult: TestResult | null = null; private _testResult: TestResult | null = null;
private _suite: Suite;
constructor(entry: TestRunnerEntry, config: RunnerConfig, workerId: number) { constructor(entry: TestRunnerEntry, config: RunnerConfig, workerId: number) {
super(); super();
this._file = entry.file; this._suite = new Suite('');
this._ordinals = new Set(entry.ordinals); this._suite.file = entry.file;
this._remaining = new Set(entry.ordinals); this._suite._configurationString = entry.configurationString;
this._ids = new Set(entry.ids);
this._remaining = new Set(entry.ids);
this._trialRun = config.trialRun; this._trialRun = config.trialRun;
this._timeout = config.timeout; this._timeout = config.timeout;
this._config = config; this._config = config;
@ -68,7 +70,7 @@ export class TestRunner extends EventEmitter {
for (const {name, value} of entry.configuration) for (const {name, value} of entry.configuration)
this._parsedGeneratorConfiguration[name] = value; this._parsedGeneratorConfiguration[name] = value;
this._parsedGeneratorConfiguration['parallelIndex'] = workerId; this._parsedGeneratorConfiguration['parallelIndex'] = workerId;
setCurrentTestFile(this._file); setCurrentTestFile(this._suite.file);
} }
stop() { stop() {
@ -108,14 +110,13 @@ export class TestRunner extends EventEmitter {
async run() { async run() {
setParameters(this._parsedGeneratorConfiguration); setParameters(this._parsedGeneratorConfiguration);
const suite = new Suite(''); const revertBabelRequire = spec(this._suite, this._suite.file, this._timeout);
const revertBabelRequire = spec(suite, this._file, this._timeout); require(this._suite.file);
require(this._file);
revertBabelRequire(); revertBabelRequire();
suite._renumber(); this._suite._renumber();
rerunRegistrations(this._file, 'test'); rerunRegistrations(this._suite.file, 'test');
await this._runSuite(suite); await this._runSuite(this._suite);
this._reportDone(); this._reportDone();
} }
@ -144,11 +145,11 @@ export class TestRunner extends EventEmitter {
private async _runTest(test: Test) { private async _runTest(test: Test) {
if (this._failedTestId) if (this._failedTestId)
return false; return false;
if (this._ordinals.size && !this._ordinals.has(test._ordinal)) if (this._ids.size && !this._ids.has(test._id))
return; return;
this._remaining.delete(test._ordinal); this._remaining.delete(test._id);
const id = `${test._ordinal}@${this._configuredFile}`; const id = test._id;
this._testId = id; this._testId = id;
this.emit('testBegin', { id }); this.emit('testBegin', { id });

View File

@ -29,13 +29,13 @@ global.console = new Console({
}); });
process.stdout.write = chunk => { process.stdout.write = chunk => {
if (testRunner && !closed) if (testRunner)
testRunner.stdout(chunk); testRunner.stdout(chunk);
return true; return true;
}; };
process.stderr.write = chunk => { process.stderr.write = chunk => {
if (testRunner && !closed) if (testRunner)
testRunner.stderr(chunk); testRunner.stderr(chunk);
return true; return true;
}; };
@ -48,12 +48,12 @@ let workerId: number;
let testRunner: TestRunner; let testRunner: TestRunner;
process.on('unhandledRejection', (reason, promise) => { process.on('unhandledRejection', (reason, promise) => {
if (testRunner && !closed) if (testRunner)
testRunner.fatalError(reason); testRunner.fatalError(reason);
}); });
process.on('uncaughtException', error => { process.on('uncaughtException', error => {
if (testRunner && !closed) if (testRunner)
testRunner.fatalError(error); testRunner.fatalError(error);
}); });
@ -73,8 +73,6 @@ process.on('message', async message => {
testRunner.on(event, sendMessageToParent.bind(null, event)); testRunner.on(event, sendMessageToParent.bind(null, event));
await testRunner.run(); await testRunner.run();
testRunner = null; testRunner = null;
// Mocha runner adds these; if we don't remove them, we'll get a leak.
process.removeAllListeners('uncaughtException');
} }
}); });

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { parameters } = require('../../');
if (typeof parameters.parallelIndex === 'number')
throw new Error('Suite error');
it('passes',() => {
expect(1 + 1).toBe(2);
});

View File

@ -93,6 +93,13 @@ it('should repeat each', async () => {
expect(suite.tests.length).toBe(1); expect(suite.tests.length).toBe(1);
}); });
it('should report suite errors', async () => {
const { exitCode, failed, output } = await runTest('suite-error.js');
expect(exitCode).toBe(1);
expect(failed).toBe(1);
expect(output).toContain('Suite error');
});
async function runTest(filePath: string, params: any = {}) { async function runTest(filePath: string, params: any = {}) {
const outputDir = path.join(__dirname, 'test-results'); const outputDir = path.join(__dirname, 'test-results');
const reportFile = path.join(outputDir, 'results.json'); const reportFile = path.join(outputDir, 'results.json');