chore(test): run doclint tests with mocha, delete testrunner again (#3447)

This commit is contained in:
Joel Einbinder 2020-08-13 13:57:27 -07:00 committed by GitHub
parent e2cfb05786
commit 84441f8f77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 47 additions and 3734 deletions

View File

@ -18,9 +18,8 @@
"tsc-installer": "tsc -p ./src/install/tsconfig.json", "tsc-installer": "tsc -p ./src/install/tsconfig.json",
"doc": "node utils/doclint/cli.js", "doc": "node utils/doclint/cli.js",
"doc-no-channel": "node utils/doclint/cli.js --no-channel", "doc-no-channel": "node utils/doclint/cli.js --no-channel",
"test-infra": "node utils/doclint/check_public_api/test/test.js && node utils/doclint/preprocessor/test.js", "test-infra": "node test/runner utils/doclint/check_public_api/test/test.js && node test/runner utils/doclint/preprocessor/test.js",
"lint": "npm run eslint && npm run tsc && npm run doc && npm run doc-no-channel && npm run check-deps && npm run generate-channels && npm run test-types && npm run test-infra", "lint": "npm run eslint && npm run tsc && npm run doc && npm run doc-no-channel && npm run check-deps && npm run generate-channels && npm run test-types && npm run test-infra",
"debug-test": "node --inspect-brk test/test.js",
"clean": "rimraf lib && rimraf types", "clean": "rimraf lib && rimraf types",
"prepare": "node install-from-github.js", "prepare": "node install-from-github.js",
"build": "node utils/runWebpack.js --mode='development' && tsc -p . && npm run generate-types", "build": "node utils/runWebpack.js --mode='development' && tsc -p . && npm run generate-types",

View File

@ -92,8 +92,8 @@ function compare(actual, golden) {
const goldenPath = path.normalize(golden.goldenPath); const goldenPath = path.normalize(golden.goldenPath);
const outputPath = path.normalize(golden.outputPath); const outputPath = path.normalize(golden.outputPath);
const goldenName = golden.goldenName; const goldenName = golden.goldenName;
const expectedPath = path.join(goldenPath, goldenName); const expectedPath = path.resolve(goldenPath, goldenName);
const actualPath = path.join(outputPath, goldenName); const actualPath = path.resolve(outputPath, goldenName);
const messageSuffix = 'Output is saved in "' + path.basename(outputPath + '" directory'); const messageSuffix = 'Output is saved in "' + path.basename(outputPath + '" directory');

View File

@ -19,7 +19,7 @@ const Mocha = require('mocha');
const { registerWorkerFixture } = require('./fixturePool'); const { registerWorkerFixture } = require('./fixturePool');
const { fixturesUI, fixturePool } = require('./fixturesUI'); const { fixturesUI, fixturePool } = require('./fixturesUI');
const { gracefullyCloseAll } = require('../../lib/server/processLauncher'); const { gracefullyCloseAll } = require('../../lib/server/processLauncher');
const GoldenUtils = require('../../utils/testrunner/GoldenUtils'); const GoldenUtils = require('./GoldenUtils');
const browserName = process.env.BROWSER || 'chromium'; const browserName = process.env.BROWSER || 'chromium';
const goldenPath = path.join(__dirname, '..', 'golden-' + browserName); const goldenPath = path.join(__dirname, '..', 'golden-' + browserName);

View File

@ -21,70 +21,58 @@ const Source = require('../../Source');
const mdBuilder = require('../MDBuilder'); const mdBuilder = require('../MDBuilder');
const jsBuilder = require('../JSBuilder'); const jsBuilder = require('../JSBuilder');
const TestRunner = require('../../../testrunner/'); registerWorkerFixture('page', async({}, test) => {
const runner = new TestRunner({ const browser = await playwright.chromium.launch();
goldenPath: __dirname, const page = await browser.newPage();
outputPath: __dirname await test(page);
});
const {describe, xdescribe, fdescribe} = runner.api();
const {it, fit, xit} = runner.api();
const {beforeAll, beforeEach, afterAll, afterEach} = runner.api();
const {expect} = runner.api();
let browser;
let page;
beforeAll(async function() {
browser = await playwright.chromium.launch();
page = await browser.newPage();
});
afterAll(async function() {
await browser.close(); await browser.close();
}); });
describe('checkPublicAPI', function() { describe('checkPublicAPI', function() {
it('diff-classes', testLint); testLint('diff-classes');
it('diff-methods', testLint); testLint('diff-methods');
it('diff-properties', testLint); testLint('diff-properties');
it('diff-arguments', testLint); testLint('diff-arguments');
it('diff-events', testLint); testLint('diff-events');
it('check-duplicates', testLint); testLint('check-duplicates');
it('check-sorting', testLint); testLint('check-sorting');
it('check-returns', testLint); testLint('check-returns');
it('check-nullish', testLint); testLint('check-nullish');
it('js-builder-common', testJSBuilder); testJSBuilder('js-builder-common');
it('js-builder-inheritance', testJSBuilder); testJSBuilder('js-builder-inheritance');
it('md-builder-common', testMDBuilder); testMDBuilder('md-builder-common');
it('md-builder-comments', testMDBuilder); testMDBuilder('md-builder-comments');
}); });
runner.run(); async function testLint(name) {
it(name, async({page}) => {
async function testLint(state, testRun) { const dirPath = path.join(__dirname, name);
const dirPath = path.join(__dirname, testRun.test().name()); const mdSources = await Source.readdir(dirPath, '.md');
const mdSources = await Source.readdir(dirPath, '.md'); const tsSources = await Source.readdir(dirPath, '.ts');
const tsSources = await Source.readdir(dirPath, '.ts'); const jsSources = await Source.readdir(dirPath, '.js');
const jsSources = await Source.readdir(dirPath, '.js'); const messages = await checkPublicAPI(page, mdSources, jsSources.concat(tsSources));
const messages = await checkPublicAPI(page, mdSources, jsSources.concat(tsSources)); const errors = messages.map(message => message.text);
const errors = messages.map(message => message.text); expect(errors.join('\n')).toBeGolden(path.join(dirPath, 'result.txt'));
expect(errors.join('\n')).toBeGolden(path.join(testRun.test().name(), 'result.txt')); });
} }
async function testMDBuilder(state, testRun) { async function testMDBuilder(name) {
const dirPath = path.join(__dirname, testRun.test().name()); it(name, async({page}) => {
const sources = await Source.readdir(dirPath, '.md'); const dirPath = path.join(__dirname, name);
const {documentation} = await mdBuilder(page, sources); const sources = await Source.readdir(dirPath, '.md');
expect(serialize(documentation)).toBeGolden(path.join(testRun.test().name(), 'result.txt')); const {documentation} = await mdBuilder(page, sources);
expect(serialize(documentation)).toBeGolden(path.join(dirPath, 'result.txt'));
});
} }
async function testJSBuilder(state, testRun) { async function testJSBuilder(name) {
const dirPath = path.join(__dirname, testRun.test().name()); it(name, async() => {
const jsSources = await Source.readdir(dirPath, '.js'); const dirPath = path.join(__dirname, name);
const tsSources = await Source.readdir(dirPath, '.ts'); const jsSources = await Source.readdir(dirPath, '.js');
const {documentation} = await jsBuilder.checkSources(jsSources.concat(tsSources)); const tsSources = await Source.readdir(dirPath, '.ts');
expect(serialize(documentation)).toBeGolden(path.join(testRun.test().name(), 'result.txt')); const {documentation} = await jsBuilder.checkSources(jsSources.concat(tsSources));
expect(serialize(documentation)).toBeGolden(path.join(dirPath, 'result.txt'));
});
} }
/** /**

View File

@ -14,15 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
const {runCommands, ensureTipOfTreeAPILinks} = require('.'); const {runCommands} = require('.');
const Source = require('../Source'); const Source = require('../Source');
const TestRunner = require('../../testrunner/');
const runner = new TestRunner();
const {describe, xdescribe, fdescribe} = runner.api();
const {it, fit, xit} = runner.api();
const {beforeAll, beforeEach, afterAll, afterEach} = runner.api();
const {expect} = runner.api();
describe('runCommands', function() { describe('runCommands', function() {
const OPTIONS_REL = { const OPTIONS_REL = {
@ -202,5 +195,3 @@ describe('runCommands', function() {
}); });
}); });
runner.run();

View File

@ -1,13 +0,0 @@
# exclude all examples and README.md
examples/
README.md
# repeats from .gitignore
node_modules
.npmignore
.DS_Store
*.swp
*.pyc
.vscode
package-lock.json
yarn.lock

View File

@ -1,88 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* 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');
// Hack for our own tests.
const testRunnerTestFile = path.join(__dirname, 'test', 'testrunner.spec.js');
class Location {
constructor() {
this._fileName = '';
this._filePath = '';
this._lineNumber = 0;
this._columnNumber = 0;
}
fileName() {
return this._fileName;
}
filePath() {
return this._filePath;
}
lineNumber() {
return this._lineNumber;
}
columnNumber() {
return this._columnNumber;
}
toString() {
return this._fileName + ':' + this._lineNumber;
}
toDetailedString() {
return this._fileName + ':' + this._lineNumber + ':' + this._columnNumber;
}
static getCallerLocation(ignorePrefix = __dirname) {
const error = new Error();
const stackFrames = error.stack.split('\n').slice(1);
const location = new Location();
// Find first stackframe that doesn't point to this file.
for (let frame of stackFrames) {
frame = frame.trim();
if (!frame.startsWith('at '))
return null;
if (frame.endsWith(')')) {
const from = frame.indexOf('(');
frame = frame.substring(from + 1, frame.length - 1);
} else {
frame = frame.substring('at '.length);
}
const match = frame.match(/^(.*):(\d+):(\d+)$/);
if (!match)
return null;
const filePath = match[1];
if (filePath === __filename || (filePath.startsWith(ignorePrefix) && filePath !== testRunnerTestFile))
continue;
location._filePath = filePath;
location._fileName = filePath.split(path.sep).pop();
location._lineNumber = parseInt(match[2], 10);
location._columnNumber = parseInt(match[3], 10);
return location;
}
return location;
}
}
module.exports = Location;

View File

@ -1,247 +0,0 @@
/**
* Copyright 2017 Google Inc. 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 Location = require('./Location.js');
const colors = require('colors/safe');
const Diff = require('text-diff');
const GoldenUtils = require('./GoldenUtils');
class Matchers {
constructor(config) {
this.expect = this.expect.bind(this);
this._matchers = {
toBe: function(received, expected, message) {
message = message || `${received} == ${expected}`;
return { pass: received === expected, message, formatter: toBeFormatter.bind(null, received, expected) };
},
toBeFalsy: function(received, message) {
message = message || `${received}`;
return { pass: !received, message };
},
toBeTruthy: function(received, message) {
message = message || `${received}`;
return { pass: !!received, message };
},
toBeGreaterThan: function(received, other, message) {
message = message || `${received} > ${other}`;
return { pass: received > other, message };
},
toBeGreaterThanOrEqual: function(received, other, message) {
message = message || `${received} >= ${other}`;
return { pass: received >= other, message };
},
toBeLessThan: function(received, other, message) {
message = message || `${received} < ${other}`;
return { pass: received < other, message };
},
toBeLessThanOrEqual: function(received, other, message) {
message = message || `${received} <= ${other}`;
return { pass: received <= other, message };
},
toBeNull: function(received, message) {
message = message || `${received} == null`;
return { pass: received === null, message };
},
toContain: function(received, other, message) {
message = message || `${received}${other}`;
return { pass: received.includes(other), message };
},
toEqual: function(received, other, message) {
let receivedJson = stringify(received);
let otherJson = stringify(other);
let formatter = objectFormatter.bind(null, receivedJson, otherJson);
if (receivedJson.length < 40 && otherJson.length < 40) {
receivedJson = receivedJson.split('\n').map(line => line.trim()).join(' ');
otherJson = otherJson.split('\n').map(line => line.trim()).join(' ');
formatter = stringFormatter.bind(null, receivedJson, otherJson);
}
message = message || `\n${receivedJson}${otherJson}`;
return { pass: receivedJson === otherJson, message, formatter };
},
toBeCloseTo: function(received, other, precision, message) {
return {
pass: Math.abs(received - other) < Math.pow(10, -precision),
message
};
},
toBeInstanceOf: function(received, other, message) {
message = message || `${received.constructor.name} instanceof ${other.name}`;
return { pass: received instanceof other, message };
},
toBeGolden: function(received, goldenName) {
return GoldenUtils.compare(received, {
goldenPath: config.goldenPath,
outputPath: config.outputPath,
goldenName
});
},
toMatch: function(received, other, message) {
message = message || `${received}`;
return { pass: received.match(other), message };
}
}
}
expect(received) {
return new Expect(received, this._matchers);
}
};
class MatchError extends Error {
constructor(message, formatter) {
super(message);
this.name = this.constructor.name;
this.formatter = formatter;
this.location = Location.getCallerLocation();
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = {Matchers, MatchError};
class Expect {
constructor(received, matchers) {
this.not = {};
this.not.not = this;
for (const matcherName of Object.keys(matchers)) {
const matcher = matchers[matcherName];
this[matcherName] = applyMatcher.bind(null, matcherName, matcher, false /* inverse */, received);
this.not[matcherName] = applyMatcher.bind(null, matcherName, matcher, true /* inverse */, received);
}
function applyMatcher(matcherName, matcher, inverse, received, ...args) {
const result = matcher.call(null, received, ...args);
const message = `expect.${inverse ? 'not.' : ''}${matcherName} failed` + (result.message ? `: ${result.message}` : '');
if (result.pass === inverse)
throw new MatchError(message, result.formatter || defaultFormatter.bind(null, received));
}
}
}
function defaultFormatter(received) {
return `Received: ${colors.red(JSON.stringify(received))}`;
}
function stringFormatter(received, expected) {
const diff = new Diff();
const result = diff.main(expected, received);
diff.cleanupSemantic(result);
const highlighted = result.map(([type, text]) => {
if (type === -1)
return colors.bgRed(text);
if (type === 1)
return colors.bgGreen.black(text);
return text;
}).join('');
const output = [
`Expected: ${expected}`,
`Received: ${received}`,
` Diff: ${highlighted}`,
];
for (let i = 0; i < Math.min(expected.length, received.length); ++i) {
if (expected[i] !== received[i]) {
const padding = ' '.repeat(' Diff: '.length);
const firstDiffCharacter = '~'.repeat(i) + '^';
output.push(colors.red(padding + firstDiffCharacter));
break;
}
}
return output.join('\n');
}
function objectFormatter(received, expected) {
const encodingMap = new Map();
const decodingMap = new Map();
const doEncodeLines = (lines) => {
let encoded = '';
for (const line of lines) {
let code = encodingMap.get(line);
if (!code) {
code = String.fromCodePoint(encodingMap.size);
encodingMap.set(line, code);
decodingMap.set(code, line);
}
encoded += code;
}
return encoded;
};
const doDecodeLines = (text) => {
let decoded = [];
for (const codepoint of [...text])
decoded.push(decodingMap.get(codepoint));
return decoded;
}
let receivedEncoded = doEncodeLines(received.split('\n'));
let expectedEncoded = doEncodeLines(expected.split('\n'));
const diff = new Diff();
const result = diff.main(expectedEncoded, receivedEncoded);
diff.cleanupSemantic(result);
const highlighted = result.map(([type, text]) => {
const lines = doDecodeLines(text);
if (type === -1)
return lines.map(line => '- ' + colors.bgRed(line));
if (type === 1)
return lines.map(line => '+ ' + colors.bgGreen.black(line));
return lines.map(line => ' ' + line);
});
const flattened = [];
for (const list of highlighted)
flattened.push(...list);
return `Received:\n${flattened.join('\n')}`;
}
function toBeFormatter(received, expected) {
if (typeof expected === 'string' && typeof received === 'string') {
return stringFormatter(JSON.stringify(received), JSON.stringify(expected));
}
return [
`Expected: ${JSON.stringify(expected)}`,
`Received: ${colors.red(JSON.stringify(received))}`,
].join('\n');
}
function stringify(value) {
function stabilize(key, object) {
if (typeof object !== 'object' || object === undefined || object === null || Array.isArray(object))
return object;
const result = {};
for (const key of Object.keys(object).sort())
result[key] = object[key];
return result;
}
return JSON.stringify(stabilize(null, value), stabilize, 2);
}

View File

@ -1,55 +0,0 @@
# TestRunner
This test runner is used internally by Playwright to test Playwright itself.
- testrunner is a *library*: tests are `node.js` scripts
- parallel wrt IO operations
- supports async/await
- modular
- well-isolated state per execution thread
### Example
Save the following as `test.js` and run using `node`:
```sh
node test.js
```
```js
const {TestRunner, Reporter, Matchers} = require('.');
// Runner holds and runs all the tests
const runner = new TestRunner({
parallel: 2, // run 2 parallel threads
timeout: 1000, // setup timeout of 1 second per test
});
// Simple expect-like matchers
const {expect} = new Matchers();
// Extract jasmine-like DSL into the global namespace
const {describe, xdescribe, fdescribe} = runner;
const {it, fit, xit} = runner;
const {beforeAll, beforeEach, afterAll, afterEach} = runner;
// Test hooks can be async.
beforeAll(async state => {
state.parallelIndex; // either 0 or 1 in this example, depending on the executing thread
state.foo = 'bar'; // set state for every test
});
describe('math', () => {
it('to be sane', async (state, test) => {
state.parallelIndex; // Very first test will always be ran by the 0's thread
state.foo; // this will be 'bar'
expect(2 + 2).toBe(4);
});
});
// Reporter subscribes to TestRunner events and displays information in terminal
const reporter = new Reporter(runner);
// Run all tests.
runner.run();
```

View File

@ -1,271 +0,0 @@
/**
* Copyright 2017 Google Inc. 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 fs = require('fs');
const path = require('path');
const colors = require('colors/safe');
const {MatchError} = require('./Matchers.js');
class Reporter {
constructor(delegate, options = {}) {
const {
showSlowTests = 3,
showMarkedAsFailingTests = Infinity,
verbose = false,
summary = true,
lineBreak = 0,
} = options;
this._filePathToLines = new Map();
this._delegate = delegate;
this._showSlowTests = showSlowTests;
this._showMarkedAsFailingTests = showMarkedAsFailingTests;
this._verbose = verbose;
this._summary = summary;
this._lineBreak = lineBreak;
this._testCounter = 0;
}
onStarted(testRuns) {
this._testCounter = 0;
this._timestamp = Date.now();
if (!this._delegate.hasFocusedTestsOrSuitesOrFiles()) {
console.log(`Running all ${colors.yellow(testRuns.length)} tests on ${colors.yellow(this._delegate.parallel())} worker${this._delegate.parallel() > 1 ? 's' : ''}:\n`);
} else {
console.log(`Running ${colors.yellow(testRuns.length)} focused tests out of total ${colors.yellow(this._delegate.testCount())} on ${colors.yellow(this._delegate.parallel())} worker${this._delegate.parallel() > 1 ? 's' : ''}`);
console.log('');
const focusedFilePaths = this._delegate.focusedFilePaths();
if (focusedFilePaths.length) {
console.log('Focused Files:');
for (let i = 0; i < focusedFilePaths.length; ++i)
console.log(` ${i + 1}) ${colors.yellow(path.basename(focusedFilePaths[i]))}`);
console.log('');
}
const focusedEntities = [
...this._delegate.focusedSuites(),
...this._delegate.focusedTests(),
];
if (focusedEntities.length) {
console.log('Focused Suites and Tests:');
for (let i = 0; i < focusedEntities.length; ++i)
console.log(` ${i + 1}) ${focusedEntities[i].fullName()} (${formatLocation(focusedEntities[i].location())})`);
console.log('');
}
}
}
_printFailedResult(result) {
console.log(colors.red(`## ${result.result.toUpperCase()} ##`));
if (result.message) {
console.log('Message:');
console.log(` ${colors.red(result.message)}`);
}
for (let i = 0; i < result.errors.length; i++) {
const { message, error, runs } = result.errors[i];
console.log(`\n${colors.magenta('NON-TEST ERROR #' + i)}: ${message}`);
if (error && error.stack)
console.log(padLines(error.stack, 2));
const lastRuns = runs.slice(runs.length - Math.min(10, runs.length));
if (lastRuns.length)
console.log(`WORKER STATE`);
for (let j = 0; j < lastRuns.length; j++)
this._printVerboseTestRunResult(j, lastRuns[j]);
}
console.log('');
console.log('');
}
onFinished(result) {
this._printTestResults(result);
if (!result.ok())
this._printFailedResult(result);
process.exitCode = result.exitCode;
}
_printTestResults(result) {
// 2 newlines after completing all tests.
console.log('\n');
const runs = result.runs;
const failedRuns = runs.filter(run => run.isFailure());
const executedRuns = runs.filter(run => run.result());
const okRuns = runs.filter(run => run.ok());
const skippedRuns = runs.filter(run => run.result() === 'skipped');
const markedAsFailingRuns = runs.filter(run => run.result() === 'markedAsFailing');
if (this._summary && failedRuns.length > 0) {
console.log('\nFailures:');
for (let i = 0; i < failedRuns.length; ++i) {
this._printVerboseTestRunResult(i + 1, failedRuns[i]);
console.log('');
}
}
if (this._showMarkedAsFailingTests && this._summary && markedAsFailingRuns.length) {
if (markedAsFailingRuns.length > 0) {
console.log('\nMarked as failing:');
markedAsFailingRuns.slice(0, this._showMarkedAsFailingTests).forEach((testRun, index) => {
console.log(`${index + 1}) ${testRun.test().fullName()} (${formatLocation(testRun.test().location())})`);
});
}
if (this._showMarkedAsFailingTests < markedAsFailingRuns.length) {
console.log('');
console.log(`... and ${colors.yellow(markedAsFailingRuns.length - this._showMarkedAsFailingTests)} more marked as failing tests ...`);
}
}
if (this._showSlowTests) {
const slowRuns = okRuns.sort((a, b) => b.duration() - a.duration()).slice(0, this._showSlowTests);
console.log(`\nSlowest tests:`);
for (let i = 0; i < slowRuns.length; ++i) {
const run = slowRuns[i];
console.log(` (${i + 1}) ${colors.yellow((run.duration() / 1000) + 's')} - ${run.test().fullName()} (${formatLocation(run.test().location())})`);
}
}
let summaryText = '';
if (failedRuns.length || markedAsFailingRuns.length) {
const summary = [`ok - ${colors.green(okRuns.length)}`];
if (failedRuns.length)
summary.push(`failed - ${colors.red(failedRuns.length)}`);
if (markedAsFailingRuns.length)
summary.push(`marked as failing - ${colors.yellow(markedAsFailingRuns.length)}`);
if (skippedRuns.length)
summary.push(`skipped - ${colors.yellow(skippedRuns.length)}`);
summaryText = ` (${summary.join(', ')})`;
}
console.log(`\nRan ${executedRuns.length}${summaryText} of ${runs.length} test${runs.length > 1 ? 's' : ''}`);
const milliseconds = Date.now() - this._timestamp;
const seconds = milliseconds / 1000;
console.log(`Finished in ${colors.yellow(seconds)} seconds`);
}
onTestRunStarted(testRun) {
}
onTestRunFinished(testRun) {
++this._testCounter;
if (this._verbose) {
this._printVerboseTestRunResult(this._testCounter, testRun);
} else {
if (testRun.result() === 'ok')
process.stdout.write(colors.green('\u00B7'));
else if (testRun.result() === 'skipped')
process.stdout.write(colors.yellow('\u00B7'));
else if (testRun.result() === 'markedAsFailing')
process.stdout.write(colors.yellow('\u00D7'));
else if (testRun.result() === 'failed')
process.stdout.write(colors.red('F'));
else if (testRun.result() === 'crashed')
process.stdout.write(colors.red('C'));
else if (testRun.result() === 'terminated')
process.stdout.write(colors.magenta('.'));
else if (testRun.result() === 'timedout')
process.stdout.write(colors.red('T'));
if (this._lineBreak && !(this._testCounter % this._lineBreak))
process.stdout.write('\n');
}
}
_printVerboseTestRunResult(resultIndex, testRun) {
const test = testRun.test();
let prefix = `${resultIndex})`;
if (this._delegate.parallel() > 1)
prefix += ' ' + colors.gray(`[worker = ${testRun.workerId()}]`);
if (testRun.result() === 'ok') {
console.log(`${prefix} ${colors.green('[OK]')} ${test.fullName()} (${formatLocation(test.location())})`);
} else if (testRun.result() === 'terminated') {
console.log(`${prefix} ${colors.magenta('[TERMINATED]')} ${test.fullName()} (${formatLocation(test.location())})`);
} else if (testRun.result() === 'crashed') {
console.log(`${prefix} ${colors.red('[CRASHED]')} ${test.fullName()} (${formatLocation(test.location())})`);
} else if (testRun.result() === 'skipped') {
} else if (testRun.result() === 'markedAsFailing') {
console.log(`${prefix} ${colors.yellow('[MARKED AS FAILING]')} ${test.fullName()} (${formatLocation(test.location())})`);
} else if (testRun.result() === 'timedout') {
console.log(`${prefix} ${colors.red(`[TIMEOUT ${test.timeout()}ms]`)} ${test.fullName()} (${formatLocation(test.location())})`);
const output = testRun.output();
if (output.length) {
console.log(' Output:');
for (const line of output)
console.log(' ' + line);
}
} else if (testRun.result() === 'failed') {
console.log(`${prefix} ${colors.red('[FAIL]')} ${test.fullName()} (${formatLocation(test.location())})`);
if (testRun.error() instanceof MatchError) {
const location = testRun.error().location;
let lines = this._filePathToLines.get(location.filePath());
if (!lines) {
try {
lines = fs.readFileSync(location.filePath(), 'utf8').split('\n');
} catch (e) {
lines = [];
}
this._filePathToLines.set(location.filePath(), lines);
}
const lineNumber = location.lineNumber();
if (lineNumber < lines.length) {
const lineNumberLength = (lineNumber + 1 + '').length;
const FROM = Math.max(0, lineNumber - 5);
const snippet = lines.slice(FROM, lineNumber).map((line, index) => ` ${(FROM + index + 1 + '').padStart(lineNumberLength, ' ')} | ${line}`).join('\n');
const pointer = ` ` + ' '.repeat(lineNumberLength) + ' ' + '~'.repeat(location.columnNumber() - 1) + '^';
console.log('\n' + snippet + '\n' + colors.grey(pointer) + '\n');
}
console.log(padLines(testRun.error().formatter(), 4));
console.log('');
} else {
console.log(' Message:');
let message = '' + (testRun.error().message || testRun.error());
if (testRun.error().stack && message.includes(testRun.error().stack))
message = message.substring(0, message.indexOf(testRun.error().stack));
if (message)
console.log(` ${colors.red(message)}`);
if (testRun.error().stack) {
console.log(' Stack:');
let stack = testRun.error().stack;
// Highlight first test location, if any.
const match = stack.match(new RegExp(test.location().filePath() + ':(\\d+):(\\d+)'));
if (match) {
const [, line, column] = match;
const fileName = `${test.location().fileName()}:${line}:${column}`;
stack = stack.substring(0, match.index) + stack.substring(match.index).replace(fileName, colors.yellow(fileName));
}
console.log(padLines(stack, 4));
}
}
const output = testRun.output();
if (output.length) {
console.log(' Output:');
for (const line of output)
console.log(' ' + line);
}
}
}
}
function formatLocation(location) {
if (!location)
return '';
return colors.yellow(`${location.toDetailedString()}`);
}
function padLines(text, spaces = 0) {
const indent = ' '.repeat(spaces);
return text.split('\n').map(line => indent + line).join('\n');
}
module.exports = Reporter;

View File

@ -1,460 +0,0 @@
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
const path = require('path');
function upperBound(array, object, comparator, left, right) {
function defaultComparator(a, b) {
return a < b ? -1 : (a > b ? 1 : 0);
}
comparator = comparator || defaultComparator;
let l = left || 0;
let r = right !== undefined ? right : array.length;
while (l < r) {
const m = (l + r) >> 1;
if (comparator(object, array[m]) >= 0) {
l = m + 1;
} else {
r = m;
}
}
return r;
}
/**
* @interface
*/
class SourceMap {
/**
* @return {string}
*/
compiledURL() {
}
/**
* @return {string}
*/
url() {
}
/**
* @return {!Array<string>}
*/
sourceURLs() {
}
/**
* @param {string} sourceURL
* @return {?string}
*/
embeddedContentByURL(sourceURL) {
}
/**
* @param {number} lineNumber in compiled resource
* @param {number} columnNumber in compiled resource
* @return {?SourceMapEntry}
*/
findEntry(lineNumber, columnNumber) {
}
/**
* @param {string} sourceURL
* @param {number} lineNumber
* @param {number} columnNumber
* @return {?SourceMapEntry}
*/
sourceLineMapping(sourceURL, lineNumber, columnNumber) {
}
/**
* @return {!Array<!SourceMapEntry>}
*/
mappings() {
}
dispose() {
}
}
/**
* @unrestricted
*/
class SourceMapV3 {
constructor() {
/** @type {number} */ this.version;
/** @type {string|undefined} */ this.file;
/** @type {!Array.<string>} */ this.sources;
/** @type {!Array.<!SourceMapV3.Section>|undefined} */ this.sections;
/** @type {string} */ this.mappings;
/** @type {string|undefined} */ this.sourceRoot;
/** @type {!Array.<string>|undefined} */ this.names;
}
}
/**
* @unrestricted
*/
SourceMapV3.Section = class {
constructor() {
/** @type {!SourceMapV3} */ this.map;
/** @type {!SourceMapV3.Offset} */ this.offset;
}
};
/**
* @unrestricted
*/
SourceMapV3.Offset = class {
constructor() {
/** @type {number} */ this.line;
/** @type {number} */ this.column;
}
};
/**
* @unrestricted
*/
class SourceMapEntry {
/**
* @param {number} lineNumber
* @param {number} columnNumber
* @param {string=} sourceURL
* @param {number=} sourceLineNumber
* @param {number=} sourceColumnNumber
* @param {string=} name
*/
constructor(lineNumber, columnNumber, sourceURL, sourceLineNumber, sourceColumnNumber, name) {
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
this.sourceURL = sourceURL;
this.sourceLineNumber = sourceLineNumber;
this.sourceColumnNumber = sourceColumnNumber;
this.name = name;
}
/**
* @param {!SourceMapEntry} entry1
* @param {!SourceMapEntry} entry2
* @return {number}
*/
static compare(entry1, entry2) {
if (entry1.lineNumber !== entry2.lineNumber) {
return entry1.lineNumber - entry2.lineNumber;
}
return entry1.columnNumber - entry2.columnNumber;
}
}
/**
* @implements {SourceMap}
* @unrestricted
*/
class TextSourceMap {
/**
* Implements Source Map V3 model. See https://github.com/google/closure-compiler/wiki/Source-Maps
* for format description.
* @param {string} compiledURL
* @param {string} sourceMappingURL
* @param {!SourceMapV3} payload
*/
constructor(compiledURL, sourceMappingURL, payload) {
if (!TextSourceMap._base64Map) {
const base64Digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
TextSourceMap._base64Map = {};
for (let i = 0; i < base64Digits.length; ++i) {
TextSourceMap._base64Map[base64Digits.charAt(i)] = i;
}
}
this._json = payload;
this._compiledURL = compiledURL;
this._sourceMappingURL = sourceMappingURL;
this._baseURL = sourceMappingURL.startsWith('data:') ? compiledURL : sourceMappingURL;
/** @type {?Array<!SourceMapEntry>} */
this._mappings = null;
/** @type {!Map<string, !TextSourceMap.SourceInfo>} */
this._sourceInfos = new Map();
if (this._json.sections) {
const sectionWithURL = !!this._json.sections.find(section => !!section.url);
if (sectionWithURL) {
cosole.warn(`SourceMap "${sourceMappingURL}" contains unsupported "URL" field in one of its sections.`);
}
}
this._eachSection(this._parseSources.bind(this));
}
/**
* @override
* @return {string}
*/
compiledURL() {
return this._compiledURL;
}
/**
* @override
* @return {string}
*/
url() {
return this._sourceMappingURL;
}
/**
* @override
* @return {!Array.<string>}
*/
sourceURLs() {
return Array.from(this._sourceInfos.keys());
}
/**
* @override
* @param {string} sourceURL
* @return {?string}
*/
embeddedContentByURL(sourceURL) {
if (!this._sourceInfos.has(sourceURL)) {
return null;
}
return this._sourceInfos.get(sourceURL).content;
}
/**
* @override
* @param {number} lineNumber in compiled resource
* @param {number} columnNumber in compiled resource
* @return {?SourceMapEntry}
*/
findEntry(lineNumber, columnNumber) {
const mappings = this.mappings();
const index = upperBound(mappings, undefined, (unused, entry) => lineNumber - entry.lineNumber || columnNumber - entry.columnNumber);
return index ? mappings[index - 1] : null;
}
/**
* @override
* @return {!Array<!SourceMapEntry>}
*/
mappings() {
if (this._mappings === null) {
this._mappings = [];
this._eachSection(this._parseMap.bind(this));
this._json = null;
}
return /** @type {!Array<!SourceMapEntry>} */ (this._mappings);
}
/**
* @param {function(!SourceMapV3, number, number)} callback
*/
_eachSection(callback) {
if (!this._json.sections) {
callback(this._json, 0, 0);
return;
}
for (const section of this._json.sections) {
callback(section.map, section.offset.line, section.offset.column);
}
}
/**
* @param {!SourceMapV3} sourceMap
*/
_parseSources(sourceMap) {
const sourcesList = [];
let sourceRoot = sourceMap.sourceRoot || '';
if (sourceRoot && !sourceRoot.endsWith('/')) {
sourceRoot += '/';
}
for (let i = 0; i < sourceMap.sources.length; ++i) {
const href = sourceRoot + sourceMap.sources[i];
let url = path.resolve(path.dirname(this._baseURL), href);
const source = sourceMap.sourcesContent && sourceMap.sourcesContent[i];
if (url === this._compiledURL && source) {
url += '? [sm]';
}
this._sourceInfos.set(url, new TextSourceMap.SourceInfo(source, null));
sourcesList.push(url);
}
sourceMap[TextSourceMap._sourcesListSymbol] = sourcesList;
}
/**
* @param {!SourceMapV3} map
* @param {number} lineNumber
* @param {number} columnNumber
*/
_parseMap(map, lineNumber, columnNumber) {
let sourceIndex = 0;
let sourceLineNumber = 0;
let sourceColumnNumber = 0;
let nameIndex = 0;
const sources = map[TextSourceMap._sourcesListSymbol];
const names = map.names || [];
const stringCharIterator = new TextSourceMap.StringCharIterator(map.mappings);
let sourceURL = sources[sourceIndex];
while (true) {
if (stringCharIterator.peek() === ',') {
stringCharIterator.next();
} else {
while (stringCharIterator.peek() === ';') {
lineNumber += 1;
columnNumber = 0;
stringCharIterator.next();
}
if (!stringCharIterator.hasNext()) {
break;
}
}
columnNumber += this._decodeVLQ(stringCharIterator);
if (!stringCharIterator.hasNext() || this._isSeparator(stringCharIterator.peek())) {
this._mappings.push(new SourceMapEntry(lineNumber, columnNumber));
continue;
}
const sourceIndexDelta = this._decodeVLQ(stringCharIterator);
if (sourceIndexDelta) {
sourceIndex += sourceIndexDelta;
sourceURL = sources[sourceIndex];
}
sourceLineNumber += this._decodeVLQ(stringCharIterator);
sourceColumnNumber += this._decodeVLQ(stringCharIterator);
if (!stringCharIterator.hasNext() || this._isSeparator(stringCharIterator.peek())) {
this._mappings.push(
new SourceMapEntry(lineNumber, columnNumber, sourceURL, sourceLineNumber, sourceColumnNumber));
continue;
}
nameIndex += this._decodeVLQ(stringCharIterator);
this._mappings.push(new SourceMapEntry(
lineNumber, columnNumber, sourceURL, sourceLineNumber, sourceColumnNumber, names[nameIndex]));
}
// As per spec, mappings are not necessarily sorted.
this._mappings.sort(SourceMapEntry.compare);
}
/**
* @param {string} char
* @return {boolean}
*/
_isSeparator(char) {
return char === ',' || char === ';';
}
/**
* @param {!TextSourceMap.StringCharIterator} stringCharIterator
* @return {number}
*/
_decodeVLQ(stringCharIterator) {
// Read unsigned value.
let result = 0;
let shift = 0;
let digit;
do {
digit = TextSourceMap._base64Map[stringCharIterator.next()];
result += (digit & TextSourceMap._VLQ_BASE_MASK) << shift;
shift += TextSourceMap._VLQ_BASE_SHIFT;
} while (digit & TextSourceMap._VLQ_CONTINUATION_MASK);
// Fix the sign.
const negative = result & 1;
result >>= 1;
return negative ? -result : result;
}
/**
* @override
*/
dispose() {
}
}
TextSourceMap._VLQ_BASE_SHIFT = 5;
TextSourceMap._VLQ_BASE_MASK = (1 << 5) - 1;
TextSourceMap._VLQ_CONTINUATION_MASK = 1 << 5;
/**
* @unrestricted
*/
TextSourceMap.StringCharIterator = class {
/**
* @param {string} string
*/
constructor(string) {
this._string = string;
this._position = 0;
}
/**
* @return {string}
*/
next() {
return this._string.charAt(this._position++);
}
/**
* @return {string}
*/
peek() {
return this._string.charAt(this._position);
}
/**
* @return {boolean}
*/
hasNext() {
return this._position < this._string.length;
}
};
/**
* @unrestricted
*/
TextSourceMap.SourceInfo = class {
/**
* @param {?string} content
* @param {?Array<!SourceMapEntry>} reverseMappings
*/
constructor(content, reverseMappings) {
this.content = content;
this.reverseMappings = reverseMappings;
}
};
TextSourceMap._sourcesListSymbol = Symbol('sourcesList');
module.exports = {TextSourceMap};

View File

@ -1,84 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* 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 fs = require('fs');
const path = require('path');
const {TextSourceMap} = require('./SourceMap');
const util = require('util');
const readFileAsync = util.promisify(fs.readFile.bind(fs));
class SourceMapSupport {
constructor() {
this._sourceMapPromises = new Map();
}
async rewriteStackTraceWithSourceMaps(error) {
if (!error.stack || typeof error.stack !== 'string')
return;
const stackFrames = error.stack.split('\n');
for (let i = 0; i < stackFrames.length; ++i) {
const stackFrame = stackFrames[i];
let match = stackFrame.match(/\((.*):(\d+):(\d+)\)$/);
if (!match)
match = stackFrame.match(/^\s*at (.*):(\d+):(\d+)$/);
if (!match)
continue;
const filePath = match[1];
const sourceMap = await this._maybeLoadSourceMapForPath(filePath);
if (!sourceMap)
continue;
const compiledLineNumber = parseInt(match[2], 10);
const compiledColumnNumber = parseInt(match[3], 10);
if (isNaN(compiledLineNumber) || isNaN(compiledColumnNumber))
continue;
const entry = sourceMap.findEntry(compiledLineNumber, compiledColumnNumber);
if (!entry)
continue;
stackFrames[i] = stackFrame.replace(filePath + ':' + compiledLineNumber + ':' + compiledColumnNumber, entry.sourceURL + ':' + entry.sourceLineNumber + ':' + entry.sourceColumnNumber);
}
error.stack = stackFrames.join('\n');
}
async _maybeLoadSourceMapForPath(filePath) {
let sourceMapPromise = this._sourceMapPromises.get(filePath);
if (sourceMapPromise === undefined) {
sourceMapPromise = this._loadSourceMapForPath(filePath);
this._sourceMapPromises.set(filePath, sourceMapPromise);
}
return sourceMapPromise;
}
async _loadSourceMapForPath(filePath) {
try {
const fileContent = await readFileAsync(filePath, 'utf8');
const magicCommentLine = fileContent.trim().split('\n').pop().trim();
const magicCommentMatch = magicCommentLine.match('^//#\\s*sourceMappingURL\\s*=(.*)$');
if (!magicCommentMatch)
return null;
const sourceMappingURL = magicCommentMatch[1].trim();
const sourceMapPath = path.resolve(path.dirname(filePath), sourceMappingURL);
const json = JSON.parse(await readFileAsync(sourceMapPath, 'utf8'));
return new TextSourceMap(filePath, sourceMapPath, json);
} catch(e) {
return null;
}
}
}
module.exports = {SourceMapSupport};

View File

@ -1,179 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* 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 Location = require('./Location');
const TestExpectation = {
Ok: 'ok',
Fail: 'fail',
};
class Test {
constructor(suite, name, callback, location) {
this._suite = suite;
this._name = name;
this._fullName = (suite.fullName() + ' ' + name).trim();
this._skipped = false;
this._expectation = TestExpectation.Ok;
this._body = callback;
this._location = location;
this._timeout = 100000000;
this._environments = [];
this.Expectations = { ...TestExpectation };
}
titles() {
if (!this._name)
return this._suite.titles();
return [...this._suite.titles(), this._name];
}
suite() {
return this._suite;
}
name() {
return this._name;
}
fullName() {
return this._fullName;
}
location() {
return this._location;
}
body() {
return this._body;
}
skipped() {
return this._skipped;
}
setSkipped(skipped) {
this._skipped = skipped;
return this;
}
timeout() {
return this._timeout;
}
setTimeout(timeout) {
this._timeout = timeout;
return this;
}
expectation() {
return this._expectation;
}
setExpectation(expectation) {
this._expectation = expectation;
return this;
}
addEnvironment(environment) {
this._environments.push(environment);
return this;
}
removeEnvironment(environment) {
const index = this._environments.indexOf(environment);
if (index === -1)
throw new Error(`Environment "${environment.name()}" cannot be removed because it was not added to the suite "${this.fullName()}"`);
this._environments.splice(index, 1);
return this;
}
}
class Suite {
constructor(parentSuite, name, location) {
this._parentSuite = parentSuite;
this._name = name;
const fullName = (parentSuite ? parentSuite.fullName() + ' ' + name : name).trim();
this._fullName = fullName;
this._location = location;
this._skipped = false;
this._expectation = TestExpectation.Ok;
this._defaultEnvironment = {
name() { return fullName; },
};
this._environments = [this._defaultEnvironment];
this.Expectations = { ...TestExpectation };
}
_addHook(name, callback) {
if (this._defaultEnvironment[name])
throw new Error(`ERROR: cannot re-assign hook "${name}" for suite "${this._fullName}"`);
this._defaultEnvironment[name] = callback;
}
beforeEach(callback) { this._addHook('beforeEach', callback); }
afterEach(callback) { this._addHook('afterEach', callback); }
beforeAll(callback) { this._addHook('beforeAll', callback); }
afterAll(callback) { this._addHook('afterAll', callback); }
globalSetup(callback) { this._addHook('globalSetup', callback); }
globalTeardown(callback) { this._addHook('globalTeardown', callback); }
titles() {
if (!this._parentSuite)
return this._name ? [this._name] : [];
return this._name ? [...this._parentSuite.titles(), this._name] : this._parentSuite.titles();
}
parentSuite() { return this._parentSuite; }
name() { return this._name; }
fullName() { return this._fullName; }
skipped() { return this._skipped; }
setSkipped(skipped) {
this._skipped = skipped;
return this;
}
location() { return this._location; }
expectation() { return this._expectation; }
setExpectation(expectation) {
this._expectation = expectation;
return this;
}
addEnvironment(environment) {
this._environments.push(environment);
return this;
}
removeEnvironment(environment) {
const index = this._environments.indexOf(environment);
if (index === -1)
throw new Error(`Environment "${environment.name()}" cannot be removed because it was not added to the suite "${this.fullName()}"`);
this._environments.splice(index, 1);
return this;
}
}
module.exports = { TestExpectation, Test, Suite };

View File

@ -1,259 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* 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 Location = require('./Location');
const { Test, Suite } = require('./Test');
const { TestRun } = require('./TestRunner');
class FocusedFilter {
constructor() {
this._focusedTests = new Set();
this._focusedSuites = new Set();
this._focusedFilePaths = new Set();
}
focusTest(test) { this._focusedTests.add(test); }
focusSuite(suite) { this._focusedSuites.add(suite); }
focusFilePath(filePath) { this._focusedFilePaths.add(filePath); }
hasFocusedTestsOrSuitesOrFiles() {
return !!this._focusedTests.size || !!this._focusedSuites.size || !!this._focusedFilePaths.size;
}
focusedTests(tests) {
return tests.filter(test => this._focusedTests.has(test));
}
focusedSuites(suites) {
return suites.filter(suite => this._focusedSuites.has(suite));
}
focusedFilePaths(filePaths) {
return filePaths.filter(filePath => this._focusedFilePaths.has(filePath));
}
filter(tests) {
if (!this.hasFocusedTestsOrSuitesOrFiles())
return tests;
const ignoredSuites = new Set();
const ignoredFilePaths = new Set();
for (const test of tests) {
if (this._focusedTests.has(test)) {
// Focused tests should be run even if skipped.
test.setSkipped(false);
// TODO: remove next line once we run failing tests.
test.setExpectation(test.Expectations.Ok);
ignoredFilePaths.add(test.location().filePath());
}
for (let suite = test.suite(); suite; suite = suite.parentSuite()) {
if (this._focusedSuites.has(suite)) {
// Focused suites should be run even if skipped.
suite.setSkipped(false);
// TODO: remove next line once we run failing tests.
suite.setExpectation(suite.Expectations.Ok);
}
// Mark parent suites of focused tests as ignored.
if (this._focusedTests.has(test))
ignoredSuites.add(suite);
}
}
// Pick all tests that are focused or belong to focused suites.
const result = [];
for (const test of tests) {
let focused = this._focusedTests.has(test) || (this._focusedFilePaths.has(test.location().filePath()) && !ignoredFilePaths.has(test.location().filePath()));
for (let suite = test.suite(); suite; suite = suite.parentSuite())
focused = focused || (this._focusedSuites.has(suite) && !ignoredSuites.has(suite));
if (focused)
result.push(test);
}
return result;
}
}
class Repeater {
constructor() {
this._repeatCount = new Map();
}
repeat(testOrSuite, count) {
this._repeatCount.set(testOrSuite, count);
}
_get(testOrSuite) {
const repeat = this._repeatCount.get(testOrSuite);
return repeat === undefined ? 1 : repeat;
}
createTestRuns(tests) {
const suiteToChildren = new Map();
const rootSuites = new Set();
for (const test of tests) {
let children = suiteToChildren.get(test.suite());
if (!children) {
children = new Set();
suiteToChildren.set(test.suite(), children);
}
children.add(test);
for (let suite = test.suite(); suite; suite = suite.parentSuite()) {
let children = suiteToChildren.get(suite.parentSuite());
if (!children) {
children = new Set();
suiteToChildren.set(suite.parentSuite(), children);
}
children.add(suite);
// Add root suites.
if (!suite.parentSuite())
rootSuites.add(suite);
}
}
const collectTests = (testOrSuite) => {
const testOrder = [];
if (testOrSuite instanceof Test) {
testOrder.push(testOrSuite);
} else {
for (const child of suiteToChildren.get(testOrSuite))
testOrder.push(...collectTests(child));
}
const repeat = this._repeatCount.has(testOrSuite) ? this._repeatCount.get(testOrSuite) : 1;
const result = [];
for (let i = 0; i < repeat; ++i)
result.push(...testOrder);
return result;
}
const testOrder = [];
for (const rootSuite of rootSuites)
testOrder.push(...collectTests(rootSuite));
return testOrder.map(test => new TestRun(test));
}
}
function specBuilder(modifiers, attributes, specCallback) {
function builder(specs) {
return new Proxy((...args) => specCallback(specs, ...args), {
get: (obj, prop) => {
if (modifiers.has(prop))
return (...args) => builder([...specs, { callback: modifiers.get(prop), args }]);
if (attributes.has(prop))
return builder([...specs, { callback: attributes.get(prop), args: [] }]);
return obj[prop];
},
});
}
return builder([]);
}
class TestCollector {
constructor(options = {}) {
let { timeout = 10 * 1000 } = options;
if (timeout === 0)
timeout = 100000000; // Inifinite timeout.
this._tests = [];
this._suites = [];
this._suiteModifiers = new Map();
this._suiteAttributes = new Map();
this._testModifiers = new Map();
this._testAttributes = new Map();
this._testCallbackWrappers = [];
this._api = {};
this._currentSuite = new Suite(null, '', new Location());
this._rootSuite = this._currentSuite;
this._api.describe = specBuilder(this._suiteModifiers, this._suiteAttributes, (specs, name, suiteCallback, ...suiteArgs) => {
const location = Location.getCallerLocation();
const suite = new Suite(this._currentSuite, name, location);
for (const { callback, args } of specs)
callback(suite, ...args);
this._currentSuite = suite;
suiteCallback(...suiteArgs);
this._suites.push(suite);
this._currentSuite = suite.parentSuite();
});
this._api.it = specBuilder(this._testModifiers, this._testAttributes, (specs, name, testCallback) => {
const location = Location.getCallerLocation();
for (const wrapper of this._testCallbackWrappers)
testCallback = wrapper(testCallback);
const test = new Test(this._currentSuite, name, testCallback, location);
test.setTimeout(timeout);
for (const { callback, args } of specs)
callback(test, ...args);
this._tests.push(test);
});
this._api.beforeAll = callback => this._currentSuite.beforeAll(callback);
this._api.beforeEach = callback => this._currentSuite.beforeEach(callback);
this._api.afterAll = callback => this._currentSuite.afterAll(callback);
this._api.afterEach = callback => this._currentSuite.afterEach(callback);
this._api.globalSetup = callback => this._currentSuite.globalSetup(callback);
this._api.globalTeardown = callback => this._currentSuite.globalTeardown(callback);
}
useEnvironment(environment) {
return this._currentSuite.addEnvironment(environment);
}
addTestCallbackWrapper(wrapper) {
this._testCallbackWrappers.push(wrapper);
}
addTestModifier(name, callback) {
this._testModifiers.set(name, callback);
}
addTestAttribute(name, callback) {
this._testAttributes.set(name, callback);
}
addSuiteModifier(name, callback) {
this._suiteModifiers.set(name, callback);
}
addSuiteAttribute(name, callback) {
this._suiteAttributes.set(name, callback);
}
api() {
return this._api;
}
tests() {
return this._tests;
}
suites() {
return this._suites;
}
filePaths() {
const filePaths = new Set();
for (const test of this._tests)
filePaths.add(test.location().filePath());
for (const suite of this._suites)
filePaths.add(suite.location().filePath());
return [...filePaths];
}
rootSuite() {
return this._rootSuite;
}
}
module.exports = { TestCollector, specBuilder, FocusedFilter, Repeater };

View File

@ -1,581 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* 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 { SourceMapSupport } = require('./SourceMapSupport');
const debug = require('debug');
const { TestExpectation } = require('./Test');
const TimeoutError = new Error('Timeout');
const TerminatedError = new Error('Terminated');
function runUserCallback(callback, timeout, args) {
let terminateCallback;
let timeoutId;
const promise = Promise.race([
Promise.resolve().then(callback.bind(null, ...args)).then(() => null).catch(e => e),
new Promise(resolve => {
timeoutId = setTimeout(resolve.bind(null, TimeoutError), timeout);
}),
new Promise(resolve => terminateCallback = resolve),
]).catch(e => e).finally(() => clearTimeout(timeoutId));
const terminate = () => terminateCallback(TerminatedError);
return { promise, terminate };
}
const TestResult = {
Ok: 'ok',
MarkedAsFailing: 'markedAsFailing', // User marked as failed
Skipped: 'skipped', // User marked as skipped
Failed: 'failed', // Exception happened during running
TimedOut: 'timedout', // Timeout Exceeded while running
Terminated: 'terminated', // Execution terminated
Crashed: 'crashed', // If testrunner crashed due to this test
};
function isEmptyEnvironment(env) {
return !env.afterEach && !env.afterAll && !env.beforeEach && !env.beforeAll &&
!env.globalSetup && !env.globalTeardown;
}
class TestRun {
constructor(test) {
this._test = test;
this._result = null;
this._error = null;
this._startTimestamp = 0;
this._endTimestamp = 0;
this._workerId = null;
this._output = [];
this._environments = test._environments.filter(env => !isEmptyEnvironment(env)).reverse();
for (let suite = test.suite(); suite; suite = suite.parentSuite())
this._environments.push(...suite._environments.filter(env => !isEmptyEnvironment(env)).reverse());
this._environments.reverse();
}
finished() {
return this._result !== null && this._result !== 'running';
}
isFailure() {
return this._result === TestResult.Failed || this._result === TestResult.TimedOut || this._result === TestResult.Crashed;
}
ok() {
return this._result === TestResult.Ok;
}
result() {
return this._result;
}
error() {
return this._error;
}
duration() {
return this._endTimestamp - this._startTimestamp;
}
test() {
return this._test;
}
workerId() {
return this._workerId;
}
log(log) {
this._output.push(log);
}
output() {
return this._output;
}
}
class Result {
constructor() {
this.result = TestResult.Ok;
this.exitCode = 0;
this.message = '';
this.errors = [];
this.runs = [];
}
setResult(result, message) {
if (!this.ok())
return;
this.result = result;
this.message = message || '';
if (result === TestResult.Ok)
this.exitCode = 0;
else if (result === TestResult.Terminated)
this.exitCode = 130;
else if (result === TestResult.Crashed)
this.exitCode = 2;
else
this.exitCode = 1;
}
addError(message, error, worker) {
const data = { message, error, runs: [] };
if (worker)
data.runs = worker._runs.slice();
this.errors.push(data);
}
ok() {
return this.result === TestResult.Ok;
}
}
class TestWorker {
constructor(testRunner, hookRunner, workerId, parallelIndex) {
this._testRunner = testRunner;
this._hookRunner = hookRunner;
this._state = { parallelIndex };
this._environmentStack = [];
this._terminating = false;
this._workerId = workerId;
this._runningTestTerminate = null;
this._runs = [];
}
terminate(terminateHooks) {
this._terminating = true;
if (this._runningTestTerminate)
this._runningTestTerminate();
this._hookRunner.terminateWorker(this);
}
_markTerminated(testRun) {
if (!this._terminating)
return false;
testRun._result = TestResult.Terminated;
return true;
}
async run(testRun) {
this._runs.push(testRun);
const test = testRun.test();
let skipped = test.skipped();
for (let suite = test.suite(); suite; suite = suite.parentSuite())
skipped = skipped || suite.skipped();
if (skipped) {
await this._willStartTestRun(testRun);
testRun._result = TestResult.Skipped;
await this._didFinishTestRun(testRun);
return;
}
let expectedToFail = test.expectation() === TestExpectation.Fail;
for (let suite = test.suite(); suite; suite = suite.parentSuite())
expectedToFail = expectedToFail || (suite.expectation() === TestExpectation.Fail);
if (expectedToFail) {
await this._willStartTestRun(testRun);
testRun._result = TestResult.MarkedAsFailing;
await this._didFinishTestRun(testRun);
return;
}
const environmentStack = testRun._environments;
let common = 0;
while (common < environmentStack.length && this._environmentStack[common] === environmentStack[common])
common++;
while (this._environmentStack.length > common) {
if (this._markTerminated(testRun))
return;
const environment = this._environmentStack.pop();
if (!await this._hookRunner.runHook(environment, 'afterAll', [this._state], this, testRun))
return;
if (!await this._hookRunner.maybeRunGlobalTeardown(environment))
return;
}
while (this._environmentStack.length < environmentStack.length) {
if (this._markTerminated(testRun))
return;
const environment = environmentStack[this._environmentStack.length];
this._environmentStack.push(environment);
if (!await this._hookRunner.maybeRunGlobalSetup(environment))
return;
if (!await this._hookRunner.runHook(environment, 'beforeAll', [this._state], this, testRun))
return;
}
if (this._markTerminated(testRun))
return;
// From this point till the end, we have to run all hooks
// no matter what happens.
await this._willStartTestRun(testRun);
for (const environment of this._environmentStack) {
await this._hookRunner.runHook(environment, 'beforeEach', [this._state, testRun], this, testRun);
}
if (!testRun._error && !this._markTerminated(testRun)) {
await this._willStartTestBody(testRun);
const { promise, terminate } = runUserCallback(test.body(), test.timeout(), [this._state, testRun]);
this._runningTestTerminate = terminate;
testRun._error = await promise;
this._runningTestTerminate = null;
if (testRun._error && testRun._error.stack)
await this._testRunner._sourceMapSupport.rewriteStackTraceWithSourceMaps(testRun._error);
if (!testRun._error)
testRun._result = TestResult.Ok;
else if (testRun._error === TimeoutError)
testRun._result = TestResult.TimedOut;
else if (testRun._error === TerminatedError)
testRun._result = TestResult.Terminated;
else
testRun._result = TestResult.Failed;
await this._didFinishTestBody(testRun);
}
for (const environment of this._environmentStack.slice().reverse())
await this._hookRunner.runHook(environment, 'afterEach', [this._state, testRun], this, testRun);
await this._didFinishTestRun(testRun);
}
async _willStartTestRun(testRun) {
testRun._startTimestamp = Date.now();
testRun._workerId = this._workerId;
await this._testRunner._runDelegateCallback(this._testRunner._delegate.onTestRunStarted, [testRun]);
}
async _didFinishTestRun(testRun) {
testRun._endTimestamp = Date.now();
testRun._workerId = this._workerId;
this._hookRunner.markFinishedTestRun(testRun);
await this._testRunner._runDelegateCallback(this._testRunner._delegate.onTestRunFinished, [testRun]);
}
async _willStartTestBody(testRun) {
debug('testrunner:test')(`[${this._workerId}] starting "${testRun.test().fullName()}" (${testRun.test().location()})`);
}
async _didFinishTestBody(testRun) {
debug('testrunner:test')(`[${this._workerId}] ${testRun._result.toUpperCase()} "${testRun.test().fullName()}" (${testRun.test().location()})`);
}
async shutdown() {
while (this._environmentStack.length > 0) {
const environment = this._environmentStack.pop();
await this._hookRunner.runHook(environment, 'afterAll', [this._state], this, null);
await this._hookRunner.maybeRunGlobalTeardown(environment);
}
}
}
class HookRunner {
constructor(testRunner, testRuns) {
this._testRunner = testRunner;
this._runningHookTerminations = new Map();
this._environmentToGlobalState = new Map();
for (const testRun of testRuns) {
for (const env of testRun._environments) {
let globalState = this._environmentToGlobalState.get(env);
if (!globalState) {
globalState = {
pendingTestRuns: new Set(),
globalSetupPromise: null,
globalTeardownPromise: null,
};
this._environmentToGlobalState.set(env, globalState);
}
globalState.pendingTestRuns.add(testRun);
}
}
}
terminateWorker(worker) {
let termination = this._runningHookTerminations.get(worker);
this._runningHookTerminations.delete(worker);
if (termination)
termination();
}
terminateAll() {
for (const termination of this._runningHookTerminations.values())
termination();
this._runningHookTerminations.clear();
}
markFinishedTestRun(testRun) {
for (const environment of testRun._environments) {
const globalState = this._environmentToGlobalState.get(environment);
globalState.pendingTestRuns.delete(testRun);
}
}
async _runHookInternal(worker, testRun, hook, fullName, hookArgs = []) {
await this._willStartHook(worker, testRun, hook, fullName);
const timeout = this._testRunner._hookTimeout;
const { promise, terminate } = runUserCallback(hook.body, timeout, hookArgs);
this._runningHookTerminations.set(worker, terminate);
let error = await promise;
this._runningHookTerminations.delete(worker);
if (error) {
if (testRun && testRun._result !== TestResult.Terminated) {
// Prefer terminated result over any hook failures.
testRun._result = error === TerminatedError ? TestResult.Terminated : TestResult.Crashed;
}
let message;
if (error === TimeoutError) {
message = `Timeout Exceeded ${timeout}ms while running "${hook.name}" in "${fullName}"`;
error = null;
} else if (error === TerminatedError) {
// Do not report termination details - it's just noise.
message = '';
error = null;
} else {
if (error.stack)
await this._testRunner._sourceMapSupport.rewriteStackTraceWithSourceMaps(error);
message = `FAILED while running "${hook.name}" in suite "${fullName}": `;
}
await this._didFailHook(worker, testRun, hook, fullName, message, error);
if (testRun)
testRun._error = error;
return false;
}
await this._didCompleteHook(worker, testRun, hook, fullName);
return true;
}
async runHook(environment, hookName, hookArgs, worker = null, testRun = null) {
const hookBody = environment[hookName];
if (!hookBody)
return true;
const envName = environment.name ? environment.name() : environment.constructor.name;
return await this._runHookInternal(worker, testRun, {name: hookName, body: hookBody.bind(environment)}, envName, hookArgs);
}
async maybeRunGlobalSetup(environment) {
const globalState = this._environmentToGlobalState.get(environment);
if (!globalState.globalSetupPromise)
globalState.globalSetupPromise = this.runHook(environment, 'globalSetup', []);
if (!await globalState.globalSetupPromise) {
await this._testRunner._terminate(TestResult.Crashed, 'Global setup failed!', false, null);
return false;
}
return true;
}
async maybeRunGlobalTeardown(environment) {
const globalState = this._environmentToGlobalState.get(environment);
if (!globalState.globalTeardownPromise) {
if (!globalState.pendingTestRuns.size || (this._testRunner._terminating && globalState.globalSetupPromise))
globalState.globalTeardownPromise = this.runHook(environment, 'globalTeardown', []);
}
if (!globalState.globalTeardownPromise)
return true;
if (!await globalState.globalTeardownPromise) {
await this._testRunner._terminate(TestResult.Crashed, 'Global teardown failed!', false, null);
return false;
}
return true;
}
async _willStartHook(worker, testRun, hook, fullName) {
debug('testrunner:hook')(`${workerName(worker)} "${fullName}.${hook.name}" started for "${testRun ? testRun.test().fullName() : ''}"`);
}
async _didFailHook(worker, testRun, hook, fullName, message, error) {
debug('testrunner:hook')(`${workerName(worker)} "${fullName}.${hook.name}" FAILED for "${testRun ? testRun.test().fullName() : ''}"`);
if (message)
this._testRunner._result.addError(message, error, worker);
this._testRunner._result.setResult(TestResult.Crashed, message);
}
async _didCompleteHook(worker, testRun, hook, fullName) {
debug('testrunner:hook')(`${workerName(worker)} "${fullName}.${hook.name}" OK for "${testRun ? testRun.test().fullName() : ''}"`);
}
}
function workerName(worker) {
return worker ? `<worker ${worker._workerId}>` : `<_global_>`;
}
class TestRunner {
constructor() {
this._sourceMapSupport = new SourceMapSupport();
this._nextWorkerId = 1;
this._workers = [];
this._terminating = false;
this._result = null;
this._hookRunner = null;
}
async _runDelegateCallback(callback, args) {
let { promise, terminate } = runUserCallback(callback, this._hookTimeout, args);
// Note: we do not terminate the delegate to keep reporting even when terminating.
const e = await promise;
if (e) {
debug('testrunner')(`Error while running delegate method: ${e}`);
const { message, error } = this._toError('INTERNAL ERROR', e);
this._terminate(TestResult.Crashed, message, false, error);
}
}
_toError(message, error) {
if (!(error instanceof Error)) {
message += ': ' + error;
error = new Error();
error.stack = '';
}
return { message, error };
}
async run(testRuns, options = {}) {
const {
parallel = 1,
breakOnFailure = false,
hookTimeout = 10 * 1000,
totalTimeout = 0,
onStarted = async (testRuns) => {},
onFinished = async (result) => {},
onTestRunStarted = async (testRun) => {},
onTestRunFinished = async (testRun) => {},
} = options;
this._breakOnFailure = breakOnFailure;
this._hookTimeout = hookTimeout === 0 ? 100000000 : hookTimeout;
this._delegate = {
onStarted,
onFinished,
onTestRunStarted,
onTestRunFinished
};
this._result = new Result();
this._result.runs = testRuns;
const terminationPromises = [];
const handleSIGINT = () => this._terminate(TestResult.Terminated, 'SIGINT received', false, null);
const handleSIGHUP = () => this._terminate(TestResult.Terminated, 'SIGHUP received', false, null);
const handleSIGTERM = () => this._terminate(TestResult.Terminated, 'SIGTERM received', true, null);
const handleRejection = e => {
const { message, error } = this._toError('UNHANDLED PROMISE REJECTION', e);
terminationPromises.push(this._terminate(TestResult.Crashed, message, false, error));
};
const handleException = e => {
const { message, error } = this._toError('UNHANDLED ERROR', e);
terminationPromises.push(this._terminate(TestResult.Crashed, message, false, error));
};
process.on('SIGINT', handleSIGINT);
process.on('SIGHUP', handleSIGHUP);
process.on('SIGTERM', handleSIGTERM);
process.on('unhandledRejection', handleRejection);
process.on('uncaughtException', handleException);
let timeoutId;
if (totalTimeout) {
timeoutId = setTimeout(() => {
terminationPromises.push(this._terminate(TestResult.Terminated, `Total timeout of ${totalTimeout}ms reached.`, true /* force */, null /* error */));
}, totalTimeout);
}
await this._runDelegateCallback(this._delegate.onStarted, [testRuns]);
this._hookRunner = new HookRunner(this, testRuns);
const workerCount = Math.min(parallel, testRuns.length);
const workerPromises = [];
for (let i = 0; i < workerCount; ++i) {
const initialTestRunIndex = i * Math.floor(testRuns.length / workerCount);
workerPromises.push(this._runWorker(initialTestRunIndex, testRuns, i));
}
await Promise.all(workerPromises);
await Promise.all(terminationPromises);
if (testRuns.some(run => run.isFailure()))
this._result.setResult(TestResult.Failed, '');
await this._runDelegateCallback(this._delegate.onFinished, [this._result]);
clearTimeout(timeoutId);
process.removeListener('SIGINT', handleSIGINT);
process.removeListener('SIGHUP', handleSIGHUP);
process.removeListener('SIGTERM', handleSIGTERM);
process.removeListener('unhandledRejection', handleRejection);
process.removeListener('uncaughtException', handleException);
return this._result;
}
async _runWorker(testRunIndex, testRuns, parallelIndex) {
let worker = new TestWorker(this, this._hookRunner, this._nextWorkerId++, parallelIndex);
this._workers[parallelIndex] = worker;
while (!this._terminating) {
let skipped = 0;
while (skipped < testRuns.length && testRuns[testRunIndex]._result !== null) {
testRunIndex = (testRunIndex + 1) % testRuns.length;
skipped++;
}
const testRun = testRuns[testRunIndex];
if (testRun._result !== null) {
// All tests have been run.
break;
}
// Mark as running so that other workers do not run it again.
testRun._result = 'running';
await worker.run(testRun);
if (testRun.isFailure()) {
// Something went wrong during test run, let's use a fresh worker.
await worker.shutdown();
if (this._breakOnFailure) {
const message = `Terminating because a test has failed and |testRunner.breakOnFailure| is enabled`;
await this._terminate(TestResult.Terminated, message, false /* force */, null /* error */);
return;
}
worker = new TestWorker(this, this._hookRunner, this._nextWorkerId++, parallelIndex);
this._workers[parallelIndex] = worker;
}
}
await worker.shutdown();
}
async _terminate(result, message, force, error) {
debug('testrunner')(`TERMINATED result = ${result}, message = ${message}`);
this._terminating = true;
for (const worker of this._workers)
worker.terminate(force /* terminateHooks */);
if (this._hookRunner)
this._hookRunner.terminateAll();
this._result.setResult(result, message);
if (this._result.message === 'SIGINT received' && message === 'SIGTERM received')
this._result.message = message;
if (error) {
if (error.stack)
await this._sourceMapSupport.rewriteStackTraceWithSourceMaps(error);
this._result.addError(message, error, this._workers.length === 1 ? this._workers[0] : null);
}
}
async terminate() {
if (!this._result)
return;
await this._terminate(TestResult.Terminated, 'Terminated with |TestRunner.terminate()| call', true /* force */, null /* error */);
}
}
module.exports = { TestRunner, TestRun, TestResult, Result };

View File

@ -1,13 +0,0 @@
body {
font-family: monospace;
white-space: pre;
}
ins {
background-color: #9cffa0;
text-decoration: none;
}
del {
background-color: #ff9e9e;
}

View File

@ -1,54 +0,0 @@
/**
* Copyright 2017 Google Inc. 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 {TestRunner, Reporter, Matchers} = require('..');
const runner = new TestRunner();
const reporter = new Reporter(runner);
const {expect} = new Matchers();
const {describe, xdescribe, fdescribe} = runner;
const {it, fit, xit} = runner;
const {beforeAll, beforeEach, afterAll, afterEach} = runner;
describe('testsuite', () => {
it('toBe', async (state) => {
expect(2 + 2).toBe(5);
});
it('toBeFalsy', async (state) => {
expect(true).toBeFalsy();
});
it('toBeTruthy', async (state) => {
expect(false).toBeTruthy();
});
it('toBeGreaterThan', async (state) => {
expect(2).toBeGreaterThan(3);
});
it('toBeNull', async (state) => {
expect(2).toBeNull();
});
it('toContain', async (state) => {
expect('asdf').toContain('e');
});
it('not.toContain', async (state) => {
expect('asdf').not.toContain('a');
});
it('toEqual', async (state) => {
expect([1,2,3]).toEqual([1,2,3,4]);
});
});
runner.run();

View File

@ -1,35 +0,0 @@
/**
* Copyright 2017 Google Inc. 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 {TestRunner, Reporter, Matchers} = require('..');
const runner = new TestRunner();
const reporter = new Reporter(runner);
const {expect} = new Matchers();
const {describe, xdescribe, fdescribe} = runner;
const {it, fit, xit} = runner;
const {beforeAll, beforeEach, afterAll, afterEach} = runner;
describe('testsuite', () => {
beforeAll(() => {
expect(false).toBeTruthy();
});
it('test', async () => {
});
});
runner.run();

View File

@ -1,35 +0,0 @@
/**
* Copyright 2017 Google Inc. 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 {TestRunner, Reporter, Matchers} = require('..');
const runner = new TestRunner({ timeout: 100 });
const reporter = new Reporter(runner);
const {expect} = new Matchers();
const {describe, xdescribe, fdescribe} = runner;
const {it, fit, xit} = runner;
const {beforeAll, beforeEach, afterAll, afterEach} = runner;
describe('testsuite', () => {
beforeAll(async () => {
await new Promise(() => {});
});
it('something', async (state) => {
});
});
runner.run();

View File

@ -1,32 +0,0 @@
/**
* Copyright 2017 Google Inc. 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 {TestRunner, Reporter} = require('..');
const runner = new TestRunner({ timeout: 100 });
const reporter = new Reporter(runner);
const {describe, xdescribe, fdescribe} = runner;
const {it, fit, xit} = runner;
const {beforeAll, beforeEach, afterAll, afterEach} = runner;
describe('testsuite', () => {
it('timeout', async (state) => {
await new Promise(() => {});
});
});
runner.run();

View File

@ -1,35 +0,0 @@
/**
* Copyright 2017 Google Inc. 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 {TestRunner, Reporter} = require('..');
const runner = new TestRunner();
const reporter = new Reporter(runner);
const {describe, xdescribe, fdescribe} = runner;
const {it, fit, xit} = runner;
const {beforeAll, beforeEach, afterAll, afterEach} = runner;
describe('testsuite', () => {
it('failure', async (state) => {
Promise.reject(new Error('fail!'));
});
it('slow', async () => {
await new Promise(x => setTimeout(x, 1000));
});
});
runner.run();

View File

@ -1,168 +0,0 @@
/**
* Copyright 2017 Google Inc. 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 { TestRunner, Result, TestResult } = require('./TestRunner');
const { TestCollector, FocusedFilter, Repeater } = require('./TestCollector');
const Reporter = require('./Reporter');
const { Matchers } = require('./Matchers');
class DefaultTestRunner {
constructor(options = {}) {
const {
// Our options.
crashIfTestsAreFocusedOnCI = true,
exit = true,
reporter = true,
// Collector options.
timeout,
// Runner options.
parallel = 1,
breakOnFailure,
totalTimeout,
hookTimeout = timeout,
// Reporting options.
showSlowTests,
showMarkedAsFailingTests,
verbose,
summary,
lineBreak,
goldenPath,
outputPath,
} = options;
this._crashIfTestsAreFocusedOnCI = crashIfTestsAreFocusedOnCI;
this._exit = exit;
this._parallel = parallel;
this._breakOnFailure = breakOnFailure;
this._totalTimeout = totalTimeout;
this._hookTimeout = hookTimeout;
this._needReporter = reporter;
this._showSlowTests = showSlowTests;
this._showMarkedAsFailingTests = showMarkedAsFailingTests;
this._verbose = verbose;
this._summary = summary;
this._lineBreak = lineBreak;
this._filter = new FocusedFilter();
this._repeater = new Repeater();
this._collector = new TestCollector({ timeout });
this._api = {
...this._collector.api(),
expect: new Matchers({ goldenPath, outputPath }).expect,
};
this._collector.addSuiteAttribute('only', s => this._filter.focusSuite(s));
this._collector.addSuiteAttribute('skip', s => s.setSkipped(true));
this._collector.addSuiteModifier('repeat', (s, count) => this._repeater.repeat(s, count));
this._collector.addTestAttribute('only', t => this._filter.focusTest(t));
this._collector.addTestAttribute('skip', t => t.setSkipped(true));
this._collector.addTestAttribute('todo', t => t.setSkipped(true));
this._collector.addTestAttribute('slow', t => t.setTimeout(t.timeout() * 3));
this._collector.addTestModifier('repeat', (t, count) => this._repeater.repeat(t, count));
this._api.fdescribe = this._api.describe.only;
this._api.xdescribe = this._api.describe.skip;
this._api.fit = this._api.it.only;
this._api.xit = this._api.it.skip;
}
collector() {
return this._collector;
}
api() {
return this._api;
}
focusMatchingNameTests(fullNameRegex) {
const focusedTests = [];
for (const test of this._collector.tests()) {
if (fullNameRegex.test(test.fullName())) {
this._filter.focusTest(test);
focusedTests.push(test);
}
}
return focusedTests;
}
focusMatchingFileName(filenameRegex) {
const focusedFilePaths = [];
for (const filePath of this._collector.filePaths()) {
if (filenameRegex.test(path.basename(filePath))) {
this._filter.focusFilePath(filePath);
focusedFilePaths.push(filePath);
}
}
return focusedFilePaths;
}
repeatAll(repeatCount) {
this._repeater.repeat(this._collector.rootSuite(), repeatCount);
}
async run() {
let reporter = null;
if (this._needReporter) {
const reporterDelegate = {
focusedSuites: () => this._filter.focusedSuites(this._collector.suites()),
focusedTests: () => this._filter.focusedTests(this._collector.tests()),
focusedFilePaths: () => this._filter.focusedFilePaths(this._collector.filePaths()),
hasFocusedTestsOrSuitesOrFiles: () => this._filter.hasFocusedTestsOrSuitesOrFiles(),
parallel: () => this._parallel,
testCount: () => this._collector.tests().length,
};
const reporterOptions = {
showSlowTests: this._showSlowTests,
showMarkedAsFailingTests: this._showMarkedAsFailingTests,
verbose: this._verbose,
summary: this._summary,
lineBreak: this._lineBreak,
};
reporter = new Reporter(reporterDelegate, reporterOptions);
}
if (this._crashIfTestsAreFocusedOnCI && process.env.CI && this._filter.hasFocusedTestsOrSuitesOrFiles()) {
if (reporter)
await reporter.onStarted([]);
const result = new Result();
result.setResult(TestResult.Crashed, '"focused" tests or suites are probitted on CI');
if (reporter)
await reporter.onFinished(result);
if (this._exit)
process.exit(result.exitCode);
return result;
}
const testRuns = this._repeater.createTestRuns(this._filter.filter(this._collector.tests()));
const testRunner = new TestRunner();
const result = await testRunner.run(testRuns, {
parallel: this._parallel,
breakOnFailure: this._breakOnFailure,
totalTimeout: this._totalTimeout,
hookTimeout: this._hookTimeout,
onStarted: (...args) => reporter && reporter.onStarted(...args),
onFinished: (...args) => reporter && reporter.onFinished(...args),
onTestRunStarted: (...args) => reporter && reporter.onTestRunStarted(...args),
onTestRunFinished: (...args) => reporter && reporter.onTestRunFinished(...args),
});
if (this._exit)
process.exit(result.exitCode);
return result;
}
}
module.exports = DefaultTestRunner;

View File

@ -1,4 +0,0 @@
const TestRunner = require('..');
const testRunner = new TestRunner();
require('./testrunner.spec.js').addTests(testRunner.api());
testRunner.run();

File diff suppressed because it is too large Load Diff