chore: respect deps when watching files (#20695)

This commit is contained in:
Pavel Feldman 2023-02-06 17:09:16 -08:00 committed by GitHub
parent 430d08f4fb
commit 361ea949aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 43 additions and 27 deletions

View File

@ -32,7 +32,7 @@ const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwri
const sourceMaps: Map<string, string> = new Map();
const memoryCache = new Map<string, MemoryCache>();
const fileDependencies = new Map<string, string[]>();
const fileDependencies = new Map<string, Set<string>>();
Error.stackTraceLimit = 200;
@ -92,7 +92,7 @@ export function serializeCompilationCache(): any {
return {
sourceMaps: [...sourceMaps.entries()],
memoryCache: [...memoryCache.entries()],
fileDependencies: [...fileDependencies.entries()],
fileDependencies: [...fileDependencies.entries()].map(([filename, deps]) => ([filename, [...deps]])),
};
}
@ -107,7 +107,7 @@ export function addToCompilationCache(payload: any) {
for (const entry of payload.memoryCache)
memoryCache.set(entry[0], entry[1]);
for (const entry of payload.fileDependencies)
fileDependencies.set(entry[0], entry[1]);
fileDependencies.set(entry[0], new Set(entry[1]));
}
function calculateCachePath(content: string, filePath: string, isModule: boolean): string {
@ -134,8 +134,11 @@ export function stopCollectingFileDeps(filename: string) {
if (!depsCollector)
return;
depsCollector.delete(filename);
const deps = [...depsCollector!].filter(f => !belongsToNodeModules(f));
fileDependencies.set(filename, deps);
for (const dep of depsCollector) {
if (belongsToNodeModules(dep))
depsCollector.delete(dep);
}
fileDependencies.set(filename, depsCollector);
depsCollector = undefined;
}
@ -147,6 +150,14 @@ export function fileDependenciesForTest() {
return fileDependencies;
}
export function collectAffectedTestFiles(dependency: string, testFileCollector: Set<string>) {
testFileCollector.add(dependency);
for (const [testFile, deps] of fileDependencies) {
if (deps.has(dependency))
testFileCollector.add(testFile);
}
}
// These two are only used in the dev mode, they are specifically excluding
// files from packages/playwright*. In production mode, node_modules covers
// that.

View File

@ -221,10 +221,8 @@ function installTransform(): () => void {
}
const collectCJSDependencies = (module: Module, dependencies: Set<string>) => {
if (dependencies.has(module.filename))
return;
module.children.forEach(child => {
if (!belongsToNodeModules(child.filename)) {
if (!belongsToNodeModules(child.filename) && !dependencies.has(child.filename)) {
dependencies.add(child.filename);
collectCJSDependencies(child, dependencies);
}

View File

@ -19,6 +19,6 @@ import { fileDependenciesForTest } from './common/compilationCache';
export function fileDependencies() {
return Object.fromEntries([...fileDependenciesForTest().entries()].map(entry => (
[path.basename(entry[0]), entry[1].map(f => path.basename(f))]
[path.basename(entry[0]), [...entry[1]].map(f => path.basename(f)).sort()]
)));
}

View File

@ -23,7 +23,7 @@ import type { Matcher } from '../util';
import { createTaskRunnerForWatch } from './tasks';
import type { TaskRunnerState } from './tasks';
import { buildProjectsClosure, filterProjects } from './projectUtils';
import { clearCompilationCache } from '../common/compilationCache';
import { clearCompilationCache, collectAffectedTestFiles } from '../common/compilationCache';
import type { FullResult, TestCase } from 'packages/playwright-test/reporter';
import chokidar from 'chokidar';
import { WatchModeReporter } from './reporters';
@ -149,14 +149,19 @@ ${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')}
}
}
async function runChangedTests(config: FullConfigInternal, failedTestIds: Set<string>, projectClosure: FullProjectInternal[], files: Set<string>) {
async function runChangedTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, projectClosure: FullProjectInternal[], changedFiles: Set<string>) {
const commandLineFileMatcher = config._internal.cliFileFilters.length ? createFileMatcherFromFilters(config._internal.cliFileFilters) : () => true;
// Resolve files that depend on the changed files.
const testFiles = new Set<string>();
for (const file of changedFiles)
collectAffectedTestFiles(file, testFiles);
// Collect projects with changes.
const filesByProject = new Map<FullProjectInternal, string[]>();
for (const project of projectClosure) {
const projectFiles: string[] = [];
for (const file of files) {
for (const file of testFiles) {
if (!file.startsWith(project.testDir))
continue;
if (project._internal.type === 'dependency' || commandLineFileMatcher(file))
@ -174,11 +179,11 @@ async function runChangedTests(config: FullConfigInternal, failedTestIds: Set<st
// If there are affected dependency projects, do the full run, respect the original CLI.
// if there are no affected dependency projects, intersect CLI with dirty files
const additionalFileMatcher = affectsAnyDependency ? () => true : (file: string) => files.has(file);
return await runTests(config, failedTestIds, projectsToIgnore, additionalFileMatcher);
const additionalFileMatcher = affectsAnyDependency ? () => true : (file: string) => testFiles.has(file);
return await runTests(config, failedTestIdCollector, projectsToIgnore, additionalFileMatcher);
}
async function runTests(config: FullConfigInternal, failedTestIds: Set<string>, projectsToIgnore?: Set<FullProjectInternal>, additionalFileMatcher?: Matcher) {
async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, projectsToIgnore?: Set<FullProjectInternal>, additionalFileMatcher?: Matcher) {
const reporter = new Multiplexer([new WatchModeReporter()]);
const taskRunner = createTaskRunnerForWatch(config, reporter, projectsToIgnore, additionalFileMatcher);
const context: TaskRunnerState = {
@ -194,9 +199,9 @@ async function runTests(config: FullConfigInternal, failedTestIds: Set<string>,
let hasFailedTests = false;
for (const test of context.rootSuite?.allTests() || []) {
if (test.outcome() === 'expected') {
failedTestIds.delete(test.id);
failedTestIdCollector.delete(test.id);
} else {
failedTestIds.add(test.id);
failedTestIdCollector.add(test.id);
hasFailedTests = true;
}
}

View File

@ -24,13 +24,14 @@ test('should print dependencies in CJS mode', async ({ runInlineTest }) => {
globalTeardown: './globalTeardown.ts',
});
`,
'helper.ts': `export function foo() {}`,
'helperA.ts': `export function foo() {}`,
'helperB.ts': `import './helperA';`,
'a.test.ts': `
import './helper';
import './helperA';
pwt.test('passes', () => {});
`,
'b.test.ts': `
import './helper';
import './helperB';
pwt.test('passes', () => {});
`,
'globalTeardown.ts': `
@ -46,8 +47,8 @@ test('should print dependencies in CJS mode', async ({ runInlineTest }) => {
const output = stripAnsi(result.output);
const deps = JSON.parse(output.match(/###(.*)###/)![1]);
expect(deps).toEqual({
'a.test.ts': ['helper.ts'],
'b.test.ts': ['helper.ts'],
'a.test.ts': ['helperA.ts'],
'b.test.ts': ['helperA.ts', 'helperB.ts'],
});
});
@ -61,13 +62,14 @@ test('should print dependencies in ESM mode', async ({ runInlineTest, nodeVersio
globalTeardown: './globalTeardown.ts',
});
`,
'helper.ts': `export function foo() {}`,
'helperA.ts': `export function foo() {}`,
'helperB.ts': `import './helperA.js';`,
'a.test.ts': `
import './helper.js';
import './helperA.js';
pwt.test('passes', () => {});
`,
'b.test.ts': `
import './helper.js';
import './helperB.js';
pwt.test('passes', () => {});
`,
'globalTeardown.ts': `
@ -83,7 +85,7 @@ test('should print dependencies in ESM mode', async ({ runInlineTest, nodeVersio
const output = stripAnsi(result.output);
const deps = JSON.parse(output.match(/###(.*)###/)![1]);
expect(deps).toEqual({
'a.test.ts': ['helper.ts'],
'b.test.ts': ['helper.ts'],
'a.test.ts': ['helperA.ts'],
'b.test.ts': ['helperA.ts', 'helperB.ts'],
});
});