Files
gitlabhq/scripts/frontend/check_jest_vue3_quarantine.js

315 lines
9.0 KiB
JavaScript
Executable File

#!/usr/bin/env node
const { spawnSync } = require('node:child_process');
const { readFile, open, stat, mkdir } = require('node:fs/promises');
const { join, relative, dirname } = require('node:path');
const defaultChalk = require('chalk');
const { program } = require('commander');
const IS_EE = require('../../config/helpers/is_ee_env');
const { getLocalQuarantinedFiles } = require('./jest_vue3_quarantine_utils');
const ROOT = join(__dirname, '..', '..');
const IS_CI = Boolean(process.env.CI);
const FIXTURES_HELP_URL =
// eslint-disable-next-line no-restricted-syntax
'https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html#download-fixtures';
const DIR = join(ROOT, 'tmp/tests/frontend');
const JEST_JSON_OUTPUT = join(DIR, 'jest_results.json');
const JEST_STDERR = join(DIR, 'jest_stderr');
// Force basic color output in CI
const chalk = new defaultChalk.constructor({ level: IS_CI ? 1 : undefined });
let quarantinedFiles;
let filesThatChanged;
function parseArguments() {
program
.description(
`
Checks whether Jest specs quarantined under Vue 3 should be unquarantined.
Usage examples
--------------
In CI:
# Check quarantined files which were affected by changes in the merge request.
$ scripts/frontend/check_jest_vue3_quarantine.js
# Check all quarantined files, still subject to sharding/fixture separation.
# Useful for tier 3 pipelines, or when dependencies change.
$ scripts/frontend/check_jest_vue3_quarantine.js --all
Locally:
# Run all quarantined files, including those which need fixtures.
# See ${FIXTURES_HELP_URL}
$ scripts/frontend/check_jest_vue3_quarantine.js --all
# Run a particular spec
$ scripts/frontend/check_jest_vue3_quarantine.js spec/frontend/foo_spec.js
# Run specs in this branch that were modified since master
$ scripts/frontend/check_jest_vue3_quarantine.js $(git diff master... --name-only)
# Write to stdio normally instead of to temporary files
$ scripts/frontend/check_jest_vue3_quarantine.js --stdio spec/frontend/foo_spec.js
`.trim(),
)
.option(
'--all',
'Run all quarantined specs. Good for local testing, or in CI when configuration files have changed.',
)
.option(
'--stdio',
`Let Jest write to stderr as normal. By default, it writes to ${JEST_STDERR}. Should not be used in CI, as it can exceed maximum job log size.`,
)
.argument('[spec...]', 'List of spec files to run (incompatible with --all)')
.parse(process.argv);
const options = program.opts();
let invalidArgumentsMessage;
if (!IS_CI) {
if (!options.all && program.args.length === 0) {
invalidArgumentsMessage =
'No spec files to check!\n\nWhen run locally, either add the --all option, or a list of spec files to check.';
}
if (options.all && program.args.length > 0) {
invalidArgumentsMessage = `Do not pass arguments in addition to the --all option.`;
}
}
if (invalidArgumentsMessage) {
console.warn(`${chalk.red(invalidArgumentsMessage)}\n`);
program.help();
}
}
async function parseResults() {
let results;
try {
results = JSON.parse(await readFile(JEST_JSON_OUTPUT, 'UTF-8'));
} catch (e) {
console.warn(e);
// No JUnit report exists, or there was a parsing error. Either way, we
// should not block the MR.
return [];
}
return results.testResults.reduce((acc, { name, status }) => {
if (status === 'passed') {
acc.push(relative(ROOT, name));
}
return acc;
}, []);
}
function reportSpecsShouldBeUnquarantined(files) {
const docsLink =
// eslint-disable-next-line no-restricted-syntax
'https://docs.gitlab.com/ee/development/testing_guide/testing_vue3.html#quarantine-list';
console.warn(' ');
console.warn(
`The following ${files.length} spec files either now pass under Vue 3, or no longer exist, and so must be removed from quarantine:`,
);
console.warn(' ');
console.warn(files.join('\n'));
console.warn(' ');
console.warn(
chalk.red(
`To fix this job, remove the files listed above from the file ${chalk.underline('scripts/frontend/quarantined_vue3_specs.txt')}.`,
),
);
console.warn(`For more information, please see ${docsLink}.`);
}
async function changedFiles() {
if (!IS_CI) {
// We're not in CI, so `detect-tests` artifacts won't be available.
return [];
}
const { GLCI_PREDICTIVE_CHANGED_FILES_PATH, GLCI_PREDICTIVE_MATCHING_JS_FILES_PATH } =
process.env;
const files = await Promise.all(
[GLCI_PREDICTIVE_CHANGED_FILES_PATH, GLCI_PREDICTIVE_MATCHING_JS_FILES_PATH].map((path) =>
readFile(path, 'UTF-8').then((content) => content.split(/\s+/).filter(Boolean)),
),
);
return files.flat();
}
function filterSet(set, predicate) {
const result = new Set();
for (const element of set) {
if (predicate(element)) result.add(element);
}
return result;
}
function intersection(a, b) {
return filterSet(a, (element) => b.has(element));
}
async function getRemovedQuarantinedSpecs() {
const removedQuarantinedSpecs = [];
const filesToCheckIfTheyExist = IS_CI
? // In CI, only check quarantined files the author has touched.
// If we're in a FOSS pipeline, ignore EE specs which do not exist.
filterSet(intersection(filesThatChanged, quarantinedFiles), (path) => {
if (IS_EE) return true;
if (path.startsWith('ee/')) {
console.warn(`Ignoring non-existent EE spec ${path} as we are in FOSS mode.`);
return false;
}
return true;
})
: // Locally, check all quarantined files
quarantinedFiles;
for (const file of filesToCheckIfTheyExist) {
try {
// eslint-disable-next-line no-await-in-loop
await stat(file);
} catch (e) {
if (e.code === 'ENOENT') removedQuarantinedSpecs.push(file);
}
}
return removedQuarantinedSpecs;
}
function getTestArguments() {
if (IS_CI) {
const nodeIndex = process.env.CI_NODE_INDEX ?? '1';
const nodeTotal = process.env.CI_NODE_TOTAL ?? '1';
const ciArguments = (touchedFiles) => [
'--findRelatedTests',
...touchedFiles,
'--passWithNoTests',
`--shard=${nodeIndex}/${nodeTotal}`,
'--testSequencer',
'./scripts/frontend/check_jest_vue3_quarantine_sequencer.js',
];
if (program.opts().all) {
console.warn(
'Running in CI with --all. Checking all quarantined specs, subject to FixtureCISequencer sharding behavior.',
);
return ciArguments(quarantinedFiles);
}
console.warn(
'Running in CI. Only specs affected by changes in the merge request will be checked.',
);
return ciArguments(filesThatChanged);
}
if (program.opts().all) {
console.warn('Running locally with --all. Checking all quarantined specs.');
return ['--runTestsByPath', ...quarantinedFiles];
}
if (program.args.length > 0) {
const specs = program.args.filter((spec) => {
const isQuarantined = quarantinedFiles.has(relative(ROOT, spec));
if (!isQuarantined) console.warn(`Omitting file as it is not in quarantine list: ${spec}`);
return isQuarantined;
});
if (specs.length === 0) {
console.warn(`No quarantined specs to run!`);
process.exit(1);
}
console.warn('Running locally. Checking given specs.');
return ['--runTestsByPath', ...specs];
}
// ESLint's consistent-return rule requires something like this.
return ['--this-should-never-happen-and-jest-should-fail'];
}
async function getStdio() {
if (program.opts().stdio) {
return 'inherit';
}
await mkdir(dirname(JEST_STDERR), { recursive: true });
const jestStderr = (await open(JEST_STDERR, 'w')).createWriteStream();
return ['inherit', 'inherit', jestStderr];
}
async function main() {
parseArguments();
filesThatChanged = await changedFiles();
quarantinedFiles = new Set(await getLocalQuarantinedFiles());
// Note: we don't care what Jest's exit code is.
//
// If it's zero, then either:
// - all specs passed, or
// - no specs were run.
//
// Both situations are handled later.
//
// If it's non-zero, then either:
// - one or more specs failed (which is expected!), or
// - there was some unknown error. We shouldn't block MRs in this case.
spawnSync(
'node_modules/.bin/jest',
[
'--config',
'jest.config.js',
'--ci',
'--logHeapUsage',
'--json',
`--outputFile=${JEST_JSON_OUTPUT}`,
...getTestArguments(),
],
{
stdio: await getStdio(),
env: {
...process.env,
VUE_VERSION: '3',
},
},
);
const passed = await parseResults();
const removedQuarantinedSpecs = await getRemovedQuarantinedSpecs();
const filesToReport = [...passed, ...removedQuarantinedSpecs];
if (filesToReport.length === 0) {
// No tests ran, or there was some unexpected error. Either way, exit
// successfully.
console.warn('No spec files need to be removed from quarantine.');
return;
}
process.exitCode = 1;
reportSpecsShouldBeUnquarantined(filesToReport);
}
main().catch((e) => {
// Don't block on unexpected errors.
console.warn(e);
});