mirror of
https://github.com/gitlabhq/gitlabhq.git
synced 2025-08-01 16:46:16 +00:00
314 lines
9.0 KiB
JavaScript
Executable File
314 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 { RSPEC_CHANGED_FILES_PATH, RSPEC_MATCHING_JS_FILES_PATH } = process.env;
|
|
|
|
const files = await Promise.all(
|
|
[RSPEC_CHANGED_FILES_PATH, RSPEC_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);
|
|
});
|