Files
gitlab-foss/scripts/frontend/jest_ci.js
2025-02-05 18:12:45 +00:00

192 lines
5.6 KiB
JavaScript
Executable File

#!/usr/bin/env node
const { spawnSync } = require('node:child_process');
const { readFileSync } = require('node:fs');
const defaultChalk = require('chalk');
const { program } = require('commander');
const IS_CI = Boolean(process.env.CI);
const VUE_3_TESTING_DOCS_URL =
// eslint-disable-next-line no-restricted-syntax
'https://docs.gitlab.com/ee/development/testing_guide/testing_vue3.html';
const VUE_3_TESTING_EPIC = 'https://gitlab.com/groups/gitlab-org/-/epics/11740';
// Force basic color output in CI
const chalk = new defaultChalk.constructor({ level: IS_CI ? 1 : undefined });
function showVue3Help() {
console.warn(' ');
console.warn(
chalk.green.bold('Having trouble getting tests to pass under Vue 3? These resources may help:'),
);
console.warn(' ');
console.warn(` - ${chalk.green('Vue 3 testing documentation:')} ${VUE_3_TESTING_DOCS_URL}`);
console.warn(` - ${chalk.green('Epic for fixing tests under Vue 3:')} w${VUE_3_TESTING_EPIC}`);
}
function parseArgumentsAndEnvironment() {
program
.description(`Runs Jest under CI.`)
.option(
'--vue3',
'Run tests under Vue 3 (via @vue/compat). The default is to run under Vue 2. The VUE_VERSION environment variable must agree with this option.',
)
.option(
'--include-vue3-quarantined',
'Include tests normally quarantined under Vue 3. This is currently used for nightly pipelines. Has no effect without --vue3. The default (given --vue3) is to exclude quarantined tests.',
)
.option(
'--predictive',
'Only run specs affected by the changes in the merge request. The default is to run all specs.',
)
.option(
'--fixtures',
'Only run specs which rely on generated fixtures. The default is to only run specs which do not rely on generated fixtures.',
)
.option(
'--coverage',
"Tell Jest to generate coverage. If not specified, it's enabled only on non-FOSS branch or tag pipelines under Vue 2, non-predictive runs.",
)
.parse(process.argv);
const options = program.opts();
if (!IS_CI) {
console.warn('This script is intended to run in CI only.');
if (options.vue3) showVue3Help();
process.exit(1);
}
if (options.vue3 && process.env.VUE_VERSION !== '3') {
console.warn(
`Expected environment variable VUE_VERSION=3 given option '--vue3', got VUE_VERSION="${process.env.VUE_VERSION}".`,
);
process.exit(1);
}
if (!options.vue3 && ![undefined, '2'].includes(process.env.VUE_VERSION)) {
console.warn(
`Expected unset environment variable VUE_VERSION, or VUE_VERSION=2, got VUE_VERSION="${process.env.VUE_VERSION}".`,
);
process.exit(1);
}
const changedFiles = [];
if (options.predictive) {
const { RSPEC_MATCHING_JS_FILES_PATH, RSPEC_CHANGED_FILES_PATH } = process.env;
for (const [name, path] of Object.entries({
RSPEC_CHANGED_FILES_PATH,
RSPEC_MATCHING_JS_FILES_PATH,
})) {
try {
const contents = readFileSync(path, { encoding: 'UTF-8' });
changedFiles.push(...contents.split(/\s+/).filter(Boolean));
} catch (error) {
console.warn(
`Failed to read from path ${path} given by environment variable ${name}`,
error,
);
}
}
if (!changedFiles) {
console.warn('No changed files detected; will not run Jest.');
process.exit(0);
}
}
const coverage =
options.coverage ||
(!process.env.CI_MERGE_REQUEST_IID &&
!/^as-if-foss\//.test(process.env.CI_COMMIT_BRANCH) &&
!options.vue3 &&
!options.predictive);
return {
vue3: options.vue3,
includeVue3Quarantined: options.includeVue3Quarantined,
predictive: options.predictive,
fixtures: options.fixtures,
coverage,
nodeIndex: process.env.CI_NODE_INDEX ?? '1',
nodeTotal: process.env.CI_NODE_TOTAL ?? '1',
changedFiles,
};
}
function loggedSpawnSync(command, args, options) {
const env = ['JEST_FIXTURE_JOBS_ONLY', 'VUE_VERSION']
.map((name) => `${name}=${options.env[name] ?? ''}`)
.join(' ');
const fullCommand = `${env} ${command} ${args.join(' ')}`;
console.warn(`Running command:\n${fullCommand}`);
const childProcess = spawnSync(command, args, options);
console.warn(`Command ${fullCommand} exited with status ${childProcess.status}`);
return childProcess;
}
function runJest({
vue3,
includeVue3Quarantined,
predictive,
fixtures,
coverage,
nodeIndex,
nodeTotal,
changedFiles,
}) {
const commonArguments = [
'--config',
'jest.config.js',
'--ci',
`--shard=${nodeIndex}/${nodeTotal}`,
'--logHeapUsage',
];
const sequencerArguments = [
'--testSequencer',
vue3 && !includeVue3Quarantined
? './scripts/frontend/skip_specs_broken_in_vue_compat_fixture_ci_sequencer.js'
: './scripts/frontend/fixture_ci_sequencer.js',
];
const predictiveArguments = predictive
? ['--passWithNoTests', '--findRelatedTests', ...changedFiles]
: [];
const coverageArguments = coverage ? ['--coverage'] : [];
const childProcess = loggedSpawnSync(
'node_modules/.bin/jest',
[...commonArguments, ...sequencerArguments, ...predictiveArguments, ...coverageArguments],
{
stdio: 'inherit',
env: {
...process.env,
...(fixtures ? { JEST_FIXTURE_JOBS_ONLY: '1' } : {}),
},
},
);
return childProcess;
}
function main() {
const config = parseArgumentsAndEnvironment();
const childProcess = runJest(config);
if (childProcess.status !== 0 && config.vue3) {
showVue3Help();
}
return childProcess.status;
}
try {
process.exitCode = main();
} catch (error) {
process.exitCode = 1;
console.error(error);
}