UNPKG

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
"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