/* eslint-env node */ /* eslint import/no-nodejs-modules:0 */ import path from 'path'; import process from 'process'; import type {Config} from '@jest/types'; import babelConfig from './babel.config'; const { CI, JEST_TESTS, JEST_TEST_BALANCER, CI_NODE_TOTAL, CI_NODE_INDEX, GITHUB_PR_SHA, GITHUB_PR_REF, GITHUB_RUN_ID, GITHUB_RUN_ATTEMPT, } = process.env; /** * In CI we may need to shard our jest tests so that we can parellize the test runs * * `JEST_TESTS` is a list of all tests that will run, captured by `jest --listTests` * Then we split up the tests based on the total number of CI instances that will * be running the tests. */ let testMatch: string[] | undefined; const BALANCE_RESULTS_PATH = path.resolve( __dirname, 'tests', 'js', 'test-balancer', 'jest-balance.json' ); /** * Given a Map of and a number of total groups, split the * tests into n groups whose total test run times should be roughly equal * * The source results should be sorted with the slowest tests first. We insert * the test into the smallest group on each iteration. This isn't perfect, but * should be good enough. * * Returns a map of */ function balancer( allTests: string[], source: Record, numberGroups: number ) { const results = new Map(); const totalRunTimes = Array(numberGroups).fill(0); /** * Find the index of the smallest group (totalRunTimes) */ function findSmallestGroup() { let index = 0; let smallestRunTime = null; for (let i = 0; i < totalRunTimes.length; i++) { const runTime = totalRunTimes[i]; if (!smallestRunTime || runTime <= smallestRunTime) { smallestRunTime = totalRunTimes[i]; index = i; } if (runTime === 0) { break; } } return index; } /** * We may not have a duration for all tests (e.g. a test that was just added) * as the `source` needs to be generated */ for (const test of allTests) { const index = findSmallestGroup(); results.set(test, index); if (source[test] !== undefined) { totalRunTimes[index] = totalRunTimes[index] + source[test]; } } return results; } if ( JEST_TESTS && typeof CI_NODE_TOTAL !== 'undefined' && typeof CI_NODE_INDEX !== 'undefined' ) { let balance: null | Record = null; try { balance = require(BALANCE_RESULTS_PATH); } catch (err) { // Just ignore if balance results doesn't exist } // Taken from https://github.com/facebook/jest/issues/6270#issue-326653779 const envTestList = JSON.parse(JEST_TESTS).map(file => file.replace(__dirname, '') ) as string[]; const tests = envTestList.sort((a, b) => b.localeCompare(a)); const nodeTotal = Number(CI_NODE_TOTAL); const nodeIndex = Number(CI_NODE_INDEX); if (balance) { const results = balancer(envTestList, balance, nodeTotal); testMatch = [ // First, we only want the tests that we have test durations for and belong // to the current node's index ...Object.entries(Object.fromEntries(results)) .filter(([, index]) => index === nodeIndex) .map(([test]) => `${path.join(__dirname, test)}`), ]; } else { const length = tests.length; const size = Math.floor(length / nodeTotal); const remainder = length % nodeTotal; const offset = Math.min(nodeIndex, remainder) + nodeIndex * size; const chunk = size + (nodeIndex < remainder ? 1 : 0); testMatch = tests.slice(offset, offset + chunk); } } /** * For performance we don't want to try and compile everything in the * node_modules, but some packages which use ES6 syntax only NEED to be * transformed. */ const ESM_NODE_MODULES = ['copy-text-to-clipboard']; const config: Config.InitialOptions = { verbose: false, collectCoverageFrom: [ 'tests/js/spec/**/*.{js,jsx,tsx}', 'static/app/**/*.{js,jsx,ts,tsx}', ], coverageReporters: ['html', 'cobertura'], coverageDirectory: '.artifacts/coverage', moduleNameMapper: { '^sentry/(.*)': '/static/app/$1', '^sentry-test/(.*)': '/tests/js/sentry-test/$1', '^sentry-locale/(.*)': '/src/sentry/locale/$1', '\\.(css|less|png|jpg|mp4)$': '/tests/js/sentry-test/importStyleMock.js', '\\.(svg)$': '/tests/js/sentry-test/svgMock.js', 'integration-docs-platforms': '/fixtures/integration-docs/_platforms.json', // Disable echarts in test, since they're very slow and take time to // transform '^echarts/(.*)': '/tests/js/sentry-test/echartsMock.js', '^zrender/(.*)': '/tests/js/sentry-test/echartsMock.js', }, setupFiles: [ '/static/app/utils/silence-react-unsafe-warnings.ts', 'jest-canvas-mock', ], setupFilesAfterEnv: [ '/tests/js/setup.ts', '/tests/js/setupFramework.ts', '@testing-library/jest-dom/extend-expect', ], testMatch: testMatch || [ '/static/**/?(*.)+(spec|test).[jt]s?(x)', '/tests/js/**/*(*.)@(spec|test).(js|ts)?(x)', ], testPathIgnorePatterns: ['/tests/sentry/lang/javascript/'], unmockedModulePathPatterns: [ '/node_modules/react', '/node_modules/reflux', ], transform: { '^.+\\.jsx?$': ['babel-jest', babelConfig as any], '^.+\\.tsx?$': ['babel-jest', babelConfig as any], '^.+\\.pegjs?$': '/tests/js/jest-pegjs-transform.js', }, transformIgnorePatterns: [`/node_modules/(?!${ESM_NODE_MODULES.join('|')})`], moduleFileExtensions: ['js', 'ts', 'jsx', 'tsx'], globals: {}, reporters: [ 'default', [ 'jest-junit', { outputDirectory: '.artifacts', outputName: 'jest.junit.xml', }, ], [ '/tests/js/test-balancer', { enabled: !!JEST_TEST_BALANCER, resultsPath: BALANCE_RESULTS_PATH, }, ], ], testEnvironment: '/tests/js/instrumentedEnv', testEnvironmentOptions: { sentryConfig: { init: { // jest project under Sentry organization (dev productivity team) dsn: 'https://3fe1dce93e3a4267979ebad67f3de327@sentry.io/4857230', environment: CI ? 'ci' : 'local', tracesSampleRate: 1, profilesSampleRate: 1, }, transactionOptions: { tags: { branch: GITHUB_PR_REF, commit: GITHUB_PR_SHA, github_run_attempt: GITHUB_RUN_ATTEMPT, github_actions_run: `https://github.com/getsentry/sentry/actions/runs/${GITHUB_RUN_ID}`, }, }, }, output: path.resolve(__dirname, '.artifacts', 'visual-snapshots', 'jest'), }, }; export default config;