2021-06-06 17:09:53 -07:00
|
|
|
|
/**
|
|
|
|
|
* Copyright 2019 Google Inc. All rights reserved.
|
|
|
|
|
* Modifications 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.
|
|
|
|
|
*/
|
|
|
|
|
|
2021-06-23 11:08:35 +02:00
|
|
|
|
/* eslint-disable no-console */
|
2021-06-06 17:09:53 -07:00
|
|
|
|
import rimraf from 'rimraf';
|
|
|
|
|
import * as fs from 'fs';
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
import { promisify } from 'util';
|
|
|
|
|
import { Dispatcher } from './dispatcher';
|
2021-06-24 10:02:34 +02:00
|
|
|
|
import { createMatcher, FilePatternFilter, monotonicTime, raceAgainstDeadline } from './util';
|
2021-07-15 22:02:10 -07:00
|
|
|
|
import { Test, Suite } from './test';
|
2021-06-06 17:09:53 -07:00
|
|
|
|
import { Loader } from './loader';
|
2021-07-16 21:15:03 -07:00
|
|
|
|
import { Reporter } from '../../types/testReporter';
|
2021-06-06 17:09:53 -07:00
|
|
|
|
import { Multiplexer } from './reporters/multiplexer';
|
|
|
|
|
import DotReporter from './reporters/dot';
|
|
|
|
|
import LineReporter from './reporters/line';
|
|
|
|
|
import ListReporter from './reporters/list';
|
|
|
|
|
import JSONReporter from './reporters/json';
|
|
|
|
|
import JUnitReporter from './reporters/junit';
|
|
|
|
|
import EmptyReporter from './reporters/empty';
|
|
|
|
|
import { ProjectImpl } from './project';
|
|
|
|
|
import { Minimatch } from 'minimatch';
|
2021-07-16 22:34:55 -07:00
|
|
|
|
import { Config, FullConfig } from './types';
|
2021-07-15 01:19:45 +02:00
|
|
|
|
import { LaunchServers } from './launchServer';
|
2021-06-06 17:09:53 -07:00
|
|
|
|
|
|
|
|
|
const removeFolderAsync = promisify(rimraf);
|
|
|
|
|
const readDirAsync = promisify(fs.readdir);
|
|
|
|
|
const readFileAsync = promisify(fs.readFile);
|
|
|
|
|
|
2021-07-15 22:02:10 -07:00
|
|
|
|
type RunResultStatus = 'passed' | 'failed' | 'sigint' | 'forbid-only' | 'clashing-test-titles' | 'no-tests' | 'timedout';
|
2021-06-28 22:13:35 +02:00
|
|
|
|
|
|
|
|
|
type RunResult = {
|
2021-07-15 22:02:10 -07:00
|
|
|
|
status: Exclude<RunResultStatus, 'forbid-only' | 'clashing-test-titles'>;
|
2021-06-28 22:13:35 +02:00
|
|
|
|
} | {
|
|
|
|
|
status: 'forbid-only',
|
|
|
|
|
locations: string[]
|
|
|
|
|
} | {
|
2021-07-15 22:02:10 -07:00
|
|
|
|
status: 'clashing-test-titles',
|
|
|
|
|
clashingTests: Map<string, Test[]>
|
2021-06-28 22:13:35 +02:00
|
|
|
|
};
|
2021-06-06 17:09:53 -07:00
|
|
|
|
|
|
|
|
|
export class Runner {
|
|
|
|
|
private _loader: Loader;
|
|
|
|
|
private _reporter!: Reporter;
|
|
|
|
|
private _didBegin = false;
|
|
|
|
|
|
|
|
|
|
constructor(defaultConfig: Config, configOverrides: Config) {
|
|
|
|
|
this._loader = new Loader(defaultConfig, configOverrides);
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-12 11:59:58 -05:00
|
|
|
|
private async _createReporter() {
|
2021-06-06 17:09:53 -07:00
|
|
|
|
const reporters: Reporter[] = [];
|
|
|
|
|
const defaultReporters = {
|
|
|
|
|
dot: DotReporter,
|
|
|
|
|
line: LineReporter,
|
|
|
|
|
list: ListReporter,
|
|
|
|
|
json: JSONReporter,
|
|
|
|
|
junit: JUnitReporter,
|
|
|
|
|
null: EmptyReporter,
|
|
|
|
|
};
|
|
|
|
|
for (const r of this._loader.fullConfig().reporter) {
|
|
|
|
|
const [name, arg] = r;
|
|
|
|
|
if (name in defaultReporters) {
|
|
|
|
|
reporters.push(new defaultReporters[name as keyof typeof defaultReporters](arg));
|
|
|
|
|
} else {
|
2021-07-12 11:59:58 -05:00
|
|
|
|
const reporterConstructor = await this._loader.loadReporter(name);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
reporters.push(new reporterConstructor(arg));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return new Multiplexer(reporters);
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-12 11:59:58 -05:00
|
|
|
|
loadConfigFile(file: string): Promise<Config> {
|
2021-06-06 17:09:53 -07:00
|
|
|
|
return this._loader.loadConfigFile(file);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadEmptyConfig(rootDir: string) {
|
|
|
|
|
this._loader.loadEmptyConfig(rootDir);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-28 22:13:35 +02:00
|
|
|
|
async run(list: boolean, filePatternFilters: FilePatternFilter[], projectName?: string): Promise<RunResultStatus> {
|
2021-07-16 22:34:55 -07:00
|
|
|
|
this._reporter = list ? new ListModeReporter() : await this._createReporter();
|
2021-06-06 17:09:53 -07:00
|
|
|
|
const config = this._loader.fullConfig();
|
|
|
|
|
const globalDeadline = config.globalTimeout ? config.globalTimeout + monotonicTime() : undefined;
|
2021-06-24 10:02:34 +02:00
|
|
|
|
const { result, timedOut } = await raceAgainstDeadline(this._run(list, filePatternFilters, projectName), globalDeadline);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
if (timedOut) {
|
|
|
|
|
if (!this._didBegin)
|
2021-07-16 12:40:33 -07:00
|
|
|
|
this._reporter.onBegin?.(config, new Suite(''));
|
|
|
|
|
await this._reporter.onEnd?.({ status: 'timedout' });
|
2021-06-06 17:09:53 -07:00
|
|
|
|
await this._flushOutput();
|
|
|
|
|
return 'failed';
|
|
|
|
|
}
|
2021-06-28 22:13:35 +02:00
|
|
|
|
if (result?.status === 'forbid-only') {
|
2021-06-06 17:09:53 -07:00
|
|
|
|
console.error('=====================================');
|
|
|
|
|
console.error(' --forbid-only found a focused test.');
|
2021-06-28 22:13:35 +02:00
|
|
|
|
for (const location of result?.locations)
|
|
|
|
|
console.error(` - ${location}`);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
console.error('=====================================');
|
2021-06-28 22:13:35 +02:00
|
|
|
|
} else if (result!.status === 'no-tests') {
|
2021-06-06 17:09:53 -07:00
|
|
|
|
console.error('=================');
|
|
|
|
|
console.error(' no tests found.');
|
|
|
|
|
console.error('=================');
|
2021-07-15 22:02:10 -07:00
|
|
|
|
} else if (result?.status === 'clashing-test-titles') {
|
2021-06-28 22:13:35 +02:00
|
|
|
|
console.error('=================');
|
|
|
|
|
console.error(' duplicate test titles are not allowed.');
|
2021-07-15 22:02:10 -07:00
|
|
|
|
for (const [title, tests] of result?.clashingTests.entries()) {
|
2021-06-28 22:13:35 +02:00
|
|
|
|
console.error(` - title: ${title}`);
|
2021-07-15 22:02:10 -07:00
|
|
|
|
for (const test of tests)
|
|
|
|
|
console.error(` - ${buildItemLocation(config.rootDir, test)}`);
|
2021-06-28 22:13:35 +02:00
|
|
|
|
console.error('=================');
|
|
|
|
|
}
|
2021-06-06 17:09:53 -07:00
|
|
|
|
}
|
|
|
|
|
await this._flushOutput();
|
2021-06-28 22:13:35 +02:00
|
|
|
|
return result!.status!;
|
2021-06-06 17:09:53 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async _flushOutput() {
|
|
|
|
|
// Calling process.exit() might truncate large stdout/stderr output.
|
|
|
|
|
// See https://github.com/nodejs/node/issues/6456.
|
2021-06-25 00:54:06 +02:00
|
|
|
|
// See https://github.com/nodejs/node/issues/12921
|
|
|
|
|
await new Promise<void>(resolve => process.stdout.write('', () => resolve()));
|
|
|
|
|
await new Promise<void>(resolve => process.stderr.write('', () => resolve()));
|
2021-06-06 17:09:53 -07:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-24 10:02:34 +02:00
|
|
|
|
async _run(list: boolean, testFileReFilters: FilePatternFilter[], projectName?: string): Promise<RunResult> {
|
|
|
|
|
const testFileFilter = testFileReFilters.length ? createMatcher(testFileReFilters.map(e => e.re)) : () => true;
|
2021-06-06 17:09:53 -07:00
|
|
|
|
const config = this._loader.fullConfig();
|
|
|
|
|
|
|
|
|
|
const projects = this._loader.projects().filter(project => {
|
|
|
|
|
return !projectName || project.config.name.toLocaleLowerCase() === projectName.toLocaleLowerCase();
|
|
|
|
|
});
|
|
|
|
|
if (projectName && !projects.length) {
|
|
|
|
|
const names = this._loader.projects().map(p => p.config.name).filter(name => !!name);
|
|
|
|
|
if (!names.length)
|
|
|
|
|
throw new Error(`No named projects are specified in the configuration file`);
|
|
|
|
|
throw new Error(`Project "${projectName}" not found. Available named projects: ${names.map(name => `"${name}"`).join(', ')}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const files = new Map<ProjectImpl, string[]>();
|
|
|
|
|
const allTestFiles = new Set<string>();
|
|
|
|
|
for (const project of projects) {
|
|
|
|
|
const testDir = project.config.testDir;
|
|
|
|
|
if (!fs.existsSync(testDir))
|
|
|
|
|
throw new Error(`${testDir} does not exist`);
|
|
|
|
|
if (!fs.statSync(testDir).isDirectory())
|
|
|
|
|
throw new Error(`${testDir} is not a directory`);
|
|
|
|
|
const allFiles = await collectFiles(project.config.testDir);
|
|
|
|
|
const testMatch = createMatcher(project.config.testMatch);
|
|
|
|
|
const testIgnore = createMatcher(project.config.testIgnore);
|
2021-06-29 15:28:41 -07:00
|
|
|
|
const testFileExtension = (file: string) => ['.js', '.ts', '.mjs'].includes(path.extname(file));
|
2021-06-10 07:41:57 -07:00
|
|
|
|
const testFiles = allFiles.filter(file => !testIgnore(file) && testMatch(file) && testFileFilter(file) && testFileExtension(file));
|
2021-06-06 17:09:53 -07:00
|
|
|
|
files.set(project, testFiles);
|
|
|
|
|
testFiles.forEach(file => allTestFiles.add(file));
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-15 01:19:45 +02:00
|
|
|
|
const launchServers = await LaunchServers.create(config.launch);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
let globalSetupResult: any;
|
|
|
|
|
if (config.globalSetup)
|
2021-07-12 11:59:58 -05:00
|
|
|
|
globalSetupResult = await (await this._loader.loadGlobalHook(config.globalSetup, 'globalSetup'))(this._loader.fullConfig());
|
2021-06-06 17:09:53 -07:00
|
|
|
|
try {
|
|
|
|
|
for (const file of allTestFiles)
|
2021-06-29 15:28:41 -07:00
|
|
|
|
await this._loader.loadTestFile(file);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
|
2021-07-15 22:02:10 -07:00
|
|
|
|
const preprocessRoot = new Suite('');
|
2021-06-06 17:09:53 -07:00
|
|
|
|
for (const fileSuite of this._loader.fileSuites().values())
|
2021-07-15 22:02:10 -07:00
|
|
|
|
preprocessRoot._addSuite(fileSuite);
|
2021-06-28 22:13:35 +02:00
|
|
|
|
if (config.forbidOnly) {
|
2021-07-15 22:02:10 -07:00
|
|
|
|
const onlyTestsAndSuites = preprocessRoot._getOnlyItems();
|
2021-07-16 15:23:50 -07:00
|
|
|
|
if (onlyTestsAndSuites.length > 0) {
|
|
|
|
|
const locations = onlyTestsAndSuites.map(testOrSuite => {
|
|
|
|
|
// Skip root and file.
|
|
|
|
|
const title = testOrSuite.titlePath().slice(2).join(' ');
|
|
|
|
|
return `${buildItemLocation(config.rootDir, testOrSuite)} > ${title}`;
|
|
|
|
|
});
|
|
|
|
|
return { status: 'forbid-only', locations };
|
|
|
|
|
}
|
2021-06-28 22:13:35 +02:00
|
|
|
|
}
|
2021-07-15 22:02:10 -07:00
|
|
|
|
const clashingTests = getClashingTestsPerSuite(preprocessRoot);
|
|
|
|
|
if (clashingTests.size > 0)
|
|
|
|
|
return { status: 'clashing-test-titles', clashingTests: clashingTests };
|
|
|
|
|
filterOnly(preprocessRoot);
|
|
|
|
|
filterByFocusedLine(preprocessRoot, testFileReFilters);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
|
|
|
|
|
const fileSuites = new Map<string, Suite>();
|
2021-07-15 22:02:10 -07:00
|
|
|
|
for (const fileSuite of preprocessRoot.suites)
|
2021-06-21 11:25:15 -07:00
|
|
|
|
fileSuites.set(fileSuite._requireFile, fileSuite);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
|
|
|
|
|
const outputDirs = new Set<string>();
|
|
|
|
|
const grepMatcher = createMatcher(config.grep);
|
2021-06-18 17:56:59 -07:00
|
|
|
|
const grepInvertMatcher = config.grepInvert ? createMatcher(config.grepInvert) : null;
|
2021-07-15 22:02:10 -07:00
|
|
|
|
const rootSuite = new Suite('');
|
2021-06-06 17:09:53 -07:00
|
|
|
|
for (const project of projects) {
|
2021-07-16 15:23:50 -07:00
|
|
|
|
const projectSuite = new Suite(project.config.name);
|
|
|
|
|
rootSuite._addSuite(projectSuite);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
for (const file of files.get(project)!) {
|
|
|
|
|
const fileSuite = fileSuites.get(file);
|
|
|
|
|
if (!fileSuite)
|
|
|
|
|
continue;
|
2021-07-15 22:02:10 -07:00
|
|
|
|
for (let repeatEachIndex = 0; repeatEachIndex < project.config.repeatEach; repeatEachIndex++) {
|
2021-07-16 15:23:50 -07:00
|
|
|
|
const cloned = project.cloneFileSuite(fileSuite, repeatEachIndex, test => {
|
|
|
|
|
const grepTitle = test.titlePath().join(' ');
|
|
|
|
|
if (grepInvertMatcher?.(grepTitle))
|
2021-07-15 22:02:10 -07:00
|
|
|
|
return false;
|
2021-07-16 15:23:50 -07:00
|
|
|
|
return grepMatcher(grepTitle);
|
2021-07-15 22:02:10 -07:00
|
|
|
|
});
|
|
|
|
|
if (cloned)
|
2021-07-16 15:23:50 -07:00
|
|
|
|
projectSuite._addSuite(cloned);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
outputDirs.add(project.config.outputDir);
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-16 12:40:33 -07:00
|
|
|
|
const total = rootSuite.allTests().length;
|
2021-06-06 17:09:53 -07:00
|
|
|
|
if (!total)
|
2021-06-28 22:13:35 +02:00
|
|
|
|
return { status: 'no-tests' };
|
2021-06-06 17:09:53 -07:00
|
|
|
|
|
|
|
|
|
await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(e => {})));
|
|
|
|
|
|
|
|
|
|
let sigint = false;
|
|
|
|
|
let sigintCallback: () => void;
|
|
|
|
|
const sigIntPromise = new Promise<void>(f => sigintCallback = f);
|
|
|
|
|
const sigintHandler = () => {
|
|
|
|
|
// We remove handler so that double Ctrl+C immediately kills the runner,
|
|
|
|
|
// for the case where our shutdown takes a lot of time or is buggy.
|
|
|
|
|
// Removing the handler synchronously sometimes triggers the default handler
|
|
|
|
|
// that exits the process, so we remove asynchronously.
|
|
|
|
|
setTimeout(() => process.off('SIGINT', sigintHandler), 0);
|
|
|
|
|
sigint = true;
|
|
|
|
|
sigintCallback();
|
|
|
|
|
};
|
|
|
|
|
process.on('SIGINT', sigintHandler);
|
|
|
|
|
|
|
|
|
|
if (process.stdout.isTTY) {
|
|
|
|
|
const workers = new Set();
|
2021-07-16 12:40:33 -07:00
|
|
|
|
rootSuite.allTests().forEach(test => {
|
2021-07-15 22:02:10 -07:00
|
|
|
|
workers.add(test._requireFile + test._workerHash);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
});
|
|
|
|
|
console.log();
|
|
|
|
|
const jobs = Math.min(config.workers, workers.size);
|
|
|
|
|
const shard = config.shard;
|
|
|
|
|
const shardDetails = shard ? `, shard ${shard.current + 1} of ${shard.total}` : '';
|
|
|
|
|
console.log(`Running ${total} test${total > 1 ? 's' : ''} using ${jobs} worker${jobs > 1 ? 's' : ''}${shardDetails}`);
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-16 12:40:33 -07:00
|
|
|
|
this._reporter.onBegin?.(config, rootSuite);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
this._didBegin = true;
|
|
|
|
|
let hasWorkerErrors = false;
|
|
|
|
|
if (!list) {
|
|
|
|
|
const dispatcher = new Dispatcher(this._loader, rootSuite, this._reporter);
|
|
|
|
|
await Promise.race([dispatcher.run(), sigIntPromise]);
|
|
|
|
|
await dispatcher.stop();
|
|
|
|
|
hasWorkerErrors = dispatcher.hasWorkerErrors();
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-29 10:55:46 -07:00
|
|
|
|
if (sigint) {
|
2021-07-16 12:40:33 -07:00
|
|
|
|
await this._reporter.onEnd?.({ status: 'interrupted' });
|
2021-06-28 22:13:35 +02:00
|
|
|
|
return { status: 'sigint' };
|
2021-06-29 10:55:46 -07:00
|
|
|
|
}
|
|
|
|
|
|
2021-07-16 12:40:33 -07:00
|
|
|
|
const failed = hasWorkerErrors || rootSuite.allTests().some(test => !test.ok());
|
|
|
|
|
await this._reporter.onEnd?.({ status: failed ? 'failed' : 'passed' });
|
2021-06-29 10:55:46 -07:00
|
|
|
|
return { status: failed ? 'failed' : 'passed' };
|
2021-06-06 17:09:53 -07:00
|
|
|
|
} finally {
|
|
|
|
|
if (globalSetupResult && typeof globalSetupResult === 'function')
|
|
|
|
|
await globalSetupResult(this._loader.fullConfig());
|
|
|
|
|
if (config.globalTeardown)
|
2021-07-12 11:59:58 -05:00
|
|
|
|
await (await this._loader.loadGlobalHook(config.globalTeardown, 'globalTeardown'))(this._loader.fullConfig());
|
2021-07-15 01:19:45 +02:00
|
|
|
|
await launchServers.killAll();
|
2021-06-06 17:09:53 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterOnly(suite: Suite) {
|
2021-06-24 10:02:34 +02:00
|
|
|
|
const suiteFilter = (suite: Suite) => suite._only;
|
2021-07-15 22:02:10 -07:00
|
|
|
|
const testFilter = (test: Test) => test._only;
|
|
|
|
|
return filterSuite(suite, suiteFilter, testFilter);
|
2021-06-24 10:02:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterByFocusedLine(suite: Suite, focusedTestFileLines: FilePatternFilter[]) {
|
2021-07-15 22:02:10 -07:00
|
|
|
|
const testFileLineMatches = (testFileName: string, testLine: number) => focusedTestFileLines.some(({re, line}) => {
|
2021-06-24 10:02:34 +02:00
|
|
|
|
re.lastIndex = 0;
|
2021-07-15 22:02:10 -07:00
|
|
|
|
return re.test(testFileName) && (line === testLine || line === null);
|
2021-06-24 10:02:34 +02:00
|
|
|
|
});
|
2021-07-16 12:40:33 -07:00
|
|
|
|
const suiteFilter = (suite: Suite) => testFileLineMatches(suite.location.file, suite.location.line);
|
|
|
|
|
const testFilter = (test: Test) => testFileLineMatches(test.location.file, test.location.line);
|
2021-07-15 22:02:10 -07:00
|
|
|
|
return filterSuite(suite, suiteFilter, testFilter);
|
2021-06-24 10:02:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-07-15 22:02:10 -07:00
|
|
|
|
function filterSuite(suite: Suite, suiteFilter: (suites: Suite) => boolean, testFilter: (test: Test) => boolean) {
|
|
|
|
|
const onlySuites = suite.suites.filter(child => filterSuite(child, suiteFilter, testFilter) || suiteFilter(child));
|
|
|
|
|
const onlyTests = suite.tests.filter(testFilter);
|
2021-06-06 17:09:53 -07:00
|
|
|
|
const onlyEntries = new Set([...onlySuites, ...onlyTests]);
|
|
|
|
|
if (onlyEntries.size) {
|
|
|
|
|
suite.suites = onlySuites;
|
2021-07-15 22:02:10 -07:00
|
|
|
|
suite.tests = onlyTests;
|
2021-06-06 17:09:53 -07:00
|
|
|
|
suite._entries = suite._entries.filter(e => onlyEntries.has(e)); // Preserve the order.
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function collectFiles(testDir: string): Promise<string[]> {
|
|
|
|
|
type Rule = {
|
|
|
|
|
dir: string;
|
|
|
|
|
negate: boolean;
|
|
|
|
|
match: (s: string, partial?: boolean) => boolean
|
|
|
|
|
};
|
|
|
|
|
type IgnoreStatus = 'ignored' | 'included' | 'ignored-but-recurse';
|
|
|
|
|
|
|
|
|
|
const checkIgnores = (entryPath: string, rules: Rule[], isDirectory: boolean, parentStatus: IgnoreStatus) => {
|
|
|
|
|
let status = parentStatus;
|
|
|
|
|
for (const rule of rules) {
|
|
|
|
|
const ruleIncludes = rule.negate;
|
|
|
|
|
if ((status === 'included') === ruleIncludes)
|
|
|
|
|
continue;
|
|
|
|
|
const relative = path.relative(rule.dir, entryPath);
|
|
|
|
|
if (rule.match('/' + relative) || rule.match(relative)) {
|
|
|
|
|
// Matches "/dir/file" or "dir/file"
|
|
|
|
|
status = ruleIncludes ? 'included' : 'ignored';
|
|
|
|
|
} else if (isDirectory && (rule.match('/' + relative + '/') || rule.match(relative + '/'))) {
|
|
|
|
|
// Matches "/dir/subdir/" or "dir/subdir/" for directories.
|
|
|
|
|
status = ruleIncludes ? 'included' : 'ignored';
|
|
|
|
|
} else if (isDirectory && ruleIncludes && (rule.match('/' + relative, true) || rule.match(relative, true))) {
|
|
|
|
|
// Matches "/dir/donotskip/" when "/dir" is excluded, but "!/dir/donotskip/file" is included.
|
|
|
|
|
status = 'ignored-but-recurse';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return status;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const files: string[] = [];
|
|
|
|
|
|
|
|
|
|
const visit = async (dir: string, rules: Rule[], status: IgnoreStatus) => {
|
|
|
|
|
const entries = await readDirAsync(dir, { withFileTypes: true });
|
|
|
|
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
|
|
|
|
|
const gitignore = entries.find(e => e.isFile() && e.name === '.gitignore');
|
|
|
|
|
if (gitignore) {
|
|
|
|
|
const content = await readFileAsync(path.join(dir, gitignore.name), 'utf8');
|
|
|
|
|
const newRules: Rule[] = content.split(/\r?\n/).map(s => {
|
|
|
|
|
s = s.trim();
|
|
|
|
|
if (!s)
|
|
|
|
|
return;
|
|
|
|
|
// Use flipNegate, because we handle negation ourselves.
|
|
|
|
|
const rule = new Minimatch(s, { matchBase: true, dot: true, flipNegate: true }) as any;
|
|
|
|
|
if (rule.comment)
|
|
|
|
|
return;
|
|
|
|
|
rule.dir = dir;
|
|
|
|
|
return rule;
|
|
|
|
|
}).filter(rule => !!rule);
|
|
|
|
|
rules = [...rules, ...newRules];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
if (entry === gitignore || entry.name === '.' || entry.name === '..')
|
|
|
|
|
continue;
|
|
|
|
|
if (entry.isDirectory() && entry.name === 'node_modules')
|
|
|
|
|
continue;
|
|
|
|
|
const entryPath = path.join(dir, entry.name);
|
|
|
|
|
const entryStatus = checkIgnores(entryPath, rules, entry.isDirectory(), status);
|
|
|
|
|
if (entry.isDirectory() && entryStatus !== 'ignored')
|
|
|
|
|
await visit(entryPath, rules, entryStatus);
|
|
|
|
|
else if (entry.isFile() && entryStatus === 'included')
|
|
|
|
|
files.push(entryPath);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
await visit(testDir, [], 'included');
|
|
|
|
|
return files;
|
|
|
|
|
}
|
2021-06-28 22:13:35 +02:00
|
|
|
|
|
2021-07-15 22:02:10 -07:00
|
|
|
|
function getClashingTestsPerSuite(rootSuite: Suite): Map<string, Test[]> {
|
|
|
|
|
function visit(suite: Suite, clashingTests: Map<string, Test[]>) {
|
2021-06-28 22:13:35 +02:00
|
|
|
|
for (const childSuite of suite.suites)
|
2021-07-15 22:02:10 -07:00
|
|
|
|
visit(childSuite, clashingTests);
|
|
|
|
|
for (const test of suite.tests) {
|
2021-07-16 15:23:50 -07:00
|
|
|
|
const fullTitle = test.titlePath().slice(2).join(' ');
|
2021-07-15 22:02:10 -07:00
|
|
|
|
if (!clashingTests.has(fullTitle))
|
|
|
|
|
clashingTests.set(fullTitle, []);
|
|
|
|
|
clashingTests.set(fullTitle, clashingTests.get(fullTitle)!.concat(test));
|
2021-06-28 22:13:35 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-07-15 22:02:10 -07:00
|
|
|
|
const out = new Map<string, Test[]>();
|
2021-06-28 22:13:35 +02:00
|
|
|
|
for (const fileSuite of rootSuite.suites) {
|
2021-07-15 22:02:10 -07:00
|
|
|
|
const clashingTests = new Map<string, Test[]>();
|
|
|
|
|
visit(fileSuite, clashingTests);
|
|
|
|
|
for (const [title, tests] of clashingTests.entries()) {
|
|
|
|
|
if (tests.length > 1)
|
|
|
|
|
out.set(title, tests);
|
2021-06-28 22:13:35 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-15 22:02:10 -07:00
|
|
|
|
function buildItemLocation(rootDir: string, testOrSuite: Suite | Test) {
|
2021-07-16 12:40:33 -07:00
|
|
|
|
return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`;
|
2021-06-28 22:13:35 +02:00
|
|
|
|
}
|
2021-07-16 22:34:55 -07:00
|
|
|
|
|
|
|
|
|
class ListModeReporter implements Reporter {
|
|
|
|
|
onBegin(config: FullConfig, suite: Suite): void {
|
|
|
|
|
console.log(`Listing tests:`);
|
|
|
|
|
const tests = suite.allTests();
|
|
|
|
|
const files = new Set<string>();
|
|
|
|
|
for (const test of tests) {
|
|
|
|
|
// root, project, file, ...describes, test
|
|
|
|
|
const [, projectName, , ...titles] = test.titlePath();
|
|
|
|
|
const location = `${path.relative(config.rootDir, test.location.file)}:${test.location.line}:${test.location.column}`;
|
|
|
|
|
const projectTitle = projectName ? `[${projectName}] › ` : '';
|
|
|
|
|
console.log(` ${projectTitle}${location} › ${titles.join(' ')}`);
|
|
|
|
|
files.add(test.location.file);
|
|
|
|
|
}
|
|
|
|
|
console.log(`Total: ${tests.length} ${tests.length === 1 ? 'test' : 'tests'} in ${files.size} ${files.size === 1 ? 'file' : 'files'}`);
|
|
|
|
|
}
|
|
|
|
|
}
|