mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-01 16:49:20 +03:00
chore(test): run doclint tests with mocha, delete testrunner again (#3447)
This commit is contained in:
parent
e2cfb05786
commit
84441f8f77
@ -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",
|
||||||
|
@ -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');
|
||||||
|
|
@ -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);
|
||||||
|
@ -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'));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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();
|
|
||||||
|
|
||||||
|
@ -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
|
|
@ -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;
|
|
@ -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);
|
|
||||||
}
|
|
@ -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();
|
|
||||||
```
|
|
||||||
|
|
@ -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;
|
|
@ -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};
|
|
@ -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};
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -1,13 +0,0 @@
|
|||||||
body {
|
|
||||||
font-family: monospace;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
ins {
|
|
||||||
background-color: #9cffa0;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
del {
|
|
||||||
background-color: #ff9e9e;
|
|
||||||
}
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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;
|
|
@ -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
Loading…
Reference in New Issue
Block a user