gitlab-ce/scripts/frontend/check_jest_vue3_quarantine.js

172 lines
4.8 KiB
JavaScript

const { spawnSync } = require('node:child_process');
const { readFile, open, stat } = require('node:fs/promises');
const parser = require('fast-xml-parser');
const defaultChalk = require('chalk');
const { getLocalQuarantinedFiles } = require('./jest_vue3_quarantine_utils');
// Always use basic color output
const chalk = new defaultChalk.constructor({ level: 1 });
let quarantinedFiles;
let filesThatChanged;
async function parseJUnitReport() {
let junit;
try {
const xml = await readFile('./junit_jest.xml', 'UTF-8');
junit = parser.parse(xml, {
arrayMode: true,
attributeNamePrefix: '',
parseNodeValue: false,
ignoreAttributes: false,
});
} catch (e) {
console.warn(e);
// No JUnit report exists, or there was a parsing error. Either way, we
// should not block the MR.
return [];
}
const failuresByFile = new Map();
for (const testsuites of junit.testsuites) {
for (const testsuite of testsuites.testsuite || []) {
for (const testcase of testsuite.testcase) {
const { file } = testcase;
if (!failuresByFile.has(file)) {
failuresByFile.set(file, 0);
}
const failuresSoFar = failuresByFile.get(file);
const testcaseFailed = testcase.failure ? 1 : 0;
failuresByFile.set(file, failuresSoFar + testcaseFailed);
}
}
}
const passed = [];
for (const [file, failures] of failuresByFile.entries()) {
if (failures === 0 && quarantinedFiles.has(file)) passed.push(file);
}
return passed;
}
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() {
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 intersection(a, b) {
const result = new Set();
for (const element of a) {
if (b.has(element)) result.add(element);
}
return result;
}
async function getRemovedQuarantinedSpecs() {
const removedQuarantinedSpecs = [];
for (const file of intersection(filesThatChanged, quarantinedFiles)) {
try {
// eslint-disable-next-line no-await-in-loop
await stat(file);
} catch (e) {
if (e.code === 'ENOENT') removedQuarantinedSpecs.push(file);
}
}
return removedQuarantinedSpecs;
}
async function main() {
filesThatChanged = await changedFiles();
quarantinedFiles = new Set(await getLocalQuarantinedFiles());
const jestStdout = (await open('jest_stdout', 'w')).createWriteStream();
const jestStderr = (await open('jest_stderr', 'w')).createWriteStream();
console.log('Running quarantined specs...');
// 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',
'--findRelatedTests',
...filesThatChanged,
'--passWithNoTests',
// Explicitly have one shard, so that the `shard` method of the sequencer is called.
'--shard=1/1',
'--testSequencer',
'./scripts/frontend/check_jest_vue3_quarantine_sequencer.js',
'--logHeapUsage',
],
{
stdio: ['inherit', jestStdout, jestStderr],
env: {
...process.env,
VUE_VERSION: '3',
},
},
);
const passed = await parseJUnitReport();
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.
return;
}
process.exitCode = 1;
reportSpecsShouldBeUnquarantined(filesToReport);
}
main().catch((e) => {
// Don't block on unexpected errors.
console.warn(e);
});