blob: 94c63ca2154e2b782d288b1d1506fe10458c33c9 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import Mocha from 'mocha';
import * as path from 'node:path';
import {pathToFileURL} from 'node:url';
import {
duplicateTests,
pruneSuite,
} from '../../front_end/testing/MochaHelpers.js';
import {computeBuildTestId} from '../../front_end/testing/TestIdGeneration.js';
import {TEST_ID_REGEX} from '../conductor/paths.js';
import {TestConfig} from '../conductor/test_config.js';
import {getSkippedTests} from '../conductor/test_expectations.js';
type Options = Mocha.MochaOptions&{spec?: string[], suiteName?: string};
type OmitOptions = 'reporter'|'allowUncaught'|'retries'|'failZero';
function optionsWithDefalts(options: Options): Options {
const withDefaults = {
// This should make mocha crash on uncaught errors.
// See https://github.com/mochajs/mocha/blob/master/docs/index.md#--allow-uncaught.
allowUncaught: true,
retries: TestConfig.retries,
reporter: path.join(__dirname, 'mocha-resultsdb-reporter.js'),
slow: 1000,
failZero: false,
...options,
...TestConfig.mochaGrep,
};
if (TestConfig.debug) {
withDefaults.timeout = 0;
}
return withDefaults;
}
export async function run(options: Omit<Options, OmitOptions>) {
const mocha = new Mocha(optionsWithDefalts(options));
// Load files
const files = options.spec;
if (Array.isArray(files)) {
for (const file of files) {
mocha.addFile(file);
}
}
// Load requires if necessary (Mocha programmatic API might not automatically load requires,
// we may need to import them manually).
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const globalSetups: Function[] = [];
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const globalTeardowns: Function[] = [];
if (options.require) {
const requires = Array.isArray(options.require) ? options.require : [options.require];
for (const req of requires) {
const importTarget = path.isAbsolute(req) ? pathToFileURL(req).href : req;
const mod = await import(importTarget);
const exportsObj = mod.default || mod;
if (exportsObj.mochaGlobalSetup) {
globalSetups.push(exportsObj.mochaGlobalSetup);
}
if (exportsObj.mochaGlobalTeardown) {
globalTeardowns.push(exportsObj.mochaGlobalTeardown);
}
if (exportsObj.mochaHooks) {
mocha.rootHooks(exportsObj.mochaHooks);
}
}
}
if (globalSetups.length > 0) {
// @ts-expect-error unknown types
mocha.globalSetup(globalSetups);
}
if (globalTeardowns.length > 0) {
// @ts-expect-error unknown types
mocha.globalTeardown(globalTeardowns);
}
await mocha.loadFilesAsync();
const testIds = new Set(
TestConfig.tests.filter(testId => TEST_ID_REGEX.test(testId)),
);
const seenTestIds = new Set<string>();
const skippedTests = getSkippedTests();
function shouldIncludeTest(test: Mocha.Test) {
if (!test.file) {
throw new Error(`Test ${test.titlePath()} does not have a file.`);
}
const testId = computeBuildTestId(test.file, test.titlePath());
if (seenTestIds.has(testId)) {
throw new Error(`Duplicate test ${testId}`);
}
seenTestIds.add(testId);
const isSkipped = skippedTests.some((skippedTest: string) => {
return testId === skippedTest || testId.startsWith(`${skippedTest}:`);
});
if (isSkipped) {
test.pending = true;
}
if (testIds.size === 0) {
return true;
}
return testIds.has(testId);
}
pruneSuite(mocha.suite, shouldIncludeTest);
duplicateTests(mocha.suite, TestConfig.repetitions);
mocha.enableGlobalSetup(true);
mocha.enableGlobalTeardown(true);
const failures = await new Promise<number>(resolve => {
mocha.run(resolve);
});
if (process.exitCode === undefined) {
process.exitCode = failures > 0 ? 1 : 0;
}
}