chore: load tests file by file (#20557)

This commit is contained in:
Pavel Feldman 2023-02-01 12:33:42 -08:00 committed by GitHub
parent 635b47025e
commit a1cdd939f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 159 additions and 143 deletions

View File

@ -56,6 +56,8 @@ export type LoadError = {
location: Location; location: Location;
}; };
export type LoadErrorSink = (error: LoadError) => void;
class Fixture { class Fixture {
runner: FixtureRunner; runner: FixtureRunner;
registration: FixtureRegistration; registration: FixtureRegistration;
@ -190,9 +192,9 @@ export function isFixtureOption(value: any): value is FixtureTuple {
export class FixturePool { export class FixturePool {
readonly digest: string; readonly digest: string;
readonly registrations: Map<string, FixtureRegistration>; readonly registrations: Map<string, FixtureRegistration>;
private _onLoadError: (error: LoadError) => void; private _onLoadError: LoadErrorSink;
constructor(fixturesList: FixturesWithLocation[], onLoadError: (error: LoadError) => void, parentPool?: FixturePool, disallowWorkerFixtures?: boolean) { constructor(fixturesList: FixturesWithLocation[], onLoadError: LoadErrorSink, parentPool?: FixturePool, disallowWorkerFixtures?: boolean) {
this.registrations = new Map(parentPool ? parentPool.registrations : []); this.registrations = new Map(parentPool ? parentPool.registrations : []);
this._onLoadError = onLoadError; this._onLoadError = onLoadError;
@ -429,7 +431,7 @@ function serializeAndThrowError(e: LoadError) {
const signatureSymbol = Symbol('signature'); const signatureSymbol = Symbol('signature');
function fixtureParameterNames(fn: Function | any, location: Location, onError: (error: LoadError) => void): string[] { function fixtureParameterNames(fn: Function | any, location: Location, onError: LoadErrorSink): string[] {
if (typeof fn !== 'function') if (typeof fn !== 'function')
return []; return [];
if (!fn[signatureSymbol]) if (!fn[signatureSymbol])
@ -437,7 +439,7 @@ function fixtureParameterNames(fn: Function | any, location: Location, onError:
return fn[signatureSymbol]; return fn[signatureSymbol];
} }
function innerFixtureParameterNames(fn: Function, location: Location, onError: (error: LoadError) => void): string[] { function innerFixtureParameterNames(fn: Function, location: Location, onError: LoadErrorSink): string[] {
const text = fn.toString(); const text = fn.toString();
const match = text.match(/(?:async)?(?:\s+function)?[^(]*\(([^)]*)/); const match = text.match(/(?:async)?(?:\s+function)?[^(]*\(([^)]*)/);
if (!match) if (!match)
@ -446,8 +448,10 @@ function innerFixtureParameterNames(fn: Function, location: Location, onError: (
if (!trimmedParams) if (!trimmedParams)
return []; return [];
const [firstParam] = splitByComma(trimmedParams); const [firstParam] = splitByComma(trimmedParams);
if (firstParam[0] !== '{' || firstParam[firstParam.length - 1] !== '}') if (firstParam[0] !== '{' || firstParam[firstParam.length - 1] !== '}') {
onError({ message: 'First argument must use the object destructuring pattern: ' + firstParam, location }); onError({ message: 'First argument must use the object destructuring pattern: ' + firstParam, location });
return [];
}
const props = splitByComma(firstParam.substring(1, firstParam.length - 1)).map(prop => { const props = splitByComma(firstParam.substring(1, firstParam.length - 1)).map(prop => {
const colon = prop.indexOf(':'); const colon = prop.indexOf(':');
return colon === -1 ? prop : prop.substring(0, colon).trim(); return colon === -1 ? prop : prop.substring(0, colon).trim();

View File

@ -20,30 +20,29 @@ import type { Suite, TestCase } from './test';
import type { TestTypeImpl } from './testType'; import type { TestTypeImpl } from './testType';
import type { Fixtures, FixturesWithLocation, FullProjectInternal } from './types'; import type { Fixtures, FixturesWithLocation, FullProjectInternal } from './types';
import { formatLocation } from '../util'; import { formatLocation } from '../util';
import type { TestError } from '../../reporter';
export class PoolBuilder { export class PoolBuilder {
private _project: FullProjectInternal | undefined; private _project: FullProjectInternal | undefined;
private _testTypePools = new Map<TestTypeImpl, FixturePool>(); private _testTypePools = new Map<TestTypeImpl, FixturePool>();
private _type: 'loader' | 'worker'; private _type: 'loader' | 'worker';
private _loadErrors: LoadError[] | undefined;
static buildForLoader(suite: Suite, loadErrors: LoadError[]) { static createForLoader() {
new PoolBuilder('loader', loadErrors).buildPools(suite); return new PoolBuilder('loader');
} }
static createForWorker(project: FullProjectInternal) { static createForWorker(project: FullProjectInternal) {
return new PoolBuilder('worker', undefined, project); return new PoolBuilder('worker', project);
} }
private constructor(type: 'loader' | 'worker', loadErrors?: LoadError[], project?: FullProjectInternal) { private constructor(type: 'loader' | 'worker', project?: FullProjectInternal) {
this._type = type; this._type = type;
this._loadErrors = loadErrors;
this._project = project; this._project = project;
} }
buildPools(suite: Suite) { buildPools(suite: Suite, testErrors?: TestError[]) {
suite.forEachTest(test => { suite.forEachTest(test => {
const pool = this._buildPoolForTest(test); const pool = this._buildPoolForTest(test, testErrors);
if (this._type === 'loader') if (this._type === 'loader')
test._poolDigest = pool.digest; test._poolDigest = pool.digest;
if (this._type === 'worker') if (this._type === 'worker')
@ -51,8 +50,8 @@ export class PoolBuilder {
}); });
} }
private _buildPoolForTest(test: TestCase): FixturePool { private _buildPoolForTest(test: TestCase, testErrors?: TestError[]): FixturePool {
let pool = this._buildTestTypePool(test._testType); let pool = this._buildTestTypePool(test._testType, testErrors);
const parents: Suite[] = []; const parents: Suite[] = [];
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
@ -61,7 +60,7 @@ export class PoolBuilder {
for (const parent of parents) { for (const parent of parents) {
if (parent._use.length) if (parent._use.length)
pool = new FixturePool(parent._use, e => this._onLoadError(e), pool, parent._type === 'describe'); pool = new FixturePool(parent._use, e => this._handleLoadError(e, testErrors), pool, parent._type === 'describe');
for (const hook of parent._hooks) for (const hook of parent._hooks)
pool.validateFunction(hook.fn, hook.type + ' hook', hook.location); pool.validateFunction(hook.fn, hook.type + ' hook', hook.location);
for (const modifier of parent._modifiers) for (const modifier of parent._modifiers)
@ -72,18 +71,18 @@ export class PoolBuilder {
return pool; return pool;
} }
private _buildTestTypePool(testType: TestTypeImpl): FixturePool { private _buildTestTypePool(testType: TestTypeImpl, testErrors?: TestError[]): FixturePool {
if (!this._testTypePools.has(testType)) { if (!this._testTypePools.has(testType)) {
const fixtures = this._project ? this._applyConfigUseOptions(this._project, testType) : testType.fixtures; const fixtures = this._project ? this._applyConfigUseOptions(this._project, testType) : testType.fixtures;
const pool = new FixturePool(fixtures, e => this._onLoadError(e)); const pool = new FixturePool(fixtures, e => this._handleLoadError(e, testErrors));
this._testTypePools.set(testType, pool); this._testTypePools.set(testType, pool);
} }
return this._testTypePools.get(testType)!; return this._testTypePools.get(testType)!;
} }
private _onLoadError(e: LoadError): void { private _handleLoadError(e: LoadError, testErrors?: TestError[]): void {
if (this._loadErrors) if (testErrors)
this._loadErrors.push(e); testErrors.push(e);
else else
throw new Error(`${formatLocation(e.location)}: ${e.message}`); throw new Error(`${formatLocation(e.location)}: ${e.message}`);
} }

View File

@ -16,9 +16,7 @@
import path from 'path'; import path from 'path';
import type { TestError } from '../../reporter'; import type { TestError } from '../../reporter';
import type { LoadError } from './fixtures';
import { setCurrentlyLoadingFileSuite } from './globals'; import { setCurrentlyLoadingFileSuite } from './globals';
import { PoolBuilder } from './poolBuilder';
import { Suite } from './test'; import { Suite } from './test';
import { requireOrImport } from './transform'; import { requireOrImport } from './transform';
import { serializeError } from '../util'; import { serializeError } from '../util';
@ -29,64 +27,44 @@ export const defaultTimeout = 30000;
// we make these maps global. // we make these maps global.
const cachedFileSuites = new Map<string, Suite>(); const cachedFileSuites = new Map<string, Suite>();
export class TestLoader { export async function loadTestFile(file: string, rootDir: string, testErrors?: TestError[]): Promise<Suite> {
private _rootDir: string; if (cachedFileSuites.has(file))
return cachedFileSuites.get(file)!;
const suite = new Suite(path.relative(rootDir, file) || path.basename(file), 'file');
suite._requireFile = file;
suite.location = { file, line: 0, column: 0 };
constructor(rootDir: string) { setCurrentlyLoadingFileSuite(suite);
this._rootDir = rootDir; try {
await requireOrImport(file);
cachedFileSuites.set(file, suite);
} catch (e) {
if (!testErrors)
throw e;
testErrors.push(serializeError(e));
} finally {
setCurrentlyLoadingFileSuite(undefined);
} }
async loadTestFile(file: string, environment: 'loader' | 'worker', loadErrors: TestError[]): Promise<Suite> { {
if (cachedFileSuites.has(file)) // Test locations that we discover potentially have different file name.
return cachedFileSuites.get(file)!; // This could be due to either
const suite = new Suite(path.relative(this._rootDir, file) || path.basename(file), 'file'); // a) use of source maps or due to
suite._requireFile = file; // b) require of one file from another.
suite.location = { file, line: 0, column: 0 }; // Try fixing (a) w/o regressing (b).
setCurrentlyLoadingFileSuite(suite); const files = new Set<string>();
try { suite.allTests().map(t => files.add(t.location.file));
await requireOrImport(file); if (files.size === 1) {
cachedFileSuites.set(file, suite); // All tests point to one file.
} catch (e) { const mappedFile = files.values().next().value;
if (environment === 'worker') if (suite.location.file !== mappedFile) {
throw e; // The file is different, check for a likely source map case.
loadErrors.push(serializeError(e)); if (path.extname(mappedFile) !== path.extname(suite.location.file))
} finally { suite.location.file = mappedFile;
setCurrentlyLoadingFileSuite(undefined);
}
{
// Test locations that we discover potentially have different file name.
// This could be due to either
// a) use of source maps or due to
// b) require of one file from another.
// Try fixing (a) w/o regressing (b).
const files = new Set<string>();
suite.allTests().map(t => files.add(t.location.file));
if (files.size === 1) {
// All tests point to one file.
const mappedFile = files.values().next().value;
if (suite.location.file !== mappedFile) {
// The file is different, check for a likely source map case.
if (path.extname(mappedFile) !== path.extname(suite.location.file))
suite.location.file = mappedFile;
}
} }
} }
return suite;
} }
}
export async function loadTestFilesInProcess(rootDir: string, testFiles: string[], loadErrors: LoadError[]): Promise<Suite> { return suite;
const testLoader = new TestLoader(rootDir);
const rootSuite = new Suite('', 'root');
for (const file of testFiles) {
const fileSuite = await testLoader.loadTestFile(file, 'loader', loadErrors);
rootSuite._addSuite(fileSuite);
}
// Generate hashes.
PoolBuilder.buildForLoader(rootSuite, loadErrors);
return rootSuite;
} }

View File

@ -17,9 +17,9 @@
import type { SerializedConfig } from '../common/ipc'; import type { SerializedConfig } from '../common/ipc';
import { ConfigLoader } from '../common/configLoader'; import { ConfigLoader } from '../common/configLoader';
import { ProcessRunner } from '../common/process'; import { ProcessRunner } from '../common/process';
import { loadTestFilesInProcess } from '../common/testLoader';
import type { LoadError } from '../common/fixtures';
import type { FullConfigInternal } from '../common/types'; import type { FullConfigInternal } from '../common/types';
import { loadTestFile } from '../common/testLoader';
import type { TestError } from '../../reporter';
export class LoaderMain extends ProcessRunner { export class LoaderMain extends ProcessRunner {
private _serializedConfig: SerializedConfig; private _serializedConfig: SerializedConfig;
@ -36,11 +36,11 @@ export class LoaderMain extends ProcessRunner {
return this._configPromise; return this._configPromise;
} }
async loadTestFiles(params: { files: string[] }) { async loadTestFile(params: { file: string }) {
const loadErrors: LoadError[] = []; const testErrors: TestError[] = [];
const config = await this._config(); const config = await this._config();
const rootSuite = await loadTestFilesInProcess(config.rootDir, params.files, loadErrors); const rootSuite = await loadTestFile(params.file, config.rootDir, testErrors);
return { rootSuite: rootSuite._deepSerialize(), loadErrors }; return { rootSuite: rootSuite._deepSerialize(), testErrors };
} }
} }

View File

@ -16,18 +16,15 @@
import path from 'path'; import path from 'path';
import type { Reporter, TestError } from '../../types/testReporter'; import type { Reporter, TestError } from '../../types/testReporter';
import type { LoadError } from '../common/fixtures'; import { InProcessLoaderHost, OutOfProcessLoaderHost } from './loaderHost';
import { LoaderHost } from './loaderHost'; import type { LoaderHost } from './loaderHost';
import type { Multiplexer } from '../reporters/multiplexer';
import { Suite } from '../common/test'; import { Suite } from '../common/test';
import type { TestCase } from '../common/test'; import type { TestCase } from '../common/test';
import { loadTestFilesInProcess } from '../common/testLoader';
import type { FullConfigInternal, FullProjectInternal } from '../common/types'; import type { FullConfigInternal, FullProjectInternal } from '../common/types';
import { createFileMatcherFromFilters, createTitleMatcher, errorWithFile } from '../util'; import { createFileMatcherFromFilters, createTitleMatcher, errorWithFile } from '../util';
import type { Matcher, TestFileFilter } from '../util'; import type { Matcher, TestFileFilter } from '../util';
import { collectFilesForProject, filterProjects, projectsThatAreDependencies } from './projectUtils'; import { collectFilesForProject, filterProjects, projectsThatAreDependencies } from './projectUtils';
import { requireOrImport } from '../common/transform'; import { requireOrImport } from '../common/transform';
import { serializeConfig } from '../common/ipc';
import { buildFileSuiteForProject, filterByFocusedLine, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { buildFileSuiteForProject, filterByFocusedLine, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
import { filterForShard } from './testGroups'; import { filterForShard } from './testGroups';
@ -39,17 +36,20 @@ type LoadOptions = {
passWithNoTests?: boolean; passWithNoTests?: boolean;
}; };
export async function loadAllTests(config: FullConfigInternal, reporter: Multiplexer, options: LoadOptions, errors: TestError[]): Promise<Suite> { export async function loadAllTests(config: FullConfigInternal, options: LoadOptions, errors: TestError[]): Promise<Suite> {
const projects = filterProjects(config.projects, options.projectFilter); const projects = filterProjects(config.projects, options.projectFilter);
let filesToRunByProject = new Map<FullProjectInternal, string[]>(); let filesToRunByProject = new Map<FullProjectInternal, string[]>();
let topLevelProjects: FullProjectInternal[]; let topLevelProjects: FullProjectInternal[];
let dependencyProjects: FullProjectInternal[]; let dependencyProjects: FullProjectInternal[];
// Collect files, categorize top level and dependency projects.
{ {
const fsCache = new Map();
// 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[]>();
for (const project of projects) { for (const project of projects) {
const files = await collectFilesForProject(project); const files = await collectFilesForProject(project, fsCache);
allFilesForProject.set(project, files); allFilesForProject.set(project, files);
} }
@ -74,26 +74,34 @@ export async function loadAllTests(config: FullConfigInternal, reporter: Multipl
// (Re-)add all files for dependent projects, disregard filters. // (Re-)add all files for dependent projects, disregard filters.
for (const project of dependencyProjects) { for (const project of dependencyProjects) {
const files = allFilesForProject.get(project) || await collectFilesForProject(project); const files = allFilesForProject.get(project) || await collectFilesForProject(project, fsCache);
filesToRunByProject.set(project, files); filesToRunByProject.set(project, files);
} }
} }
// Load all test files and create a preprocessed root. Child suites are files there. // Load all test files and create a preprocessed root. Child suites are files there.
const allTestFiles = new Set<string>(); const fileSuits: Suite[] = [];
for (const files of filesToRunByProject.values()) {
files.forEach(file => allTestFiles.add(file)); const loaderHost: LoaderHost = process.env.PW_TEST_OOP_LOADER ? new OutOfProcessLoaderHost(config) : new InProcessLoaderHost(config);
const preprocessRoot = await loadTests(config, reporter, allTestFiles, errors); const allTestFiles = new Set<string>();
for (const files of filesToRunByProject.values())
files.forEach(file => allTestFiles.add(file));
for (const file of allTestFiles) {
const fileSuite = await loaderHost.loadTestFile(file, errors);
fileSuits.push(fileSuite);
}
await loaderHost.stop();
}
// Complain about duplicate titles. // Complain about duplicate titles.
errors.push(...createDuplicateTitlesErrors(config, preprocessRoot)); errors.push(...createDuplicateTitlesErrors(config, fileSuits));
// Create root suites with clones for the projects. // Create root suites with clones for the projects.
const rootSuite = new Suite('', 'root'); const rootSuite = new Suite('', 'root');
// First iterate leaf projects to focus only, then add all other projects. // First iterate leaf projects to focus only, then add all other projects.
for (const project of topLevelProjects) { for (const project of topLevelProjects) {
const projectSuite = await createProjectSuite(preprocessRoot, project, options, filesToRunByProject.get(project)!); const projectSuite = await createProjectSuite(fileSuits, project, options, filesToRunByProject.get(project)!);
if (projectSuite) if (projectSuite)
rootSuite._addSuite(projectSuite); rootSuite._addSuite(projectSuite);
} }
@ -110,7 +118,7 @@ export async function loadAllTests(config: FullConfigInternal, reporter: Multipl
// Prepend the projects that are dependencies. // Prepend the projects that are dependencies.
for (const project of dependencyProjects) { for (const project of dependencyProjects) {
const projectSuite = await createProjectSuite(preprocessRoot, project, { ...options, testFileFilters: [], testTitleMatcher: undefined }, filesToRunByProject.get(project)!); const projectSuite = await createProjectSuite(fileSuits, project, { ...options, testFileFilters: [], testTitleMatcher: undefined }, filesToRunByProject.get(project)!);
if (projectSuite) if (projectSuite)
rootSuite._prependSuite(projectSuite); rootSuite._prependSuite(projectSuite);
} }
@ -118,17 +126,17 @@ export async function loadAllTests(config: FullConfigInternal, reporter: Multipl
return rootSuite; return rootSuite;
} }
async function createProjectSuite(preprocessRoot: Suite, project: FullProjectInternal, options: LoadOptions, files: string[]): Promise<Suite | null> { async function createProjectSuite(fileSuits: Suite[], project: FullProjectInternal, options: LoadOptions, files: string[]): Promise<Suite | null> {
const fileSuites = new Map<string, Suite>(); const fileSuitesMap = new Map<string, Suite>();
for (const fileSuite of preprocessRoot.suites) for (const fileSuite of fileSuits)
fileSuites.set(fileSuite._requireFile, fileSuite); fileSuitesMap.set(fileSuite._requireFile, fileSuite);
const projectSuite = new Suite(project.name, 'project'); const projectSuite = new Suite(project.name, 'project');
projectSuite._projectConfig = project; projectSuite._projectConfig = project;
if (project._fullyParallel) if (project._fullyParallel)
projectSuite._parallelMode = 'parallel'; projectSuite._parallelMode = 'parallel';
for (const file of files) { for (const file of files) {
const fileSuite = fileSuites.get(file); const fileSuite = fileSuitesMap.get(file);
if (!fileSuite) if (!fileSuite)
continue; continue;
for (let repeatEachIndex = 0; repeatEachIndex < project.repeatEach; repeatEachIndex++) { for (let repeatEachIndex = 0; repeatEachIndex < project.repeatEach; repeatEachIndex++) {
@ -154,24 +162,6 @@ async function createProjectSuite(preprocessRoot: Suite, project: FullProjectInt
return null; return null;
} }
async function loadTests(config: FullConfigInternal, reporter: Multiplexer, testFiles: Set<string>, errors: TestError[]): Promise<Suite> {
if (process.env.PW_TEST_OOP_LOADER) {
const loaderHost = new LoaderHost();
await loaderHost.start(serializeConfig(config));
try {
return await loaderHost.loadTestFiles([...testFiles], reporter);
} finally {
await loaderHost.stop();
}
}
const loadErrors: LoadError[] = [];
try {
return await loadTestFilesInProcess(config.rootDir, [...testFiles], loadErrors);
} finally {
errors.push(...loadErrors);
}
}
function createForbidOnlyErrors(onlyTestsAndSuites: (TestCase | Suite)[]): TestError[] { function createForbidOnlyErrors(onlyTestsAndSuites: (TestCase | Suite)[]): TestError[] {
const errors: TestError[] = []; const errors: TestError[] = [];
for (const testOrSuite of onlyTestsAndSuites) { for (const testOrSuite of onlyTestsAndSuites) {
@ -186,12 +176,12 @@ function createForbidOnlyErrors(onlyTestsAndSuites: (TestCase | Suite)[]): TestE
return errors; return errors;
} }
function createDuplicateTitlesErrors(config: FullConfigInternal, rootSuite: Suite): TestError[] { function createDuplicateTitlesErrors(config: FullConfigInternal, fileSuites: Suite[]): TestError[] {
const errors: TestError[] = []; const errors: TestError[] = [];
for (const fileSuite of rootSuite.suites) { for (const fileSuite of fileSuites) {
const testsByFullTitle = new Map<string, TestCase>(); const testsByFullTitle = new Map<string, TestCase>();
for (const test of fileSuite.allTests()) { for (const test of fileSuite.allTests()) {
const fullTitle = test.titlePath().slice(2).join(' '); const fullTitle = test.titlePath().slice(1).join(' ');
const existingTest = testsByFullTitle.get(fullTitle); const existingTest = testsByFullTitle.get(fullTitle);
if (existingTest) { if (existingTest) {
const error: TestError = { const error: TestError = {

View File

@ -14,23 +14,60 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Reporter, TestError } from '../../reporter'; import type { TestError } from '../../reporter';
import type { SerializedConfig } from '../common/ipc'; import { serializeConfig } from '../common/ipc';
import { ProcessHost } from './processHost'; import { ProcessHost } from './processHost';
import { Suite } from '../common/test'; import { Suite } from '../common/test';
import { loadTestFile } from '../common/testLoader';
import type { LoadError } from '../common/fixtures';
import type { FullConfigInternal } from '../common/types';
import { PoolBuilder } from '../common/poolBuilder';
export class LoaderHost extends ProcessHost { export abstract class LoaderHost {
constructor() { protected _config: FullConfigInternal;
super(require.resolve('../loaderMain.js'), 'loader'); private _poolBuilder: PoolBuilder;
constructor(config: FullConfigInternal) {
this._config = config;
this._poolBuilder = PoolBuilder.createForLoader();
} }
async start(config: SerializedConfig) { async loadTestFile(file: string, testErrors: TestError[]): Promise<Suite> {
await this.startRunner(config, true, {}); const result = await this.doLoadTestFile(file, testErrors);
this._poolBuilder.buildPools(result, testErrors);
return result;
} }
async loadTestFiles(files: string[], reporter: Reporter): Promise<Suite> { protected abstract doLoadTestFile(file: string, testErrors: TestError[]): Promise<Suite>;
const result = await this.sendMessage({ method: 'loadTestFiles', params: { files } }) as any;
result.loadErrors.forEach((e: TestError) => reporter.onError?.(e)); async stop() {}
return Suite._deepParse(result.rootSuite); }
export class InProcessLoaderHost extends LoaderHost {
doLoadTestFile(file: string, testErrors: TestError[]): Promise<Suite> {
return loadTestFile(file, this._config.rootDir, testErrors);
}
}
export class OutOfProcessLoaderHost extends LoaderHost {
private _startPromise: Promise<void>;
private _processHost: ProcessHost;
constructor(config: FullConfigInternal) {
super(config);
this._processHost = new ProcessHost(require.resolve('../loaderMain.js'), 'loader');
this._startPromise = this._processHost.startRunner(serializeConfig(config), true, {});
}
async doLoadTestFile(file: string, loadErrors: LoadError[]): Promise<Suite> {
await this._startPromise;
const result = await this._processHost.sendMessage({ method: 'loadTestFile', params: { file } }) as any;
loadErrors.push(...result.loadErrors);
return Suite._deepParse(result.fileSuite);
}
override async stop() {
await this._processHost.stop();
} }
} }

View File

@ -42,7 +42,7 @@ export class ProcessHost extends EventEmitter {
this._processName = processName; this._processName = processName;
} }
protected async startRunner(runnerParams: any, inheritStdio: boolean, env: NodeJS.ProcessEnv) { async startRunner(runnerParams: any, inheritStdio: boolean, env: NodeJS.ProcessEnv) {
this.process = child_process.fork(require.resolve('../common/process'), { this.process = child_process.fork(require.resolve('../common/process'), {
detached: false, detached: false,
env: { ...process.env, ...env }, env: { ...process.env, ...env },
@ -104,7 +104,7 @@ export class ProcessHost extends EventEmitter {
}); });
} }
protected sendMessage(message: { method: string, params?: any }) { sendMessage(message: { method: string, params?: any }) {
const id = ++this._lastMessageId; const id = ++this._lastMessageId;
this.send({ this.send({
method: '__dispatch__', method: '__dispatch__',

View File

@ -66,10 +66,10 @@ export function projectsThatAreDependencies(projects: FullProjectInternal[]): Fu
return [...result]; return [...result];
} }
export async function collectFilesForProject(project: FullProjectInternal): Promise<string[]> { export async function collectFilesForProject(project: FullProjectInternal, fsCache = new Map<string, string[]>()): Promise<string[]> {
const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx']; const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx'];
const testFileExtension = (file: string) => extensions.includes(path.extname(file)); const testFileExtension = (file: string) => extensions.includes(path.extname(file));
const allFiles = await collectFiles(project.testDir, project._respectGitIgnore); const allFiles = await cachedCollectFiles(project.testDir, project._respectGitIgnore, fsCache);
const testMatch = createFileMatcher(project.testMatch); const testMatch = createFileMatcher(project.testMatch);
const testIgnore = createFileMatcher(project.testIgnore); const testIgnore = createFileMatcher(project.testIgnore);
const testFiles = allFiles.filter(file => { const testFiles = allFiles.filter(file => {
@ -83,6 +83,16 @@ export async function collectFilesForProject(project: FullProjectInternal): Prom
return testFiles; return testFiles;
} }
async function cachedCollectFiles(testDir: string, respectGitIgnore: boolean, fsCache: Map<string, string[]>) {
const key = testDir + ':' + respectGitIgnore;
let result = fsCache.get(key);
if (!result) {
result = await collectFiles(testDir, respectGitIgnore);
fsCache.set(key, result);
}
return result;
}
async function collectFiles(testDir: string, respectGitIgnore: boolean): Promise<string[]> { async function collectFiles(testDir: string, respectGitIgnore: boolean): Promise<string[]> {
if (!fs.existsSync(testDir)) if (!fs.existsSync(testDir))
return []; return [];

View File

@ -144,8 +144,8 @@ function createRemoveOutputDirsTask(): Task<TaskRunnerState> {
function createLoadTask(): Task<TaskRunnerState> { function createLoadTask(): Task<TaskRunnerState> {
return async (context, errors) => { return async (context, errors) => {
const { config, reporter, options } = context; const { config, options } = context;
context.rootSuite = await loadAllTests(config, reporter, options, errors); context.rootSuite = await loadAllTests(config, options, errors);
// Fail when no tests. // Fail when no tests.
if (!context.rootSuite.allTests().length && !context.options.passWithNoTests && !config.shard) if (!context.rootSuite.allTests().length && !context.options.passWithNoTests && !config.shard)
throw new Error(`No tests found`); throw new Error(`No tests found`);

View File

@ -28,7 +28,7 @@ import { TestInfoImpl } from '../common/testInfo';
import type { TimeSlot } from '../common/timeoutManager'; import type { TimeSlot } from '../common/timeoutManager';
import { TimeoutManager } from '../common/timeoutManager'; import { TimeoutManager } from '../common/timeoutManager';
import { ProcessRunner } from '../common/process'; import { ProcessRunner } from '../common/process';
import { TestLoader } from '../common/testLoader'; import { loadTestFile } from '../common/testLoader';
import { buildFileSuiteForProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { buildFileSuiteForProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
import { PoolBuilder } from '../common/poolBuilder'; import { PoolBuilder } from '../common/poolBuilder';
@ -37,7 +37,6 @@ const removeFolderAsync = util.promisify(rimraf);
export class WorkerMain extends ProcessRunner { export class WorkerMain extends ProcessRunner {
private _params: WorkerInitParams; private _params: WorkerInitParams;
private _config!: FullConfigInternal; private _config!: FullConfigInternal;
private _testLoader!: TestLoader;
private _project!: FullProjectInternal; private _project!: FullProjectInternal;
private _poolBuilder!: PoolBuilder; private _poolBuilder!: PoolBuilder;
private _fixtureRunner: FixtureRunner; private _fixtureRunner: FixtureRunner;
@ -195,7 +194,6 @@ export class WorkerMain extends ProcessRunner {
const configLoader = await ConfigLoader.deserialize(this._params.config); const configLoader = await ConfigLoader.deserialize(this._params.config);
this._config = configLoader.fullConfig(); this._config = configLoader.fullConfig();
this._testLoader = new TestLoader(this._config.rootDir);
this._project = this._config.projects.find(p => p._id === this._params.projectId)!; this._project = this._config.projects.find(p => p._id === this._params.projectId)!;
this._poolBuilder = PoolBuilder.createForWorker(this._project); this._poolBuilder = PoolBuilder.createForWorker(this._project);
} }
@ -206,7 +204,7 @@ export class WorkerMain extends ProcessRunner {
let fatalUnknownTestIds; let fatalUnknownTestIds;
try { try {
await this._loadIfNeeded(); await this._loadIfNeeded();
const fileSuite = await this._testLoader.loadTestFile(runPayload.file, 'worker', []); const fileSuite = await loadTestFile(runPayload.file, this._config.rootDir);
const suite = buildFileSuiteForProject(this._project, fileSuite, this._params.repeatEachIndex); const suite = buildFileSuiteForProject(this._project, fileSuite, this._params.repeatEachIndex);
const hasEntries = filterTestsRemoveEmptySuites(suite, test => entries.has(test.id)); const hasEntries = filterTestsRemoveEmptySuites(suite, test => entries.has(test.id));
if (hasEntries) { if (hasEntries) {