430 lines
18 KiB
JavaScript
Raw Normal View History

2024-11-25 16:53:40 -06:00
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.TestRun = void 0;
exports.createApplyRebaselinesTask = createApplyRebaselinesTask;
exports.createClearCacheTask = createClearCacheTask;
exports.createGlobalSetupTasks = createGlobalSetupTasks;
exports.createListFilesTask = createListFilesTask;
exports.createLoadTask = createLoadTask;
exports.createPluginSetupTasks = createPluginSetupTasks;
exports.createReportBeginTask = createReportBeginTask;
exports.createRunTestsTasks = createRunTestsTasks;
exports.createStartDevServerTask = createStartDevServerTask;
exports.runTasks = runTasks;
exports.runTasksDeferCleanup = runTasksDeferCleanup;
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 _util2 = require("../util");
var _test = require("../common/test");
var _projectUtils = require("./projectUtils");
var _failureTracker = require("./failureTracker");
var _vcs = require("./vcs");
var _compilationCache = require("../transform/compilationCache");
var _rebase = require("./rebase");
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.config = void 0;
this.reporter = void 0;
this.failureTracker = void 0;
this.rootSuite = undefined;
this.phases = [];
this.projectFiles = new Map();
this.projectSuites = new Map();
this.config = config;
this.reporter = reporter;
this.failureTracker = new _failureTracker.FailureTracker(config);
}
}
exports.TestRun = TestRun;
async function runTasks(testRun, tasks, globalTimeout, cancelPromise) {
const deadline = globalTimeout ? (0, _utils.monotonicTime)() + globalTimeout : 0;
const taskRunner = new _taskRunner.TaskRunner(testRun.reporter, globalTimeout || 0);
for (const task of tasks) taskRunner.addTask(task);
testRun.reporter.onConfigure(testRun.config.config);
const status = await taskRunner.run(testRun, deadline, cancelPromise);
return await finishTaskRun(testRun, status);
}
async function runTasksDeferCleanup(testRun, tasks) {
const taskRunner = new _taskRunner.TaskRunner(testRun.reporter, 0);
for (const task of tasks) taskRunner.addTask(task);
testRun.reporter.onConfigure(testRun.config.config);
const {
status,
cleanup
} = await taskRunner.runDeferCleanup(testRun, 0);
return {
status: await finishTaskRun(testRun, status),
cleanup
};
}
async function finishTaskRun(testRun, status) {
if (status === 'passed') status = testRun.failureTracker.result();
const modifiedResult = await testRun.reporter.onEnd({
status
});
if (modifiedResult && modifiedResult.status) status = modifiedResult.status;
await testRun.reporter.onExit();
return status;
}
function createGlobalSetupTasks(config) {
const tasks = [];
if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS) tasks.push(createRemoveOutputDirsTask());
tasks.push(...createPluginSetupTasks(config), ...config.globalTeardowns.map(file => createGlobalTeardownTask(file, config)).reverse(), ...config.globalSetups.map(file => createGlobalSetupTask(file, config)));
return tasks;
}
function createRunTestsTasks(config) {
return [createPhasesTask(), createReportBeginTask(), ...config.plugins.map(plugin => createPluginBeginTask(plugin)), createRunTestsTask()];
}
function createClearCacheTask(config) {
return {
title: 'clear cache',
setup: async () => {
await (0, _util2.removeDirAndLogToConsole)(_compilationCache.cacheDir);
for (const plugin of config.plugins) {
var _plugin$instance, _plugin$instance$clea;
await ((_plugin$instance = plugin.instance) === null || _plugin$instance === void 0 || (_plugin$instance$clea = _plugin$instance.clearCache) === null || _plugin$instance$clea === void 0 ? void 0 : _plugin$instance$clea.call(_plugin$instance));
}
}
};
}
function createReportBeginTask() {
return {
title: 'report begin',
setup: async testRun => {
var _testRun$reporter$onB, _testRun$reporter;
(_testRun$reporter$onB = (_testRun$reporter = testRun.reporter).onBegin) === null || _testRun$reporter$onB === void 0 || _testRun$reporter$onB.call(_testRun$reporter, testRun.rootSuite);
},
teardown: async ({}) => {}
};
}
function createPluginSetupTasks(config) {
return config.plugins.map(plugin => ({
title: 'plugin setup',
setup: async ({
reporter
}) => {
var _plugin$instance2, _plugin$instance2$set;
if (typeof plugin.factory === 'function') plugin.instance = await plugin.factory();else plugin.instance = plugin.factory;
await ((_plugin$instance2 = plugin.instance) === null || _plugin$instance2 === void 0 || (_plugin$instance2$set = _plugin$instance2.setup) === null || _plugin$instance2$set === void 0 ? void 0 : _plugin$instance2$set.call(_plugin$instance2, config.config, config.configDir, reporter));
},
teardown: async () => {
var _plugin$instance3, _plugin$instance3$tea;
await ((_plugin$instance3 = plugin.instance) === null || _plugin$instance3 === void 0 || (_plugin$instance3$tea = _plugin$instance3.teardown) === null || _plugin$instance3$tea === void 0 ? void 0 : _plugin$instance3$tea.call(_plugin$instance3));
}
}));
}
function createPluginBeginTask(plugin) {
return {
title: 'plugin begin',
setup: async testRun => {
var _plugin$instance4, _plugin$instance4$beg;
await ((_plugin$instance4 = plugin.instance) === null || _plugin$instance4 === void 0 || (_plugin$instance4$beg = _plugin$instance4.begin) === null || _plugin$instance4$beg === void 0 ? void 0 : _plugin$instance4$beg.call(_plugin$instance4, testRun.rootSuite));
},
teardown: async () => {
var _plugin$instance5, _plugin$instance5$end;
await ((_plugin$instance5 = plugin.instance) === null || _plugin$instance5 === void 0 || (_plugin$instance5$end = _plugin$instance5.end) === null || _plugin$instance5$end === void 0 ? void 0 : _plugin$instance5$end.call(_plugin$instance5));
}
};
}
function createGlobalSetupTask(file, config) {
let title = 'global setup';
if (config.globalSetups.length > 1) title += ` (${file})`;
let globalSetupResult;
return {
title,
setup: async ({
config
}) => {
const setupHook = await (0, _loadUtils.loadGlobalHook)(config, file);
globalSetupResult = await setupHook(config.config);
},
teardown: async () => {
if (typeof globalSetupResult === 'function') await globalSetupResult();
}
};
}
function createGlobalTeardownTask(file, config) {
let title = 'global teardown';
if (config.globalTeardowns.length > 1) title += ` (${file})`;
return {
title,
teardown: async ({
config
}) => {
const teardownHook = await (0, _loadUtils.loadGlobalHook)(config, file);
await teardownHook(config.config);
}
};
}
function createRemoveOutputDirsTask() {
return {
title: 'clear output',
setup: async ({
config
}) => {
const outputDirs = new Set();
const projects = (0, _projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
projects.forEach(p => 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 createListFilesTask() {
return {
title: 'load tests',
setup: async (testRun, errors) => {
testRun.rootSuite = await (0, _loadUtils.createRootSuite)(testRun, errors, false);
testRun.failureTracker.onRootSuite(testRun.rootSuite);
await (0, _loadUtils.collectProjectsAndTestFiles)(testRun, false);
for (const [project, files] of testRun.projectFiles) {
const projectSuite = new _test.Suite(project.project.name, 'project');
projectSuite._fullProject = project;
testRun.rootSuite._addSuite(projectSuite);
const suites = files.map(file => {
const title = _path.default.relative(testRun.config.config.rootDir, file);
const suite = new _test.Suite(title, 'file');
suite.location = {
file,
line: 0,
column: 0
};
projectSuite._addSuite(suite);
return suite;
});
testRun.projectSuites.set(project, suites);
}
}
};
}
function createLoadTask(mode, options) {
return {
title: 'load tests',
setup: async (testRun, errors, softErrors) => {
await (0, _loadUtils.collectProjectsAndTestFiles)(testRun, !!options.doNotRunDepsOutsideProjectFilter);
await (0, _loadUtils.loadFileSuites)(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
if (testRun.config.cliOnlyChanged || options.populateDependencies) {
for (const plugin of testRun.config.plugins) {
var _plugin$instance6, _plugin$instance6$pop;
await ((_plugin$instance6 = plugin.instance) === null || _plugin$instance6 === void 0 || (_plugin$instance6$pop = _plugin$instance6.populateDependencies) === null || _plugin$instance6$pop === void 0 ? void 0 : _plugin$instance6$pop.call(_plugin$instance6));
}
}
let cliOnlyChangedMatcher = undefined;
if (testRun.config.cliOnlyChanged) {
const changedFiles = await (0, _vcs.detectChangedTestFiles)(testRun.config.cliOnlyChanged, testRun.config.configDir);
cliOnlyChangedMatcher = file => changedFiles.has(file);
}
testRun.rootSuite = await (0, _loadUtils.createRootSuite)(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly, cliOnlyChangedMatcher);
testRun.failureTracker.onRootSuite(testRun.rootSuite);
// Fail when no tests.
if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard && !testRun.config.cliOnlyChanged) {
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 createApplyRebaselinesTask() {
return {
title: 'apply rebaselines',
teardown: async ({
config,
reporter
}) => {
await (0, _rebase.applySuggestedRebaselines)(config, reporter);
}
};
}
function createPhasesTask() {
return {
title: 'create phases',
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 {
title: 'test suite',
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();
}
};
}
function createStartDevServerTask() {
return {
title: 'start dev server',
setup: async ({
config
}, errors, softErrors) => {
if (config.plugins.some(plugin => !!plugin.devServerCleanup)) {
errors.push({
message: `DevServer is already running`
});
return;
}
for (const plugin of config.plugins) {
var _plugin$instance7, _plugin$instance7$sta;
plugin.devServerCleanup = await ((_plugin$instance7 = plugin.instance) === null || _plugin$instance7 === void 0 || (_plugin$instance7$sta = _plugin$instance7.startDevServer) === null || _plugin$instance7$sta === void 0 ? void 0 : _plugin$instance7$sta.call(_plugin$instance7));
}
if (!config.plugins.some(plugin => !!plugin.devServerCleanup)) errors.push({
message: `DevServer is not available in the package you are using. Did you mean to use component testing?`
});
},
teardown: async ({
config
}) => {
for (const plugin of config.plugins) {
var _plugin$devServerClea;
await ((_plugin$devServerClea = plugin.devServerCleanup) === null || _plugin$devServerClea === void 0 ? void 0 : _plugin$devServerClea.call(plugin));
plugin.devServerCleanup = undefined;
}
}
};
}