test: introduce test collector (#3515)

This commit is contained in:
Pavel Feldman 2020-08-18 14:12:31 -07:00 committed by GitHub
parent 510182f0b9
commit 77cab8bed3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 288 additions and 146 deletions

View File

@ -25,14 +25,13 @@ import { Transport } from '../lib/rpc/transport';
import { setUnderTest } from '../lib/helper'; import { setUnderTest } from '../lib/helper';
import { installCoverageHooks } from './runner/coverage'; import { installCoverageHooks } from './runner/coverage';
import { valueFromEnv } from './runner/utils'; import { valueFromEnv } from './runner/utils';
import { registerFixture, registerWorkerFixture } from './runner/fixtures'; import { registerFixture, registerWorkerFixture, registerWorkerGenerator } from './runner/fixtures';
import './runner/builtin.fixtures'; import './runner/builtin.fixtures';
import {mkdtempAsync, removeFolderAsync} from './utils'; import {mkdtempAsync, removeFolderAsync} from './utils';
setUnderTest(); // Note: we must call setUnderTest before requiring Playwright setUnderTest(); // Note: we must call setUnderTest before requiring Playwright
const browserName = process.env.BROWSER || 'chromium';
const platform = os.platform(); const platform = os.platform();
declare global { declare global {
@ -58,6 +57,8 @@ declare global {
} }
} }
const browserName = process.env.BROWSER;
(global as any).MAC = platform === 'darwin'; (global as any).MAC = platform === 'darwin';
(global as any).LINUX = platform === 'linux'; (global as any).LINUX = platform === 'linux';
(global as any).WIN = platform === 'win32'; (global as any).WIN = platform === 'win32';
@ -92,7 +93,6 @@ const getExecutablePath = (browserName) => {
return process.env.FFPATH; return process.env.FFPATH;
if (browserName === 'webkit' && process.env.WKPATH) if (browserName === 'webkit' && process.env.WKPATH)
return process.env.WKPATH; return process.env.WKPATH;
return
} }
registerWorkerFixture('defaultBrowserOptions', async({browserName}, test) => { registerWorkerFixture('defaultBrowserOptions', async({browserName}, test) => {
@ -151,7 +151,7 @@ registerFixture('toImpl', async ({playwright}, test) => {
}); });
registerWorkerFixture('browserType', async ({playwright, browserName}, test) => { registerWorkerFixture('browserType', async ({playwright, browserName}, test) => {
const browserType = playwright[process.env.BROWSER || 'chromium'] const browserType = playwright[browserName];
const executablePath = getExecutablePath(browserName) const executablePath = getExecutablePath(browserName)
if (executablePath) if (executablePath)
browserType._executablePath = executablePath browserType._executablePath = executablePath
@ -184,8 +184,10 @@ registerFixture('server', async ({httpService}, test) => {
await test(httpService.server); await test(httpService.server);
}); });
registerWorkerFixture('browserName', async ({}, test) => { registerWorkerGenerator('browserName', () => {
await test(browserName); if (process.env.BROWSER)
return [process.env.BROWSER];
return ['chromium', 'webkit', 'firefox'];
}); });
registerWorkerFixture('isChromium', async ({browserName}, test) => { registerWorkerFixture('isChromium', async ({browserName}, test) => {
@ -216,5 +218,5 @@ registerWorkerFixture('asset', async ({}, test) => {
}); });
registerWorkerFixture('golden', async ({browserName}, test) => { registerWorkerFixture('golden', async ({browserName}, test) => {
await test(p => path.join(`${browserName}`, p)); await test(p => path.join(browserName, p));
}); });

View File

@ -19,6 +19,7 @@ const debug = require('debug');
const registrations = new Map(); const registrations = new Map();
const registrationsByFile = new Map(); const registrationsByFile = new Map();
const generatorRegistrations = new Map();
class Fixture { class Fixture {
constructor(pool, name, scope, fn) { constructor(pool, name, scope, fn) {
@ -28,10 +29,13 @@ class Fixture {
this.fn = fn; this.fn = fn;
this.deps = fixtureParameterNames(this.fn); this.deps = fixtureParameterNames(this.fn);
this.usages = new Set(); this.usages = new Set();
this.value = null; this.generatorValue = this.pool.generators.get(name);
this.value = this.generatorValue || null;
} }
async setup() { async setup() {
if (this.generatorValue)
return;
for (const name of this.deps) { for (const name of this.deps) {
await this.pool.setupFixture(name); await this.pool.setupFixture(name);
this.pool.instances.get(name).usages.add(this.name); this.pool.instances.get(name).usages.add(this.name);
@ -55,6 +59,8 @@ class Fixture {
} }
async teardown() { async teardown() {
if (this.generatorValue)
return;
if (this._teardown) if (this._teardown)
return; return;
this._teardown = true; this._teardown = true;
@ -76,6 +82,7 @@ class Fixture {
class FixturePool { class FixturePool {
constructor() { constructor() {
this.instances = new Map(); this.instances = new Map();
this.generators = new Map();
} }
async setupFixture(name) { async setupFixture(name) {
@ -120,21 +127,23 @@ class FixturePool {
} }
}; };
} }
}
fixtures(callback) { function fixturesForCallback(callback) {
const result = new Set(); const names = new Set();
const visit = (callback) => { const visit = (callback) => {
for (const name of fixtureParameterNames(callback)) { for (const name of fixtureParameterNames(callback)) {
if (name in result) if (name in names)
continue; continue;
result.add(name); names.add(name);
const { fn } = registrations.get(name) const { fn } = registrations.get(name)
visit(fn); visit(fn);
} }
}; };
visit(callback); visit(callback);
const result = [...names];
result.sort();
return result; return result;
}
} }
function fixtureParameterNames(fn) { function fixtureParameterNames(fn) {
@ -165,6 +174,11 @@ function registerWorkerFixture(name, fn) {
innerRegisterFixture(name, 'worker', fn); innerRegisterFixture(name, 'worker', fn);
}; };
function registerWorkerGenerator(name, fn) {
innerRegisterFixture(name, 'worker', () => {});
generatorRegistrations.set(name, fn);
}
function collectRequires(file, result) { function collectRequires(file, result) {
if (result.has(file)) if (result.has(file))
return; return;
@ -179,12 +193,16 @@ function lookupRegistrations(file, scope) {
const deps = new Set(); const deps = new Set();
collectRequires(file, deps); collectRequires(file, deps);
const allDeps = [...deps].reverse(); const allDeps = [...deps].reverse();
let result = []; let result = new Map();
for (const dep of allDeps) { for (const dep of allDeps) {
const registrationList = registrationsByFile.get(dep); const registrationList = registrationsByFile.get(dep);
if (!registrationList) if (!registrationList)
continue; continue;
result = result.concat(registrationList.filter(r => r.scope === scope)); for (const r of registrationList) {
if (scope && r.scope !== scope)
continue;
result.set(r.name, r);
}
} }
return result; return result;
} }
@ -192,7 +210,7 @@ function lookupRegistrations(file, scope) {
function rerunRegistrations(file, scope) { function rerunRegistrations(file, scope) {
// When we are running several tests in the same worker, we should re-run registrations before // When we are running several tests in the same worker, we should re-run registrations before
// each file. That way we erase potential fixture overrides from the previous test runs. // each file. That way we erase potential fixture overrides from the previous test runs.
for (const registration of lookupRegistrations(file, scope)) for (const registration of lookupRegistrations(file, scope).values())
registrations.set(registration.name, registration); registrations.set(registration.name, registration);
} }
@ -202,9 +220,9 @@ function computeWorkerHash(file) {
// This collection of fixtures is the fingerprint of the worker setup, a "worker hash". // This collection of fixtures is the fingerprint of the worker setup, a "worker hash".
// Tests with the matching "worker hash" will reuse the same worker. // Tests with the matching "worker hash" will reuse the same worker.
const hash = crypto.createHash('sha1'); const hash = crypto.createHash('sha1');
for (const registration of lookupRegistrations(file, 'worker')) for (const registration of lookupRegistrations(file, 'worker').values())
hash.update(registration.location); hash.update(registration.location);
return hash.digest('hex'); return hash.digest('hex');
} }
module.exports = { FixturePool, registerFixture, registerWorkerFixture, computeWorkerHash, rerunRegistrations }; module.exports = { FixturePool, registerFixture, registerWorkerFixture, computeWorkerHash, rerunRegistrations, lookupRegistrations, fixturesForCallback, registerWorkerGenerator, generatorRegistrations };

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
const { FixturePool, registerFixture, registerWorkerFixture, rerunRegistrations } = require('./fixtures'); const { registerFixture, registerWorkerFixture, registerWorkerGenerator } = require('./fixtures');
const { Test, Suite } = require('mocha'); const { Test, Suite } = require('mocha');
const { installTransform } = require('./transform'); const { installTransform } = require('./transform');
const commonSuite = require('mocha/lib/interfaces/common'); const commonSuite = require('mocha/lib/interfaces/common');
@ -23,8 +23,8 @@ Error.stackTraceLimit = 15;
global.testOptions = require('./testOptions'); global.testOptions = require('./testOptions');
global.registerFixture = registerFixture; global.registerFixture = registerFixture;
global.registerWorkerFixture = registerWorkerFixture; global.registerWorkerFixture = registerWorkerFixture;
global.registerWorkerGenerator = registerWorkerGenerator;
const fixturePool = new FixturePool();
let revertBabelRequire; let revertBabelRequire;
function specBuilder(modifiers, specCallback) { function specBuilder(modifiers, specCallback) {
@ -57,7 +57,7 @@ function specBuilder(modifiers, specCallback) {
return builder({}, null); return builder({}, null);
} }
function fixturesUI(testRunner, suite) { function fixturesUI(wrappers, 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) {
@ -65,26 +65,18 @@ function fixturesUI(testRunner, suite) {
const it = specBuilder(['skip', 'fail', 'slow', 'only'], (specs, title, fn) => { const it = specBuilder(['skip', 'fail', 'slow', 'only'], (specs, title, fn) => {
const suite = suites[0]; const suite = suites[0];
if (suite.isPending()) if (suite.isPending())
fn = null; fn = null;
let wrapper; const wrapper = fn ? wrappers.testWrapper(fn) : undefined;
const wrapped = fixturePool.wrapTestCallback(fn);
wrapper = wrapped ? (done, ...args) => {
if (!testRunner.shouldRunTest()) {
done();
return;
}
wrapped(...args).then(done).catch(done);
} : undefined;
if (wrapper) { if (wrapper) {
wrapper.toString = () => fn.toString(); wrapper.toString = () => fn.toString();
wrapper.__original = fn; wrapper.__original = fn;
} }
const test = new Test(title, wrapper); const test = new Test(title, wrapper);
test.__fixtures = fixturePool.fixtures(fn);
test.file = file; test.file = file;
suite.addTest(test); suite.addTest(test);
const only = specs.only && specs.only[0]; const only = wrappers.ignoreOnly ? false : specs.only && specs.only[0];
if (specs.slow && specs.slow[0]) if (specs.slow && specs.slow[0])
test.timeout(90000); test.timeout(90000);
if (only) if (only)
@ -102,7 +94,7 @@ function fixturesUI(testRunner, suite) {
file: file, file: file,
fn: fn fn: fn
}); });
const only = specs.only && specs.only[0]; const only = wrappers.ignoreOnly ? false : specs.only && specs.only[0];
if (only) if (only)
suite.markOnly(); suite.markOnly();
if (!only && specs.skip && specs.skip[0]) if (!only && specs.skip && specs.skip[0])
@ -112,23 +104,9 @@ function fixturesUI(testRunner, suite) {
return suite; return suite;
}); });
context.beforeEach = (fn) => { context.beforeEach = fn => wrappers.hookWrapper(common.beforeEach.bind(common), fn);
if (!testRunner.shouldRunTest(true)) context.afterEach = fn => wrappers.hookWrapper(common.afterEach.bind(common), fn);
return;
return common.beforeEach(async () => {
return await fixturePool.resolveParametersAndRun(fn);
});
};
context.afterEach = (fn) => {
if (!testRunner.shouldRunTest(true))
return;
return common.afterEach(async () => {
return await fixturePool.resolveParametersAndRun(fn);
});
};
context.run = mocha.options.delay && common.runWithSuite(suite); context.run = mocha.options.delay && common.runWithSuite(suite);
context.describe = describe; context.describe = describe;
context.fdescribe = describe.only(true); context.fdescribe = describe.only(true);
context.xdescribe = describe.skip(true); context.xdescribe = describe.skip(true);
@ -141,8 +119,7 @@ function fixturesUI(testRunner, suite) {
suite.on(Suite.constants.EVENT_FILE_POST_REQUIRE, function(context, file, mocha) { suite.on(Suite.constants.EVENT_FILE_POST_REQUIRE, function(context, file, mocha) {
revertBabelRequire(); revertBabelRequire();
rerunRegistrations(file, 'test');
}); });
}; };
module.exports = { fixturesUI, fixturePool, registerFixture, registerWorkerFixture }; module.exports = { fixturesUI };

View File

@ -18,9 +18,7 @@ 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 { TestRunner, createTestSuite } = require('./testRunner'); const { TestCollector } = require('./testCollector');
class NullReporter {}
program program
.version('Version ' + require('../../package.json').version) .version('Version ' + require('../../package.json').version)
@ -38,42 +36,31 @@ program
// Collect files] // Collect files]
const testDir = path.join(process.cwd(), command.args[0]); const testDir = path.join(process.cwd(), command.args[0]);
const files = collectFiles(testDir, '', command.args.slice(1)); const files = collectFiles(testDir, '', command.args.slice(1));
const rootSuite = new createTestSuite();
let total = 0; const testCollector = new TestCollector({
// Build the test model, suite per file.
for (const file of files) {
const testRunner = new TestRunner(file, [], {
forbidOnly: command.forbidOnly || undefined, forbidOnly: command.forbidOnly || undefined,
grep: command.grep, grep: command.grep,
reporter: NullReporter,
testDir,
timeout: command.timeout, timeout: command.timeout,
trialRun: true,
}); });
total += testRunner.grepTotal(); for (const file of files)
rootSuite.addSuite(testRunner.suite); testCollector.addFile(file);
testRunner.suite.title = path.basename(file);
}
const rootSuite = testCollector.suite;
const total = rootSuite.total();
if (!total) { if (!total) {
console.error('No tests found.'); console.error('=================');
console.error(' No tests found.');
console.error('=================');
process.exit(1); process.exit(1);
} }
// Filter tests. // Filter tests.
if (rootSuite.hasOnly()) if (rootSuite.hasOnly())
rootSuite.filterOnly(); rootSuite.filterOnly();
if (!command.reporter) {
console.log();
total = Math.min(total, rootSuite.total()); // First accounts for grep, second for only.
const workers = Math.min(command.jobs, files.length);
console.log(`Running ${total} test${ total > 1 ? 's' : '' } using ${workers} worker${ workers > 1 ? 's' : ''}`);
}
// Trial run does not need many workers, use one. // Trial run does not need many workers, use one.
const jobs = (command.trialRun || command.debug) ? 1 : command.jobs; const jobs = (command.trialRun || command.debug) ? 1 : command.jobs;
const runner = new Runner(rootSuite, { const runner = new Runner(rootSuite, total, {
debug: command.debug, debug: command.debug,
quiet: command.quiet, quiet: command.quiet,
grep: command.grep, grep: command.grep,

View File

@ -27,7 +27,7 @@ const constants = Mocha.Runner.constants;
process.setMaxListeners(0); process.setMaxListeners(0);
class Runner extends EventEmitter { class Runner extends EventEmitter {
constructor(suite, options) { constructor(suite, total, options) {
super(); super();
this._suite = suite; this._suite = suite;
this._options = options; this._options = options;
@ -45,30 +45,36 @@ class Runner extends EventEmitter {
const reporterFactory = builtinReporters[options.reporter] || DotRunner; const reporterFactory = builtinReporters[options.reporter] || DotRunner;
this._reporter = new reporterFactory(this, {}); this._reporter = new reporterFactory(this, {});
this._tests = new Map(); this._testById = new Map();
this._files = new Map(); this._testsByConfiguredFile = new Map();
let grep;
if (options.grep) {
const match = options.grep.match(/^\/(.*)\/(g|i|)$|.*/);
grep = new RegExp(match[1] || match[0], match[2]);
}
suite.eachTest(test => { suite.eachTest(test => {
if (grep && !grep.test(test.fullTitle())) const configuredFile = `${test.file}::[${test.__configurationString}]`;
return; if (!this._testsByConfiguredFile.has(configuredFile)) {
if (!this._files.has(test.file)) this._testsByConfiguredFile.set(configuredFile, {
this._files.set(test.file, 0); file: test.file,
const counter = this._files.get(test.file); configuredFile,
this._files.set(test.file, counter + 1); ordinals: [],
this._tests.set(`${test.file}::${counter}`, test); configurationObject: test.__configurationObject,
configurationString: test.__configurationString
}); });
} }
const { ordinals } = this._testsByConfiguredFile.get(configuredFile);
ordinals.push(test.__ordinal);
this._testById.set(`${test.__ordinal}@${configuredFile}`, test);
});
if (process.stdout.isTTY) {
console.log();
const jobs = Math.min(options.jobs, this._testsByConfiguredFile.size);
console.log(`Running ${total} test${ total > 1 ? 's' : '' } using ${jobs} worker${ jobs > 1 ? 's' : ''}`);
}
}
_filesSortedByWorkerHash() { _filesSortedByWorkerHash() {
const result = []; const result = [];
for (const [file, count] of this._files.entries()) for (const entry of this._testsByConfiguredFile.values())
result.push({ file, hash: computeWorkerHash(file), ordinals: new Array(count).fill(0).map((_, i) => i) }); result.push({ ...entry, hash: entry.configurationString + '@' + computeWorkerHash(entry.file) });
result.sort((a, b) => a.hash < b.hash ? -1 : (a.hash === b.hash ? 0 : 1)); result.sort((a, b) => a.hash < b.hash ? -1 : (a.hash === b.hash ? 0 : 1));
return result; return result;
} }
@ -170,7 +176,7 @@ class Runner extends EventEmitter {
} }
_updateTest(serialized) { _updateTest(serialized) {
const test = this._tests.get(serialized.id); const test = this._testById.get(serialized.id);
test.duration = serialized.duration; test.duration = serialized.duration;
return test; return test;
} }
@ -228,7 +234,7 @@ class OopWorker extends EventEmitter {
run(entry) { run(entry) {
this.hash = entry.hash; this.hash = entry.hash;
this.process.send({ method: 'run', params: { file: entry.file, ordinals: entry.ordinals, options: this.runner._options } }); this.process.send({ method: 'run', params: { entry, options: this.runner._options } });
} }
stop() { stop() {
@ -252,7 +258,7 @@ class InProcessWorker extends EventEmitter {
constructor(runner) { constructor(runner) {
super(); super();
this.runner = runner; this.runner = runner;
this.fixturePool = require('./fixturesUI').fixturePool; this.fixturePool = require('./testRunner').fixturePool;
} }
async init() { async init() {
@ -265,7 +271,7 @@ class InProcessWorker extends EventEmitter {
async run(entry) { async run(entry) {
delete require.cache[entry.file]; delete require.cache[entry.file];
const { TestRunner } = require('./testRunner'); const { TestRunner } = require('./testRunner');
const testRunner = new TestRunner(entry.file, entry.ordinals, this.runner._options); const testRunner = new TestRunner(entry, this.runner._options);
for (const event of ['test', 'pending', 'pass', 'fail', 'done']) for (const event of ['test', 'pending', 'pass', 'fail', 'done'])
testRunner.on(event, this.emit.bind(this, event)); testRunner.on(event, this.emit.bind(this, event));
testRunner.run(); testRunner.run();

View File

@ -0,0 +1,135 @@
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* 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 path = require('path');
const Mocha = require('mocha');
const { fixturesForCallback, generatorRegistrations } = require('./fixtures');
const { fixturesUI } = require('./fixturesUI');
global.testOptions = require('./testOptions');
class NullReporter {}
class TestCollector {
constructor(options) {
this._options = options;
this.suite = new Mocha.Suite('', new Mocha.Context(), true);
this._total = 0;
if (options.grep) {
const match = options.grep.match(/^\/(.*)\/(g|i|)$|.*/);
this._grep = new RegExp(match[1] || match[0], match[2]);
}
}
addFile(file) {
const mocha = new Mocha({
forbidOnly: this._options.forbidOnly,
reporter: NullReporter,
timeout: this._options.timeout,
ui: fixturesUI.bind(null, {
testWrapper: (fn) => done => done(),
hookWrapper: (hook, fn) => {},
ignoreOnly: false,
}),
});
mocha.addFile(file);
mocha.loadFiles();
const workerGeneratorConfigurations = new Map();
let ordinal = 0;
mocha.suite.eachTest(test => {
// All tests are identified with their ordinals.
test.__ordinal = ordinal++;
// Get all the fixtures that the test needs.
const fixtures = fixturesForCallback(test.fn.__original);
// For generator fixtures, collect all variants of the fixture values
// to build different workers for them.
const generatorConfigurations = [];
for (const name of fixtures) {
if (!generatorRegistrations.has(name))
continue;
const values = generatorRegistrations.get(name)();
let state = generatorConfigurations.length ? generatorConfigurations.slice() : [[]];
generatorConfigurations.length = 0;
for (const gen of state) {
for (const value of values)
generatorConfigurations.push([...gen, { name, value }]);
}
}
// No generator fixtures for test, include empty set.
if (!generatorConfigurations.length)
generatorConfigurations.push([]);
for (const configurationObject of generatorConfigurations) {
// Serialize configuration as readable string, we will use it as a hash.
const tokens = [];
for (const { name, value } of configurationObject)
tokens.push(`${name}=${value}`);
const configurationString = tokens.join(', ');
// Allocate worker for this configuration, add test into it.
if (!workerGeneratorConfigurations.has(configurationString))
workerGeneratorConfigurations.set(configurationString, { configurationObject, configurationString, tests: new Set() });
workerGeneratorConfigurations.get(configurationString).tests.add(test);
}
});
if (mocha.suite.hasOnly())
mocha.suite.filterOnly();
// 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(mocha.suite, configurationObject, configurationString, tests);
this.suite.addSuite(clone);
clone.title = path.basename(file) + (hash.length ? `::[${hash}]` : '');
}
}
_cloneSuite(suite, configurationObject, configurationString, tests) {
const copy = suite.clone();
copy.__configurationObject = configurationObject;
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.__ordinal = test.__ordinal;
testCopy.__configurationObject = configurationObject;
testCopy.__configurationString = configurationString;
copy.addTest(testCopy);
}
return copy;
}
}
function grepTotal(mocha, suite) {
let total = 0;
suite.eachTest(test => {
if (mocha.options.grep.test(test.fullTitle()))
total++;
});
return total;
}
module.exports = { TestCollector };

View File

@ -16,9 +16,11 @@
const path = require('path'); const path = require('path');
const Mocha = require('mocha'); const Mocha = require('mocha');
const { FixturePool, rerunRegistrations, fixturesForCallback } = require('./fixtures');
const { fixturesUI } = require('./fixturesUI'); const { fixturesUI } = require('./fixturesUI');
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const fixturePool = new FixturePool();
global.expect = require('expect'); global.expect = require('expect');
global.testOptions = require('./testOptions'); global.testOptions = require('./testOptions');
const GoldenUtils = require('./GoldenUtils'); const GoldenUtils = require('./GoldenUtils');
@ -26,27 +28,34 @@ const GoldenUtils = require('./GoldenUtils');
class NullReporter {} class NullReporter {}
class TestRunner extends EventEmitter { class TestRunner extends EventEmitter {
constructor(file, ordinals, options) { constructor(entry, options) {
super(); super();
this.mocha = new Mocha({ this.mocha = new Mocha({
forbidOnly: options.forbidOnly,
reporter: NullReporter, reporter: NullReporter,
timeout: options.timeout, timeout: options.timeout,
ui: fixturesUI.bind(null, this), ui: fixturesUI.bind(null, {
testWrapper: fn => this._testWrapper(fn),
hookWrapper: (hook, fn) => this._hookWrapper(hook, fn),
ignoreOnly: true
}),
}); });
if (options.grep)
this.mocha.grep(options.grep);
this._currentOrdinal = -1; this._currentOrdinal = -1;
this._failedWithError = false; this._failedWithError = false;
this._ordinals = new Set(ordinals); this._file = entry.file;
this._remaining = new Set(ordinals); this._ordinals = new Set(entry.ordinals);
this._remaining = new Set(entry.ordinals);
this._trialRun = options.trialRun; this._trialRun = options.trialRun;
this._passes = 0; this._passes = 0;
this._failures = 0; this._failures = 0;
this._pending = 0; this._pending = 0;
this._relativeTestFile = path.relative(options.testDir, file); this._configuredFile = entry.configuredFile;
this.mocha.addFile(file); this._configurationObject = entry.configurationObject;
this.mocha.suite.filterOnly(); this._configurationString = entry.configurationString;
this._parsedGeneratorConfiguration = new Map();
for (const {name, value} of this._configurationObject)
this._parsedGeneratorConfiguration.set(name, value);
this._relativeTestFile = path.relative(options.testDir, this._file);
this.mocha.addFile(this._file);
this.mocha.loadFiles(); this.mocha.loadFiles();
this.suite = this.mocha.suite; this.suite = this.mocha.suite;
} }
@ -54,6 +63,9 @@ class TestRunner extends EventEmitter {
async run() { async run() {
let callback; let callback;
const result = new Promise(f => callback = f); const result = new Promise(f => callback = f);
rerunRegistrations(this._file, 'test');
for (const [name, value] of this._parsedGeneratorConfiguration)
fixturePool.generators.set(name, value);
const runner = this.mocha.run(callback); const runner = this.mocha.run(callback);
const constants = Mocha.Runner.constants; const constants = Mocha.Runner.constants;
@ -65,7 +77,7 @@ class TestRunner extends EventEmitter {
if (this._ordinals.size && !this._ordinals.has(ordinal)) if (this._ordinals.size && !this._ordinals.has(ordinal))
return; return;
this._remaining.delete(ordinal); this._remaining.delete(ordinal);
this.emit('test', { test: serializeTest(test, ordinal) }); this.emit('test', { test: this._serializeTest(test, ordinal) });
}); });
runner.on(constants.EVENT_TEST_PENDING, test => { runner.on(constants.EVENT_TEST_PENDING, test => {
@ -76,7 +88,7 @@ class TestRunner extends EventEmitter {
return; return;
this._remaining.delete(ordinal); this._remaining.delete(ordinal);
++this._pending; ++this._pending;
this.emit('pending', { test: serializeTest(test, ordinal) }); this.emit('pending', { test: this._serializeTest(test, ordinal) });
}); });
runner.on(constants.EVENT_TEST_PASS, test => { runner.on(constants.EVENT_TEST_PASS, test => {
@ -87,7 +99,7 @@ class TestRunner extends EventEmitter {
if (this._ordinals.size && !this._ordinals.has(ordinal)) if (this._ordinals.size && !this._ordinals.has(ordinal))
return; return;
++this._passes; ++this._passes;
this.emit('pass', { test: serializeTest(test, ordinal) }); this.emit('pass', { test: this._serializeTest(test, ordinal) });
}); });
runner.on(constants.EVENT_TEST_FAIL, (test, error) => { runner.on(constants.EVENT_TEST_FAIL, (test, error) => {
@ -96,7 +108,7 @@ class TestRunner extends EventEmitter {
++this._failures; ++this._failures;
this._failedWithError = error; this._failedWithError = error;
this.emit('fail', { this.emit('fail', {
test: serializeTest(test, this._currentOrdinal), test: this._serializeTest(test, this._currentOrdinal),
error: serializeError(error), error: serializeError(error),
}); });
}); });
@ -112,7 +124,7 @@ class TestRunner extends EventEmitter {
await result; await result;
} }
shouldRunTest(hook) { _shouldRunTest(hook) {
if (this._trialRun || this._failedWithError) if (this._trialRun || this._failedWithError)
return false; return false;
if (hook) { if (hook) {
@ -126,13 +138,30 @@ class TestRunner extends EventEmitter {
return true; return true;
} }
grepTotal() { _testWrapper(fn) {
let total = 0; const wrapped = fixturePool.wrapTestCallback(fn);
this.suite.eachTest(test => { return wrapped ? (done, ...args) => {
if (this.mocha.options.grep.test(test.fullTitle())) if (!this._shouldRunTest()) {
total++; 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);
}); });
return total; }
_serializeTest(test, ordinal) {
return {
id: `${ordinal}@${this._configuredFile}`,
duration: test.duration,
};
} }
_serializeStats(stats) { _serializeStats(stats) {
@ -145,17 +174,6 @@ class TestRunner extends EventEmitter {
} }
} }
function createTestSuite() {
return new Mocha.Suite('', new Mocha.Context(), true);
}
function serializeTest(test, origin) {
return {
id: `${test.file}::${origin}`,
duration: test.duration,
};
}
function trimCycles(obj) { function trimCycles(obj) {
const cache = new Set(); const cache = new Set();
return JSON.parse( return JSON.parse(
@ -190,4 +208,4 @@ function initializeImageMatcher(options) {
global.expect.extend({ toMatchImage }); global.expect.extend({ toMatchImage });
} }
module.exports = { TestRunner, createTestSuite, initializeImageMatcher }; module.exports = { TestRunner, initializeImageMatcher, fixturePool };

View File

@ -14,9 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
const { fixturePool } = require('./fixturesUI');
const { gracefullyCloseAll } = require('../../lib/server/processLauncher'); const { gracefullyCloseAll } = require('../../lib/server/processLauncher');
const { TestRunner, initializeImageMatcher } = require('./testRunner'); const { TestRunner, initializeImageMatcher, fixturePool } = require('./testRunner');
const { initializeWorker } = require('./builtin.fixtures'); const { initializeWorker } = require('./builtin.fixtures');
const util = require('util'); const util = require('util');
@ -57,7 +56,7 @@ process.on('message', async message => {
return; return;
} }
if (message.method === 'run') { if (message.method === 'run') {
const testRunner = new TestRunner(message.params.file, message.params.ordinals, message.params.options); const testRunner = new TestRunner(message.params.entry, message.params.options);
for (const event of ['test', 'pending', 'pass', 'fail', 'done']) for (const event of ['test', 'pending', 'pass', 'fail', 'done'])
testRunner.on(event, sendMessageToParent.bind(null, event)); testRunner.on(event, sendMessageToParent.bind(null, event));
await testRunner.run(); await testRunner.run();