chore(watch): print current filters (#20696)

This commit is contained in:
Pavel Feldman 2023-02-07 09:48:46 -08:00 committed by GitHub
parent 303c5998f8
commit 98e348d16a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 97 additions and 68 deletions

View File

@ -21,8 +21,7 @@ import fs from 'fs';
import path from 'path';
import { Runner } from './runner/runner';
import { stopProfiling, startProfiling } from './common/profiler';
import { createFileFilterForArg, experimentalLoaderOption, fileIsModule, forceRegExp } from './util';
import { createTitleMatcher } from './util';
import { experimentalLoaderOption, fileIsModule } from './util';
import { showHTMLReport } from './reporters/html';
import { baseFullConfig, builtInReporters, ConfigLoader, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader';
import type { TraceMode } from './common/types';
@ -160,10 +159,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
configLoader.ignoreProjectDependencies();
const config = configLoader.fullConfig();
config._internal.cliFileFilters = args.map(arg => createFileFilterForArg(arg));
const grepMatcher = opts.grep ? createTitleMatcher(forceRegExp(opts.grep)) : () => true;
const grepInvertMatcher = opts.grepInvert ? createTitleMatcher(forceRegExp(opts.grepInvert)) : () => false;
config._internal.cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title);
config._internal.cliArgs = args;
config._internal.cliGrep = opts.grep as string | undefined;
config._internal.cliGrepInvert = opts.grepInvert as string | undefined;
config._internal.listOnly = !!opts.list;
config._internal.cliProjectFilter = opts.project || undefined;
config._internal.passWithNoTests = !!opts.passWithNoTests;

View File

@ -450,8 +450,9 @@ export const baseFullConfig: FullConfigInternal = {
maxConcurrentTestGroups: 0,
ignoreSnapshots: false,
plugins: [],
cliTitleMatcher: () => true,
cliFileFilters: [],
cliArgs: [],
cliGrep: undefined,
cliGrepInvert: undefined,
listOnly: false,
}
};

View File

@ -17,7 +17,7 @@
import type { Fixtures, TestInfoError, Project } from '../../types/test';
import type { Location } from '../../types/testReporter';
import type { TestRunnerPluginRegistration } from '../plugins';
import type { Matcher, TestFileFilter } from '../util';
import type { Matcher } from '../util';
import type { ConfigCLIOverrides } from './ipc';
import type { FullConfig as FullConfigPublic, FullProject as FullProjectPublic } from './types';
export * from '../../types/test';
@ -50,8 +50,9 @@ type ConfigInternal = {
webServers: Exclude<FullConfigPublic['webServer'], null>[];
plugins: TestRunnerPluginRegistration[];
listOnly: boolean;
cliFileFilters: TestFileFilter[];
cliTitleMatcher: Matcher;
cliArgs: string[];
cliGrep: string | undefined;
cliGrepInvert: string | undefined;
cliProjectFilter?: string[];
testIdMatcher?: Matcher;
passWithNoTests?: boolean;

View File

@ -231,10 +231,8 @@ export class BaseReporter implements Reporter {
}
private _printSummary(summary: string) {
if (summary.trim()) {
console.log('');
if (summary.trim())
console.log(summary);
}
}
willRetry(test: TestCase): boolean {
@ -487,3 +485,8 @@ function fitToWidth(line: string, width: number, prefix?: string): string {
function belongsToNodeModules(file: string) {
return file.includes(`${path.sep}node_modules${path.sep}`);
}
export function separator(): string {
const columns = process.stdout?.columns || 30;
return colors.dim('⎯'.repeat(Math.min(100, columns)));
}

View File

@ -21,7 +21,7 @@ import type { LoaderHost } from './loaderHost';
import { Suite } from '../common/test';
import type { TestCase } from '../common/test';
import type { FullConfigInternal, FullProjectInternal } from '../common/types';
import { createTitleMatcher, errorWithFile } from '../util';
import { createFileFiltersFromArguments, createTitleMatcher, errorWithFile, forceRegExp } from '../util';
import type { Matcher, TestFileFilter } from '../util';
import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils';
import { requireOrImport } from '../common/transform';
@ -98,9 +98,15 @@ export async function loadAllTests(mode: 'out-of-process' | 'in-process', config
// Create root suites with clones for the projects.
const rootSuite = new Suite('', 'root');
// Interpret cli parameters.
const cliFileFilters = createFileFiltersFromArguments(config._internal.cliArgs);
const grepMatcher = config._internal.cliGrep ? createTitleMatcher(forceRegExp(config._internal.cliGrep)) : () => true;
const grepInvertMatcher = config._internal.cliGrepInvert ? createTitleMatcher(forceRegExp(config._internal.cliGrepInvert)) : () => false;
const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title);
// First iterate leaf projects to focus only, then add all other projects.
for (const project of topLevelProjects) {
const projectSuite = await createProjectSuite(fileSuits, project, config._internal, filesToRunByProject.get(project)!);
const projectSuite = await createProjectSuite(fileSuits, project, { cliFileFilters, cliTitleMatcher, testIdMatcher: config._internal.testIdMatcher }, filesToRunByProject.get(project)!);
if (projectSuite)
rootSuite._addSuite(projectSuite);
}

View File

@ -16,7 +16,7 @@
import path from 'path';
import type { Reporter, TestError } from '../../types/testReporter';
import { formatError } from '../reporters/base';
import { separator, formatError } from '../reporters/base';
import DotReporter from '../reporters/dot';
import EmptyReporter from '../reporters/empty';
import GitHubReporter from '../reporters/github';
@ -30,6 +30,7 @@ import type { Suite } from '../common/test';
import type { FullConfigInternal } from '../common/types';
import { loadReporter } from './loadUtils';
import type { BuiltInReporter } from '../common/configLoader';
import { colors } from 'playwright-core/lib/utilsBundle';
export async function createReporter(config: FullConfigInternal, mode: 'list' | 'watch' | 'run') {
const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = {
@ -104,5 +105,23 @@ export class ListModeReporter implements Reporter {
}
}
export class WatchModeReporter extends LineReporter {
let seq = 0;
export class WatchModeReporter extends ListReporter {
override generateStartingMessage(): string {
const tokens: string[] = [];
tokens.push('npx playwright test');
tokens.push(...(this.config._internal.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`)));
if (this.config._internal.cliGrep)
tokens.push(colors.red(`--grep ${this.config._internal.cliGrep}`));
if (this.config._internal.cliArgs)
tokens.push(...this.config._internal.cliArgs.map(a => colors.bold(a)));
tokens.push(colors.dim(`#${++seq}`));
const lines: string[] = [];
const sep = separator();
lines.push('\x1Bc' + sep);
lines.push(`${tokens.join(' ')}`);
lines.push(sep + super.generateStartingMessage());
return lines.join('\n');
}
}

View File

@ -86,7 +86,7 @@ export class Runner {
await reporter.onExit({ status });
if (watchMode)
await runWatchModeLoop(config, failedTests);
status = await runWatchModeLoop(config, failedTests);
// Calling process.exit() might truncate large stdout/stderr output.
// See https://github.com/nodejs/node/issues/6456.

View File

@ -28,7 +28,7 @@ import { TaskRunner } from './taskRunner';
import type { Suite } from '../common/test';
import type { FullConfigInternal, FullProjectInternal } from '../common/types';
import { loadAllTests, loadGlobalHook } from './loadUtils';
import { createFileMatcherFromFilters } from '../util';
import { createFileMatcherFromArguments } from '../util';
import type { Matcher } from '../util';
const removeFolderAsync = promisify(rimraf);
@ -149,7 +149,7 @@ function createRemoveOutputDirsTask(): Task<TaskRunnerState> {
function createLoadTask(mode: 'out-of-process' | 'in-process', projectsToIgnore = new Set<FullProjectInternal>(), additionalFileMatcher?: Matcher): Task<TaskRunnerState> {
return async (context, errors) => {
const { config } = context;
const cliMatcher = config._internal.cliFileFilters.length ? createFileMatcherFromFilters(config._internal.cliFileFilters) : () => true;
const cliMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : () => true;
const fileMatcher = (value: string) => cliMatcher(value) && (additionalFileMatcher ? additionalFileMatcher(value) : true);
context.rootSuite = await loadAllTests(mode, config, projectsToIgnore, fileMatcher, errors);
// Fail when no tests.

View File

@ -18,7 +18,7 @@ import readline from 'readline';
import { ManualPromise } from 'playwright-core/lib/utils';
import type { FullConfigInternal, FullProjectInternal } from '../common/types';
import { Multiplexer } from '../reporters/multiplexer';
import { createFileFilterForArg, createFileMatcherFromFilters, createTitleMatcher, forceRegExp } from '../util';
import { createFileMatcherFromArguments } from '../util';
import type { Matcher } from '../util';
import { createTaskRunnerForWatch } from './tasks';
import type { TaskRunnerState } from './tasks';
@ -29,6 +29,7 @@ import chokidar from 'chokidar';
import { WatchModeReporter } from './reporters';
import { colors } from 'playwright-core/lib/utilsBundle';
import { enquirer } from '../utilsBundle';
import { separator } from '../reporters/base';
class FSWatcher {
private _dirtyFiles = new Set<string>();
@ -61,22 +62,21 @@ class FSWatcher {
}
}
export async function runWatchModeLoop(config: FullConfigInternal, failedTests: TestCase[]) {
export async function runWatchModeLoop(config: FullConfigInternal, failedTests: TestCase[]): Promise<FullResult['status']> {
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 originalTitleMatcher = config._internal.cliTitleMatcher;
const originalFileFilters = config._internal.cliFileFilters;
const originalCliArgs = config._internal.cliArgs;
const originalCliGrep = config._internal.cliGrep;
const fsWatcher = new FSWatcher(projectClosure.map(p => p.testDir));
let lastFilePattern: string | undefined;
let lastTestPattern: string | undefined;
while (true) {
const sep = separator();
process.stdout.write(`
Waiting for file changes...
${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')} ${colors.bold('q')} ${colors.dim('to quit')}
${sep}
Waiting for file changes. Press ${colors.bold('h')} for help or ${colors.bold('q')} to quit.
`);
const readCommandPromise = readCommand();
await Promise.race([
@ -88,17 +88,13 @@ ${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')}
const command = await readCommandPromise;
if (command === 'changed') {
process.stdout.write('\x1Bc');
await runChangedTests(config, failedTestIdCollector, projectClosure, fsWatcher.takeDirtyFiles());
continue;
}
if (command === 'all') {
process.stdout.write('\x1Bc');
// All means reset filters.
config._internal.cliTitleMatcher = originalTitleMatcher;
config._internal.cliFileFilters = originalFileFilters;
lastFilePattern = undefined;
lastTestPattern = undefined;
config._internal.cliArgs = originalCliArgs;
config._internal.cliGrep = originalCliGrep;
await runTests(config, failedTestIdCollector);
continue;
}
@ -107,15 +103,12 @@ ${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')}
type: 'text',
name: 'filePattern',
message: 'Input filename pattern (regex)',
initial: lastFilePattern,
initial: config._internal.cliArgs.join(' '),
});
if (filePattern.trim()) {
lastFilePattern = filePattern;
config._internal.cliFileFilters = [createFileFilterForArg(filePattern)];
} else {
lastFilePattern = undefined;
config._internal.cliFileFilters = originalFileFilters;
}
if (filePattern.trim())
config._internal.cliArgs = [filePattern];
else
config._internal.cliArgs = [];
await runTests(config, failedTestIdCollector);
continue;
}
@ -124,20 +117,16 @@ ${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')}
type: 'text',
name: 'testPattern',
message: 'Input test name pattern (regex)',
initial: lastTestPattern,
initial: config._internal.cliGrep,
});
if (testPattern.trim()) {
lastTestPattern = testPattern;
config._internal.cliTitleMatcher = createTitleMatcher(forceRegExp(testPattern));
} else {
lastTestPattern = undefined;
config._internal.cliTitleMatcher = originalTitleMatcher;
}
if (testPattern.trim())
config._internal.cliGrep = testPattern;
else
config._internal.cliGrep = undefined;
await runTests(config, failedTestIdCollector);
continue;
}
if (command === 'failed') {
process.stdout.write('\x1Bc');
config._internal.testIdMatcher = id => failedTestIdCollector.has(id);
try {
await runTests(config, failedTestIdCollector);
@ -146,11 +135,15 @@ ${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')}
}
continue;
}
if (command === 'exit')
return 'passed';
if (command === 'interrupted')
return 'interrupted';
}
}
async function runChangedTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, projectClosure: FullProjectInternal[], changedFiles: Set<string>) {
const commandLineFileMatcher = config._internal.cliFileFilters.length ? createFileMatcherFromFilters(config._internal.cliFileFilters) : () => true;
const commandLineFileMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : () => true;
// Resolve files that depend on the changed files.
const testFiles = new Set<string>();
@ -234,19 +227,24 @@ function readCommand(): ManualPromise<Command> {
process.stdin.setRawMode(true);
const handler = (text: string, key: any) => {
if (text === '\x03' || text === '\x1B' || (key && key.name === 'escape') || (key && key.ctrl && key.name === 'c'))
return process.exit(130);
if (text === '\x03' || text === '\x1B' || (key && key.name === 'escape') || (key && key.ctrl && key.name === 'c')) {
result.resolve('interrupted');
return;
}
if (process.platform !== 'win32' && key && key.ctrl && key.name === 'z') {
process.kill(process.ppid, 'SIGTSTP');
process.kill(process.pid, 'SIGTSTP');
}
const name = key?.name;
if (name === 'q')
process.exit(0);
if (name === 'q') {
result.resolve('exit');
return;
}
if (name === 'h') {
process.stdout.write(`
process.stdout.write(`${separator()}
Watch Usage
${commands.map(i => colors.dim(' press ') + colors.reset(colors.bold(i[0])) + colors.dim(` to ${i[1]}`)).join('\n')}
${commands.map(i => ' ' + colors.bold(i[0]) + `: ${i[1]}`).join('\n')}
`);
return;
}
@ -269,7 +267,7 @@ ${commands.map(i => colors.dim(' press ') + colors.reset(colors.bold(i[0])) + c
return result;
}
type Command = 'all' | 'failed' | 'changed' | 'file' | 'grep';
type Command = 'all' | 'failed' | 'changed' | 'file' | 'grep' | 'exit' | 'interrupted';
const commands = [
['a', 'rerun all tests'],

View File

@ -106,16 +106,19 @@ export type TestFileFilter = {
column: number | null;
};
export function createFileFilterForArg(arg: string): TestFileFilter {
const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg);
return {
re: forceRegExp(match ? match[1] : arg),
line: match ? parseInt(match[2], 10) : null,
column: match?.[3] ? parseInt(match[3], 10) : null,
};
export function createFileFiltersFromArguments(args: string[]): TestFileFilter[] {
return args.map(arg => {
const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg);
return {
re: forceRegExp(match ? match[1] : arg),
line: match ? parseInt(match[2], 10) : null,
column: match?.[3] ? parseInt(match[3], 10) : null,
};
});
}
export function createFileMatcherFromFilters(filters: TestFileFilter[]): Matcher {
export function createFileMatcherFromArguments(args: string[]): Matcher {
const filters = createFileFiltersFromArguments(args);
return createFileMatcher(filters.map(filter => filter.re || filter.exact || ''));
}