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

View File

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

View File

@ -19,6 +19,6 @@ import { fileDependenciesForTest } from './common/compilationCache';
export function fileDependencies() { export function fileDependencies() {
return Object.fromEntries([...fileDependenciesForTest().entries()].map(entry => ( 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 { createTaskRunnerForWatch } from './tasks';
import type { TaskRunnerState } from './tasks'; import type { TaskRunnerState } from './tasks';
import { buildProjectsClosure, filterProjects } from './projectUtils'; 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 type { FullResult, TestCase } from 'packages/playwright-test/reporter';
import chokidar from 'chokidar'; import chokidar from 'chokidar';
import { WatchModeReporter } from './reporters'; 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; 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. // Collect projects with changes.
const filesByProject = new Map<FullProjectInternal, string[]>(); const filesByProject = new Map<FullProjectInternal, string[]>();
for (const project of projectClosure) { for (const project of projectClosure) {
const projectFiles: string[] = []; const projectFiles: string[] = [];
for (const file of files) { for (const file of testFiles) {
if (!file.startsWith(project.testDir)) if (!file.startsWith(project.testDir))
continue; continue;
if (project._internal.type === 'dependency' || commandLineFileMatcher(file)) 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 affected dependency projects, do the full run, respect the original CLI.
// if there are no affected dependency projects, intersect CLI with dirty files // if there are no affected dependency projects, intersect CLI with dirty files
const additionalFileMatcher = affectsAnyDependency ? () => true : (file: string) => files.has(file); const additionalFileMatcher = affectsAnyDependency ? () => true : (file: string) => testFiles.has(file);
return await runTests(config, failedTestIds, projectsToIgnore, additionalFileMatcher); 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 reporter = new Multiplexer([new WatchModeReporter()]);
const taskRunner = createTaskRunnerForWatch(config, reporter, projectsToIgnore, additionalFileMatcher); const taskRunner = createTaskRunnerForWatch(config, reporter, projectsToIgnore, additionalFileMatcher);
const context: TaskRunnerState = { const context: TaskRunnerState = {
@ -194,9 +199,9 @@ async function runTests(config: FullConfigInternal, failedTestIds: Set<string>,
let hasFailedTests = false; let hasFailedTests = false;
for (const test of context.rootSuite?.allTests() || []) { for (const test of context.rootSuite?.allTests() || []) {
if (test.outcome() === 'expected') { if (test.outcome() === 'expected') {
failedTestIds.delete(test.id); failedTestIdCollector.delete(test.id);
} else { } else {
failedTestIds.add(test.id); failedTestIdCollector.add(test.id);
hasFailedTests = true; hasFailedTests = true;
} }
} }

View File

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