feat(test runner): --only-changed
This commit is contained in:
parent
d23ea26947
commit
da65da5d79
|
|
@ -49,6 +49,7 @@ export class FullConfigInternal {
|
||||||
cliArgs: string[] = [];
|
cliArgs: string[] = [];
|
||||||
cliGrep: string | undefined;
|
cliGrep: string | undefined;
|
||||||
cliGrepInvert: string | undefined;
|
cliGrepInvert: string | undefined;
|
||||||
|
cliOnlyChanged = false;
|
||||||
cliProjectFilter?: string[];
|
cliProjectFilter?: string[];
|
||||||
cliListOnly = false;
|
cliListOnly = false;
|
||||||
cliPassWithNoTests?: boolean;
|
cliPassWithNoTests?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||||
|
|
||||||
config.cliArgs = args;
|
config.cliArgs = args;
|
||||||
config.cliGrep = opts.grep as string | undefined;
|
config.cliGrep = opts.grep as string | undefined;
|
||||||
|
config.cliOnlyChanged = !!opts.onlyChanged;
|
||||||
config.cliGrepInvert = opts.grepInvert as string | undefined;
|
config.cliGrepInvert = opts.grepInvert as string | undefined;
|
||||||
config.cliListOnly = !!opts.list;
|
config.cliListOnly = !!opts.list;
|
||||||
config.cliProjectFilter = opts.project || undefined;
|
config.cliProjectFilter = opts.project || undefined;
|
||||||
|
|
@ -352,6 +353,7 @@ const testOptions: [string, string][] = [
|
||||||
['--max-failures <N>', `Stop after the first N failures`],
|
['--max-failures <N>', `Stop after the first N failures`],
|
||||||
['--no-deps', 'Do not run project dependencies'],
|
['--no-deps', 'Do not run project dependencies'],
|
||||||
['--output <dir>', `Folder for output artifacts (default: "test-results")`],
|
['--output <dir>', `Folder for output artifacts (default: "test-results")`],
|
||||||
|
['--only-changed', `something something docs`],
|
||||||
['--pass-with-no-tests', `Makes test run succeed even if no tests were found`],
|
['--pass-with-no-tests', `Makes test run succeed even if no tests were found`],
|
||||||
['--project <project-name...>', `Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects)`],
|
['--project <project-name...>', `Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects)`],
|
||||||
['--quiet', `Suppress stdio`],
|
['--quiet', `Suppress stdio`],
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTest
|
||||||
const config = testRun.config;
|
const config = testRun.config;
|
||||||
const fsCache = new Map();
|
const fsCache = new Map();
|
||||||
const sourceMapCache = new Map();
|
const sourceMapCache = new Map();
|
||||||
const cliFileMatcher = config.cliArgs.length ? createFileMatcherFromArguments(config.cliArgs) : null;
|
const cliFileMatcher = (config.cliArgs.length || config.cliOnlyChanged) ? await createFileMatcherFromArguments(config.cliArgs, config.cliOnlyChanged) : null;
|
||||||
|
|
||||||
// First collect all files for the projects in the command line, don't apply any file filters.
|
// First collect all files for the projects in the command line, don't apply any file filters.
|
||||||
const allFilesForProject = new Map<FullProjectInternal, string[]>();
|
const allFilesForProject = new Map<FullProjectInternal, string[]>();
|
||||||
|
|
@ -128,7 +128,7 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
|
||||||
// Filter all the projects using grep, testId, file names.
|
// Filter all the projects using grep, testId, file names.
|
||||||
{
|
{
|
||||||
// Interpret cli parameters.
|
// Interpret cli parameters.
|
||||||
const cliFileFilters = createFileFiltersFromArguments(config.cliArgs);
|
const cliFileFilters = await createFileFiltersFromArguments(config.cliArgs, config.cliOnlyChanged);
|
||||||
const grepMatcher = config.cliGrep ? createTitleMatcher(forceRegExp(config.cliGrep)) : () => true;
|
const grepMatcher = config.cliGrep ? createTitleMatcher(forceRegExp(config.cliGrep)) : () => true;
|
||||||
const grepInvertMatcher = config.cliGrepInvert ? createTitleMatcher(forceRegExp(config.cliGrepInvert)) : () => false;
|
const grepInvertMatcher = config.cliGrepInvert ? createTitleMatcher(forceRegExp(config.cliGrepInvert)) : () => false;
|
||||||
const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title);
|
const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class FSWatcher {
|
||||||
private _timer: NodeJS.Timeout | undefined;
|
private _timer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
async update(config: FullConfigInternal) {
|
async update(config: FullConfigInternal) {
|
||||||
const commandLineFileMatcher = config.cliArgs.length ? createFileMatcherFromArguments(config.cliArgs) : () => true;
|
const commandLineFileMatcher = (config.cliArgs.length || config.cliOnlyChanged) ? await createFileMatcherFromArguments(config.cliArgs, config.cliOnlyChanged) : () => true;
|
||||||
const projects = filterProjects(config.projects, config.cliProjectFilter);
|
const projects = filterProjects(config.projects, config.cliProjectFilter);
|
||||||
const projectClosure = buildProjectsClosure(projects);
|
const projectClosure = buildProjectsClosure(projects);
|
||||||
const projectFilters = new Map<FullProjectInternal, Matcher>();
|
const projectFilters = new Map<FullProjectInternal, Matcher>();
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import type { StackFrame } from '@protocol/channels';
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
import childProcess from 'child_process';
|
||||||
import { debug, mime, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
|
import { debug, mime, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
|
||||||
import { formatCallLog } from 'playwright-core/lib/utils';
|
import { formatCallLog } from 'playwright-core/lib/utils';
|
||||||
import type { TestInfoError } from './../types/test';
|
import type { TestInfoError } from './../types/test';
|
||||||
|
|
@ -79,7 +80,12 @@ export type TestFileFilter = {
|
||||||
column: number | null;
|
column: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createFileFiltersFromArguments(args: string[]): TestFileFilter[] {
|
export async function createFileFiltersFromArguments(args: string[], onlyChanged: boolean): Promise<TestFileFilter[]> {
|
||||||
|
if (onlyChanged) {
|
||||||
|
const untrackedFiles = childProcess.execSync('git ls-files --others --exclude-standard', { encoding: 'utf-8' }).split('\n').filter(Boolean);
|
||||||
|
args = untrackedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
return args.map(arg => {
|
return args.map(arg => {
|
||||||
const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg);
|
const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg);
|
||||||
return {
|
return {
|
||||||
|
|
@ -90,8 +96,8 @@ export function createFileFiltersFromArguments(args: string[]): TestFileFilter[]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFileMatcherFromArguments(args: string[]): Matcher {
|
export async function createFileMatcherFromArguments(args: string[], onlyChanged: boolean): Promise<Matcher> {
|
||||||
const filters = createFileFiltersFromArguments(args);
|
const filters = await createFileFiltersFromArguments(args, onlyChanged);
|
||||||
return createFileMatcher(filters.map(filter => filter.re || filter.exact || ''));
|
return createFileMatcher(filters.map(filter => filter.re || filter.exact || ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
47
tests/playwright-test/only-changed.spec.ts
Normal file
47
tests/playwright-test/only-changed.spec.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect, magicFileCreationSymbol } from './playwright-test-fixtures';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { writeFile } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
test('should filter by file name', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('fails', () => { expect(1).toBe(2); });
|
||||||
|
`,
|
||||||
|
'b.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('fails', () => { expect(1).toBe(2); });
|
||||||
|
`,
|
||||||
|
async [magicFileCreationSymbol](baseDir) {
|
||||||
|
execSync(`git init --initial-branch=main`, { cwd: baseDir });
|
||||||
|
execSync(`git add .`, { cwd: baseDir });
|
||||||
|
execSync(`git commit -m init`, { cwd: baseDir });
|
||||||
|
|
||||||
|
await writeFile(join(baseDir, 'c.spec.ts'), `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('fails', () => { expect(1).toBe(2); });
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}, { 'only-changed': true });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.output).toContain('c.spec.ts');
|
||||||
|
});
|
||||||
|
|
@ -56,7 +56,11 @@ type TSCResult = {
|
||||||
exitCode: number;
|
exitCode: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Files = { [key: string]: string | Buffer };
|
export const magicFileCreationSymbol = Symbol();
|
||||||
|
export type Files = {
|
||||||
|
[key: string]: string | Buffer;
|
||||||
|
[magicFileCreationSymbol]?: (baseDir: string) => Promise<void>;
|
||||||
|
};
|
||||||
type Params = { [key: string]: string | number | boolean | string[] };
|
type Params = { [key: string]: string | number | boolean | string[] };
|
||||||
|
|
||||||
export async function writeFiles(testInfo: TestInfo, files: Files, initial: boolean) {
|
export async function writeFiles(testInfo: TestInfo, files: Files, initial: boolean) {
|
||||||
|
|
@ -84,6 +88,9 @@ export async function writeFiles(testInfo: TestInfo, files: Files, initial: bool
|
||||||
await fs.promises.writeFile(fullName, files[name]);
|
await fs.promises.writeFile(fullName, files[name]);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (magicFileCreationSymbol in files)
|
||||||
|
await files[magicFileCreationSymbol](baseDir);
|
||||||
|
|
||||||
return baseDir;
|
return baseDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,7 +239,7 @@ export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
PWTEST_BOT_NAME: undefined,
|
PWTEST_BOT_NAME: undefined,
|
||||||
TEST_WORKER_INDEX: undefined,
|
TEST_WORKER_INDEX: undefined,
|
||||||
TEST_PARALLEL_INDEX: undefined,
|
TEST_PARALLEL_INDEX: undefined,
|
||||||
NODE_OPTIONS: undefined,
|
// NODE_OPTIONS: undefined,
|
||||||
...env,
|
...env,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue