test: use mocha in ci/cd (#3406)
This commit is contained in:
parent
079b6e0a66
commit
7e07634cc6
43
.github/workflows/tests.yml
vendored
43
.github/workflows/tests.yml
vendored
|
|
@ -38,16 +38,11 @@ jobs:
|
||||||
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
|
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
|
||||||
# Wrap `npm run` in a subshell to redirect STDERR to file.
|
# Wrap `npm run` in a subshell to redirect STDERR to file.
|
||||||
# Enable core dumps in the subshell.
|
# Enable core dumps in the subshell.
|
||||||
- run: xvfb-run --auto-servernum -- bash -c "ulimit -c unlimited && npm run jest -- --testTimeout=30000 && npm run coverage"
|
- run: xvfb-run --auto-servernum -- bash -c "ulimit -c unlimited && node test/mocha/index.js --max-workers=1 --timeout=30000 && npm run coverage"
|
||||||
env:
|
env:
|
||||||
BROWSER: ${{ matrix.browser }}
|
BROWSER: ${{ matrix.browser }}
|
||||||
DEBUG: "pw:*,-pw:wrapped*"
|
DEBUG: "pw:*,-pw:wrapped*,-pw:test*"
|
||||||
DEBUG_FILE: "testrun.log"
|
DEBUG_FILE: "testrun.log"
|
||||||
- uses: actions/upload-artifact@v1
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.browser }}-${{ matrix.os }}-jest-report
|
|
||||||
path: jest-report.json
|
|
||||||
- uses: actions/upload-artifact@v1
|
- uses: actions/upload-artifact@v1
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
|
|
@ -74,16 +69,11 @@ jobs:
|
||||||
- uses: microsoft/playwright-github-action@v1
|
- uses: microsoft/playwright-github-action@v1
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run jest -- --testTimeout=30000
|
- run: node test/mocha/index.js --max-workers=1 --timeout=30000
|
||||||
env:
|
env:
|
||||||
BROWSER: ${{ matrix.browser }}
|
BROWSER: ${{ matrix.browser }}
|
||||||
DEBUG: "pw:*,-pw:wrapped*"
|
DEBUG: "pw:*,-pw:wrapped*,-pw:test*"
|
||||||
DEBUG_FILE: "testrun.log"
|
DEBUG_FILE: "testrun.log"
|
||||||
- uses: actions/upload-artifact@v1
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.browser }}-mac-jest-report
|
|
||||||
path: jest-report.json
|
|
||||||
- uses: actions/upload-artifact@v1
|
- uses: actions/upload-artifact@v1
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
|
|
@ -113,17 +103,12 @@ jobs:
|
||||||
- uses: microsoft/playwright-github-action@v1
|
- uses: microsoft/playwright-github-action@v1
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run jest -- --testTimeout=30000
|
- run: node test/mocha/index.js --max-workers=1 --timeout=30000
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
BROWSER: ${{ matrix.browser }}
|
BROWSER: ${{ matrix.browser }}
|
||||||
DEBUG: "pw:*,-pw:wrapped*"
|
DEBUG: "pw:*,-pw:wrapped*,-pw:test*"
|
||||||
DEBUG_FILE: "testrun.log"
|
DEBUG_FILE: "testrun.log"
|
||||||
- uses: actions/upload-artifact@v1
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.browser }}-win-jest-report
|
|
||||||
path: jest-report.json
|
|
||||||
- uses: actions/upload-artifact@v1
|
- uses: actions/upload-artifact@v1
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
|
|
@ -175,17 +160,12 @@ jobs:
|
||||||
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
|
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
|
||||||
# Wrap `npm run` in a subshell to redirect STDERR to file.
|
# Wrap `npm run` in a subshell to redirect STDERR to file.
|
||||||
# Enable core dumps in the subshell.
|
# Enable core dumps in the subshell.
|
||||||
- run: xvfb-run --auto-servernum -- bash -c "ulimit -c unlimited && npm run jest -- --testTimeout=30000"
|
- run: xvfb-run --auto-servernum -- bash -c "ulimit -c unlimited && node test/mocha/index.js --max-workers=1 --timeout=30000"
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
env:
|
env:
|
||||||
BROWSER: ${{ matrix.browser }}
|
BROWSER: ${{ matrix.browser }}
|
||||||
HEADLESS: "false"
|
HEADLESS: "false"
|
||||||
DEBUG_FILE: "testrun.log"
|
DEBUG_FILE: "testrun.log"
|
||||||
- uses: actions/upload-artifact@v1
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
|
||||||
name: headful-${{ matrix.browser }}-linux-jest-report
|
|
||||||
path: jest-report.json
|
|
||||||
- uses: actions/upload-artifact@v1
|
- uses: actions/upload-artifact@v1
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
|
|
@ -214,17 +194,12 @@ jobs:
|
||||||
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
|
# XVFB-RUN merges both STDOUT and STDERR, whereas we need only STDERR
|
||||||
# Wrap `npm run` in a subshell to redirect STDERR to file.
|
# Wrap `npm run` in a subshell to redirect STDERR to file.
|
||||||
# Enable core dumps in the subshell.
|
# Enable core dumps in the subshell.
|
||||||
- run: xvfb-run --auto-servernum -- bash -c "ulimit -c unlimited && npm run jest -- --testTimeout=30000"
|
- run: xvfb-run --auto-servernum -- bash -c "ulimit -c unlimited && node test/mocha/index.js --max-workers=1 --timeout=30000"
|
||||||
env:
|
env:
|
||||||
BROWSER: ${{ matrix.browser }}
|
BROWSER: ${{ matrix.browser }}
|
||||||
DEBUG: "pw:*,-pw:wrapped*"
|
DEBUG: "pw:*,-pw:wrapped*,-pw:test*"
|
||||||
DEBUG_FILE: "testrun.log"
|
DEBUG_FILE: "testrun.log"
|
||||||
PWCHANNEL: ${{ matrix.transport }}
|
PWCHANNEL: ${{ matrix.transport }}
|
||||||
- uses: actions/upload-artifact@v1
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
|
||||||
name: rpc-${{ matrix.transport }}-${{ matrix.browser }}-linux-jest-report
|
|
||||||
path: jest-report.json
|
|
||||||
- uses: actions/upload-artifact@v1
|
- uses: actions/upload-artifact@v1
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
4081
package-lock.json
generated
4081
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
|
@ -9,13 +9,9 @@
|
||||||
"node": ">=10.15.0"
|
"node": ">=10.15.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ctest": "cross-env BROWSER=chromium jest",
|
"ctest": "cross-env BROWSER=chromium node test/mocha/index.js",
|
||||||
"ftest": "cross-env BROWSER=firefox jest",
|
"ftest": "cross-env BROWSER=firefox node test/mocha/index.js",
|
||||||
"wtest": "cross-env BROWSER=webkit jest",
|
"wtest": "cross-env BROWSER=webkit node test/mocha/index.js",
|
||||||
"ctestd": "cross-env BROWSER=chromium jest --reporters=./jest/dot.js --colors",
|
|
||||||
"ftestd": "cross-env BROWSER=firefox jest --reporters=./jest/dot.js --colors",
|
|
||||||
"wtestd": "cross-env BROWSER=webkit jest --reporters=./jest/dot.js --colors",
|
|
||||||
"nojest": "cross-env BROWSER=chromium node --unhandled-rejections=strict ./test/nojest/nojest.js",
|
|
||||||
"test": "npm run ctest && npm run ftest && npm run wtest",
|
"test": "npm run ctest && npm run ftest && npm run wtest",
|
||||||
"eslint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts ./src || eslint --ext js,ts ./src",
|
"eslint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts ./src || eslint --ext js,ts ./src",
|
||||||
"tsc": "tsc -p .",
|
"tsc": "tsc -p .",
|
||||||
|
|
@ -34,7 +30,6 @@
|
||||||
"generate-channels": "node utils/generate_channels.js",
|
"generate-channels": "node utils/generate_channels.js",
|
||||||
"typecheck-tests": "tsc -p ./test/",
|
"typecheck-tests": "tsc -p ./test/",
|
||||||
"roll-browser": "node utils/roll_browser.js",
|
"roll-browser": "node utils/roll_browser.js",
|
||||||
"jest": "jest",
|
|
||||||
"coverage": "node test/jest/checkCoverage.js",
|
"coverage": "node test/jest/checkCoverage.js",
|
||||||
"check-deps": "node utils/check_deps.js"
|
"check-deps": "node utils/check_deps.js"
|
||||||
},
|
},
|
||||||
|
|
@ -77,10 +72,8 @@
|
||||||
"eslint": "^6.6.0",
|
"eslint": "^6.6.0",
|
||||||
"eslint-plugin-notice": "^0.9.10",
|
"eslint-plugin-notice": "^0.9.10",
|
||||||
"esprima": "^4.0.0",
|
"esprima": "^4.0.0",
|
||||||
|
"expect": "^26.3.0",
|
||||||
"formidable": "^1.2.1",
|
"formidable": "^1.2.1",
|
||||||
"jest": "^26.2.2",
|
|
||||||
"jest-circus": "^26.2.2",
|
|
||||||
"jest-image-snapshot": "^4.0.2",
|
|
||||||
"mocha": "^8.1.1",
|
"mocha": "^8.1.1",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"node-stream-zip": "^1.8.2",
|
"node-stream-zip": "^1.8.2",
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ registerWorkerFixture('defaultBrowserOptions', async({}, test) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
registerWorkerFixture('playwright', async({}, test) => {
|
registerWorkerFixture('playwright', async({parallelIndex}, test) => {
|
||||||
const {coverage, uninstall} = installCoverageHooks(browserName);
|
const {coverage, uninstall} = installCoverageHooks(browserName);
|
||||||
if (process.env.PWCHANNEL === 'wire') {
|
if (process.env.PWCHANNEL === 'wire') {
|
||||||
const connection = new Connection();
|
const connection = new Connection();
|
||||||
|
|
@ -126,8 +126,7 @@ registerWorkerFixture('playwright', async({}, test) => {
|
||||||
|
|
||||||
async function teardownCoverage() {
|
async function teardownCoverage() {
|
||||||
uninstall();
|
uninstall();
|
||||||
const relativeTestPath = path.relative(__dirname, testPath);
|
const coveragePath = path.join(path.join(__dirname, 'output-' + browserName), 'coverage', parallelIndex + '.json');
|
||||||
const coveragePath = path.join(path.join(__dirname, 'output-' + browserName), 'coverage', relativeTestPath + '.json');
|
|
||||||
const coverageJSON = [...coverage.keys()].filter(key => coverage.get(key));
|
const coverageJSON = [...coverage.keys()].filter(key => coverage.get(key));
|
||||||
await fs.promises.mkdir(path.dirname(coveragePath), { recursive: true });
|
await fs.promises.mkdir(path.dirname(coveragePath), { recursive: true });
|
||||||
await fs.promises.writeFile(coveragePath, JSON.stringify(coverageJSON, undefined, 2), 'utf8');
|
await fs.promises.writeFile(coveragePath, JSON.stringify(coverageJSON, undefined, 2), 'utf8');
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,34 @@ function fixturesUI(trialRun, 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) {
|
||||||
const common = commonSuite(suites, context, mocha);
|
const common = commonSuite(suites, context, mocha);
|
||||||
|
|
||||||
|
const itBuilder = (markers) => {
|
||||||
|
return function(title, fn) {
|
||||||
|
const suite = suites[0];
|
||||||
|
if (suite.isPending())
|
||||||
|
fn = null;
|
||||||
|
let wrapper;
|
||||||
|
if (trialRun) {
|
||||||
|
if (fn)
|
||||||
|
wrapper = () => {};
|
||||||
|
} else {
|
||||||
|
const wrapped = fixturePool.wrapTestCallback(fn);
|
||||||
|
wrapper = wrapped ? (done, ...args) => {
|
||||||
|
wrapped(...args).then(done).catch(done);
|
||||||
|
} : undefined;
|
||||||
|
}
|
||||||
|
if (wrapper) {
|
||||||
|
wrapper.toString = () => fn.toString();
|
||||||
|
wrapper.__original = fn;
|
||||||
|
}
|
||||||
|
const test = new Test(title, wrapper);
|
||||||
|
if (markers && markers.includes('slow'))
|
||||||
|
test.timeout(90000);
|
||||||
|
test.file = file;
|
||||||
|
suite.addTest(test);
|
||||||
|
return test;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
context.beforeEach = common.beforeEach;
|
context.beforeEach = common.beforeEach;
|
||||||
context.afterEach = common.afterEach;
|
context.afterEach = common.afterEach;
|
||||||
if (trialRun) {
|
if (trialRun) {
|
||||||
|
|
@ -56,7 +84,6 @@ function fixturesUI(trialRun, suite) {
|
||||||
fn: fn
|
fn: fn
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
context.xdescribe = (title, fn) => {
|
context.xdescribe = (title, fn) => {
|
||||||
return common.suite.skip({
|
return common.suite.skip({
|
||||||
title: title,
|
title: title,
|
||||||
|
|
@ -64,11 +91,9 @@ function fixturesUI(trialRun, suite) {
|
||||||
fn: fn
|
fn: fn
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
context.describe.skip = function(condition) {
|
context.describe.skip = function(condition) {
|
||||||
return condition ? context.xdescribe : context.describe;
|
return condition ? context.xdescribe : context.describe;
|
||||||
};
|
};
|
||||||
|
|
||||||
context.describe.only = (title, fn) => {
|
context.describe.only = (title, fn) => {
|
||||||
return common.suite.only({
|
return common.suite.only({
|
||||||
title: title,
|
title: title,
|
||||||
|
|
@ -79,52 +104,21 @@ function fixturesUI(trialRun, suite) {
|
||||||
|
|
||||||
context.fdescribe = context.describe.only;
|
context.fdescribe = context.describe.only;
|
||||||
|
|
||||||
context.it = context.specify = function(title, fn) {
|
context.it = itBuilder();
|
||||||
const suite = suites[0];
|
|
||||||
if (suite.isPending())
|
|
||||||
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 test = new Test(title, wrapper);
|
|
||||||
test.file = file;
|
|
||||||
suite.addTest(test);
|
|
||||||
return test;
|
|
||||||
};
|
|
||||||
|
|
||||||
context.it.only = function(title, fn) {
|
context.it.only = function(title, fn) {
|
||||||
return common.test.only(mocha, context.it(title, fn));
|
return common.test.only(mocha, context.it(title, fn));
|
||||||
};
|
};
|
||||||
|
|
||||||
context.fit = context.it.only;
|
context.fit = context.it.only;
|
||||||
|
|
||||||
context.xit = function(title) {
|
context.xit = function(title) {
|
||||||
return context.it(title);
|
return context.it(title);
|
||||||
};
|
};
|
||||||
|
|
||||||
context.it.skip = function(condition) {
|
context.it.skip = function(condition) {
|
||||||
return condition ? context.xit : context.it;
|
return condition ? context.xit : context.it;
|
||||||
};
|
};
|
||||||
|
|
||||||
context.it.fail = function(condition) {
|
context.it.fail = function(condition) {
|
||||||
return condition ? context.xit : context.it;
|
return condition ? context.xit : context.it;
|
||||||
};
|
};
|
||||||
|
context.it.slow = () => itBuilder(['slow']);
|
||||||
context.it.slow = function(condition) {
|
|
||||||
return context.it;
|
|
||||||
};
|
|
||||||
|
|
||||||
context.it.retries = function(n) {
|
context.it.retries = function(n) {
|
||||||
context.retries(n);
|
context.retries(n);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ program
|
||||||
.option('--retries <retries>', 'number of times to retry a failing test', 1)
|
.option('--retries <retries>', 'number of times to retry a failing test', 1)
|
||||||
.action(async (command) => {
|
.action(async (command) => {
|
||||||
// Collect files
|
// Collect files
|
||||||
const files = collectFiles(command.args);
|
const files = [];
|
||||||
|
collectFiles(path.join(process.cwd(), 'test'), command.args, files);
|
||||||
const rootSuite = new Mocha.Suite('', new Mocha.Context(), true);
|
const rootSuite = new Mocha.Suite('', new Mocha.Context(), true);
|
||||||
|
|
||||||
// Build the test model, suite per file.
|
// Build the test model, suite per file.
|
||||||
|
|
@ -49,10 +50,6 @@ program
|
||||||
await new Promise(f => mocha.run(f));
|
await new Promise(f => mocha.run(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rootSuite.hasOnly())
|
|
||||||
rootSuite.filterOnly();
|
|
||||||
|
|
||||||
console.log(`Running ${rootSuite.total()} tests`);
|
|
||||||
const runner = new Runner(rootSuite, {
|
const runner = new Runner(rootSuite, {
|
||||||
maxWorkers: command.maxWorkers,
|
maxWorkers: command.maxWorkers,
|
||||||
reporter: command.reporter,
|
reporter: command.reporter,
|
||||||
|
|
@ -65,22 +62,23 @@ program
|
||||||
|
|
||||||
program.parse(process.argv);
|
program.parse(process.argv);
|
||||||
|
|
||||||
function collectFiles(args) {
|
function collectFiles(dir, filters, files) {
|
||||||
const testDir = path.join(process.cwd(), 'test');
|
for (const name of fs.readdirSync(dir)) {
|
||||||
const files = [];
|
if (fs.lstatSync(path.join(dir, name)).isDirectory()) {
|
||||||
for (const name of fs.readdirSync(testDir)) {
|
collectFiles(path.join(dir, name), filters, files);
|
||||||
if (!name.includes('.spec.'))
|
|
||||||
continue;
|
|
||||||
if (!args.length) {
|
|
||||||
files.push(path.join(testDir, name));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const filter of args) {
|
if (!name.includes('spec'))
|
||||||
|
continue;
|
||||||
|
if (!filters.length) {
|
||||||
|
files.push(path.join(dir, name));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const filter of filters) {
|
||||||
if (name.includes(filter)) {
|
if (name.includes(filter)) {
|
||||||
files.push(path.join(testDir, name));
|
files.push(path.join(dir, name));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return files;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ const builtinReporters = require('mocha/lib/reporters');
|
||||||
const DotRunner = require('./dotReporter');
|
const DotRunner = require('./dotReporter');
|
||||||
|
|
||||||
const constants = Mocha.Runner.constants;
|
const constants = Mocha.Runner.constants;
|
||||||
|
// Mocha runner does not remove uncaughtException listeners.
|
||||||
|
process.setMaxListeners(0);
|
||||||
|
|
||||||
class Runner extends EventEmitter {
|
class Runner extends EventEmitter {
|
||||||
constructor(suite, options) {
|
constructor(suite, options) {
|
||||||
|
|
@ -31,8 +33,9 @@ class Runner extends EventEmitter {
|
||||||
this._maxWorkers = options.maxWorkers;
|
this._maxWorkers = options.maxWorkers;
|
||||||
this._workers = new Set();
|
this._workers = new Set();
|
||||||
this._freeWorkers = [];
|
this._freeWorkers = [];
|
||||||
this._callbacks = [];
|
this._workerClaimers = [];
|
||||||
this._workerId = 0;
|
this._workerId = 0;
|
||||||
|
this._pendingJobs = 0;
|
||||||
this.stats = {
|
this.stats = {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
failures: 0,
|
failures: 0,
|
||||||
|
|
@ -45,6 +48,10 @@ class Runner extends EventEmitter {
|
||||||
|
|
||||||
this._tests = new Map();
|
this._tests = new Map();
|
||||||
this._files = new Map();
|
this._files = new Map();
|
||||||
|
|
||||||
|
if (suite.hasOnly())
|
||||||
|
suite.filterOnly();
|
||||||
|
console.log(`Running ${suite.total()} tests`);
|
||||||
this._traverse(suite);
|
this._traverse(suite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,60 +69,90 @@ class Runner extends EventEmitter {
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
this.emit(constants.EVENT_RUN_BEGIN, {});
|
this.emit(constants.EVENT_RUN_BEGIN, {});
|
||||||
const result = new Promise(f => this._runCallback = f);
|
|
||||||
for (const file of this._files.keys()) {
|
for (const file of this._files.keys()) {
|
||||||
const worker = await this._obtainWorker();
|
const worker = await this._obtainWorker();
|
||||||
worker.send({ method: 'run', params: { file, options: this._options } });
|
this._runJob(worker, file);
|
||||||
}
|
}
|
||||||
await result;
|
await new Promise(f => this._runCompleteCallback = f);
|
||||||
this.emit(constants.EVENT_RUN_END, {});
|
this.emit(constants.EVENT_RUN_END, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _obtainWorker() {
|
_runJob(worker, file) {
|
||||||
if (this._freeWorkers.length)
|
++this._pendingJobs;
|
||||||
return this._freeWorkers.pop();
|
worker.send({ method: 'run', params: { file, options: this._options } });
|
||||||
|
const messageListener = (message) => {
|
||||||
|
const { method, params } = message;
|
||||||
|
if (method !== 'done') {
|
||||||
|
this._messageFromWorker(method, params);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
worker.off('message', messageListener);
|
||||||
|
|
||||||
if (this._workers.size < this._maxWorkers) {
|
--this._pendingJobs;
|
||||||
const worker = child_process.fork(path.join(__dirname, 'worker.js'), {
|
this.stats.duration += params.stats.duration;
|
||||||
detached: false
|
this.stats.failures += params.stats.failures;
|
||||||
});
|
this.stats.passes += params.stats.passes;
|
||||||
let readyCallback;
|
this.stats.pending += params.stats.pending;
|
||||||
const result = new Promise(f => readyCallback = f);
|
this.stats.tests += params.stats.tests;
|
||||||
worker.send({ method: 'init', params: { workerId: ++this._workerId } });
|
if (params.error)
|
||||||
worker.on('message', message => {
|
this._restartWorker(worker);
|
||||||
if (message.method === 'ready')
|
else
|
||||||
readyCallback();
|
this._workerAvailable(worker);
|
||||||
this._messageFromWorker(worker, message);
|
if (this._runCompleteCallback && !this._pendingJobs)
|
||||||
});
|
this._runCompleteCallback();
|
||||||
worker.on('exit', () => {
|
};
|
||||||
this._workers.delete(worker);
|
worker.on('message', messageListener)
|
||||||
if (!this._workers.size)
|
|
||||||
this._stopCallback();
|
|
||||||
});
|
|
||||||
this._workers.add(worker);
|
|
||||||
await result;
|
|
||||||
return worker;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise(f => this._callbacks.push(f));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_messageFromWorker(worker, message) {
|
async _obtainWorker() {
|
||||||
const { method, params } = message;
|
// If there is worker, use it.
|
||||||
|
if (this._freeWorkers.length)
|
||||||
|
return this._freeWorkers.pop();
|
||||||
|
// If we can create worker, create it.
|
||||||
|
if (this._workers.size < this._maxWorkers)
|
||||||
|
this._createWorker();
|
||||||
|
// Wait for the next available worker.
|
||||||
|
await new Promise(f => this._workerClaimers.push(f));
|
||||||
|
return this._freeWorkers.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _workerAvailable(worker) {
|
||||||
|
this._freeWorkers.push(worker);
|
||||||
|
if (this._workerClaimers.length) {
|
||||||
|
const callback = this._workerClaimers.shift();
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createWorker() {
|
||||||
|
const worker = child_process.fork(path.join(__dirname, 'worker.js'), {
|
||||||
|
detached: false,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
worker.on('exit', () => {
|
||||||
|
this._workers.delete(worker);
|
||||||
|
if (this._stopCallback && !this._workers.size)
|
||||||
|
this._stopCallback();
|
||||||
|
});
|
||||||
|
this._workers.add(worker);
|
||||||
|
worker.send({ method: 'init', params: { workerId: ++this._workerId } });
|
||||||
|
worker.once('message', () => {
|
||||||
|
// Ready ack.
|
||||||
|
this._workerAvailable(worker);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopWorker(worker) {
|
||||||
|
worker.send({ method: 'stop' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async _restartWorker(worker) {
|
||||||
|
this._stopWorker(worker);
|
||||||
|
this._createWorker();
|
||||||
|
}
|
||||||
|
|
||||||
|
_messageFromWorker(method, params) {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'done': {
|
|
||||||
if (this._callbacks.length) {
|
|
||||||
const callback = this._callbacks.shift();
|
|
||||||
callback(worker);
|
|
||||||
} else {
|
|
||||||
this._freeWorkers.push(worker);
|
|
||||||
if (this._freeWorkers.length === this._workers.size)
|
|
||||||
this._runCallback();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'start':
|
|
||||||
break;
|
|
||||||
case 'test':
|
case 'test':
|
||||||
this.emit(constants.EVENT_TEST_BEGIN, this._updateTest(params.test));
|
this.emit(constants.EVENT_TEST_BEGIN, this._updateTest(params.test));
|
||||||
break;
|
break;
|
||||||
|
|
@ -126,30 +163,21 @@ class Runner extends EventEmitter {
|
||||||
this.emit(constants.EVENT_TEST_PASS, this._updateTest(params.test));
|
this.emit(constants.EVENT_TEST_PASS, this._updateTest(params.test));
|
||||||
break;
|
break;
|
||||||
case 'fail':
|
case 'fail':
|
||||||
const test = this._updateTest(params.test);
|
this.emit(constants.EVENT_TEST_FAIL, this._updateTest(params.test), params.error);
|
||||||
this.emit(constants.EVENT_TEST_FAIL, test, params.error);
|
|
||||||
break;
|
|
||||||
case 'end':
|
|
||||||
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.tests;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateTest(serialized) {
|
_updateTest(serialized) {
|
||||||
const test = this._tests.get(serialized.id);
|
const test = this._tests.get(serialized.id);
|
||||||
test._currentRetry = serialized.currentRetry;
|
test.duration = serialized.duration;
|
||||||
this.duration = serialized.duration;
|
|
||||||
return test;
|
return test;
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
const result = new Promise(f => this._stopCallback = f);
|
const result = new Promise(f => this._stopCallback = f);
|
||||||
for (const worker of this._workers)
|
for (const worker of this._workers)
|
||||||
worker.send({ method: 'stop' });
|
this._stopWorker(worker);
|
||||||
await result;
|
await result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ const outputPath = path.join(__dirname, '..', 'output-' + browserName);
|
||||||
global.expect = require('expect');
|
global.expect = require('expect');
|
||||||
global.testOptions = require('../harness/testOptions');
|
global.testOptions = require('../harness/testOptions');
|
||||||
|
|
||||||
|
const constants = Mocha.Runner.constants;
|
||||||
|
|
||||||
extendExpects();
|
extendExpects();
|
||||||
|
|
||||||
let closed = false;
|
let closed = false;
|
||||||
|
|
@ -33,9 +35,10 @@ 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.workerId;
|
process.env.JEST_WORKER_ID = message.params.workerId;
|
||||||
if (message.method === 'stop')
|
if (message.method === 'stop') {
|
||||||
|
await fixturePool.teardownScope('worker');
|
||||||
await gracefullyCloseAndExit();
|
await gracefullyCloseAndExit();
|
||||||
if (message.method === 'run')
|
} if (message.method === 'run')
|
||||||
await runSingleTest(message.params.file, message.params.options);
|
await runSingleTest(message.params.file, message.params.options);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -55,50 +58,45 @@ async function gracefullyCloseAndExit() {
|
||||||
|
|
||||||
class NullReporter {}
|
class NullReporter {}
|
||||||
|
|
||||||
|
let failedWithError = false;
|
||||||
|
|
||||||
async function runSingleTest(file, options) {
|
async function runSingleTest(file, options) {
|
||||||
let nextOrdinal = 0;
|
let lastOrdinal = -1;
|
||||||
const mocha = new Mocha({
|
const mocha = new Mocha({
|
||||||
ui: fixturesUI.bind(null, false),
|
ui: fixturesUI.bind(null, false),
|
||||||
retries: options.retries === 1 ? undefined : options.retries,
|
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
reporter: NullReporter
|
reporter: NullReporter
|
||||||
});
|
});
|
||||||
mocha.addFile(file);
|
mocha.addFile(file);
|
||||||
|
mocha.suite.filterOnly();
|
||||||
|
|
||||||
const runner = mocha.run();
|
const runner = mocha.run(() => {
|
||||||
|
// Runner adds these; if we don't remove them, we'll get a leak.
|
||||||
const constants = Mocha.Runner.constants;
|
process.removeAllListeners('uncaughtException');
|
||||||
runner.on(constants.EVENT_RUN_BEGIN, () => {
|
|
||||||
sendMessageToParent('start');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
runner.on(constants.EVENT_TEST_BEGIN, test => {
|
runner.on(constants.EVENT_TEST_BEGIN, test => {
|
||||||
// Retries will produce new test instances, store ordinal on the original function.
|
sendMessageToParent('test', { test: serializeTest(test, ++lastOrdinal) });
|
||||||
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 => {
|
||||||
// Pending does not get test begin signal, so increment ordinal.
|
sendMessageToParent('pending', { test: serializeTest(test, ++lastOrdinal) });
|
||||||
sendMessageToParent('pending', { test: serializeTest(test, nextOrdinal++) });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
runner.on(constants.EVENT_TEST_PASS, test => {
|
runner.on(constants.EVENT_TEST_PASS, test => {
|
||||||
sendMessageToParent('pass', { test: serializeTest(test, test.fn.__original.__ordinal) });
|
sendMessageToParent('pass', { test: serializeTest(test, lastOrdinal) });
|
||||||
});
|
});
|
||||||
|
|
||||||
runner.on(constants.EVENT_TEST_FAIL, (test, error) => {
|
runner.on(constants.EVENT_TEST_FAIL, (test, error) => {
|
||||||
|
failedWithError = error;
|
||||||
sendMessageToParent('fail', {
|
sendMessageToParent('fail', {
|
||||||
test: serializeTest(test, test.fn.__original.__ordinal),
|
test: serializeTest(test, lastOrdinal),
|
||||||
error: serializeError(error),
|
error: serializeError(error),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
runner.once(constants.EVENT_RUN_END, async () => {
|
runner.once(constants.EVENT_RUN_END, async () => {
|
||||||
sendMessageToParent('end', { stats: serializeStats(runner.stats) });
|
sendMessageToParent('done', { stats: serializeStats(runner.stats), error: failedWithError });
|
||||||
sendMessageToParent('done');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,9 +113,7 @@ function sendMessageToParent(method, params = {}) {
|
||||||
function serializeTest(test, origin) {
|
function serializeTest(test, origin) {
|
||||||
return {
|
return {
|
||||||
id: `${test.file}::${origin}`,
|
id: `${test.file}::${origin}`,
|
||||||
currentRetry: test.currentRetry(),
|
|
||||||
duration: test.duration,
|
duration: test.duration,
|
||||||
title: test.title,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue