test: make fit run single test (#3394)

This commit is contained in:
Pavel Feldman 2020-08-11 19:44:13 -07:00 committed by GitHub
parent da95b73b59
commit 4061bc696a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 137 additions and 78 deletions

View File

@ -29,7 +29,7 @@ process.env.JEST_WORKER_ID = 1;
const fixturePool = new FixturePool(); const fixturePool = new FixturePool();
let revertBabelRequire; let revertBabelRequire;
function fixturesUI(suite) { function fixturesUI(trialRun, suite) {
const suites = [suite]; const suites = [suite];
suite.on(Suite.constants.EVENT_FILE_PRE_REQUIRE, function(context, file, mocha) { suite.on(Suite.constants.EVENT_FILE_PRE_REQUIRE, function(context, file, mocha) {
@ -37,8 +37,15 @@ function fixturesUI(suite) {
context.beforeEach = common.beforeEach; context.beforeEach = common.beforeEach;
context.afterEach = common.afterEach; context.afterEach = common.afterEach;
fixturePool.patchToEnableFixtures(context, 'beforeEach'); if (trialRun) {
fixturePool.patchToEnableFixtures(context, 'afterEach'); context.beforeEach = () => {};
context.afterEach = () => {};
} else {
context.beforeEach = common.beforeEach;
context.afterEach = common.afterEach;
fixturePool.patchToEnableFixtures(context, 'beforeEach');
fixturePool.patchToEnableFixtures(context, 'afterEach');
}
context.run = mocha.options.delay && common.runWithSuite(suite); context.run = mocha.options.delay && common.runWithSuite(suite);
@ -74,14 +81,22 @@ function fixturesUI(suite) {
context.it = context.specify = function(title, fn) { context.it = context.specify = function(title, fn) {
const suite = suites[0]; const suite = suites[0];
if (suite.isPending()) { if (suite.isPending())
fn = null; fn = null;
let wrapper = fn;
if (trialRun) {
if (wrapper)
wrapper = () => {};
} else {
const wrapped = fixturePool.wrapTestCallback(wrapper);
wrapper = wrapped ? (done, ...args) => {
wrapped(...args).then(done).catch(done);
} : undefined;
}
if (wrapper) {
wrapper.toString = () => fn.toString();
wrapper.__original = fn;
} }
const wrapped = fixturePool.wrapTestCallback(fn);
const wrapper = wrapped ? (done, ...args) => {
wrapped(...args).then(done).catch(done);
} : undefined;
const test = new Test(title, wrapper); const test = new Test(title, wrapper);
test.file = file; test.file = file;
suite.addTest(test); suite.addTest(test);

View File

@ -14,42 +14,73 @@
* limitations under the License. * limitations under the License.
*/ */
const builtinReporters = require('mocha/lib/reporters');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const program = require('commander'); const program = require('commander');
const { Runner } = require('./runner'); const { Runner } = require('./runner');
const DotRunner = require('./dotReporter'); const Mocha = require('mocha');
const { fixturesUI } = require('./fixturesUI');
class NullReporter {}
program program
.version('Version ' + require('../../package.json').version) .version('Version ' + require('../../package.json').version)
.option('--timeout <timeout>', 'timeout', 10000)
.option('--reporter <reporter>', 'reporter to use', '') .option('--reporter <reporter>', 'reporter to use', '')
.option('--max-workers <maxWorkers>', 'reporter to use', '') .option('--max-workers <maxWorkers>', 'max workers to use', Math.ceil(require('os').cpus().length / 2))
.action(async (command, args) => { .option('--retries <retries>', 'number of times to retry a failing test', 1)
const testDir = path.join(process.cwd(), 'test'); .action(async (command) => {
const files = []; // Collect files
for (const name of fs.readdirSync(testDir)) { const files = collectFiles(command.args);
if (!name.includes('.spec.')) const rootSuite = new Mocha.Suite('', new Mocha.Context(), true);
continue;
if (!command.args.length) { // Build the test model, suite per file.
files.push(path.join(testDir, name)); for (const file of files) {
continue; const mocha = new Mocha({
} ui: fixturesUI.bind(null, true),
for (const filter of command.args) { retries: command.retries,
if (name.includes(filter)) { timeout: command.timeout,
files.push(path.join(testDir, name)); reporter: NullReporter
break; });
} mocha.addFile(file);
} mocha.suite.title = path.basename(file);
mocha.suite.root = false;
rootSuite.suites.push(mocha.suite);
await new Promise(f => mocha.run(f));
} }
const runner = new Runner({ if (rootSuite.hasOnly())
reporter: command.reporter ? builtinReporters[command.reporter] : DotRunner, rootSuite.filterOnly();
maxWorkers: command.maxWorkers || Math.ceil(require('os').cpus().length / 2)
console.log(`Running ${rootSuite.total()} tests`);
const runner = new Runner(rootSuite, {
maxWorkers: command.maxWorkers,
reporter: command.reporter,
retries: command.retries,
timeout: command.timeout,
}); });
await runner.run(files); await runner.run(files);
await runner.stop(); await runner.stop();
}); });
program.parse(process.argv); program.parse(process.argv);
function collectFiles(args) {
const testDir = path.join(process.cwd(), 'test');
const files = [];
for (const name of fs.readdirSync(testDir)) {
if (!name.includes('.spec.'))
continue;
if (!args.length) {
files.push(path.join(testDir, name));
continue;
}
for (const filter of args) {
if (name.includes(filter)) {
files.push(path.join(testDir, name));
break;
}
}
}
return files;
}

View File

@ -18,13 +18,16 @@ const child_process = require('child_process');
const path = require('path'); const path = require('path');
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const Mocha = require('mocha'); const Mocha = require('mocha');
const { Serializer } = require('v8'); const builtinReporters = require('mocha/lib/reporters');
const DotRunner = require('./dotReporter');
const constants = Mocha.Runner.constants; const constants = Mocha.Runner.constants;
class Runner extends EventEmitter { class Runner extends EventEmitter {
constructor(options) { constructor(suite, options) {
super(); super();
this._suite = suite;
this._options = options;
this._maxWorkers = options.maxWorkers; this._maxWorkers = options.maxWorkers;
this._workers = new Set(); this._workers = new Set();
this._freeWorkers = []; this._freeWorkers = [];
@ -37,15 +40,32 @@ class Runner extends EventEmitter {
pending: 0, pending: 0,
tests: 0, tests: 0,
}; };
this._reporter = new options.reporter(this, {}); const reporterFactory = builtinReporters[options.reporter] || DotRunner;
this._reporter = new reporterFactory(this, {});
this._tests = new Map();
this._files = new Map();
this._traverse(suite);
} }
async run(files) { _traverse(suite) {
for (const child of suite.suites)
this._traverse(child);
for (const test of suite.tests) {
if (!this._files.has(test.file))
this._files.set(test.file, 0);
const counter = this._files.get(test.file);
this._files.set(test.file, counter + 1);
this._tests.set(`${test.file}::${counter}`, test);
}
}
async run() {
this.emit(constants.EVENT_RUN_BEGIN, {}); this.emit(constants.EVENT_RUN_BEGIN, {});
const result = new Promise(f => this._runCallback = f); const result = new Promise(f => this._runCallback = f);
for (const file of files) { for (const file of this._files.keys()) {
const worker = await this._obtainWorker(); const worker = await this._obtainWorker();
worker.send({ method: 'run', params: file }); worker.send({ method: 'run', params: { file, options: this._options } });
} }
await result; await result;
this.emit(constants.EVENT_RUN_END, {}); this.emit(constants.EVENT_RUN_END, {});
@ -61,16 +81,16 @@ class Runner extends EventEmitter {
}); });
let readyCallback; let readyCallback;
const result = new Promise(f => readyCallback = f); const result = new Promise(f => readyCallback = f);
worker.send({ method: 'init', params: ++this._workerId }); worker.send({ method: 'init', params: { workerId: ++this._workerId } });
worker.on('message', message => { worker.on('message', message => {
if (message.method === 'ready') if (message.method === 'ready')
readyCallback(); readyCallback();
this._messageFromWorker(worker, message); this._messageFromWorker(worker, message);
}); });
worker.on('exit', () => { worker.on('exit', () => {
this._workers.delete(worker); this._workers.delete(worker);
if (!this._workers.size) if (!this._workers.size)
this._runCallback(); this._stopCallback();
}); });
this._workers.add(worker); this._workers.add(worker);
await result; await result;
@ -89,25 +109,24 @@ class Runner extends EventEmitter {
callback(worker); callback(worker);
} else { } else {
this._freeWorkers.push(worker); this._freeWorkers.push(worker);
if (this._freeWorkers.length === this._workers.size) { if (this._freeWorkers.length === this._workers.size)
this._runCallback(); this._runCallback();
}
} }
break; break;
} }
case 'start': case 'start':
break; break;
case 'test': case 'test':
this.emit(constants.EVENT_TEST_BEGIN, this._parse(params.test)); this.emit(constants.EVENT_TEST_BEGIN, this._updateTest(params.test));
break; break;
case 'pending': case 'pending':
this.emit(constants.EVENT_TEST_PENDING, this._parse(params.test)); this.emit(constants.EVENT_TEST_PENDING, this._updateTest(params.test));
break; break;
case 'pass': case 'pass':
this.emit(constants.EVENT_TEST_PASS, this._parse(params.test)); this.emit(constants.EVENT_TEST_PASS, this._updateTest(params.test));
break; break;
case 'fail': case 'fail':
const test = this._parse(params.test); const test = this._updateTest(params.test);
this.emit(constants.EVENT_TEST_FAIL, test, params.error); this.emit(constants.EVENT_TEST_FAIL, test, params.error);
break; break;
case 'end': case 'end':
@ -120,19 +139,11 @@ class Runner extends EventEmitter {
} }
} }
_parse(serialized) { _updateTest(serialized) {
return { const test = this._tests.get(serialized.id);
...serialized, test._currentRetry = serialized.currentRetry;
currentRetry: () => serialized.currentRetry, this.duration = serialized.duration;
fullTitle: () => serialized.fullTitle, return test;
slow: () => serialized.slow,
timeout: () => serialized.timeout,
titlePath: () => serialized.titlePath,
isPending: () => serialized.isPending,
parent: {
fullTitle: () => ''
}
};
} }
async stop() { async stop() {

View File

@ -16,7 +16,7 @@
const path = require('path'); const path = require('path');
const Mocha = require('mocha'); const Mocha = require('mocha');
const { fixturesUI } = require('./fixturesUI'); const { fixturesUI, fixturePool } = require('./fixturesUI');
const { gracefullyCloseAll } = require('../../lib/server/processLauncher'); const { gracefullyCloseAll } = require('../../lib/server/processLauncher');
const GoldenUtils = require('../../utils/testrunner/GoldenUtils'); const GoldenUtils = require('../../utils/testrunner/GoldenUtils');
@ -32,11 +32,11 @@ let closed = false;
process.on('message', async message => { process.on('message', async message => {
if (message.method === 'init') if (message.method === 'init')
process.env.JEST_WORKER_ID = message.params; process.env.JEST_WORKER_ID = message.params.workerId;
if (message.method === 'stop') if (message.method === 'stop')
gracefullyCloseAndExit(); await gracefullyCloseAndExit();
if (message.method === 'run') if (message.method === 'run')
await runSingleTest(message.params); await runSingleTest(message.params.file, message.params.options);
}); });
process.on('disconnect', gracefullyCloseAndExit); process.on('disconnect', gracefullyCloseAndExit);
@ -55,10 +55,12 @@ async function gracefullyCloseAndExit() {
class NullReporter {} class NullReporter {}
async function runSingleTest(file) { async function runSingleTest(file, options) {
let nextOrdinal = 0;
const mocha = new Mocha({ const mocha = new Mocha({
ui: fixturesUI, ui: fixturesUI.bind(null, false),
timeout: 10000, retries: options.retries === 1 ? undefined : options.retries,
timeout: options.timeout,
reporter: NullReporter reporter: NullReporter
}); });
mocha.addFile(file); mocha.addFile(file);
@ -71,22 +73,27 @@ async function runSingleTest(file) {
}); });
runner.on(constants.EVENT_TEST_BEGIN, test => { runner.on(constants.EVENT_TEST_BEGIN, test => {
sendMessageToParent('test', { test: sanitizeTest(test) }); // Retries will produce new test instances, store ordinal on the original function.
let ordinal = nextOrdinal++;
if (typeof test.fn.__original.__ordinal !== 'number')
test.fn.__original.__ordinal = ordinal;
sendMessageToParent('test', { test: serializeTest(test, ordinal) });
}); });
runner.on(constants.EVENT_TEST_PENDING, test => { runner.on(constants.EVENT_TEST_PENDING, test => {
sendMessageToParent('pending', { test: sanitizeTest(test) }); // Pending does not get test begin signal, so increment ordinal.
sendMessageToParent('pending', { test: serializeTest(test, nextOrdinal++) });
}); });
runner.on(constants.EVENT_TEST_PASS, test => { runner.on(constants.EVENT_TEST_PASS, test => {
sendMessageToParent('pass', { test: sanitizeTest(test) }); sendMessageToParent('pass', { test: serializeTest(test, test.fn.__original.__ordinal) });
}); });
runner.on(constants.EVENT_TEST_FAIL, (test, error) => { runner.on(constants.EVENT_TEST_FAIL, (test, error) => {
sendMessageToParent('fail', { sendMessageToParent('fail', {
test: sanitizeTest(test), test: serializeTest(test, test.fn.__original.__ordinal),
error: serializeError(error), error: serializeError(error),
}); });
}); });
runner.once(constants.EVENT_RUN_END, async () => { runner.once(constants.EVENT_RUN_END, async () => {
@ -105,17 +112,12 @@ function sendMessageToParent(method, params = {}) {
} }
} }
function sanitizeTest(test) { function serializeTest(test, origin) {
return { return {
id: `${test.file}::${origin}`,
currentRetry: test.currentRetry(), currentRetry: test.currentRetry(),
duration: test.duration, duration: test.duration,
file: test.file,
fullTitle: test.fullTitle(),
isPending: test.isPending(),
slow: test.slow(),
timeout: test.timeout(),
title: test.title, title: test.title,
titlePath: test.titlePath(),
}; };
} }