2021-06-06 17:09:53 -07:00
|
|
|
/**
|
|
|
|
* Copyright 2019 Google Inc. All rights reserved.
|
|
|
|
* Modifications copyright (c) Microsoft Corporation.
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2023-01-25 15:38:23 -08:00
|
|
|
import { monotonicTime } from 'playwright-core/lib/utils';
|
2023-01-26 13:20:05 -08:00
|
|
|
import type { FullResult } from '../types/testReporter';
|
2023-01-17 17:16:36 -08:00
|
|
|
import { ConfigLoader } from './configLoader';
|
2022-10-10 16:42:48 -07:00
|
|
|
import type { TestRunnerPlugin } from './plugins';
|
|
|
|
import { setRunnerToAddPluginsTo } from './plugins';
|
|
|
|
import { dockerPlugin } from './plugins/dockerPlugin';
|
|
|
|
import { webServerPluginsForConfig } from './plugins/webServerPlugin';
|
2023-01-26 13:20:05 -08:00
|
|
|
import { collectFilesForProjects, collectProjects } from './runner/projectUtils';
|
|
|
|
import { createReporter } from './runner/reporters';
|
|
|
|
import { createTaskRunner } from './runner/tasks';
|
|
|
|
import type { TaskRunnerState } from './runner/tasks';
|
|
|
|
import type { Config, FullConfigInternal } from './types';
|
2022-10-12 14:34:22 -07:00
|
|
|
import type { Matcher, TestFileFilter } from './util';
|
2022-01-05 15:49:01 -08:00
|
|
|
|
2022-04-29 12:32:39 -08:00
|
|
|
export type ConfigCLIOverrides = {
|
|
|
|
forbidOnly?: boolean;
|
|
|
|
fullyParallel?: boolean;
|
|
|
|
globalTimeout?: number;
|
|
|
|
maxFailures?: number;
|
|
|
|
outputDir?: string;
|
|
|
|
quiet?: boolean;
|
|
|
|
repeatEach?: number;
|
|
|
|
retries?: number;
|
|
|
|
reporter?: string;
|
|
|
|
shard?: { current: number, total: number };
|
|
|
|
timeout?: number;
|
2022-09-01 05:34:36 -07:00
|
|
|
ignoreSnapshots?: boolean;
|
2022-04-29 12:32:39 -08:00
|
|
|
updateSnapshots?: 'all'|'none'|'missing';
|
|
|
|
workers?: number;
|
|
|
|
projects?: { name: string, use?: any }[],
|
|
|
|
use?: any;
|
|
|
|
};
|
|
|
|
|
2023-01-26 13:20:05 -08:00
|
|
|
export type RunOptions = {
|
|
|
|
listOnly: boolean;
|
|
|
|
testFileFilters: TestFileFilter[];
|
|
|
|
testTitleMatcher: Matcher;
|
|
|
|
projectFilter?: string[];
|
|
|
|
passWithNoTests?: boolean;
|
|
|
|
};
|
|
|
|
|
2021-06-06 17:09:53 -07:00
|
|
|
export class Runner {
|
2023-01-17 17:16:36 -08:00
|
|
|
private _configLoader: ConfigLoader;
|
2022-05-03 13:25:56 -08:00
|
|
|
private _plugins: TestRunnerPlugin[] = [];
|
2021-06-06 17:09:53 -07:00
|
|
|
|
2022-04-29 12:32:39 -08:00
|
|
|
constructor(configCLIOverrides?: ConfigCLIOverrides) {
|
2023-01-17 17:16:36 -08:00
|
|
|
this._configLoader = new ConfigLoader(configCLIOverrides);
|
2022-05-03 13:25:56 -08:00
|
|
|
setRunnerToAddPluginsTo(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
addPlugin(plugin: TestRunnerPlugin) {
|
|
|
|
this._plugins.push(plugin);
|
2022-01-05 13:44:29 -08:00
|
|
|
}
|
|
|
|
|
2022-04-29 12:32:39 -08:00
|
|
|
async loadConfigFromResolvedFile(resolvedConfigFile: string): Promise<FullConfigInternal> {
|
2023-01-17 17:16:36 -08:00
|
|
|
return await this._configLoader.loadConfigFile(resolvedConfigFile);
|
2022-03-01 12:56:26 -08:00
|
|
|
}
|
|
|
|
|
2022-04-28 16:22:20 -07:00
|
|
|
loadEmptyConfig(configFileOrDirectory: string): Promise<Config> {
|
2023-01-17 17:16:36 -08:00
|
|
|
return this._configLoader.loadEmptyConfig(configFileOrDirectory);
|
2022-03-01 12:56:26 -08:00
|
|
|
}
|
|
|
|
|
2022-11-08 12:05:00 -08:00
|
|
|
async listTestFiles(projectNames: string[] | undefined): Promise<any> {
|
2023-01-26 13:20:05 -08:00
|
|
|
const projects = collectProjects(this._configLoader.fullConfig(), projectNames);
|
|
|
|
const filesByProject = await collectFilesForProjects(projects, []);
|
2022-01-21 19:11:22 -08:00
|
|
|
const report: any = {
|
|
|
|
projects: []
|
|
|
|
};
|
|
|
|
for (const [project, files] of filesByProject) {
|
|
|
|
report.projects.push({
|
2022-11-08 12:05:00 -08:00
|
|
|
...sanitizeConfigForJSON(project, new Set()),
|
2022-11-17 16:31:04 -08:00
|
|
|
files
|
2022-01-21 19:11:22 -08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return report;
|
|
|
|
}
|
|
|
|
|
2023-01-25 15:38:23 -08:00
|
|
|
async runAllTests(options: RunOptions): Promise<FullResult['status']> {
|
2023-01-23 17:44:23 -08:00
|
|
|
const config = this._configLoader.fullConfig();
|
2023-01-26 13:20:05 -08:00
|
|
|
const deadline = config.globalTimeout ? monotonicTime() + config.globalTimeout : 0;
|
|
|
|
|
2023-01-25 15:38:23 -08:00
|
|
|
// Legacy webServer support.
|
|
|
|
this._plugins.push(...webServerPluginsForConfig(config));
|
|
|
|
// Docker support.
|
|
|
|
this._plugins.push(dockerPlugin);
|
|
|
|
|
2023-01-26 13:20:05 -08:00
|
|
|
const reporter = await createReporter(this._configLoader, options.listOnly);
|
|
|
|
const taskRunner = createTaskRunner(config, reporter, this._plugins, options);
|
2023-01-23 17:44:23 -08:00
|
|
|
|
2023-01-26 13:20:05 -08:00
|
|
|
const context: TaskRunnerState = {
|
|
|
|
config,
|
|
|
|
configLoader: this._configLoader,
|
|
|
|
options,
|
|
|
|
reporter,
|
|
|
|
};
|
2023-01-25 15:38:23 -08:00
|
|
|
|
2023-01-26 13:20:05 -08:00
|
|
|
reporter.onConfigure(config);
|
|
|
|
const taskStatus = await taskRunner.run(context, deadline);
|
2023-01-25 15:38:23 -08:00
|
|
|
let status: FullResult['status'] = 'passed';
|
2023-01-26 13:20:05 -08:00
|
|
|
if (context.dispatcher?.hasWorkerErrors() || context.rootSuite?.allTests().some(test => !test.ok()))
|
|
|
|
status = 'failed';
|
2023-01-25 15:38:23 -08:00
|
|
|
if (status === 'passed' && taskStatus !== 'passed')
|
|
|
|
status = taskStatus;
|
2023-01-26 13:20:05 -08:00
|
|
|
await reporter.onExit({ status });
|
|
|
|
|
2023-01-25 15:38:23 -08:00
|
|
|
// Calling process.exit() might truncate large stdout/stderr output.
|
|
|
|
// See https://github.com/nodejs/node/issues/6456.
|
|
|
|
// See https://github.com/nodejs/node/issues/12921
|
|
|
|
await new Promise<void>(resolve => process.stdout.write('', () => resolve()));
|
|
|
|
await new Promise<void>(resolve => process.stderr.write('', () => resolve()));
|
|
|
|
return status;
|
2022-11-01 23:44:30 -07:00
|
|
|
}
|
2021-11-11 16:48:08 -08:00
|
|
|
}
|
|
|
|
|
2022-11-08 12:05:00 -08:00
|
|
|
function sanitizeConfigForJSON(object: any, visited: Set<any>): any {
|
|
|
|
const type = typeof object;
|
|
|
|
if (type === 'function' || type === 'symbol')
|
|
|
|
return undefined;
|
|
|
|
if (!object || type !== 'object')
|
|
|
|
return object;
|
|
|
|
|
|
|
|
if (object instanceof RegExp)
|
|
|
|
return String(object);
|
|
|
|
if (object instanceof Date)
|
|
|
|
return object.toISOString();
|
|
|
|
|
|
|
|
if (visited.has(object))
|
|
|
|
return undefined;
|
|
|
|
visited.add(object);
|
|
|
|
|
|
|
|
if (Array.isArray(object))
|
|
|
|
return object.map(a => sanitizeConfigForJSON(a, visited));
|
|
|
|
|
|
|
|
const result: any = {};
|
|
|
|
const keys = Object.keys(object).slice(0, 100);
|
|
|
|
for (const key of keys) {
|
|
|
|
if (key.startsWith('_'))
|
|
|
|
continue;
|
|
|
|
result[key] = sanitizeConfigForJSON(object[key], visited);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|