diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 6b0d0958471b..49518528cba0 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -1,18 +1,17 @@ -# WARNING! It is currently being investigated how to make this faster -# DO NOT blindly copy this workflow, not noticing the slow down, -# because suddenly our tests will take hours to pass CI. -# Hopefully this comment here will help prevent that. -# https://github.com/MetaMask/metamask-extension/issues/25680 - name: Run unit tests on: + push: + branches: [develop, master] pull_request: types: [opened,reopened,synchronize] jobs: - test-unit-jest: + test-unit: runs-on: ubuntu-latest + strategy: + matrix: + shard: [1, 2, 3, 4, 5, 6] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -20,13 +19,34 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment - - name: test:coverage:jest:dev - run: yarn test:coverage:jest:dev + - name: test:unit:coverage + run: yarn test:unit:coverage --shard=${{ matrix.shard }}/${{ strategy.job-total }} + + - name: Rename coverage to shard coverage + run: mv coverage/coverage-final.json coverage/coverage-${{matrix.shard}}.json + + - uses: actions/upload-artifact@v4 + with: + name: coverage-${{matrix.shard}} + path: coverage/coverage-${{matrix.shard}}.json + + report-coverage: + runs-on: ubuntu-latest + needs: + - test-unit + steps: + - name: Checkout repository + uses: actions/checkout@v4 - - name: test:coverage:jest - run: yarn test:coverage:jest + - name: Download coverage from shards + uses: actions/download-artifact@v4 + with: + path: coverage + pattern: coverage-* + merge-multiple: true - - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/development/build/transforms/remove-fenced-code.test.js b/development/build/transforms/remove-fenced-code.test.js index 345fc55a0926..de6960ba1ec9 100644 --- a/development/build/transforms/remove-fenced-code.test.js +++ b/development/build/transforms/remove-fenced-code.test.js @@ -1,3 +1,6 @@ +/** + * @jest-environment node + */ const buildUtils = require('@metamask/build-utils'); const { createRemoveFencedCodeTransform } = require('./remove-fenced-code'); const transformUtils = require('./utils'); @@ -36,6 +39,10 @@ describe('build/transforms/remove-fenced-code', () => { lintTransformedFileMock.mockImplementation(() => Promise.resolve()); }); + afterEach(() => { + jest.resetAllMocks(); + }); + it('returns a PassThrough stream for files with ignored extensions', async () => { const fileContent = '"Valid JSON content"\n'; const stream = createRemoveFencedCodeTransform( diff --git a/development/build/transforms/utils.test.js b/development/build/transforms/utils.test.js index d5cd57bfd461..952d0f46f181 100644 --- a/development/build/transforms/utils.test.js +++ b/development/build/transforms/utils.test.js @@ -1,3 +1,6 @@ +/** + * @jest-environment node + */ const { getESLintInstance } = require('./utils'); let mockESLint; diff --git a/development/jest.config.js b/development/jest.config.js deleted file mode 100644 index d95157a49635..000000000000 --- a/development/jest.config.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - displayName: '/development', - collectCoverageFrom: ['/build/transforms/**/*.js'], - coverageDirectory: '../coverage', - coverageReporters: ['json'], - resetMocks: true, - restoreMocks: true, - testEnvironment: 'node', - testMatch: ['/build/transforms/**/*.test.js'], - testTimeout: 2500, -}; diff --git a/jest.config.js b/jest.config.js index f52466a9d183..e9aabdeff85f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,10 +3,11 @@ module.exports = { '/app/scripts/**/*.(js|ts|tsx)', '/shared/**/*.(js|ts|tsx)', '/ui/**/*.(js|ts|tsx)', + '/development/build/transforms/**/*.js', ], coverageDirectory: './coverage', coveragePathIgnorePatterns: ['.stories.*', '.snap'], - coverageReporters: ['html', 'json'], + coverageReporters: process.env.CI ? ['json'] : ['html', 'json'], reporters: [ 'default', [ @@ -26,7 +27,7 @@ module.exports = { '/app/scripts/**/*.test.(js|ts|tsx)', '/shared/**/*.test.(js|ts|tsx)', '/ui/**/*.test.(js|ts|tsx)', - '/development/fitness-functions/**/*.test.(js|ts|tsx)', + '/development/**/*.test.(js|ts|tsx)', '/test/e2e/helpers.test.js', ], testTimeout: 5500, diff --git a/package.json b/package.json index 1a9086d623e4..a5853dce3d3f 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,12 @@ "dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && yarn dapp'", "forwarder": "node ./development/static-server.js ./node_modules/@metamask/forwarder/dist/ --port 9010", "dapp-forwarder": "concurrently -k -n forwarder,dapp -p '[{time}][{name}]' 'yarn forwarder' 'yarn dapp'", + "test:unit": "jest", + "test:unit:watch": "jest --watch", + "test:unit:global": "mocha test/unit-global/*.test.js", + "test:unit:coverage": "jest --coverage", "test:integration": "jest --config jest.integration.config.js", "test:integration:coverage": "jest --config jest.integration.config.js --coverage", - "test:unit": "node ./test/run-unit-tests.js --jestGlobal --jestDev", - "test:unit:jest": "node ./test/run-unit-tests.js --jestGlobal --jestDev", - "test:unit:jest:watch": "node --inspect ./node_modules/.bin/jest --watch", - "test:unit:global": "mocha test/unit-global/*.test.js", "test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi", "test:e2e:chrome:flask": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --build-type flask", @@ -61,10 +61,6 @@ "test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js", "test:e2e:firefox:flask": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js --build-type flask", "test:e2e:single": "node test/e2e/run-e2e-test.js", - "test:coverage:jest": "node ./test/run-unit-tests.js --jestGlobal --coverage", - "test:coverage:jest:dev": "node ./test/run-unit-tests.js --jestDev --coverage", - "test:coverage": "node ./test/run-unit-tests.js --jestGlobal --jestDev --coverage", - "test:coverage:html": "yarn test:coverage --html", "ganache:start": "./development/run-ganache.sh", "sentry:publish": "node ./development/sentry-publish.js", "lint": "yarn lint:prettier && yarn lint:eslint && yarn lint:tsc && yarn lint:styles", diff --git a/test/run-unit-tests.js b/test/run-unit-tests.js deleted file mode 100644 index 645bcfc02e1b..000000000000 --- a/test/run-unit-tests.js +++ /dev/null @@ -1,156 +0,0 @@ -const { hideBin } = require('yargs/helpers'); -const yargs = require('yargs/yargs'); -const { runCommand, runInShell } = require('../development/lib/run-command'); - -const { CIRCLE_NODE_INDEX, CIRCLE_NODE_TOTAL } = process.env; - -const GLOBAL_JEST_CONFIG = './jest.config.js'; -const DEVELOPMENT_JEST_CONFIG = './development/jest.config.js'; - -start().catch((error) => { - console.error(error); - process.exit(1); -}); - -/** - * @typedef {object} JestParams - * @property {'global' | 'dev'} target - Which configuration to use for Jest. - * @property {boolean} [coverage] - Whether to collect coverage during testing. - * @property {number} [currentShard] - Current process number when using test - * splitting across many processes. - * @property {number} [totalShards] - Total number of processes tests will be - * split across. - * @property {number} [maxWorkers] - Total number of workers to use when - * running tests. - */ - -/** - * Execute jest test runner with given params - * - * @param {JestParams} params - Configuration for jest test runner - */ -async function runJest( - { target, coverage, currentShard, totalShards, maxWorkers } = { - target: 'global', - coverage: false, - currentShard: 1, - totalShards: 1, - maxWorkers: 2, - }, -) { - const options = [ - 'jest', - `--config=${ - target === 'global' ? GLOBAL_JEST_CONFIG : DEVELOPMENT_JEST_CONFIG - }`, - ]; - options.push(`--maxWorkers=${maxWorkers}`); - if (coverage) { - options.push('--coverage'); - } - // We use jest's new 'shard' feature to run tests in parallel across many - // different processes if totalShards > 1 - if (totalShards > 1) { - options.push(`--shard=${currentShard}/${totalShards}`); - } - await runInShell('yarn', options); - if (coverage) { - // Once done we rename the coverage file so that it is unique among test - // runners and job number - await runCommand('mv', [ - './coverage/coverage-final.json', - `./coverage/coverage-final-${target}-${currentShard}.json`, - ]); - } -} - -async function start() { - const { - argv: { jestGlobal, jestDev, coverage, fakeParallelism, maxWorkers }, - } = yargs(hideBin(process.argv)).usage( - '$0 [options]', - 'Run unit tests on the application code.', - (yargsInstance) => - yargsInstance - .option('jestDev', { - alias: ['d'], - default: false, - description: 'Run Jest tests with development folder config', - type: 'boolean', - }) - .option('jestGlobal', { - alias: ['g'], - default: false, - demandOption: false, - description: 'Run Jest global (primary config) tests', - type: 'boolean', - }) - .option('coverage', { - alias: ['c'], - default: true, - demandOption: false, - description: 'Collect coverage', - type: 'boolean', - }) - .option('fakeParallelism', { - alias: ['f'], - default: 0, - demandOption: false, - description: - 'Pretend to be CircleCI and fake parallelism (use at your own risk)', - type: 'number', - }) - .option('maxWorkers', { - alias: ['mw'], - default: 2, - demandOption: false, - description: - 'The safer way to increase performance locally, sets the number of processes to use internally. Recommended 2', - type: 'number', - }) - .strict(), - ); - - const circleNodeIndex = parseInt(CIRCLE_NODE_INDEX ?? '0', 10); - const circleNodeTotal = parseInt(CIRCLE_NODE_TOTAL ?? '1', 10); - - const maxProcesses = fakeParallelism > 0 ? fakeParallelism : circleNodeTotal; - const currentProcess = circleNodeIndex; - - if (fakeParallelism) { - console.log( - `Using fake parallelism of ${fakeParallelism}. Your machine may become as useful as a brick during this operation.`, - ); - if (jestGlobal && jestDev) { - throw new Error( - 'Do not try to run both jest test configs with fakeParallelism, bad things could happen.', - ); - } else { - const processes = []; - for (let x = 0; x < fakeParallelism; x++) { - processes.push( - runJest({ - target: jestGlobal ? 'global' : 'dev', - totalShards: fakeParallelism, - currentShard: x + 1, - maxWorkers: 1, // ignore maxWorker option on purpose - }), - ); - } - await Promise.all(processes); - } - } else { - const options = { - coverage, - currentShard: currentProcess + 1, - totalShards: maxProcesses, - maxWorkers, - }; - if (jestDev) { - await runJest({ target: 'dev', ...options }); - } - if (jestGlobal) { - await runJest({ target: 'global', ...options }); - } - } -}