blob: 8168b7c95ef122596d2fd323d5d0ab23402d3d19 [file] [log] [blame]
#!/usr/bin/env node
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const path = require('path');
const fs = require('fs');
const os = require('os');
const childProcess = require('child_process');
const {
nodePath,
mochaExecutablePath,
downloadedChromeBinaryPath,
devtoolsRootPath,
} = require('../devtools_paths.js');
function log(...msg) {
console.log('[run_test_suite.js]', ...msg);
}
function err(...msg) {
console.error('[run_test_suite.js]', ...msg);
}
const yargsObject =
require('yargs')
.option(
'test-suite-path',
{type: 'string', desc: 'Path to the test suite, starting from out/Target directory.', demandOption: true})
.option('test-suite-source-dir', {
type: 'string',
desc: 'Path to the source folder containing the tests, relative to the current working directory.',
demandOption: true
})
.option('autoninja', {
type: 'boolean',
desc: 'If true, will trigger an autoninja build before executing the test suite',
default: false,
})
.option('target', {type: 'string', default: 'Default', desc: 'Name of the Ninja output directory.'})
.option('node-modules-path', {
type: 'string',
desc:
'Path to the node_modules directory for Node to use, relative to the current working directory. Defaults to local node_modules folder.'
})
.option('test-server-type', {
'choices': ['hosted-mode', 'component-docs', 'none'],
'describe':
'The type of test-server to run for the tests. Will be set automatically if your test-suite-path ends with e2e or interactions, but you need to set it otherwise. If you do not need a test-server, you must explicitly pass the "none" option.',
})
.option('test-file-pattern', {
type: 'string',
desc: 'A comma separated glob (or just a file path) to select specific test files to execute.'
})
.option('mocha-fgrep', {
type: 'string',
desc:
'Mocha\'s fgrep option [https://mochajs.org/#-fgrep-string-f-string] which only runs tests whose titles contain the provided string',
})
.option('mocha-grep', {
type: 'string',
desc:
'Mocha\'s grep option [https://mochajs.org/#-grep-regexp-g-regexp] which only runs tests whose titles matches the provided regular expression',
})
.option('invert', {
type: 'boolean',
desc:
'Mocha\'s invert option [https://mochajs.org/#-invert] which inverts the match specified by mocha-fgrep and mocha-grep',
default: false,
})
.option('mocha-reporter', {
type: 'string',
desc: 'Mocha\'s --reporter option',
})
.option('mocha-reporter-option', {
type: 'string',
desc: 'Mocha\'s --reporter-option flag to pass options through to the Mocha reporter',
})
// test-file-pattern can be provided as a flag or as a positional
// argument. $0 here is Yarg's syntax for the default command:
// https://github.com/yargs/yargs/blob/master/docs/advanced.md#default-commands
.command('$0 [test-file-pattern]')
.option('component-server-base-path', {
type: 'string',
desc:
'The component serve assumes examples live in out/TARGET/gen/front_end/ui/components/docs, but you can set this option to add a prefix. Passing `foo` will redirect the server to look in out/TARGET/gen/foo/front_end/ui/components/docs.',
default: '',
})
.option('component-server-shared-resources-path', {
type: 'string',
desc:
'Configures the base of the URLs that are injected into each component example. By default it is "/", so we load from "/front_end", but you can provide a different prefix if the shared resources are based elsewhere in the directory structure.',
default: '/',
})
.option('hosted-server-devtools-url', {
type: 'string',
desc: 'Configures the page that will be loaded by conductor when using the hosted-server for tests.',
default: 'front_end/devtools_app.html'
})
.option('hosted-server-e2e-resources-path', {
type: 'string',
desc: 'The base URL that will be used when loading e2e test resources',
default: '/test/e2e/resources'
})
.option(
'chrome-binary-path',
{type: 'string', desc: 'Path to the Chromium binary.', default: downloadedChromeBinaryPath()})
.option('chrome-features', {
type: 'string',
desc: 'Comma separated list of strings passed to --enable-features on the Chromium command line.'
})
.option('jobs', {
type: 'number',
desc: 'Number of parallel runners to use (if supported). Defaults to 1.',
default: 1,
})
.option('cwd', {
type: 'string',
desc: 'Path to the directory containing the out/TARGET folder.',
default: devtoolsRootPath()
})
.option('coverage', {
type: 'boolean',
desc: 'Whether to collect code coverage for this test suite',
default: false,
})
.option('swarming-output-file', {
type: 'string',
desc: 'Path to copy goldens and coverage files',
default: '',
})
.parserConfiguration({
// So that if we pass --foo-bar, Yargs only populates
// argv with '--foo-bar', not '--foo-bar' and 'fooBar'.
'camel-case-expansion': false
})
// Take options via --config config.json
.config()
// Fail on any unknown arguments
.strict()
.argv;
function validatePathExistsOrError(nameOfPath, filePath) {
try {
fs.accessSync(filePath, fs.constants.R_OK);
} catch (_error) {
err(`Failed: ${nameOfPath} [${filePath}] does not exist.`);
process.exit(1);
}
}
function getAbsoluteTestSuitePath(target) {
const pathInput = yargsObject['test-suite-path'];
// We take the input with Linux path separators, but need to split and join to make sure this works on Windows.
const testSuitePathParts = pathInput.split('/');
log(`Using test suite ${path.join(pathInput, path.sep)}`);
const fullPath = path.join(yargsObject['cwd'], 'out', target, ...testSuitePathParts);
return fullPath;
}
function setEnvValueIfValuePresent(name, value) {
if (value) {
process.env[name] = value;
}
}
function setNodeModulesPath(nodeModulesPathsInput) {
if (nodeModulesPathsInput) {
/** You can provide multiple paths split by either ; (windows) or : (everywhere else)
* So we need to split our input, ensure each part is absolute, check it
* exists, and then set NODE_PATH again.
*/
const delimiter = os.platform() === 'win32' ? ';' : ':';
const inputPaths = nodeModulesPathsInput.split(delimiter);
const outputPaths = [];
inputPaths.forEach(nodePath => {
if (path.isAbsolute(nodePath)) {
validatePathExistsOrError('node-modules-path', nodePath);
outputPaths.push(nodePath);
return;
}
// Node requires the path to be absolute
const absolutePath = path.resolve(path.join(yargsObject['cwd'], nodePath));
validatePathExistsOrError('node-modules-path', nodePath);
outputPaths.push(absolutePath);
});
setEnvValueIfValuePresent('NODE_PATH', outputPaths.join(delimiter));
}
}
function triggerAutoninja(target) {
const ninjaCommand = os.platform() === 'win32' ? 'autoninja.bat' : 'autoninja';
const ninjaArgs = ['-C', `out/${target}`];
const cwd = devtoolsRootPath();
const result = childProcess.spawnSync(ninjaCommand, ninjaArgs, {encoding: 'utf-8', stdio: 'inherit', cwd});
if (result.error) {
throw result.error;
}
return result.status;
}
function executeTestSuite({
absoluteTestSuitePath,
jobs,
target,
nodeModulesPath,
chromeBinaryPath,
chromeFeatures,
testFilePattern,
coverage,
cwd,
mochaOptions = {},
}) {
/**
* Internally within various scripts (Mocha configs, Conductor, etc), we rely on
* process.env.FOO. We are moving to exposing the entire configuration to
* process.env.TEST_CONFIG_JSON but for now we need to still expose the values
* directly on the environment whilst we roll out this script and make all the
* required changes.
*/
setEnvValueIfValuePresent('CHROME_BIN', chromeBinaryPath);
setEnvValueIfValuePresent('CHROME_FEATURES', chromeFeatures);
setEnvValueIfValuePresent('JOBS', jobs);
setEnvValueIfValuePresent('TARGET', target);
setEnvValueIfValuePresent('TEST_PATTERNS', testFilePattern);
setEnvValueIfValuePresent('COVERAGE', coverage);
/**
* This one has to be set as an ENV variable as Node looks for the NODE_PATH environment variable.
*/
setNodeModulesPath(nodeModulesPath);
const argumentsForNode = [
mochaExecutablePath(),
];
if (process.env.DEBUG_TEST) {
argumentsForNode.unshift('--inspect');
}
const testSuiteConfig = path.join(absoluteTestSuitePath, '.mocharc.js');
validatePathExistsOrError('.mocharc.js location', testSuiteConfig);
argumentsForNode.push('--config', testSuiteConfig);
for (const [mochaKey, mochaValue] of Object.entries(mochaOptions)) {
if (mochaValue !== undefined) {
argumentsForNode.push(`--${mochaKey}`, mochaValue);
log(`extra mocha flag: --${mochaKey}=${mochaValue}`);
}
}
if (jobs > 1) {
argumentsForNode.push(`--jobs=${jobs}`);
}
const result = childProcess.spawnSync(nodePath(), argumentsForNode, {encoding: 'utf-8', stdio: 'inherit', cwd});
if (result.error) {
throw result.error;
}
return result.status;
}
function fileIsExecutable(filePath) {
try {
fs.accessSync(filePath, fs.constants.X_OK);
return true;
} catch (e) {
return false;
}
}
function validateChromeBinaryExistsAndExecutable(chromeBinaryPath) {
return (
fs.existsSync(chromeBinaryPath) && fs.statSync(chromeBinaryPath).isFile(chromeBinaryPath) &&
fileIsExecutable(chromeBinaryPath));
}
function main() {
if (yargsObject['autoninja']) {
triggerAutoninja(yargsObject['target']);
}
const chromeBinaryPath = yargsObject['chrome-binary-path'];
if (!validateChromeBinaryExistsAndExecutable(chromeBinaryPath)) {
err(`Chrome binary path ${chromeBinaryPath} is not valid`);
}
const target = yargsObject['target'];
const targetPath = path.join(yargsObject['cwd'], 'out', target);
validatePathExistsOrError(`Target out/${target}`, targetPath);
/*
* Pull out all the configuration flags, ignoring the Yargs special $0 and _
* keys, which we can ignore.
*/
// eslint-disable-next-line no-unused-vars
const {$0, _, ...configurationFlags} = yargsObject;
if (!configurationFlags['test-server-type']) {
if (configurationFlags['test-suite-path'].match(/e2e\/?/)) {
configurationFlags['test-server-type'] = 'hosted-mode';
} else if (configurationFlags['test-suite-path'].match(/interactions\/?/)) {
configurationFlags['test-server-type'] = 'component-docs';
} else {
err('test-server-type could not be intelligently set based on your test-suite-path, you must manually set --test-server-type. Set it to "none" if you do not need a test-server to be run.');
process.exit(1);
}
}
/**
* Expose the configuration to any downstream test runners (Mocha, Conductor,
* Test servers, etc).
*/
process.env.TEST_RUNNER_JSON_CONFIG = JSON.stringify(configurationFlags);
log(`Using Chromium binary ${chromeBinaryPath}`);
if (configurationFlags['chrome-features']) {
log(`with --enable-features=${configurationFlags['chrome-features']}`);
}
log(`Using target ${target}`);
const testSuitePath = getAbsoluteTestSuitePath(target);
validatePathExistsOrError('Full path to test suite', testSuitePath);
let resultStatusCode = -1;
try {
resultStatusCode = executeTestSuite({
absoluteTestSuitePath: testSuitePath,
chromeBinaryPath,
chromeFeatures: configurationFlags['chrome-features'],
nodeModulesPath: configurationFlags['node-modules-path'],
jobs: configurationFlags['jobs'],
testFilePattern: configurationFlags['test-file-pattern'],
coverage: configurationFlags['coverage'] && '1',
target,
cwd: configurationFlags['cwd'],
mochaOptions: {
fgrep: configurationFlags['mocha-fgrep'],
grep: configurationFlags['mocha-grep'],
invert: configurationFlags['invert'],
reporter: configurationFlags['mocha-reporter'],
'reporter-option': configurationFlags['mocha-reporter-option'],
}
});
} catch (error) {
log('Unexpected error when running test suite', error);
resultStatusCode = 1;
}
if (resultStatusCode !== 0) {
log('ERRORS DETECTED');
}
if (yargsObject['swarming-output-file']) {
if (yargsObject['coverage']) {
fs.cpSync(
'interactions-coverage', `${yargsObject['swarming-output-file']}/interactions-coverage`, {recursive: true});
}
if (fs.existsSync('perf-data')) {
fs.cpSync('perf-data', `${yargsObject['swarming-output-file']}/perf-data`, {recursive: true});
}
fs.cpSync('test/interactions/goldens', `${yargsObject['swarming-output-file']}/goldens`, {recursive: true});
}
process.exit(resultStatusCode);
}
main();