diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index ff46b572373d458430f48222b62e54920da5e73c..67cfb6f183b8f2fa362bb081535be30051c565d3 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -263,7 +263,7 @@ jest-build-cache: paths: - tmp/cache/jest/ script: - - run_timed_command "yarn jest:ci:build-cache" + - run_timed_command "scripts/frontend/warm_jest_cache.mjs" variables: # Propagate exit code correctly. See https://gitlab.com/groups/gitlab-org/-/epics/6074. FF_USE_NEW_BASH_EVAL_STRATEGY: 'true' @@ -312,7 +312,7 @@ jest: parallel: 11 script: - if [[ -z "${CI_MERGE_REQUEST_IID}" ]] && ! [[ "$CI_COMMIT_BRANCH" =~ ^as-if-foss/ ]]; then export JEST_COVERAGE="--coverage"; fi - - run_timed_command "yarn jest:ci:without-fixtures $JEST_COVERAGE" + - run_timed_command "scripts/frontend/jest_ci.js $JEST_COVERAGE" jest-with-fixtures: extends: @@ -327,7 +327,7 @@ jest-with-fixtures: parallel: 2 script: - if [[ -z "${CI_MERGE_REQUEST_IID}" ]] && ! [[ "$CI_COMMIT_BRANCH" =~ ^as-if-foss/ ]]; then export JEST_COVERAGE="--coverage"; fi - - run_timed_command "yarn jest:ci:with-fixtures $JEST_COVERAGE" + - run_timed_command "scripts/frontend/jest_ci.js --fixtures $JEST_COVERAGE" jest vue3: extends: @@ -363,7 +363,7 @@ jest vue3 mr: - junit_jest.xml parallel: 6 script: - - run_timed_command "yarn jest:ci:vue3-mr:without-fixtures" + - run_timed_command "scripts/frontend/jest_ci.js --vue3" allow_failure: false jest-with-fixtures vue3 mr: @@ -383,7 +383,7 @@ jest-with-fixtures vue3 mr: - junit_jest.xml parallel: 1 script: - - run_timed_command "yarn jest:ci:vue3-mr:with-fixtures" + - run_timed_command "scripts/frontend/jest_ci.js --vue3 --fixtures" jest predictive: extends: @@ -393,7 +393,7 @@ jest predictive: - !reference [jest, needs] - "detect-tests" script: - - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "yarn jest:ci:predictive-without-fixtures"; fi + - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "scripts/frontend/jest_ci.js --predictive"; fi parallel: 4 jest-with-fixtures predictive: @@ -404,7 +404,7 @@ jest-with-fixtures predictive: - !reference [jest-with-fixtures, needs] - "detect-tests" script: - - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "yarn jest:ci:predictive-with-fixtures"; fi + - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "scripts/frontend/jest_ci.js --predictive --fixtures"; fi jest vue3 predictive: extends: @@ -414,7 +414,7 @@ jest vue3 predictive: - !reference [jest vue3 mr, needs] - "detect-tests" script: - - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "yarn jest:ci:vue3-mr:predictive-without-fixtures"; fi + - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "scripts/frontend/jest_ci.js --vue3 --predictive"; fi jest-with-fixtures vue3 predictive: extends: @@ -424,7 +424,7 @@ jest-with-fixtures vue3 predictive: - !reference [jest-with-fixtures vue3 mr, needs] - "detect-tests" script: - - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "yarn jest:ci:vue3-mr:predictive-with-fixtures"; fi + - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "scripts/frontend/jest_ci.js --vue3 --predictive --fixtures"; fi allow_failure: false jest vue3 check quarantined predictive: diff --git a/package.json b/package.json index b853df16dfcafb8f89fd7710dc43b293de1d5268..94dca631d05a32f7dad59d12e0e179938e56075a 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,6 @@ "tailwindcss:build": "node scripts/frontend/tailwindcss.mjs", "jest": "jest --config jest.config.js", "jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand", - "jest:ci:build-cache": "./scripts/frontend/warm_jest_cache.mjs", - "jest:ci": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/parallel_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage", - "jest:ci:without-fixtures": "jest --config jest.config.js --ci --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage", - "jest:ci:with-fixtures": "JEST_FIXTURE_JOBS_ONLY=1 jest --config jest.config.js --ci --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage", - "jest:ci:predictive-without-fixtures": "jest --config jest.config.js --ci --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage", - "jest:ci:predictive-with-fixtures": "JEST_FIXTURE_JOBS_ONLY=1 jest --config jest.config.js --ci --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage", - "jest:ci:vue3-mr:without-fixtures": "jest --config jest.config.js --ci --testSequencer ./scripts/frontend/skip_specs_broken_in_vue_compat_fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage", - "jest:ci:vue3-mr:with-fixtures": "JEST_FIXTURE_JOBS_ONLY=1 jest --config jest.config.js --ci --testSequencer ./scripts/frontend/skip_specs_broken_in_vue_compat_fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage", - "jest:ci:vue3-mr:predictive-without-fixtures": "jest --config jest.config.js --ci --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/skip_specs_broken_in_vue_compat_fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage", - "jest:ci:vue3-mr:predictive-with-fixtures": "JEST_FIXTURE_JOBS_ONLY=1 jest --config jest.config.js --ci --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/skip_specs_broken_in_vue_compat_fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage", "jest:contract": "PACT_DO_NOT_TRACK=true jest --config jest.config.contract.js --runInBand", "jest:integration": "jest --config jest.config.integration.js", "jest:scripts": "jest --config jest.config.scripts.js", diff --git a/scripts/frontend/jest_ci.js b/scripts/frontend/jest_ci.js new file mode 100755 index 0000000000000000000000000000000000000000..fe853063682eff8531a64d61ce0e2fdeaf7d8658 --- /dev/null +++ b/scripts/frontend/jest_ci.js @@ -0,0 +1,151 @@ +#!/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 + .usage('[options]') + .description(`Runs Jest under CI.`) + .option('--vue3', 'Run tests under Vue 3 (via @vue/compat). The default is to run under Vue 2.') + .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. The default is not to.') + .parse(process.argv); + + if (!IS_CI) { + console.warn('This script is intended to run in CI only.'); + if (program.vue3) showVue3Help(); + process.exit(1); + } + + const changedFiles = []; + if (program.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); + } + } + + return { + vue3: program.vue3, + predictive: program.predictive, + fixtures: program.fixtures, + coverage: program.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, predictive, fixtures, coverage, nodeIndex, nodeTotal, changedFiles }) { + const commonArguments = [ + '--config', + 'jest.config.js', + '--ci', + `--shard=${nodeIndex}/${nodeTotal}`, + '--logHeapUsage', + ]; + + const sequencerArguments = [ + '--testSequencer', + vue3 + ? './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' } : {}), + ...(vue3 ? { VUE_VERSION: '3' } : {}), + }, + }, + ); + + 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); +}