folio
Version:
A customizable test framework to build your own test frameworks. Foundation for the [Playwright test runner](https://github.com/microsoft/playwright-test).
319 lines • 12.8 kB
JavaScript
"use strict";
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkerRunner = exports.fixturePool = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const events_1 = require("events");
const workerTest_1 = require("./workerTest");
const util_1 = require("./util");
const workerSpec_1 = require("./workerSpec");
const debug_1 = require("./debug");
const spec_1 = require("./spec");
const fixtures_1 = require("./fixtures");
// We rely on the fact that worker only receives tests with the same FixturePool.
exports.fixturePool = spec_1.rootFixtures._pool;
class WorkerRunner extends events_1.EventEmitter {
constructor(runPayload, config, workerIndex) {
super();
this._parsedParameters = {};
this._testInfo = null;
this._loaded = false;
fixtures_1.assignConfig(config);
this._suite = new workerTest_1.WorkerSuite(spec_1.rootFixtures, '');
this._suite.file = runPayload.file;
this._workerIndex = workerIndex;
this._repeatEachIndex = runPayload.repeatEachIndex;
this._parametersString = runPayload.parametersString;
this._entries = new Map(runPayload.entries.map(e => [e.testId, e]));
this._remaining = new Map(runPayload.entries.map(e => [e.testId, e]));
this._parsedParameters = runPayload.parameters;
this._parsedParameters['testWorkerIndex'] = workerIndex;
}
stop() {
this._isStopped = true;
this._testId = null;
this._setCurrentTestInfo(null);
}
unhandledError(error) {
if (this._isStopped)
return;
if (this._testInfo) {
this._testInfo.status = 'failed';
this._testInfo.error = util_1.serializeError(error);
this._failedTestId = this._testId;
this.emit('testEnd', buildTestEndPayload(this._testId, this._testInfo));
}
else if (!this._loaded) {
// No current test - fatal error.
this._fatalError = util_1.serializeError(error);
}
this._reportDoneAndStop();
}
async run() {
fixtures_1.assignParameters(this._parsedParameters);
const revertBabelRequire = workerSpec_1.workerSpec(this._suite);
require(this._suite.file);
revertBabelRequire();
// Enumerate tests to assign ordinals.
this._suite._renumber();
// Build ids from ordinals + parameters strings.
this._suite._assignIds(this._parametersString);
this._loaded = true;
await this._runSuite(this._suite);
this._reportDoneAndStop();
}
async _runSuite(suite) {
if (this._isStopped)
return;
exports.fixturePool = suite._folio._pool;
try {
await this._runHooks(suite, 'beforeAll', 'before');
}
catch (e) {
this._fatalError = util_1.serializeError(e);
this._reportDoneAndStop();
}
for (const entry of suite._entries) {
if (entry instanceof workerTest_1.WorkerSuite)
await this._runSuite(entry);
else
await this._runTest(entry);
}
try {
await this._runHooks(suite, 'afterAll', 'after');
}
catch (e) {
this._fatalError = util_1.serializeError(e);
this._reportDoneAndStop();
}
}
async _runTest(test) {
if (this._isStopped)
return;
if (!this._entries.has(test._id))
return;
const { timeout, expectedStatus, skipped, retry } = this._entries.get(test._id);
const deadline = timeout ? util_1.monotonicTime() + timeout : 0;
this._remaining.delete(test._id);
const testId = test._id;
this._testId = testId;
exports.fixturePool = test._folio._pool;
this._setCurrentTestInfo({
title: test.title,
file: test.file,
line: test.line,
column: test.column,
fn: test.fn,
parameters: fixtures_1.parameters,
repeatEachIndex: this._repeatEachIndex,
workerIndex: this._workerIndex,
retry,
expectedStatus,
duration: 0,
status: 'passed',
stdout: [],
stderr: [],
timeout,
data: {},
relativeArtifactsPath: '',
outputPath: () => '',
snapshotPath: () => ''
});
fixtures_1.assignParameters({ 'testInfo': this._testInfo });
this.emit('testBegin', buildTestBeginPayload(testId, this._testInfo));
if (skipped) {
// TODO: don't even send those to the worker.
this._testInfo.status = 'skipped';
this.emit('testEnd', buildTestEndPayload(testId, this._testInfo));
return;
}
const startTime = util_1.monotonicTime();
let result = await util_1.raceAgainstDeadline(this._runTestWithFixturesAndHooks(test, this._testInfo), deadline);
// Do not overwrite test failure upon timeout in fixture or hook.
if (result.timedOut && this._testInfo.status === 'passed')
this._testInfo.status = 'timedOut';
if (!result.timedOut) {
result = await util_1.raceAgainstDeadline(this._tearDownTestScope(this._testInfo), deadline);
// Do not overwrite test failure upon timeout in fixture or hook.
if (result.timedOut && this._testInfo.status === 'passed')
this._testInfo.status = 'timedOut';
}
else {
// A timed-out test gets a full additional timeout to teardown test fixture scope.
const newDeadline = timeout ? util_1.monotonicTime() + timeout : 0;
await util_1.raceAgainstDeadline(this._tearDownTestScope(this._testInfo), newDeadline);
}
// Async hop above, we could have stopped.
if (!this._testInfo)
return;
this._testInfo.duration = util_1.monotonicTime() - startTime;
this.emit('testEnd', buildTestEndPayload(testId, this._testInfo));
if (this._testInfo.status !== 'passed') {
this._failedTestId = this._testId;
this._reportDoneAndStop();
}
this._setCurrentTestInfo(null);
this._testId = null;
}
_setCurrentTestInfo(testInfo) {
this._testInfo = testInfo;
fixtures_1.setCurrentTestInfo(testInfo);
}
async _runTestWithFixturesAndHooks(test, testInfo) {
try {
await this._runHooks(test.parent, 'beforeEach', 'before');
}
catch (error) {
testInfo.status = 'failed';
testInfo.error = util_1.serializeError(error);
// Continue running afterEach hooks even after the failure.
}
debug_1.debugLog(`running test "${test.fullTitle()}"`);
try {
// Do not run the test when beforeEach hook fails.
if (!this._isStopped && testInfo.status !== 'failed') {
// Run internal fixtures to resolve artifacts and output paths
const parametersPathSegment = (await exports.fixturePool.setupFixture('testParametersPathSegment')).value;
testInfo.relativeArtifactsPath = relativeArtifactsPath(testInfo, parametersPathSegment);
testInfo.outputPath = outputPath(testInfo);
testInfo.snapshotPath = snapshotPath(testInfo);
await exports.fixturePool.resolveParametersAndRunHookOrTest(test.fn);
testInfo.status = 'passed';
}
}
catch (error) {
testInfo.status = 'failed';
testInfo.error = util_1.serializeError(error);
// Continue running afterEach hooks and fixtures teardown even after the failure.
}
debug_1.debugLog(`done running test "${test.fullTitle()}"`);
try {
await this._runHooks(test.parent, 'afterEach', 'after');
}
catch (error) {
// Do not overwrite test failure error.
if (testInfo.status === 'passed') {
testInfo.status = 'failed';
testInfo.error = util_1.serializeError(error);
// Continue running fixtures teardown even after the failure.
}
}
}
async _tearDownTestScope(testInfo) {
// Worker will tear down test scope if we are stopped.
if (this._isStopped)
return;
try {
await exports.fixturePool.teardownScope('test');
}
catch (error) {
// Do not overwrite test failure or hook error.
if (testInfo.status === 'passed') {
testInfo.status = 'failed';
testInfo.error = util_1.serializeError(error);
}
}
}
async _runHooks(suite, type, dir) {
if (this._isStopped)
return;
debug_1.debugLog(`running hooks "${type}" for suite "${suite.fullTitle()}"`);
if (!this._hasTestsToRun(suite))
return;
const all = [];
for (let s = suite; s; s = s.parent) {
const funcs = s._hooks.filter(e => e.type === type).map(e => e.fn);
all.push(...funcs.reverse());
}
if (dir === 'before')
all.reverse();
let error;
for (const hook of all) {
try {
await exports.fixturePool.resolveParametersAndRunHookOrTest(hook);
}
catch (e) {
// Always run all the hooks, and capture the first error.
error = error || e;
}
}
debug_1.debugLog(`done running hooks "${type}" for suite "${suite.fullTitle()}"`);
if (error)
throw error;
}
_reportDoneAndStop() {
if (this._isStopped)
return;
const donePayload = {
failedTestId: this._failedTestId,
fatalError: this._fatalError,
remaining: [...this._remaining.values()],
};
this.emit('done', donePayload);
this.stop();
}
_hasTestsToRun(suite) {
return suite.findSpec((test) => {
const entry = this._entries.get(test._id);
if (!entry)
return;
const { skipped } = entry;
return !skipped;
});
}
}
exports.WorkerRunner = WorkerRunner;
function buildTestBeginPayload(testId, testInfo) {
return {
testId,
workerIndex: testInfo.workerIndex
};
}
function buildTestEndPayload(testId, testInfo) {
return {
testId,
duration: testInfo.duration,
status: testInfo.status,
error: testInfo.error,
data: testInfo.data,
};
}
function relativeArtifactsPath(testInfo, parametersPathSegment) {
const relativePath = path_1.default.relative(fixtures_1.config.testDir, testInfo.file.replace(/\.(spec|test)\.(js|ts)/, ''));
const sanitizedTitle = testInfo.title.replace(/[^\w\d]+/g, '-');
return path_1.default.join(relativePath, sanitizedTitle, parametersPathSegment);
}
function outputPath(testInfo) {
const retrySuffix = testInfo.retry ? '-retry' + testInfo.retry : '';
const repeatEachSuffix = testInfo.repeatEachIndex ? '-repeat' + testInfo.repeatEachIndex : '';
const basePath = path_1.default.join(fixtures_1.config.outputDir, testInfo.relativeArtifactsPath) + retrySuffix + repeatEachSuffix;
return (...pathSegments) => {
fs_1.default.mkdirSync(basePath, { recursive: true });
return path_1.default.join(basePath, ...pathSegments);
};
}
function snapshotPath(testInfo) {
const basePath = path_1.default.join(fixtures_1.config.testDir, fixtures_1.config.snapshotDir, testInfo.relativeArtifactsPath);
return (...pathSegments) => {
return path_1.default.join(basePath, ...pathSegments);
};
}
//# sourceMappingURL=workerRunner.js.map