test: remove mocha dependency (#3576)

This commit is contained in:
Pavel Feldman 2020-08-22 00:05:24 -07:00 committed by GitHub
parent 93d8839947
commit b909924a61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 213 additions and 310 deletions

View File

@ -14,14 +14,11 @@
* limitations under the License.
*/
import Mocha from 'mocha';
import { Test, Suite } from './test';
import { installTransform } from './transform';
Error.stackTraceLimit = 15;
let revertBabelRequire: () => void;
function specBuilder(modifiers, specCallback) {
function builder(specs, last) {
const callable = (...args) => {
@ -52,65 +49,54 @@ function specBuilder(modifiers, specCallback) {
return builder({}, null);
}
export function fixturesUI(options, mochaSuite: any) {
const suites = [mochaSuite.__nomocha as Suite];
export function fixturesUI(suite: Suite, file: string, timeout: number): () => void {
const suites = [suite];
mochaSuite.on(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, function(context, file) {
const it = specBuilder(['skip', 'fail', 'slow', 'only'], (specs, title, fn) => {
const suite = suites[0];
const test = new Test(title, fn);
test.file = file;
test.slow = specs.slow && specs.slow[0];
test.timeout = options.timeout;
const it = specBuilder(['skip', 'fail', 'slow', 'only'], (specs, title, fn) => {
const suite = suites[0];
const test = new Test(title, fn);
test.file = file;
test.slow = specs.slow && specs.slow[0];
test.timeout = timeout;
const only = specs.only && specs.only[0];
if (only)
test.only = true;
if (!only && specs.skip && specs.skip[0])
test.pending = true;
if (!only && specs.fail && specs.fail[0])
test.pending = true;
test.pending = test.pending || suite.isPending();
if (test.pending)
fn = null;
const wrapper = fn ? options.testWrapper(test, fn) : undefined;
if (wrapper)
wrapper.toString = () => fn.toString();
test._materialize(wrapper);
suite.addTest(test);
return test;
});
const describe = specBuilder(['skip', 'fail', 'only'], (specs, title, fn) => {
const child = new Suite(title, suites[0]);
suites[0].addSuite(child);
child.file = file;
const only = specs.only && specs.only[0];
if (only)
child.only = true;
if (!only && specs.skip && specs.skip[0])
child.pending = true;
if (!only && specs.fail && specs.fail[0])
child.pending = true;
suites.unshift(child);
fn();
suites.shift();
});
context.beforeEach = fn => options.hookWrapper(mochaSuite.beforeEach.bind(mochaSuite), fn);
context.afterEach = fn => options.hookWrapper(mochaSuite.afterEach.bind(mochaSuite), fn);
context.describe = describe;
(context as any).fdescribe = describe.only(true);
context.xdescribe = describe.skip(true);
context.it = it;
(context as any).fit = it.only(true);
context.xit = it.skip(true);
revertBabelRequire = installTransform();
const only = specs.only && specs.only[0];
if (only)
test.only = true;
if (!only && specs.skip && specs.skip[0])
test.pending = true;
if (!only && specs.fail && specs.fail[0])
test.pending = true;
test.pending = test.pending || suite.isPending();
suite.addTest(test);
return test;
});
mochaSuite.on(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, function(context, file, mocha) {
revertBabelRequire();
const describe = specBuilder(['skip', 'fail', 'only'], (specs, title, fn) => {
const child = new Suite(title, suites[0]);
suites[0].addSuite(child);
child.file = file;
const only = specs.only && specs.only[0];
if (only)
child.only = true;
if (!only && specs.skip && specs.skip[0])
child.pending = true;
if (!only && specs.fail && specs.fail[0])
child.pending = true;
suites.unshift(child);
fn();
suites.shift();
});
};
(global as any).beforeEach = fn => suite._addHook('beforeEach', fn);
(global as any).afterEach = fn => suite._addHook('afterEach', fn);
(global as any).beforeAll = fn => suite._addHook('beforeAll', fn);
(global as any).afterAll = fn => suite._addHook('afterAll', fn);
(global as any).describe = describe;
(global as any).fdescribe = describe.only(true);
(global as any).xdescribe = describe.skip(true);
(global as any).it = it;
(global as any).fit = it.only(true);
(global as any).xit = it.skip(true);
return installTransform();
}

View File

@ -18,15 +18,10 @@ const child_process = require('child_process');
const crypto = require('crypto');
const path = require('path');
const { EventEmitter } = require('events');
const Mocha = require('mocha');
const builtinReporters = require('mocha/lib/reporters');
const DotRunner = require('./dotReporter');
const { lookupRegistrations } = require('./fixtures');
const constants = Mocha.Runner.constants;
// Mocha runner does not remove uncaughtException listeners.
process.setMaxListeners(0);
class Runner extends EventEmitter {
constructor(suite, total, options) {
super();
@ -81,12 +76,12 @@ class Runner extends EventEmitter {
}
async run() {
this.emit(constants.EVENT_RUN_BEGIN, {});
this.emit('start', {});
this._queue = this._filesSortedByWorkerHash();
// Loop in case job schedules more jobs
while (this._queue.length)
await this._dispatchQueue();
this.emit(constants.EVENT_RUN_END, {});
this.emit('end', {});
}
async _dispatchQueue() {
@ -109,16 +104,11 @@ class Runner extends EventEmitter {
let doneCallback;
const result = new Promise(f => doneCallback = f);
worker.once('done', params => {
this.stats.duration += params.stats.duration;
this.stats.failures += params.stats.failures;
this.stats.passes += params.stats.passes;
this.stats.pending += params.stats.pending;
this.stats.tests += params.stats.passes + params.stats.pending + params.stats.failures;
// When worker encounters error, we will restart it.
if (params.error) {
if (params.error || params.fatalError) {
this._restartWorker(worker);
// If there are remaining tests, we will queue them.
if (params.remaining.length)
if (params.remaining.length && !params.fatalError)
this._queue.unshift({ ...entry, ordinals: params.remaining });
} else {
this._workerAvailable(worker);
@ -150,17 +140,28 @@ class Runner extends EventEmitter {
_createWorker() {
const worker = this._options.debug ? new InProcessWorker(this) : new OopWorker(this);
worker.on('test', params => this.emit(constants.EVENT_TEST_BEGIN, this._updateTest(params.test)));
worker.on('pending', params => this.emit(constants.EVENT_TEST_PENDING, this._updateTest(params.test)));
worker.on('pass', params => this.emit(constants.EVENT_TEST_PASS, this._updateTest(params.test)));
worker.on('test', params => {
++this.stats.tests;
this.emit('test', this._updateTest(params.test));
});
worker.on('pending', params => {
++this.stats.tests;
++this.stats.pending;
this.emit('pending', this._updateTest(params.test));
});
worker.on('pass', params => {
++this.stats.passes;
this.emit('pass', this._updateTest(params.test));
});
worker.on('fail', params => {
++this.stats.failures;
const out = worker.takeOut();
if (out.length)
params.error.stack += '\n\x1b[33mstdout: ' + out.join('\n') + '\x1b[0m';
const err = worker.takeErr();
if (err.length)
params.error.stack += '\n\x1b[33mstderr: ' + err.join('\n') + '\x1b[0m';
this.emit(constants.EVENT_TEST_FAIL, this._updateTest(params.test), params.error);
this.emit('fail', this._updateTest(params.test), params.error);
});
worker.on('exit', () => {
this._workers.delete(worker);

View File

@ -14,10 +14,6 @@
* limitations under the License.
*/
import Mocha from 'mocha';
import { fixturesUI } from './fixturesUI';
import { EventEmitter } from 'events';
export type Configuration = { name: string, value: string }[];
export class Test {
@ -34,18 +30,13 @@ export class Test {
_configurationObject: Configuration;
_configurationString: string;
_overriddenFn: Function;
_impl: any;
_startTime: number;
constructor(title: string, fn: Function) {
this.title = title;
this.fn = fn;
}
_materialize(overriddenFn: Function) {
this._impl = new Mocha.Test(this.title, overriddenFn);
this._impl.pending = this.pending;
}
clone(): Test {
const test = new Test(this.title, this.fn);
test.suite = this.suite;
@ -54,7 +45,6 @@ export class Test {
test.pending = this.pending;
test.timeout = this.timeout;
test._overriddenFn = this._overriddenFn;
test._materialize(this._overriddenFn);
return test;
}
@ -80,13 +70,12 @@ export class Suite {
pending = false;
file: string;
_impl: any;
_hooks: { type: string, fn: Function } [] = [];
_entries: (Suite | Test)[] = [];
constructor(title: string, parent?: Suite) {
this.title = title;
this.parent = parent;
this._impl = new Mocha.Suite(title, new Mocha.Context());
this._impl.__nomocha = this;
}
titlePath(): string[] {
@ -97,7 +86,9 @@ export class Suite {
total(): number {
let count = 0;
this.eachTest(fn => ++count);
this.eachTest(fn => {
++count;
});
return count;
}
@ -108,20 +99,25 @@ export class Suite {
addTest(test: Test) {
test.suite = this;
this.tests.push(test);
this._impl.addTest(test._impl);
this._entries.push(test);
}
addSuite(suite: Suite) {
suite.parent = this;
this.suites.push(suite);
this._impl.addSuite(suite._impl);
this._entries.push(suite);
}
eachTest(fn: (test: Test) => void) {
for (const suite of this.suites)
suite.eachTest(fn);
for (const test of this.tests)
fn(test);
eachTest(fn: (test: Test) => boolean | void): boolean {
for (const suite of this.suites) {
if (suite.eachTest(fn))
return true;
}
for (const test of this.tests) {
if (fn(test))
return true;
}
return false;
}
clone(): Suite {
@ -129,84 +125,29 @@ export class Suite {
suite.only = this.only;
suite.file = this.file;
suite.pending = this.pending;
suite._impl = this._impl.clone();
return suite;
}
}
class NullReporter {
stats = {
suites: 0,
tests: 0,
passes: 0,
pending: 0,
failures: 0
};
runner = null;
failures = [];
epilogue: () => {};
}
type NoMockaOptions = {
forbidOnly?: boolean;
timeout: number;
testWrapper: (test: Test, fn: Function) => Function;
hookWrapper: (hook: any, fn: Function) => Function;
};
class PatchedMocha extends Mocha {
suite: any;
static pendingSuite: Suite;
constructor(suite, options) {
PatchedMocha.pendingSuite = suite;
super(options);
}
grep(...args) {
this.suite = new Mocha.Suite('', new Mocha.Context());
this.suite.__nomocha = PatchedMocha.pendingSuite;
PatchedMocha.pendingSuite._impl = this.suite;
return super.grep(...args);
}
}
export class Runner extends EventEmitter {
private _mochaRunner: any;
constructor(mochaRunner: any) {
super();
const constants = Mocha.Runner.constants;
this._mochaRunner = mochaRunner;
this._mochaRunner.on(constants.EVENT_TEST_BEGIN, test => this.emit('test', test));
this._mochaRunner.on(constants.EVENT_TEST_PENDING, test => this.emit('pending', test));
this._mochaRunner.on(constants.EVENT_TEST_PASS, test => this.emit('pass', test));
this._mochaRunner.on(constants.EVENT_TEST_FAIL, (test, err) => this.emit('fail', test, err));
this._mochaRunner.on(constants.EVENT_RUN_END, () => this.emit('done'));
}
duration(): number {
return this._mochaRunner.stats.duration || 0;
}
}
export class NoMocha {
suite: Suite;
private _mocha: Mocha;
constructor(file: string, options: NoMockaOptions) {
this.suite = new Suite('');
this._mocha = new PatchedMocha(this.suite, {
forbidOnly: options.forbidOnly,
reporter: NullReporter,
timeout: options.timeout,
ui: fixturesUI.bind(null, options)
_renumber() {
let ordinal = 0;
this.eachTest((test: Test) => {
// All tests are identified with their ordinals.
test._ordinal = ordinal++;
});
this._mocha.addFile(file);
(this._mocha as any).loadFiles();
}
run(cb: () => void): Runner {
return new Runner(this._mocha.run(cb));
_addHook(type: string, fn: any) {
this._hooks.push({ type, fn });
}
_hasTestsToRun(): boolean {
let found = false;
this.eachTest(test => {
if (!test.pending) {
found = true;
return true;
}
});
return found;
}
}

View File

@ -15,9 +15,9 @@
*/
import path from 'path';
import Mocha from 'mocha';
import { fixturesForCallback, registerWorkerFixture } from './fixtures';
import { Configuration, NoMocha, Test, Suite } from './test';
import { Configuration, Test, Suite } from './test';
import { fixturesUI } from './fixturesUI';
export class TestCollector {
suite: Suite;
@ -50,20 +50,15 @@ export class TestCollector {
}
_addFile(file) {
const noMocha = new NoMocha(file, {
forbidOnly: this._options.forbidOnly,
timeout: this._options.timeout,
testWrapper: (test, fn) => () => {},
hookWrapper: (hook, fn) => () => {},
});
const suite = new Suite('');
const revertBabelRequire = fixturesUI(suite, file, this._options.timeout);
require(file);
revertBabelRequire();
suite._renumber();
const workerGeneratorConfigurations = new Map();
let ordinal = 0;
noMocha.suite.eachTest((test: Test) => {
// All tests are identified with their ordinals.
test._ordinal = ordinal++;
suite.eachTest((test: Test) => {
// Get all the fixtures that the test needs.
const fixtures = fixturesForCallback(test.fn);
@ -102,7 +97,7 @@ export class TestCollector {
// Clone the suite as many times as there are worker hashes.
// Only include the tests that requested these generations.
for (const [hash, {configurationObject, configurationString, tests}] of workerGeneratorConfigurations.entries()) {
const clone = this._cloneSuite(noMocha.suite, configurationObject, configurationString, tests);
const clone = this._cloneSuite(suite, configurationObject, configurationString, tests);
this.suite.addSuite(clone);
clone.title = path.basename(file) + (hash.length ? `::[${hash}]` : '');
}
@ -111,19 +106,22 @@ export class TestCollector {
_cloneSuite(suite: Suite, configurationObject: Configuration, configurationString: string, tests: Set<Test>) {
const copy = suite.clone();
copy.only = suite.only;
for (const child of suite.suites)
copy.addSuite(this._cloneSuite(child, configurationObject, configurationString, tests));
for (const test of suite.tests) {
if (!tests.has(test))
continue;
if (this._grep && !this._grep.test(test.fullTitle()))
continue;
const testCopy = test.clone();
testCopy.only = test.only;
testCopy._ordinal = test._ordinal;
testCopy._configurationObject = configurationObject;
testCopy._configurationString = configurationString;
copy.addTest(testCopy);
for (const entry of suite._entries) {
if (entry instanceof Suite) {
copy.addSuite(this._cloneSuite(entry, configurationObject, configurationString, tests));
} else {
const test = entry;
if (!tests.has(test))
continue;
if (this._grep && !this._grep.test(test.fullTitle()))
continue;
const testCopy = test.clone();
testCopy.only = test.only;
testCopy._ordinal = test._ordinal;
testCopy._configurationObject = configurationObject;
testCopy._configurationString = configurationString;
copy.addTest(testCopy);
}
}
return copy;
}

View File

@ -18,7 +18,8 @@ import path from 'path';
import { FixturePool, registerWorkerFixture, rerunRegistrations, setParameters } from './fixtures';
import { EventEmitter } from 'events';
import { setCurrentTestFile } from './expect';
import { NoMocha, Runner, Test } from './test';
import { Test, Suite } from './test';
import { fixturesUI } from './fixturesUI';
export const fixturePool = new FixturePool();
@ -31,19 +32,15 @@ export type TestRunnerEntry = {
export class TestRunner extends EventEmitter {
private _currentOrdinal = -1;
private _failedWithError = false;
private _failedWithError: Error | undefined;
private _fatalError: Error | undefined;
private _file: any;
private _ordinals: Set<number>;
private _remaining: Set<number>;
private _trialRun: any;
private _passes = 0;
private _failures = 0;
private _pending = 0;
private _configuredFile: any;
private _configurationObject: any;
private _parsedGeneratorConfiguration: any = {};
private _relativeTestFile: string;
private _runner: Runner;
private _outDir: string;
private _timeout: number;
private _testDir: string;
@ -65,134 +62,114 @@ export class TestRunner extends EventEmitter {
registerWorkerFixture(name, async ({}, test) => await test(value));
}
this._parsedGeneratorConfiguration['parallelIndex'] = workerId;
this._relativeTestFile = path.relative(options.testDir, this._file);
setCurrentTestFile(path.relative(options.testDir, this._file));
}
async stop() {
stop() {
this._trialRun = true;
return new Promise(f => this._runner.once('done', f));
}
async run() {
let callback;
const result = new Promise(f => callback = f);
setParameters(this._parsedGeneratorConfiguration);
const noMocha = new NoMocha(this._file, {
timeout: 0,
testWrapper: (test, fn) => this._testWrapper(test, fn),
hookWrapper: (hook, fn) => this._hookWrapper(hook, fn),
});
const suite = new Suite('');
const revertBabelRequire = fixturesUI(suite, this._file, this._timeout);
require(this._file);
revertBabelRequire();
suite._renumber();
rerunRegistrations(this._file, 'test');
this._runner = noMocha.run(callback);
await this._runSuite(suite);
this._reportDone();
}
this._runner.on('test', test => {
setCurrentTestFile(this._relativeTestFile);
if (this._failedWithError)
return;
const ordinal = ++this._currentOrdinal;
if (this._ordinals.size && !this._ordinals.has(ordinal))
return;
this._remaining.delete(ordinal);
this.emit('test', { test: this._serializeTest(test, ordinal) });
});
private async _runSuite(suite: Suite) {
try {
await this._runHooks(suite, 'beforeAll', 'before');
} catch (e) {
this._fatalError = e;
this._reportDone();
}
for (const entry of suite._entries) {
if (entry instanceof Suite) {
await this._runSuite(entry);
} else {
await this._runTest(entry);
}
}
try {
await this._runHooks(suite, 'afterAll', 'after');
} catch (e) {
this._fatalError = e;
this._reportDone();
}
}
this._runner.on('pending', test => {
if (this._failedWithError)
return;
const ordinal = ++this._currentOrdinal;
if (this._ordinals.size && !this._ordinals.has(ordinal))
return;
this._remaining.delete(ordinal);
++this._pending;
this.emit('pending', { test: this._serializeTest(test, ordinal) });
});
private async _runTest(test: Test) {
if (this._failedWithError)
return false;
const ordinal = ++this._currentOrdinal;
if (this._ordinals.size && !this._ordinals.has(ordinal))
return;
this._remaining.delete(ordinal);
if (test.pending) {
this.emit('pending', { test: this._serializeTest(test) });
return;
}
this._runner.on('pass', test => {
if (this._failedWithError)
return;
const ordinal = this._currentOrdinal;
if (this._ordinals.size && !this._ordinals.has(ordinal))
return;
++this._passes;
this.emit('pass', { test: this._serializeTest(test, ordinal) });
});
this._runner.on('fail', (test, error) => {
if (this._failedWithError)
return;
++this._failures;
this.emit('test', { test: this._serializeTest(test) });
try {
await this._runHooks(test.suite, 'beforeEach', 'before');
test._startTime = Date.now();
if (!this._trialRun)
await this._testWrapper(test)();
this.emit('pass', { test: this._serializeTest(test) });
await this._runHooks(test.suite, 'afterEach', 'after');
} catch (error) {
this._failedWithError = error;
this.emit('fail', {
test: this._serializeTest(test, this._currentOrdinal),
test: this._serializeTest(test),
error: serializeError(error),
});
});
this._runner.once('done', async () => {
this.emit('done', {
stats: this._serializeStats(),
error: this._failedWithError,
remaining: [...this._remaining],
total: this._passes + this._failures + this._pending
});
});
await result;
}
_shouldRunTest(hook = false) {
if (this._trialRun || this._failedWithError)
return false;
if (hook) {
// Hook starts before we bump the test ordinal.
if (!this._ordinals.has(this._currentOrdinal + 1))
return false;
} else {
if (!this._ordinals.has(this._currentOrdinal))
return false;
}
return true;
}
_testWrapper(test: Test, fn: Function) {
private async _runHooks(suite: Suite, type: string, dir: 'before' | 'after') {
if (!suite._hasTestsToRun())
return;
const all = [];
for (let s = suite; s; s = s.parent) {
const funcs = s._hooks.filter(e => e.type === type).map(e => e.fn);
all.push(...funcs.reverse());
}
if (dir === 'before')
all.reverse();
for (const hook of all)
await fixturePool.resolveParametersAndRun(hook, 0);
}
private _reportDone() {
this.emit('done', {
error: this._failedWithError,
fatalError: this._fatalError,
remaining: [...this._remaining],
});
}
private _testWrapper(test: Test) {
const timeout = test.slow ? this._timeout * 3 : this._timeout;
const wrapped = fixturePool.wrapTestCallback(fn, timeout, test, {
return fixturePool.wrapTestCallback(test.fn, timeout, test, {
outputDir: this._outDir,
testDir: this._testDir,
});
return wrapped ? (done, ...args) => {
if (!this._shouldRunTest()) {
done();
return;
}
wrapped(...args).then(done).catch(done);
} : undefined;
}
_hookWrapper(hook, fn) {
if (!this._shouldRunTest(true))
return;
return hook(async () => {
return await fixturePool.resolveParametersAndRun(fn, 0);
});
}
_serializeTest(test, ordinal) {
private _serializeTest(test) {
return {
id: `${ordinal}@${this._configuredFile}`,
duration: test.duration,
id: `${test._ordinal}@${this._configuredFile}`,
duration: Date.now() - test._startTime,
};
}
_serializeStats() {
return {
passes: this._passes,
failures: this._failures,
pending: this._pending,
duration: this._runner.duration(),
}
}
}
function trimCycles(obj) {

View File

@ -15,14 +15,14 @@
*/
import { options } from './playwright.fixtures';
it.skip(options.FIREFOX)('should work', async function({page, server}) {
it.skip(options.FIREFOX)('should work', async function({page}) {
await page.setContent(`<div id=d1 tabIndex=0></div>`);
expect(await page.evaluate(() => document.activeElement.nodeName)).toBe('BODY');
await page.focus('#d1');
expect(await page.evaluate(() => document.activeElement.id)).toBe('d1');
});
it('should emit focus event', async function({page, server}) {
it('should emit focus event', async function({page}) {
await page.setContent(`<div id=d1 tabIndex=0></div>`);
let focused = false;
await page.exposeFunction('focusEvent', () => focused = true);
@ -31,7 +31,7 @@ it('should emit focus event', async function({page, server}) {
expect(focused).toBe(true);
});
it('should emit blur event', async function({page, server}) {
it('should emit blur event', async function({page}) {
await page.setContent(`<div id=d1 tabIndex=0>DIV1</div><div id=d2 tabIndex=0>DIV2</div>`);
await page.focus('#d1');
let focused = false;