"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.create = exports.WorkerMain = void 0; var _utils = require("playwright-core/lib/utils"); var _configLoader = require("../common/configLoader"); var _globals = require("../common/globals"); var _ipc = require("../common/ipc"); var _util = require("../util"); var _fixtureRunner = require("./fixtureRunner"); var _testInfo = require("./testInfo"); var _util2 = require("./util"); var _fixtures = require("../common/fixtures"); var _poolBuilder = require("../common/poolBuilder"); var _process = require("../common/process"); var _suiteUtils = require("../common/suiteUtils"); var _testLoader = require("../common/testLoader"); /** * 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. */ class WorkerMain extends _process.ProcessRunner { constructor(params) { super(); this._params = void 0; this._config = void 0; this._project = void 0; this._poolBuilder = void 0; this._fixtureRunner = void 0; // Accumulated fatal errors that cannot be attributed to a test. this._fatalErrors = []; // Whether we should skip running remaining tests in this suite because // of a setup error, usually beforeAll hook. this._skipRemainingTestsInSuite = void 0; // The stage of the full cleanup. Once "finished", we can safely stop running anything. this._didRunFullCleanup = false; // Whether the worker was requested to stop. this._isStopped = false; // This promise resolves once the single "run test group" call finishes. this._runFinished = new _utils.ManualPromise(); this._currentTest = null; this._lastRunningTests = []; this._totalRunningTests = 0; // Suites that had their beforeAll hooks, but not afterAll hooks executed. // These suites still need afterAll hooks to be executed for the proper cleanup. // Contains dynamic annotations originated by modifiers with a callback, e.g. `test.skip(() => true)`. this._activeSuites = new Map(); process.env.TEST_WORKER_INDEX = String(params.workerIndex); process.env.TEST_PARALLEL_INDEX = String(params.parallelIndex); (0, _globals.setIsWorkerProcess)(); this._params = params; this._fixtureRunner = new _fixtureRunner.FixtureRunner(); // Resolve this promise, so worker does not stall waiting for the non-existent run to finish, // when it was sopped before running any test group. this._runFinished.resolve(); process.on('unhandledRejection', reason => this.unhandledError(reason)); process.on('uncaughtException', error => this.unhandledError(error)); process.stdout.write = (chunk, cb) => { var _this$_currentTest; this.dispatchEvent('stdOut', (0, _ipc.stdioChunkToParams)(chunk)); (_this$_currentTest = this._currentTest) === null || _this$_currentTest === void 0 || _this$_currentTest._tracing.appendStdioToTrace('stdout', chunk); if (typeof cb === 'function') process.nextTick(cb); return true; }; if (!process.env.PW_RUNNER_DEBUG) { process.stderr.write = (chunk, cb) => { var _this$_currentTest2; this.dispatchEvent('stdErr', (0, _ipc.stdioChunkToParams)(chunk)); (_this$_currentTest2 = this._currentTest) === null || _this$_currentTest2 === void 0 || _this$_currentTest2._tracing.appendStdioToTrace('stderr', chunk); if (typeof cb === 'function') process.nextTick(cb); return true; }; } } _stop() { if (!this._isStopped) { var _this$_currentTest3; this._isStopped = true; (_this$_currentTest3 = this._currentTest) === null || _this$_currentTest3 === void 0 || _this$_currentTest3._interrupt(); } return this._runFinished; } async gracefullyClose() { try { await this._stop(); if (!this._config) { // We never set anything up and we can crash on attempting cleanup return; } // Ignore top-level errors, they are already inside TestInfo.errors. const fakeTestInfo = new _testInfo.TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {}); const runnable = { type: 'teardown' }; // We have to load the project to get the right deadline below. await fakeTestInfo._runWithTimeout(runnable, () => this._loadIfNeeded()).catch(() => {}); await this._fixtureRunner.teardownScope('test', fakeTestInfo, runnable).catch(() => {}); await this._fixtureRunner.teardownScope('worker', fakeTestInfo, runnable).catch(() => {}); // Close any other browsers launched in this process. This includes anything launched // manually in the test/hooks and internal browsers like Playwright Inspector. await fakeTestInfo._runWithTimeout(runnable, () => (0, _utils.gracefullyCloseAll)()).catch(() => {}); this._fatalErrors.push(...fakeTestInfo.errors); } catch (e) { this._fatalErrors.push((0, _util2.testInfoError)(e)); } if (this._fatalErrors.length) { this._appendProcessTeardownDiagnostics(this._fatalErrors[this._fatalErrors.length - 1]); const payload = { fatalErrors: this._fatalErrors }; this.dispatchEvent('teardownErrors', payload); } } _appendProcessTeardownDiagnostics(error) { if (!this._lastRunningTests.length) return; const count = this._totalRunningTests === 1 ? '1 test' : `${this._totalRunningTests} tests`; let lastMessage = ''; if (this._lastRunningTests.length < this._totalRunningTests) lastMessage = `, last ${this._lastRunningTests.length} tests were`; const message = ['', '', _utils.colors.red(`Failed worker ran ${count}${lastMessage}:`), ...this._lastRunningTests.map(test => formatTestTitle(test, this._project.project.name))].join('\n'); if (error.message) { if (error.stack) { let index = error.stack.indexOf(error.message); if (index !== -1) { index += error.message.length; error.stack = error.stack.substring(0, index) + message + error.stack.substring(index); } } error.message += message; } else if (error.value) { error.value += message; } } unhandledError(error) { // No current test - fatal error. if (!this._currentTest) { if (!this._fatalErrors.length) this._fatalErrors.push((0, _util2.testInfoError)(error)); void this._stop(); return; } // We do not differentiate between errors in the control flow // and unhandled errors - both lead to the test failing. This is good for regular tests, // so that you can, e.g. expect() from inside an event handler. The test fails, // and we restart the worker. if (!this._currentTest._hasUnhandledError) { this._currentTest._hasUnhandledError = true; this._currentTest._failWithError(error); } // For tests marked with test.fail(), this might be a problem when unhandled error // is not coming from the user test code (legit failure), but from fixtures or test runner. // // Ideally, we would mark this test as "failed unexpectedly" and show that in the report. // However, we do not have such a special test status, so the test will be considered ok (failed as expected). // // To avoid messing up future tests, we forcefully stop the worker, unless it is // an expect() error which we know does not mess things up. const isExpectError = error instanceof Error && !!error.matcherResult; const shouldContinueInThisWorker = this._currentTest.expectedStatus === 'failed' && isExpectError; if (!shouldContinueInThisWorker) void this._stop(); } async _loadIfNeeded() { if (this._config) return; const config = await (0, _configLoader.deserializeConfig)(this._params.config); const project = config.projects.find(p => p.id === this._params.projectId); if (!project) throw new Error(`Project "${this._params.projectId}" not found in the worker process. Make sure project name does not change.`); this._config = config; this._project = project; this._poolBuilder = _poolBuilder.PoolBuilder.createForWorker(this._project); } async runTestGroup(runPayload) { this._runFinished = new _utils.ManualPromise(); const entries = new Map(runPayload.entries.map(e => [e.testId, e])); let fatalUnknownTestIds; try { await this._loadIfNeeded(); const fileSuite = await (0, _testLoader.loadTestFile)(runPayload.file, this._config.config.rootDir); const suite = (0, _suiteUtils.bindFileSuiteToProject)(this._project, fileSuite); if (this._params.repeatEachIndex) (0, _suiteUtils.applyRepeatEachIndex)(this._project, suite, this._params.repeatEachIndex); const hasEntries = (0, _suiteUtils.filterTestsRemoveEmptySuites)(suite, test => entries.has(test.id)); if (hasEntries) { this._poolBuilder.buildPools(suite); this._activeSuites = new Map(); this._didRunFullCleanup = false; const tests = suite.allTests(); for (let i = 0; i < tests.length; i++) { // Do not run tests after full cleanup, because we are entirely done. if (this._isStopped && this._didRunFullCleanup) break; const entry = entries.get(tests[i].id); entries.delete(tests[i].id); (0, _util.debugTest)(`test started "${tests[i].title}"`); await this._runTest(tests[i], entry.retry, tests[i + 1]); (0, _util.debugTest)(`test finished "${tests[i].title}"`); } } else { fatalUnknownTestIds = runPayload.entries.map(e => e.testId); void this._stop(); } } catch (e) { // In theory, we should run above code without any errors. // However, in the case we screwed up, or loadTestFile failed in the worker // but not in the runner, let's do a fatal error. this._fatalErrors.push((0, _util2.testInfoError)(e)); void this._stop(); } finally { const donePayload = { fatalErrors: this._fatalErrors, skipTestsDueToSetupFailure: [], fatalUnknownTestIds }; for (const test of ((_this$_skipRemainingT = this._skipRemainingTestsInSuite) === null || _this$_skipRemainingT === void 0 ? void 0 : _this$_skipRemainingT.allTests()) || []) { var _this$_skipRemainingT; if (entries.has(test.id)) donePayload.skipTestsDueToSetupFailure.push(test.id); } this.dispatchEvent('done', donePayload); this._fatalErrors = []; this._skipRemainingTestsInSuite = undefined; this._runFinished.resolve(); } } async _runTest(test, retry, nextTest) { const testInfo = new _testInfo.TestInfoImpl(this._config, this._project, this._params, test, retry, stepBeginPayload => this.dispatchEvent('stepBegin', stepBeginPayload), stepEndPayload => this.dispatchEvent('stepEnd', stepEndPayload), attachment => this.dispatchEvent('attach', attachment)); const processAnnotation = annotation => { testInfo.annotations.push(annotation); switch (annotation.type) { case 'fixme': case 'skip': testInfo.expectedStatus = 'skipped'; break; case 'fail': if (testInfo.expectedStatus !== 'skipped') testInfo.expectedStatus = 'failed'; break; case 'slow': testInfo._timeoutManager.slow(); break; } }; if (!this._isStopped) this._fixtureRunner.setPool(test._pool); const suites = getSuites(test); const reversedSuites = suites.slice().reverse(); const nextSuites = new Set(getSuites(nextTest)); testInfo._timeoutManager.setTimeout(test.timeout); for (const annotation of test.annotations) processAnnotation(annotation); // Process existing annotations dynamically set for parent suites. for (const suite of suites) { const extraAnnotations = this._activeSuites.get(suite) || []; for (const annotation of extraAnnotations) processAnnotation(annotation); } this._currentTest = testInfo; (0, _globals.setCurrentTestInfo)(testInfo); this.dispatchEvent('testBegin', buildTestBeginPayload(testInfo)); const isSkipped = testInfo.expectedStatus === 'skipped'; const hasAfterAllToRunBeforeNextTest = reversedSuites.some(suite => { return this._activeSuites.has(suite) && !nextSuites.has(suite) && suite._hooks.some(hook => hook.type === 'afterAll'); }); if (isSkipped && nextTest && !hasAfterAllToRunBeforeNextTest) { // Fast path - this test is skipped, and there are more tests that will handle cleanup. testInfo.status = 'skipped'; this.dispatchEvent('testEnd', buildTestEndPayload(testInfo)); return; } this._totalRunningTests++; this._lastRunningTests.push(test); if (this._lastRunningTests.length > 10) this._lastRunningTests.shift(); let shouldRunAfterEachHooks = false; testInfo._allowSkips = true; // Create warning if any of the async calls were not awaited in various stages. const checkForFloatingPromises = functionDescription => { if (process.env.PW_DISABLE_FLOATING_PROMISES_WARNING) return; if (!testInfo._floatingPromiseScope.hasFloatingPromises()) return; // TODO: 1.52: Actually build annotations // testInfo.annotations.push({ type: 'warning', description: `Some async calls were not awaited by the end of ${functionDescription}. This can cause flakiness.` }); testInfo._floatingPromiseScope.clear(); }; await (async () => { await testInfo._runWithTimeout({ type: 'test' }, async () => { // Ideally, "trace" would be an config-level option belonging to the // test runner instead of a fixture belonging to Playwright. // However, for backwards compatibility, we have to read it from a fixture today. // We decided to not introduce the config-level option just yet. const traceFixtureRegistration = test._pool.resolve('trace'); if (!traceFixtureRegistration) return; if (typeof traceFixtureRegistration.fn === 'function') throw new Error(`"trace" option cannot be a function`); await testInfo._tracing.startIfNeeded(traceFixtureRegistration.fn); }); if (this._isStopped || isSkipped) { // Two reasons to get here: // - Last test is skipped, so we should not run the test, but run the cleanup. // - Worker is requested to stop, but was not able to run full cleanup yet. // We should skip the test, but run the cleanup. testInfo.status = 'skipped'; return; } await (0, _utils.removeFolders)([testInfo.outputDir]); let testFunctionParams = null; await testInfo._runAsStep({ title: 'Before Hooks', category: 'hook' }, async () => { // Run "beforeAll" hooks, unless already run during previous tests. for (const suite of suites) await this._runBeforeAllHooksForSuite(suite, testInfo); // Run "beforeEach" hooks. Once started with "beforeEach", we must run all "afterEach" hooks as well. shouldRunAfterEachHooks = true; await this._runEachHooksForSuites(suites, 'beforeEach', testInfo); // Setup fixtures required by the test. testFunctionParams = await this._fixtureRunner.resolveParametersForFunction(test.fn, testInfo, 'test', { type: 'test' }); }); checkForFloatingPromises('beforeAll/beforeEach hooks'); if (testFunctionParams === null) { // Fixture setup failed or was skipped, we should not run the test now. return; } await testInfo._runWithTimeout({ type: 'test' }, async () => { // Now run the test itself. const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]"). await fn(testFunctionParams, testInfo); checkForFloatingPromises('the test'); }); })().catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors. // Update duration, so it is available in fixture teardown and afterEach hooks. testInfo.duration = testInfo._timeoutManager.defaultSlot().elapsed | 0; // No skips in after hooks. testInfo._allowSkips = true; // After hooks get an additional timeout. const afterHooksTimeout = calculateMaxTimeout(this._project.project.timeout, testInfo.timeout); const afterHooksSlot = { timeout: afterHooksTimeout, elapsed: 0 }; await testInfo._runAsStep({ title: 'After Hooks', category: 'hook' }, async () => { let firstAfterHooksError; try { // Run "immediately upon test function finish" callback. await testInfo._runWithTimeout({ type: 'test', slot: afterHooksSlot }, async () => { var _testInfo$_onDidFinis; return (_testInfo$_onDidFinis = testInfo._onDidFinishTestFunction) === null || _testInfo$_onDidFinis === void 0 ? void 0 : _testInfo$_onDidFinis.call(testInfo); }); } catch (error) { firstAfterHooksError = firstAfterHooksError !== null && firstAfterHooksError !== void 0 ? firstAfterHooksError : error; } try { // Run "afterEach" hooks, unless we failed at beforeAll stage. if (shouldRunAfterEachHooks) await this._runEachHooksForSuites(reversedSuites, 'afterEach', testInfo, afterHooksSlot); } catch (error) { firstAfterHooksError = firstAfterHooksError !== null && firstAfterHooksError !== void 0 ? firstAfterHooksError : error; } testInfo._tracing.didFinishTestFunctionAndAfterEachHooks(); try { // Teardown test-scoped fixtures. Attribute to 'test' so that users understand // they should probably increase the test timeout to fix this issue. await this._fixtureRunner.teardownScope('test', testInfo, { type: 'test', slot: afterHooksSlot }); } catch (error) { firstAfterHooksError = firstAfterHooksError !== null && firstAfterHooksError !== void 0 ? firstAfterHooksError : error; } // Run "afterAll" hooks for suites that are not shared with the next test. // In case of failure the worker will be stopped and we have to make sure that afterAll // hooks run before worker fixtures teardown. for (const suite of reversedSuites) { if (!nextSuites.has(suite) || testInfo._isFailure()) { try { await this._runAfterAllHooksForSuite(suite, testInfo); } catch (error) { // Continue running "afterAll" hooks even after some of them timeout. firstAfterHooksError = firstAfterHooksError !== null && firstAfterHooksError !== void 0 ? firstAfterHooksError : error; } } } if (firstAfterHooksError) throw firstAfterHooksError; }).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors. checkForFloatingPromises('afterAll/afterEach hooks'); if (testInfo._isFailure()) this._isStopped = true; if (this._isStopped) { // Run all remaining "afterAll" hooks and teardown all fixtures when worker is shutting down. // Mark as "cleaned up" early to avoid running cleanup twice. this._didRunFullCleanup = true; await testInfo._runAsStep({ title: 'Worker Cleanup', category: 'hook' }, async () => { let firstWorkerCleanupError; // Give it more time for the full cleanup. const teardownSlot = { timeout: this._project.project.timeout, elapsed: 0 }; try { // Attribute to 'test' so that users understand they should probably increate the test timeout to fix this issue. await this._fixtureRunner.teardownScope('test', testInfo, { type: 'test', slot: teardownSlot }); } catch (error) { firstWorkerCleanupError = firstWorkerCleanupError !== null && firstWorkerCleanupError !== void 0 ? firstWorkerCleanupError : error; } for (const suite of reversedSuites) { try { await this._runAfterAllHooksForSuite(suite, testInfo); } catch (error) { firstWorkerCleanupError = firstWorkerCleanupError !== null && firstWorkerCleanupError !== void 0 ? firstWorkerCleanupError : error; } } try { // Attribute to 'teardown' because worker fixtures are not perceived as a part of a test. await this._fixtureRunner.teardownScope('worker', testInfo, { type: 'teardown', slot: teardownSlot }); } catch (error) { firstWorkerCleanupError = firstWorkerCleanupError !== null && firstWorkerCleanupError !== void 0 ? firstWorkerCleanupError : error; } if (firstWorkerCleanupError) throw firstWorkerCleanupError; }).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors. } const tracingSlot = { timeout: this._project.project.timeout, elapsed: 0 }; await testInfo._runWithTimeout({ type: 'test', slot: tracingSlot }, async () => { await testInfo._tracing.stopIfNeeded(); }).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors. testInfo.duration = testInfo._timeoutManager.defaultSlot().elapsed + afterHooksSlot.elapsed | 0; this._currentTest = null; (0, _globals.setCurrentTestInfo)(null); this.dispatchEvent('testEnd', buildTestEndPayload(testInfo)); const preserveOutput = this._config.config.preserveOutput === 'always' || this._config.config.preserveOutput === 'failures-only' && testInfo._isFailure(); if (!preserveOutput) await (0, _utils.removeFolders)([testInfo.outputDir]); } _collectHooksAndModifiers(suite, type, testInfo) { const runnables = []; for (const modifier of suite._modifiers) { const modifierType = this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location) ? 'beforeAll' : 'beforeEach'; if (modifierType !== type) continue; const fn = async fixtures => { const result = await modifier.fn(fixtures); testInfo[modifier.type](!!result, modifier.description); }; (0, _fixtures.inheritFixtureNames)(modifier.fn, fn); runnables.push({ title: `${modifier.type} modifier`, location: modifier.location, type: modifier.type, fn }); } // Modifiers first, then hooks. runnables.push(...suite._hooks.filter(hook => hook.type === type)); return runnables; } async _runBeforeAllHooksForSuite(suite, testInfo) { if (this._activeSuites.has(suite)) return; const extraAnnotations = []; this._activeSuites.set(suite, extraAnnotations); await this._runAllHooksForSuite(suite, testInfo, 'beforeAll', extraAnnotations); } async _runAllHooksForSuite(suite, testInfo, type, extraAnnotations) { // Always run all the hooks, and capture the first error. let firstError; for (const hook of this._collectHooksAndModifiers(suite, type, testInfo)) { try { await testInfo._runAsStep({ title: hook.title, category: 'hook', location: hook.location }, async () => { // Separate time slot for each beforeAll/afterAll hook. const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 }; const runnable = { type: hook.type, slot: timeSlot, location: hook.location }; const existingAnnotations = new Set(testInfo.annotations); try { await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only', runnable); } finally { if (extraAnnotations) { // Inherit all annotations defined in the beforeAll/modifer to all tests in the suite. const newAnnotations = testInfo.annotations.filter(a => !existingAnnotations.has(a)); extraAnnotations.push(...newAnnotations); } // Each beforeAll/afterAll hook has its own scope for test fixtures. Attribute to the same runnable and timeSlot. // Note: we must teardown even after hook fails, because we'll run more hooks. await this._fixtureRunner.teardownScope('test', testInfo, runnable); } }); } catch (error) { firstError = firstError !== null && firstError !== void 0 ? firstError : error; // Skip in beforeAll/modifier prevents others from running. if (type === 'beforeAll' && error instanceof _testInfo.TestSkipError) break; if (type === 'beforeAll' && !this._skipRemainingTestsInSuite) { // This will inform dispatcher that we should not run more tests from this group // because we had a beforeAll error. // This behavior avoids getting the same common error for each test. this._skipRemainingTestsInSuite = suite; } } } if (firstError) throw firstError; } async _runAfterAllHooksForSuite(suite, testInfo) { if (!this._activeSuites.has(suite)) return; this._activeSuites.delete(suite); await this._runAllHooksForSuite(suite, testInfo, 'afterAll'); } async _runEachHooksForSuites(suites, type, testInfo, slot) { // Always run all the hooks, unless one of the times out, and capture the first error. let firstError; const hooks = suites.map(suite => this._collectHooksAndModifiers(suite, type, testInfo)).flat(); for (const hook of hooks) { const runnable = { type: hook.type, location: hook.location, slot }; if (testInfo._timeoutManager.isTimeExhaustedFor(runnable)) { // Do not run hooks that will timeout right away. continue; } try { await testInfo._runAsStep({ title: hook.title, category: 'hook', location: hook.location }, async () => { await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'test', runnable); }); } catch (error) { firstError = firstError !== null && firstError !== void 0 ? firstError : error; // Skip in modifier prevents others from running. if (error instanceof _testInfo.TestSkipError) break; } } if (firstError) throw firstError; } } exports.WorkerMain = WorkerMain; function buildTestBeginPayload(testInfo) { return { testId: testInfo.testId, startWallTime: testInfo._startWallTime }; } function buildTestEndPayload(testInfo) { return { testId: testInfo.testId, duration: testInfo.duration, status: testInfo.status, errors: testInfo.errors, hasNonRetriableError: testInfo._hasNonRetriableError, expectedStatus: testInfo.expectedStatus, annotations: testInfo.annotations, timeout: testInfo.timeout }; } function getSuites(test) { const suites = []; for (let suite = test === null || test === void 0 ? void 0 : test.parent; suite; suite = suite.parent) suites.push(suite); suites.reverse(); // Put root suite first. return suites; } function formatTestTitle(test, projectName) { // file, ...describes, test const [, ...titles] = test.titlePath(); const location = `${(0, _util.relativeFilePath)(test.location.file)}:${test.location.line}:${test.location.column}`; const projectTitle = projectName ? `[${projectName}] › ` : ''; return `${projectTitle}${location} › ${titles.join(' › ')}`; } function calculateMaxTimeout(t1, t2) { // Zero means "no timeout". return !t1 || !t2 ? 0 : Math.max(t1, t2); } const create = params => new WorkerMain(params); exports.create = create;