430 lines
18 KiB
JavaScript
430 lines
18 KiB
JavaScript
|
"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;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
}
|