"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestRun = void 0; exports.createTaskRunner = createTaskRunner; exports.createTaskRunnerForList = createTaskRunnerForList; exports.createTaskRunnerForWatch = createTaskRunnerForWatch; exports.createTaskRunnerForWatchSetup = createTaskRunnerForWatchSetup; var _fs = _interopRequireDefault(require("fs")); var _path = _interopRequireDefault(require("path")); var _util = require("util"); var _utilsBundle = require("playwright-core/lib/utilsBundle"); var _utils = require("playwright-core/lib/utils"); var _dispatcher = require("./dispatcher"); var _testGroups = require("../runner/testGroups"); var _taskRunner = require("./taskRunner"); var _loadUtils = require("./loadUtils"); var _projectUtils = require("./projectUtils"); var _failureTracker = require("./failureTracker"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * 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. */ const readDirAsync = (0, _util.promisify)(_fs.default.readdir); class TestRun { constructor(config, reporter) { this.reporter = void 0; this.config = void 0; this.failureTracker = void 0; this.rootSuite = undefined; this.phases = []; this.projects = []; this.projectFiles = new Map(); this.projectSuites = new Map(); this.config = config; this.reporter = reporter; this.failureTracker = new _failureTracker.FailureTracker(config); } } exports.TestRun = TestRun; function createTaskRunner(config, reporter) { const taskRunner = new _taskRunner.TaskRunner(reporter, config.config.globalTimeout); addGlobalSetupTasks(taskRunner, config); taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true })); addRunTasks(taskRunner, config); return taskRunner; } function createTaskRunnerForWatchSetup(config, reporter) { const taskRunner = new _taskRunner.TaskRunner(reporter, 0); addGlobalSetupTasks(taskRunner, config); return taskRunner; } function createTaskRunnerForWatch(config, reporter, additionalFileMatcher) { const taskRunner = new _taskRunner.TaskRunner(reporter, 0); taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunTestsOutsideProjectFilter: true, additionalFileMatcher })); addRunTasks(taskRunner, config); return taskRunner; } function addGlobalSetupTasks(taskRunner, config) { for (const plugin of config.plugins) taskRunner.addTask('plugin setup', createPluginSetupTask(plugin)); if (config.config.globalSetup || config.config.globalTeardown) taskRunner.addTask('global setup', createGlobalSetupTask()); taskRunner.addTask('clear output', createRemoveOutputDirsTask()); } function addRunTasks(taskRunner, config) { taskRunner.addTask('create phases', createPhasesTask()); taskRunner.addTask('report begin', createReportBeginTask()); for (const plugin of config.plugins) taskRunner.addTask('plugin begin', createPluginBeginTask(plugin)); taskRunner.addTask('test suite', createRunTestsTask()); return taskRunner; } function createTaskRunnerForList(config, reporter, mode, options) { const taskRunner = new _taskRunner.TaskRunner(reporter, config.config.globalTimeout); taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false })); taskRunner.addTask('report begin', createReportBeginTask()); return taskRunner; } function createReportBeginTask() { return { setup: async ({ reporter, rootSuite }) => { reporter.onBegin(rootSuite); }, teardown: async ({}) => {} }; } function createPluginSetupTask(plugin) { return { setup: async ({ config, reporter }) => { var _plugin$instance, _plugin$instance$setu; if (typeof plugin.factory === 'function') plugin.instance = await plugin.factory();else plugin.instance = plugin.factory; await ((_plugin$instance = plugin.instance) === null || _plugin$instance === void 0 ? void 0 : (_plugin$instance$setu = _plugin$instance.setup) === null || _plugin$instance$setu === void 0 ? void 0 : _plugin$instance$setu.call(_plugin$instance, config.config, config.configDir, reporter)); }, teardown: async () => { var _plugin$instance2, _plugin$instance2$tea; await ((_plugin$instance2 = plugin.instance) === null || _plugin$instance2 === void 0 ? void 0 : (_plugin$instance2$tea = _plugin$instance2.teardown) === null || _plugin$instance2$tea === void 0 ? void 0 : _plugin$instance2$tea.call(_plugin$instance2)); } }; } function createPluginBeginTask(plugin) { return { setup: async ({ rootSuite }) => { var _plugin$instance3, _plugin$instance3$beg; await ((_plugin$instance3 = plugin.instance) === null || _plugin$instance3 === void 0 ? void 0 : (_plugin$instance3$beg = _plugin$instance3.begin) === null || _plugin$instance3$beg === void 0 ? void 0 : _plugin$instance3$beg.call(_plugin$instance3, rootSuite)); }, teardown: async () => { var _plugin$instance4, _plugin$instance4$end; await ((_plugin$instance4 = plugin.instance) === null || _plugin$instance4 === void 0 ? void 0 : (_plugin$instance4$end = _plugin$instance4.end) === null || _plugin$instance4$end === void 0 ? void 0 : _plugin$instance4$end.call(_plugin$instance4)); } }; } function createGlobalSetupTask() { let globalSetupResult; let globalSetupFinished = false; let teardownHook; return { setup: async ({ config }) => { const setupHook = config.config.globalSetup ? await (0, _loadUtils.loadGlobalHook)(config, config.config.globalSetup) : undefined; teardownHook = config.config.globalTeardown ? await (0, _loadUtils.loadGlobalHook)(config, config.config.globalTeardown) : undefined; globalSetupResult = setupHook ? await setupHook(config.config) : undefined; globalSetupFinished = true; }, teardown: async ({ config }) => { var _teardownHook; if (typeof globalSetupResult === 'function') await globalSetupResult(); if (globalSetupFinished) await ((_teardownHook = teardownHook) === null || _teardownHook === void 0 ? void 0 : _teardownHook(config.config)); } }; } function createRemoveOutputDirsTask() { return { setup: async ({ config }) => { if (process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS) return; const outputDirs = new Set(); for (const p of config.projects) { if (!config.cliProjectFilter || config.cliProjectFilter.includes(p.project.name)) outputDirs.add(p.project.outputDir); } await Promise.all(Array.from(outputDirs).map(outputDir => (0, _utils.removeFolders)([outputDir]).then(async ([error]) => { if (!error) return; if (error.code === 'EBUSY') { // We failed to remove folder, might be due to the whole folder being mounted inside a container: // https://github.com/microsoft/playwright/issues/12106 // Do a best-effort to remove all files inside of it instead. const entries = await readDirAsync(outputDir).catch(e => []); await Promise.all(entries.map(entry => (0, _utils.removeFolders)([_path.default.join(outputDir, entry)]))); } else { throw error; } }))); } }; } function createLoadTask(mode, options) { return { setup: async (testRun, errors, softErrors) => { await (0, _loadUtils.collectProjectsAndTestFiles)(testRun, !!options.doNotRunTestsOutsideProjectFilter, options.additionalFileMatcher); await (0, _loadUtils.loadFileSuites)(testRun, mode, options.failOnLoadErrors ? errors : softErrors); testRun.rootSuite = await (0, _loadUtils.createRootSuite)(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly); testRun.failureTracker.onRootSuite(testRun.rootSuite); // Fail when no tests. if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard) { if (testRun.config.cliArgs.length) { throw new Error([`No tests found.`, `Make sure that arguments are regular expressions matching test files.`, `You may need to escape symbols like "$" or "*" and quote the arguments.`].join('\n')); } throw new Error(`No tests found`); } } }; } function createPhasesTask() { return { setup: async testRun => { let maxConcurrentTestGroups = 0; const processed = new Set(); const projectToSuite = new Map(testRun.rootSuite.suites.map(suite => [suite._fullProject, suite])); const allProjects = [...projectToSuite.keys()]; const teardownToSetups = (0, _projectUtils.buildTeardownToSetupsMap)(allProjects); const teardownToSetupsDependents = new Map(); for (const [teardown, setups] of teardownToSetups) { const closure = (0, _projectUtils.buildDependentProjects)(setups, allProjects); closure.delete(teardown); teardownToSetupsDependents.set(teardown, [...closure]); } for (let i = 0; i < projectToSuite.size; i++) { // Find all projects that have all their dependencies processed by previous phases. const phaseProjects = []; for (const project of projectToSuite.keys()) { if (processed.has(project)) continue; const projectsThatShouldFinishFirst = [...project.deps, ...(teardownToSetupsDependents.get(project) || [])]; if (projectsThatShouldFinishFirst.find(p => !processed.has(p))) continue; phaseProjects.push(project); } // Create a new phase. for (const project of phaseProjects) processed.add(project); if (phaseProjects.length) { let testGroupsInPhase = 0; const phase = { dispatcher: new _dispatcher.Dispatcher(testRun.config, testRun.reporter, testRun.failureTracker), projects: [] }; testRun.phases.push(phase); for (const project of phaseProjects) { const projectSuite = projectToSuite.get(project); const testGroups = (0, _testGroups.createTestGroups)(projectSuite, testRun.config.config.workers); phase.projects.push({ project, projectSuite, testGroups }); testGroupsInPhase += testGroups.length; } (0, _utilsBundle.debug)('pw:test:task')(`created phase #${testRun.phases.length} with ${phase.projects.map(p => p.project.project.name).sort()} projects, ${testGroupsInPhase} testGroups`); maxConcurrentTestGroups = Math.max(maxConcurrentTestGroups, testGroupsInPhase); } } testRun.config.config.metadata.actualWorkers = Math.min(testRun.config.config.workers, maxConcurrentTestGroups); } }; } function createRunTestsTask() { return { setup: async ({ phases, failureTracker }) => { const successfulProjects = new Set(); const extraEnvByProjectId = new Map(); const teardownToSetups = (0, _projectUtils.buildTeardownToSetupsMap)(phases.map(phase => phase.projects.map(p => p.project)).flat()); for (const { dispatcher, projects } of phases) { // Each phase contains dispatcher and a set of test groups. // We don't want to run the test groups belonging to the projects // that depend on the projects that failed previously. const phaseTestGroups = []; for (const { project, testGroups } of projects) { // Inherit extra environment variables from dependencies. let extraEnv = {}; for (const dep of project.deps) extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep.id) }; for (const setup of teardownToSetups.get(project) || []) extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(setup.id) }; extraEnvByProjectId.set(project.id, extraEnv); const hasFailedDeps = project.deps.some(p => !successfulProjects.has(p)); if (!hasFailedDeps) phaseTestGroups.push(...testGroups); } if (phaseTestGroups.length) { await dispatcher.run(phaseTestGroups, extraEnvByProjectId); await dispatcher.stop(); for (const [projectId, envProduced] of dispatcher.producedEnvByProjectId()) { const extraEnv = extraEnvByProjectId.get(projectId) || {}; extraEnvByProjectId.set(projectId, { ...extraEnv, ...envProduced }); } } // If the worker broke, fail everything, we have no way of knowing which // projects failed. if (!failureTracker.hasWorkerErrors()) { for (const { project, projectSuite } of projects) { const hasFailedDeps = project.deps.some(p => !successfulProjects.has(p)); if (!hasFailedDeps && !projectSuite.allTests().some(test => !test.ok())) successfulProjects.add(project); } } } }, teardown: async ({ phases }) => { for (const { dispatcher } of phases.reverse()) await dispatcher.stop(); } }; }