Skip to content

Commit

Permalink
test: implement in-process debug mode (#3486)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Aug 15, 2020
1 parent bc23324 commit 35fbd58
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 124 deletions.
22 changes: 15 additions & 7 deletions test/runner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ program
.option('--reporter <reporter>', 'Specify reporter to use', '')
.option('--trial-run', 'Only collect the matching tests and report them as passing')
.option('--dumpio', 'Dump stdout and stderr from workers', false)
.option('--debug', 'Run tests in-process for debugging', false)
.option('--timeout <timeout>', 'Specify test timeout threshold (in milliseconds), default: 10000', 10000)
.action(async (command) => {
// Collect files
Expand All @@ -50,11 +51,8 @@ program
if (command.grep)
mocha.grep(command.grep);
mocha.addFile(file);
let runner;
await new Promise(f => {
runner = mocha.run(f);
});
total += runner.grepTotal(mocha.suite);
mocha.loadFiles();
total += grepTotal(mocha.suite, mocha.options.grep);

rootSuite.addSuite(mocha.suite);
mocha.suite.title = path.basename(file);
Expand All @@ -75,8 +73,9 @@ program
}

// Trial run does not need many workers, use one.
const jobs = command.trialRun ? 1 : command.jobs;
const jobs = (command.trialRun || command.debug) ? 1 : command.jobs;
const runner = new Runner(rootSuite, {
debug: command.debug,
dumpio: command.dumpio,
grep: command.grep,
jobs,
Expand All @@ -101,7 +100,7 @@ function collectFiles(dir, filters) {
files.push(...collectFiles(path.join(dir, name), filters));
continue;
}
if (!name.includes('spec'))
if (!name.endsWith('spec.ts'))
continue;
if (!filters.length) {
files.push(path.join(dir, name));
Expand All @@ -116,3 +115,12 @@ function collectFiles(dir, filters) {
}
return files;
}

function grepTotal(suite, grep) {
let total = 0;
suite.eachTest(test => {
if (grep.test(test.fullTitle()))
total++;
});
return total;
}
43 changes: 38 additions & 5 deletions test/runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const { EventEmitter } = require('events');
const Mocha = require('mocha');
const builtinReporters = require('mocha/lib/reporters');
const DotRunner = require('./dotReporter');
const { computeWorkerHash } = require('./fixtures');
const { computeWorkerHash, FixturePool } = require('./fixtures');

const constants = Mocha.Runner.constants;
// Mocha runner does not remove uncaughtException listeners.
Expand Down Expand Up @@ -132,7 +132,7 @@ class Runner extends EventEmitter {
}

_createWorker() {
const worker = new Worker(this);
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)));
Expand All @@ -154,8 +154,8 @@ class Runner extends EventEmitter {
worker.init().then(() => this._workerAvailable(worker));
}

_restartWorker(worker) {
worker.stop();
async _restartWorker(worker) {
await worker.stop();
this._createWorker();
}

Expand All @@ -175,7 +175,7 @@ class Runner extends EventEmitter {

let lastWorkerId = 0;

class Worker extends EventEmitter {
class OopWorker extends EventEmitter {
constructor(runner) {
super();
this.runner = runner;
Expand Down Expand Up @@ -240,4 +240,37 @@ class Worker extends EventEmitter {
}
}

class InProcessWorker extends EventEmitter {
constructor(runner) {
super();
this.runner = runner;
this.fixturePool = require('./fixturesUI').fixturePool;
}

async init() {
}

async run(file) {
delete require.cache[file];
const { TestRunner } = require('./testRunner');
const testRunner = new TestRunner(file, this.runner._options);
for (const event of ['test', 'pending', 'pass', 'fail', 'done'])
testRunner.on(event, this.emit.bind(this, event));
testRunner.run();
}

async stop() {
await this.fixturePool.teardownScope('worker');
this.emit('exit');
}

takeOut() {
return [];
}

takeErr() {
return [];
}
}

module.exports = { Runner };
125 changes: 125 additions & 0 deletions test/runner/testRunner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* 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 Mocha = require('mocha');
const { fixturesUI } = require('./fixturesUI');
const { EventEmitter } = require('events');

global.expect = require('expect');
global.testOptions = require('./testOptions');
const GoldenUtils = require('./GoldenUtils');

(function extendExpects() {
function toMatchImage(received, path) {
const {pass, message} = GoldenUtils.compare(received, path);
return {pass, message: () => message};
};
global.expect.extend({ toMatchImage });
})();

class NullReporter {}

class TestRunner extends EventEmitter {
constructor(file, options) {
super();
this.mocha = new Mocha({
ui: fixturesUI.bind(null, options.trialRun),
timeout: options.timeout,
reporter: NullReporter
});
if (options.grep)
this.mocha.grep(options.grep);
this.mocha.addFile(file);
this.mocha.suite.filterOnly();
this._lastOrdinal = -1;
this._failedWithError = false;
}

async run() {
let callback;
const result = new Promise(f => callback = f);
const runner = this.mocha.run(callback);

const constants = Mocha.Runner.constants;
runner.on(constants.EVENT_TEST_BEGIN, test => {
this.emit('test', { test: serializeTest(test, ++this._lastOrdinal) });
});

runner.on(constants.EVENT_TEST_PENDING, test => {
this.emit('pending', { test: serializeTest(test, ++this._lastOrdinal) });
});

runner.on(constants.EVENT_TEST_PASS, test => {
this.emit('pass', { test: serializeTest(test, this._lastOrdinal) });
});

runner.on(constants.EVENT_TEST_FAIL, (test, error) => {
this._failedWithError = error;
this.emit('fail', {
test: serializeTest(test, this._lastOrdinal),
error: serializeError(error),
});
});

runner.once(constants.EVENT_RUN_END, async () => {
this.emit('done', { stats: serializeStats(runner.stats), error: this._failedWithError });
});
await result;
}
}

function serializeTest(test, origin) {
return {
id: `${test.file}::${origin}`,
duration: test.duration,
};
}

function serializeStats(stats) {
return {
tests: stats.tests,
passes: stats.passes,
duration: stats.duration,
failures: stats.failures,
pending: stats.pending,
}
}

function trimCycles(obj) {
const cache = new Set();
return JSON.parse(
JSON.stringify(obj, function(key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.has(value))
return '' + value;
cache.add(value);
}
return value;
})
);
}

function serializeError(error) {
if (error instanceof Error) {
return {
message: error.message,
stack: error.stack
}
}
return trimCycles(error);
}

module.exports = { TestRunner };
Loading

0 comments on commit 35fbd58

Please sign in to comment.