chore: do not run all on watch (#20758)

This commit is contained in:
Pavel Feldman 2023-02-08 12:44:51 -08:00 committed by GitHub
parent 11168efb0e
commit 7a093329fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 118 additions and 85 deletions

View File

@ -167,9 +167,8 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
config._internal.passWithNoTests = !!opts.passWithNoTests;
const runner = new Runner(config);
const status = await runner.runAllTests(!!opts.watch);
const status = opts.watch ? await runner.watchAllTests() : await runner.runAllTests();
await stopProfiling(undefined);
if (status === 'interrupted')
process.exit(130);
process.exit(status === 'passed' ? 0 : 1);

View File

@ -47,7 +47,7 @@ export class Runner {
return report;
}
async runAllTests(watchMode: boolean): Promise<FullResult['status']> {
async runAllTests(): Promise<FullResult['status']> {
const config = this._config;
const listOnly = config._internal.listOnly;
const deadline = config.globalTimeout ? monotonicTime() + config.globalTimeout : 0;
@ -55,9 +55,9 @@ export class Runner {
// Legacy webServer support.
webServerPluginsForConfig(config).forEach(p => config._internal.plugins.push({ factory: p }));
const reporter = await createReporter(config, listOnly ? 'list' : watchMode ? 'watch' : 'run');
const reporter = await createReporter(config, listOnly ? 'list' : 'run');
const taskRunner = listOnly ? createTaskRunnerForList(config, reporter)
: createTaskRunner(config, reporter, watchMode);
: createTaskRunner(config, reporter);
const context: TaskRunnerState = {
config,
@ -78,16 +78,12 @@ export class Runner {
const taskStatus = await taskRunner.run(context, deadline);
let status: FullResult['status'] = 'passed';
const failedTests = context.rootSuite?.allTests().filter(test => !test.ok()) || [];
if (context.phases.find(p => p.dispatcher.hasWorkerErrors()) || failedTests.length)
if (context.phases.find(p => p.dispatcher.hasWorkerErrors()) || context.rootSuite?.allTests().some(test => !test.ok()))
status = 'failed';
if (status === 'passed' && taskStatus !== 'passed')
status = taskStatus;
await reporter.onExit({ status });
if (watchMode)
status = await runWatchModeLoop(config, failedTests);
// 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
@ -95,6 +91,12 @@ export class Runner {
await new Promise<void>(resolve => process.stderr.write('', () => resolve()));
return status;
}
async watchAllTests(): Promise<FullResult['status']> {
const config = this._config;
webServerPluginsForConfig(config).forEach(p => config._internal.plugins.push({ factory: p }));
return await runWatchModeLoop(config);
}
}
function sanitizeConfigForJSON(object: any, visited: Set<any>): any {

View File

@ -45,66 +45,70 @@ export class TaskRunner<Context> {
}
async run(context: Context, deadline: number): Promise<FullResult['status']> {
const { status, cleanup } = await this.runDeferCleanup(context, deadline);
const teardownStatus = await cleanup();
return status === 'passed' ? teardownStatus : status;
}
async runDeferCleanup(context: Context, deadline: number): Promise<{ status: FullResult['status'], cleanup: () => Promise<FullResult['status']> }> {
const sigintWatcher = new SigIntWatcher();
const timeoutWatcher = new TimeoutWatcher(deadline);
const teardownRunner = new TaskRunner(this._reporter, this._globalTimeoutForError);
teardownRunner._isTearDown = true;
try {
let currentTaskName: string | undefined;
const taskLoop = async () => {
for (const { name, task } of this._tasks) {
currentTaskName = name;
if (this._interrupted)
break;
debug('pw:test:task')(`"${name}" started`);
const errors: TestError[] = [];
try {
const teardown = await task(context, errors);
if (teardown)
teardownRunner._tasks.unshift({ name: `teardown for ${name}`, task: teardown });
} catch (e) {
debug('pw:test:task')(`error in "${name}": `, e);
errors.push(serializeError(e));
} finally {
for (const error of errors)
this._reporter.onError?.(error);
if (errors.length) {
if (!this._isTearDown)
this._interrupted = true;
this._hasErrors = true;
}
let currentTaskName: string | undefined;
const taskLoop = async () => {
for (const { name, task } of this._tasks) {
currentTaskName = name;
if (this._interrupted)
break;
debug('pw:test:task')(`"${name}" started`);
const errors: TestError[] = [];
try {
const teardown = await task(context, errors);
if (teardown)
teardownRunner._tasks.unshift({ name: `teardown for ${name}`, task: teardown });
} catch (e) {
debug('pw:test:task')(`error in "${name}": `, e);
errors.push(serializeError(e));
} finally {
for (const error of errors)
this._reporter.onError?.(error);
if (errors.length) {
if (!this._isTearDown)
this._interrupted = true;
this._hasErrors = true;
}
debug('pw:test:task')(`"${name}" finished`);
}
};
await Promise.race([
taskLoop(),
sigintWatcher.promise(),
timeoutWatcher.promise,
]);
// Prevent subsequent tasks from running.
this._interrupted = true;
let status: FullResult['status'] = 'passed';
if (sigintWatcher.hadSignal()) {
status = 'interrupted';
} else if (timeoutWatcher.timedOut()) {
this._reporter.onError?.({ message: `Timed out waiting ${this._globalTimeoutForError / 1000}s for the ${currentTaskName} to run` });
status = 'timedout';
} else if (this._hasErrors) {
status = 'failed';
debug('pw:test:task')(`"${name}" finished`);
}
};
return status;
} finally {
sigintWatcher.disarm();
timeoutWatcher.disarm();
if (!this._isTearDown)
await teardownRunner.run(context, deadline);
await Promise.race([
taskLoop(),
sigintWatcher.promise(),
timeoutWatcher.promise,
]);
sigintWatcher.disarm();
timeoutWatcher.disarm();
// Prevent subsequent tasks from running.
this._interrupted = true;
let status: FullResult['status'] = 'passed';
if (sigintWatcher.hadSignal()) {
status = 'interrupted';
} else if (timeoutWatcher.timedOut()) {
this._reporter.onError?.({ message: `Timed out waiting ${this._globalTimeoutForError / 1000}s for the ${currentTaskName} to run` });
status = 'timedout';
} else if (this._hasErrors) {
status = 'failed';
}
const cleanup = () => teardownRunner.runDeferCleanup(context, deadline).then(r => r.status);
return { status, cleanup };
}
}

View File

@ -50,27 +50,36 @@ export type TaskRunnerState = {
}[];
};
export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer, doNotTeardown: boolean): TaskRunner<TaskRunnerState> {
export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TaskRunnerState> {
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, config.globalTimeout);
for (const plugin of config._internal.plugins)
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin, doNotTeardown));
if (config.globalSetup || config.globalTeardown)
taskRunner.addTask('global setup', createGlobalSetupTask(doNotTeardown));
addGlobalSetupTasks(taskRunner, config);
taskRunner.addTask('load tests', createLoadTask('in-process'));
taskRunner.addTask('clear output', createRemoveOutputDirsTask());
addCommonTasks(taskRunner, config);
addRunTasks(taskRunner, config);
return taskRunner;
}
export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TaskRunnerState> {
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, 0);
addGlobalSetupTasks(taskRunner, config);
return taskRunner;
}
export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: Multiplexer, projectsToIgnore?: Set<FullProjectInternal>, additionalFileMatcher?: Matcher): TaskRunner<TaskRunnerState> {
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, config.globalTimeout);
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, 0);
taskRunner.addTask('load tests', createLoadTask('out-of-process', projectsToIgnore, additionalFileMatcher));
addCommonTasks(taskRunner, config);
addRunTasks(taskRunner, config);
return taskRunner;
}
function addCommonTasks(taskRunner: TaskRunner<TaskRunnerState>, config: FullConfigInternal) {
function addGlobalSetupTasks(taskRunner: TaskRunner<TaskRunnerState>, config: FullConfigInternal) {
for (const plugin of config._internal.plugins)
taskRunner.addTask('plugin setup', createPluginSetupTask(plugin));
if (config.globalSetup || config.globalTeardown)
taskRunner.addTask('global setup', createGlobalSetupTask());
taskRunner.addTask('clear output', createRemoveOutputDirsTask());
}
function addRunTasks(taskRunner: TaskRunner<TaskRunnerState>, config: FullConfigInternal) {
taskRunner.addTask('create tasks', createTestGroupsTask());
taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => {
reporter.onBegin?.(config, rootSuite!);
@ -93,14 +102,14 @@ export function createTaskRunnerForList(config: FullConfigInternal, reporter: Mu
return taskRunner;
}
function createPluginSetupTask(plugin: TestRunnerPluginRegistration, doNotTeardown: boolean): Task<TaskRunnerState> {
function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task<TaskRunnerState> {
return async ({ config, reporter }) => {
if (typeof plugin.factory === 'function')
plugin.instance = await plugin.factory();
else
plugin.instance = plugin.factory;
await plugin.instance?.setup?.(config, config._internal.configDir, reporter);
return doNotTeardown ? undefined : () => plugin.instance?.teardown?.();
return () => plugin.instance?.teardown?.();
};
}
@ -111,12 +120,12 @@ function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task<TaskR
};
}
function createGlobalSetupTask(doNotTeardown: boolean): Task<TaskRunnerState> {
function createGlobalSetupTask(): Task<TaskRunnerState> {
return async ({ config }) => {
const setupHook = config.globalSetup ? await loadGlobalHook(config, config.globalSetup) : undefined;
const teardownHook = config.globalTeardown ? await loadGlobalHook(config, config.globalTeardown) : undefined;
const globalSetupResult = setupHook ? await setupHook(config) : undefined;
return doNotTeardown ? undefined : async () => {
return async () => {
if (typeof globalSetupResult === 'function')
await globalSetupResult();
await teardownHook?.(config);

View File

@ -20,13 +20,13 @@ import type { FullConfigInternal, FullProjectInternal } from '../common/types';
import { Multiplexer } from '../reporters/multiplexer';
import { createFileMatcherFromArguments } from '../util';
import type { Matcher } from '../util';
import { createTaskRunnerForWatch } from './tasks';
import { createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
import type { TaskRunnerState } from './tasks';
import { buildProjectsClosure, filterProjects } from './projectUtils';
import { clearCompilationCache, collectAffectedTestFiles } from '../common/compilationCache';
import type { FullResult, TestCase } from 'packages/playwright-test/reporter';
import type { FullResult } from 'packages/playwright-test/reporter';
import chokidar from 'chokidar';
import { WatchModeReporter } from './reporters';
import { createReporter, WatchModeReporter } from './reporters';
import { colors } from 'playwright-core/lib/utilsBundle';
import { enquirer } from '../utilsBundle';
import { separator } from '../reporters/base';
@ -63,11 +63,24 @@ class FSWatcher {
}
}
export async function runWatchModeLoop(config: FullConfigInternal, failedTests: TestCase[]): Promise<FullResult['status']> {
export async function runWatchModeLoop(config: FullConfigInternal): Promise<FullResult['status']> {
// Perform global setup.
const reporter = await createReporter(config, 'watch');
const context: TaskRunnerState = {
config,
reporter,
phases: [],
};
const taskRunner = createTaskRunnerForWatchSetup(config, reporter);
reporter.onConfigure(config);
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(context, 0);
if (status !== 'passed')
return await globalCleanup();
const projects = filterProjects(config.projects, config._internal.cliProjectFilter);
const projectClosure = buildProjectsClosure(projects);
config._internal.passWithNoTests = true;
const failedTestIdCollector = new Set(failedTests.map(t => t.id));
const failedTestIdCollector = new Set<string>();
const originalCliArgs = config._internal.cliArgs;
const originalCliGrep = config._internal.cliGrep;
@ -75,12 +88,14 @@ export async function runWatchModeLoop(config: FullConfigInternal, failedTests:
let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: Set<string>, dirtyFiles?: Set<string> } = { type: 'regular' };
let result: FullResult['status'] = 'passed';
const fsWatcher = new FSWatcher(projectClosure.map(p => p.testDir));
while (true) {
const sep = separator();
process.stdout.write(`
${sep}
Waiting for file changes. Press ${colors.bold('h')} for help or ${colors.bold('q')} to quit.
Waiting for file changes. Press ${colors.bold('a')} to run all, ${colors.bold('q')} to quit or ${colors.bold('h')} for more options.
`);
const readCommandPromise = readCommand();
await Promise.race([
@ -167,11 +182,15 @@ Waiting for file changes. Press ${colors.bold('h')} for help or ${colors.bold('q
}
if (command === 'exit')
return 'passed';
break;
if (command === 'interrupted')
return 'interrupted';
if (command === 'interrupted') {
result = 'interrupted';
break;
}
}
return result === 'passed' ? await globalCleanup() : result;
}
async function runChangedTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, projectClosure: FullProjectInternal[], changedFiles: Set<string>) {

View File

@ -366,7 +366,7 @@ test('teardown after error', async ({ runInlineTest }) => {
pwt.test('test', () => {});
`,
});
expect(result.exitCode).toBe(0);
expect(result.exitCode).toBe(1);
expect(result.passed).toBe(1);
const output = result.output;
expect(output).toContain('Error: failed teardown 1');