feat(test-runner): allow to focus a test in a location (#7208)

This commit is contained in:
Max Schmitt 2021-06-24 10:02:34 +02:00 committed by GitHub
parent 5732307280
commit bd86e70465
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 9 deletions

View file

@ -22,6 +22,7 @@ import * as path from 'path';
import type { Config } from './types';
import { Runner } from './runner';
import { stopProfiling, startProfiling } from './profiler';
import type { FilePatternFilter } from './util';
const defaultTimeout = 30000;
const defaultReporter = process.env.CI ? 'dot' : 'list';
@ -131,7 +132,14 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
runner.loadEmptyConfig(process.cwd());
}
const result = await runner.run(!!opts.list, args.map(forceRegExp), opts.project || undefined);
const filePatternFilters: FilePatternFilter[] = args.map(arg => {
const match = /^(.*):(\d+)$/.exec(arg);
return {
re: forceRegExp(match ? match[1] : arg),
line: match ? parseInt(match[2], 10) : null,
};
});
const result = await runner.run(!!opts.list, filePatternFilters, opts.project || undefined);
await stopProfiling(undefined);
if (result === 'sigint')

View file

@ -21,8 +21,8 @@ import * as fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';
import { Dispatcher } from './dispatcher';
import { createMatcher, monotonicTime, raceAgainstDeadline } from './util';
import { Suite } from './test';
import { createMatcher, FilePatternFilter, monotonicTime, raceAgainstDeadline } from './util';
import { Spec, Suite } from './test';
import { Loader } from './loader';
import { Reporter } from './reporter';
import { Multiplexer } from './reporters/multiplexer';
@ -81,11 +81,11 @@ export class Runner {
this._loader.loadEmptyConfig(rootDir);
}
async run(list: boolean, testFileReFilters: RegExp[], projectName?: string): Promise<RunResult> {
async run(list: boolean, filePatternFilters: FilePatternFilter[], projectName?: string): Promise<RunResult> {
this._reporter = this._createReporter();
const config = this._loader.fullConfig();
const globalDeadline = config.globalTimeout ? config.globalTimeout + monotonicTime() : undefined;
const { result, timedOut } = await raceAgainstDeadline(this._run(list, testFileReFilters, projectName), globalDeadline);
const { result, timedOut } = await raceAgainstDeadline(this._run(list, filePatternFilters, projectName), globalDeadline);
if (timedOut) {
if (!this._didBegin)
this._reporter.onBegin(config, new Suite(''));
@ -119,8 +119,8 @@ export class Runner {
await new Promise(f => process.stderr.on('drain', f));
}
async _run(list: boolean, testFileReFilters: RegExp[], projectName?: string): Promise<RunResult> {
const testFileFilter = testFileReFilters.length ? createMatcher(testFileReFilters) : () => true;
async _run(list: boolean, testFileReFilters: FilePatternFilter[], projectName?: string): Promise<RunResult> {
const testFileFilter = testFileReFilters.length ? createMatcher(testFileReFilters.map(e => e.re)) : () => true;
const config = this._loader.fullConfig();
const projects = this._loader.projects().filter(project => {
@ -163,6 +163,7 @@ export class Runner {
if (config.forbidOnly && rootSuite._hasOnly())
return 'forbid-only';
filterOnly(rootSuite);
filterByFocusedLine(rootSuite, testFileReFilters);
const fileSuites = new Map<string, Suite>();
for (const fileSuite of rootSuite.suites)
@ -243,8 +244,24 @@ export class Runner {
}
function filterOnly(suite: Suite) {
const onlySuites = suite.suites.filter(child => filterOnly(child) || child._only);
const onlyTests = suite.specs.filter(spec => spec._only);
const suiteFilter = (suite: Suite) => suite._only;
const specFilter = (spec: Spec) => spec._only;
return filterSuite(suite, suiteFilter, specFilter);
}
function filterByFocusedLine(suite: Suite, focusedTestFileLines: FilePatternFilter[]) {
const testFileLineMatches = (specFileName: string, specLine: number) => focusedTestFileLines.some(({re, line}) => {
re.lastIndex = 0;
return re.test(specFileName) && (line === specLine || line === null);
});
const suiteFilter = (suite: Suite) => testFileLineMatches(suite.file, suite.line);
const specFilter = (spec: Spec) => testFileLineMatches(spec.file, spec.line);
return filterSuite(suite, suiteFilter, specFilter);
}
function filterSuite(suite: Suite, suiteFilter: (suites: Suite) => boolean, specFilter: (spec: Spec) => boolean) {
const onlySuites = suite.suites.filter(child => filterSuite(child, suiteFilter, specFilter) || suiteFilter(child));
const onlyTests = suite.specs.filter(specFilter);
const onlyEntries = new Set([...onlySuites, ...onlyTests]);
if (onlyEntries.size) {
suite.suites = onlySuites;

View file

@ -91,6 +91,11 @@ export function isRegExp(e: any): e is RegExp {
export type Matcher = (value: string) => boolean;
export type FilePatternFilter = {
re: RegExp;
line: number | null;
};
export function createMatcher(patterns: string | RegExp | (string | RegExp)[]): Matcher {
const reList: RegExp[] = [];
const filePatterns: string[] = [];

View file

@ -211,6 +211,23 @@ test('should match regex string argument', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0);
});
test('should match regex string with a colon argument', async ({ runInlineTest }) => {
test.skip(process.platform === 'win32', 'Windows does not support colons in the file name');
const result = await runInlineTest({
'fileb.test.ts': `
const { test } = pwt;
test('pass', ({}) => {});
`,
'weird:file.test.ts': `
const { test } = pwt;
test('pass', ({}) => {});
`
}, { args: ['/weird:file\.test\.ts/'] });
expect(result.passed).toBe(1);
expect(result.report.suites.map(s => s.file).sort()).toEqual(['weird:file.test.ts']);
expect(result.exitCode).toBe(0);
});
test('should match case insensitive', async ({ runInlineTest }) => {
const result = await runInlineTest({
'capital/A.test.ts': `
@ -231,6 +248,78 @@ test('should match case insensitive', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0);
});
test('should focus a single test spec', async ({ runInlineTest }) => {
const result = await runInlineTest({
'foo.test.ts': `
const { test } = pwt;
test('pass1', ({}) => {});
test('pass2', ({}) => {});
test('pass3', ({}) => {});
`,
'bar.test.ts': `
const { test } = pwt;
test('no-pass1', ({}) => {});
`,
}, { args: ['foo.test.ts:7'] });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(result.skipped).toBe(0);
expect(result.report.suites[0].specs[0].title).toEqual('pass2');
});
test('should focus a single nested test spec', async ({ runInlineTest }) => {
const result = await runInlineTest({
'foo.test.ts': `
const { test } = pwt;
test('pass1', ({}) => {});
test.describe('suite-1', () => {
test.describe('suite-2', () => {
test('pass2', ({}) => {});
});
});
test('pass3', ({}) => {});
`,
'bar.test.ts': `
const { test } = pwt;
test('pass3', ({}) => {});
`,
'noooo.test.ts': `
const { test } = pwt;
test('no-pass1', ({}) => {});
`,
}, { args: ['foo.test.ts:9', 'bar.test.ts'] });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
expect(result.skipped).toBe(0);
expect(result.report.suites[0].specs[0].title).toEqual('pass3');
expect(result.report.suites[1].suites[0].suites[0].specs[0].title).toEqual('pass2');
});
test('should focus a single test suite', async ({ runInlineTest }) => {
const result = await runInlineTest({
'foo.test.ts': `
const { test } = pwt;
test('pass1', ({}) => {});
test.describe('suite-1', () => {
test.describe('suite-2', () => {
test('pass2', ({}) => {});
test('pass3', ({}) => {});
});
});
test('pass4', ({}) => {});
`,
'bar.test.ts': `
const { test } = pwt;
test('no-pass1', ({}) => {});
`,
}, { args: ['foo.test.ts:8'] });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
expect(result.skipped).toBe(0);
expect(result.report.suites[0].suites[0].suites[0].specs[0].title).toEqual('pass2');
expect(result.report.suites[0].suites[0].suites[0].specs[1].title).toEqual('pass3');
});
test('should match by directory', async ({ runInlineTest }) => {
const result = await runInlineTest({
'dir-a/file.test.ts': `