From 02f61ee53ed2730c4d27e1d46603000faa2484a0 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Wed, 18 Mar 2026 10:15:39 +0000 Subject: [PATCH 1/5] CCM-14615: add temp workflow for unit tests only --- .github/workflows/temp-unit-tests-only.yaml | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/temp-unit-tests-only.yaml diff --git a/.github/workflows/temp-unit-tests-only.yaml b/.github/workflows/temp-unit-tests-only.yaml new file mode 100644 index 00000000..5b53fad6 --- /dev/null +++ b/.github/workflows/temp-unit-tests-only.yaml @@ -0,0 +1,62 @@ +name: "Temp unit tests only" + +on: + workflow_dispatch: + inputs: + nodejs_version: + description: "Node.js version, set by the CI/CD pipeline workflow" + required: true + type: string + python_version: + description: "Python version, set by the CI/CD pipeline workflow" + required: true + type: string + +env: + AWS_REGION: eu-west-2 + TERM: xterm-256color + +jobs: + test-unit: + name: "Unit tests" + runs-on: ubuntu-latest + timeout-minutes: 7 + permissions: + contents: read + packages: read + steps: + - name: "Checkout code" + uses: actions/checkout@v5 + - uses: ./.github/actions/node-install + with: + node-version: ${{ inputs.nodejs_version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Setup Python" + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python_version }} + cache: 'pip' + cache-dependency-path: '**/requirements*.txt' + - name: "Run unit test suite" + run: | + make test-unit + - name: "Save the result of fast test suite" + uses: actions/upload-artifact@v4 + with: + name: unit-tests + path: "**/.reports/unit" + include-hidden-files: true + if: always() + - name: "Save the result of code coverage" + uses: actions/upload-artifact@v4 + with: + name: code-coverage-report + path: ".reports/lcov.info" + - name: "Save Python coverage reports" + uses: actions/upload-artifact@v4 + with: + name: python-coverage-reports + path: | + src/**/coverage.xml + utils/**/coverage.xml + lambdas/**/coverage.xml From 6d198635c73e26a3d01f206b66fed10aeb351088 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Fri, 20 Mar 2026 10:30:34 +0000 Subject: [PATCH 2/5] CCM-14615: add temp workflow for unit tests only --- .github/workflows/temp-unit-tests-only.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/temp-unit-tests-only.yaml b/.github/workflows/temp-unit-tests-only.yaml index 5b53fad6..fc790c36 100644 --- a/.github/workflows/temp-unit-tests-only.yaml +++ b/.github/workflows/temp-unit-tests-only.yaml @@ -1,7 +1,7 @@ name: "Temp unit tests only" on: - workflow_dispatch: + workflow_dispatch: inputs: nodejs_version: description: "Node.js version, set by the CI/CD pipeline workflow" @@ -11,6 +11,9 @@ on: description: "Python version, set by the CI/CD pipeline workflow" required: true type: string + push: + branches: + - feature/CCM-14615_unit-test-quickening env: AWS_REGION: eu-west-2 From 3ab0de75ea90ebc0d2002e239d0b0635eaec98ba Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Fri, 20 Mar 2026 10:31:41 +0000 Subject: [PATCH 3/5] CCM-14615: run parrallel using jest workspaces --- jest.config.cjs | 151 ++++++++++++++++++ .../__tests__/app/notify-api-client.test.ts | 8 +- .../src/__tests__/domain/mapper.test.ts | 8 +- package.json | 1 + scripts/tests/unit.sh | 92 ++++++++--- .../builder/__tests__/build-schema.test.ts | 28 ++-- 6 files changed, 250 insertions(+), 38 deletions(-) create mode 100644 jest.config.cjs diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 00000000..07f9b723 --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,151 @@ +/** + * Root Jest config — runs all TypeScript/JavaScript workspace test suites in + * parallel via Jest's native `projects` support. + * + * Written as CJS (.cjs) so Jest can load it without needing a root tsconfig.json. + * The base config is inlined from jest.config.base.ts to keep this file + * self-contained and avoid any TypeScript compilation at load time, which would + * require a root tsconfig.json and risk interfering with workspace ts-jest runs. + * + * When jest.config.base.ts changes, this file must be kept in sync manually. + * + * Each project's rootDir is set to its workspace directory so that relative + * paths (coverageDirectory, HTML reporter outputPath, etc.) resolve relative + * to the workspace, not the repo root. + * + * Note: src/cloudevents has a hand-rolled jest.config.cjs; it is included via + * its directory path so Jest discovers that file directly. + * + * Note: src/digital-letters-events and tests/playwright have no Jest tests + * and are intentionally excluded. + */ + +const base = { + preset: 'ts-jest', + clearMocks: true, + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/__tests__/**', + '!src/**/*.test.{ts,tsx}', + '!src/**/*.spec.{ts,tsx}', + ], + coverageDirectory: './.reports/unit/coverage', + coverageProvider: 'babel', + coverageThreshold: { + global: { branches: 100, functions: 100, lines: 100, statements: -10 }, + }, + coveragePathIgnorePatterns: ['/__tests__/'], + transform: { '^.+\\.ts$': 'ts-jest' }, + testPathIgnorePatterns: ['.build'], + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + reporters: [ + 'default', + [ + 'jest-html-reporter', + { + pageTitle: 'Test Report', + outputPath: './.reports/unit/test-report.html', + includeFailureMsg: true, + }, + ], + ], + testEnvironment: 'node', + moduleDirectories: ['node_modules', 'src'], +}; + +// Workspaces that use the base config with no overrides +const standardWorkspaces = [ + 'lambdas/file-scanner-lambda', + 'lambdas/key-generation', + 'lambdas/refresh-apim-access-token', + 'lambdas/pdm-mock-lambda', + 'lambdas/pdm-poll-lambda', + 'lambdas/ttl-create-lambda', + 'lambdas/ttl-handle-expiry-lambda', + 'lambdas/ttl-poll-lambda', + 'lambdas/pdm-uploader-lambda', + 'lambdas/print-sender-lambda', + 'lambdas/print-analyser', + 'lambdas/report-scheduler', + 'lambdas/report-event-transformer', + 'lambdas/move-scanned-files-lambda', + 'lambdas/report-generator', + 'utils/sender-management', +]; + +/** @type {import('jest').Config} */ +module.exports = { + projects: [ + // Standard workspaces — no overrides + ...standardWorkspaces.map((ws) => ({ + ...base, + rootDir: `/${ws}`, + displayName: ws, + })), + + // utils/utils — relaxed coverage thresholds + exclude index.ts + { + ...base, + rootDir: '/utils/utils', + displayName: 'utils/utils', + coverageThreshold: { + global: { branches: 85, functions: 85, lines: 85, statements: -10 }, + }, + coveragePathIgnorePatterns: [...base.coveragePathIgnorePatterns, 'index.ts'], + }, + + // lambdas/core-notifier-lambda — moduleNameMapper unifies `crypto` and + // `node:crypto` in Jest's registry so that jest.mock('node:crypto') in the + // test files also intercepts the bare require('crypto') call made by + // node-jose at module-load time, preventing an undefined helpers.nodeCrypto + // crash in ecdsa.js. + { + ...base, + rootDir: '/lambdas/core-notifier-lambda', + displayName: 'lambdas/core-notifier-lambda', + }, + + // lambdas/print-status-handler — @nhsdigital/nhs-notify-event-schemas-supplier-api + // ships ESM source; it must be transformed by ts-jest rather than skipped. + { + ...base, + rootDir: '/lambdas/print-status-handler', + displayName: 'lambdas/print-status-handler', + transformIgnorePatterns: [ + 'node_modules/(?!@nhsdigital/nhs-notify-event-schemas-supplier-api)', + ], + }, + + // src/python-schema-generator — excludes merge-allof CLI entry point + { + ...base, + rootDir: '/src/python-schema-generator', + displayName: 'src/python-schema-generator', + coveragePathIgnorePatterns: [ + ...base.coveragePathIgnorePatterns, + 'src/merge-allof-cli.ts', + ], + }, + + // src/typescript-schema-generator — excludes CLI entry points. + // Requires --experimental-vm-modules (set via NODE_OPTIONS in the + // test:unit:parallel script) because json-schema-to-typescript uses a + // dynamic import() of prettier at runtime, which Node.js rejects inside a + // Jest VM context without the flag. + { + ...base, + rootDir: '/src/typescript-schema-generator', + displayName: 'src/typescript-schema-generator', + coveragePathIgnorePatterns: [ + ...base.coveragePathIgnorePatterns, + 'src/generate-types-cli.ts', + 'src/generate-validators-cli.ts', + ], + }, + + // src/cloudevents — uses its own jest.config.cjs (hand-rolled ts-jest options) + '/src/cloudevents', + ], +}; diff --git a/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts b/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts index 59af51fd..80077f41 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts @@ -12,7 +12,13 @@ import { IAccessTokenRepository, NotifyClient } from 'app/notify-api-client'; import { RequestAlreadyReceivedError } from 'domain/request-already-received-error'; jest.mock('utils'); -jest.mock('node:crypto'); +// Use a partial manual mock so that node-jose's require('crypto') still gets +// the real crypto implementation (needed for getHashes() etc.) while +// randomUUID is replaced with a jest.fn() for test control. +jest.mock('node:crypto', () => ({ + ...jest.requireActual('node:crypto'), + randomUUID: jest.fn(), +})); jest.mock('axios', () => { const original: AxiosStatic = jest.requireActual('axios'); diff --git a/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts b/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts index e1bde3bc..cf2c37fd 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts @@ -9,7 +9,13 @@ import { PDMResourceAvailable } from 'digital-letters-events'; import { randomUUID } from 'node:crypto'; jest.mock('utils'); -jest.mock('node:crypto'); +// Use a partial manual mock so that node-jose's require('crypto') still gets +// the real crypto implementation (needed for getHashes() etc.) while +// randomUUID is replaced with a jest.fn() for test control. +jest.mock('node:crypto', () => ({ + ...jest.requireActual('node:crypto'), + randomUUID: jest.fn(), +})); const mockLogger = jest.mocked(logger); const mockRandomUUID = jest.mocked(randomUUID); diff --git a/package.json b/package.json index ca8080a4..81b84538 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "lint:fix": "npm run lint:fix --workspaces", "start": "npm run start --workspace frontend", "test:unit": "npm run test:unit --workspaces", + "test:unit:parallel": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --config jest.config.cjs", "typecheck": "npm run typecheck --workspaces" }, "version": "0.0.1", diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index 1a4a23e6..67ee6037 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -17,59 +17,103 @@ cd "$(git rev-parse --show-toplevel)" # tests from here. If you want to run other test suites, see the predefined # tasks in scripts/test.mk. +# Timing helpers — records wall-clock duration for each labelled step and prints +# a summary table at exit so it's easy to see where the time is going. +_timer_labels=() +_timer_seconds=() + +run_timed() { + local label="$1" + shift + local start + start=$(date +%s) + local rc=0 + "$@" || rc=$? + local end + end=$(date +%s) + _timer_labels+=("$label") + _timer_seconds+=("$((end - start))") + return "$rc" +} + +print_timing_summary() { + echo "" + echo "===== Timing Summary =====" + local total=0 + for i in "${!_timer_labels[@]}"; do + printf " %-55s %4ds\n" "${_timer_labels[$i]}" "${_timer_seconds[$i]}" + total=$((total + _timer_seconds[$i])) + done + echo " ---------------------------------------------------------" + printf " %-55s %4ds\n" "TOTAL" "$total" + echo "==========================" +} + +trap print_timing_summary EXIT + # run tests # TypeScript/JavaScript projects (npm workspace) -# Note: src/cloudevents is included in workspaces, so it will be tested here -npm ci -npm run generate-dependencies -npm run test:unit --workspaces +# Runs all Jest workspaces in parallel via the root jest.config.cjs projects +# config, which is faster than sequential `npm run test:unit --workspaces`. +# Note: src/cloudevents is included in the projects list in jest.config.cjs. +# Use || to capture any Jest failure so that Python tests always run; the exit +# code is propagated at the end of the script. +run_timed "npm ci" npm ci +run_timed "npm run generate-dependencies" npm run generate-dependencies +run_timed "npm run test:unit:parallel" npm run test:unit:parallel || jest_exit=$? # Python projects - asyncapigenerator echo "Setting up and running asyncapigenerator tests..." -make -C ./src/asyncapigenerator install-dev -make -C ./src/asyncapigenerator coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "asyncapigenerator: install-dev" make -C ./src/asyncapigenerator install-dev +run_timed "asyncapigenerator: coverage" make -C ./src/asyncapigenerator coverage # Python projects - cloudeventjekylldocs echo "Setting up and running cloudeventjekylldocs tests..." -make -C ./src/cloudeventjekylldocs install-dev -make -C ./src/cloudeventjekylldocs coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "cloudeventjekylldocs: install-dev" make -C ./src/cloudeventjekylldocs install-dev +run_timed "cloudeventjekylldocs: coverage" make -C ./src/cloudeventjekylldocs coverage # Python projects - eventcatalogasyncapiimporter echo "Setting up and running eventcatalogasyncapiimporter tests..." -make -C ./src/eventcatalogasyncapiimporter install-dev -make -C ./src/eventcatalogasyncapiimporter coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "eventcatalogasyncapiimporter: install-dev" make -C ./src/eventcatalogasyncapiimporter install-dev +run_timed "eventcatalogasyncapiimporter: coverage" make -C ./src/eventcatalogasyncapiimporter coverage # Python utility packages - py-utils echo "Setting up and running py-utils tests..." -make -C ./utils/py-utils install-dev -make -C ./utils/py-utils coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "py-utils: install-dev" make -C ./utils/py-utils install-dev +run_timed "py-utils: coverage" make -C ./utils/py-utils coverage # Python projects - python-schema-generator echo "Setting up and running python-schema-generator tests..." -make -C ./src/python-schema-generator install-dev -make -C ./src/python-schema-generator coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "python-schema-generator: install-dev" make -C ./src/python-schema-generator install-dev +run_timed "python-schema-generator: coverage" make -C ./src/python-schema-generator coverage # Python Lambda - mesh-acknowledge echo "Setting up and running mesh-acknowledge tests..." -make -C ./lambdas/mesh-acknowledge install-dev -make -C ./lambdas/mesh-acknowledge coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "mesh-acknowledge: install-dev" make -C ./lambdas/mesh-acknowledge install-dev +run_timed "mesh-acknowledge: coverage" make -C ./lambdas/mesh-acknowledge coverage # Python Lambda - mesh-poll echo "Setting up and running mesh-poll tests..." -make -C ./lambdas/mesh-poll install-dev -make -C ./lambdas/mesh-poll coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "mesh-poll: install-dev" make -C ./lambdas/mesh-poll install-dev +run_timed "mesh-poll: coverage" make -C ./lambdas/mesh-poll coverage # Python Lambda - mesh-download echo "Setting up and running mesh-download tests..." -make -C ./lambdas/mesh-download install-dev -make -C ./lambdas/mesh-download coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "mesh-download: install-dev" make -C ./lambdas/mesh-download install-dev +run_timed "mesh-download: coverage" make -C ./lambdas/mesh-download coverage # Python Lambda - report-sender echo "Setting up and running report-sender tests..." -make -C ./lambdas/report-sender install-dev -make -C ./lambdas/report-sender coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "report-sender: install-dev" make -C ./lambdas/report-sender install-dev +run_timed "report-sender: coverage" make -C ./lambdas/report-sender coverage # merge coverage reports -mkdir -p .reports -TMPDIR="./.reports" ./node_modules/.bin/lcov-result-merger "**/.reports/unit/coverage/lcov.info" ".reports/lcov.info" --ignore "node_modules" --prepend-source-files --prepend-path-fix "../../.." +run_timed "lcov-result-merger" \ + bash -c 'mkdir -p .reports && TMPDIR="./.reports" ./node_modules/.bin/lcov-result-merger "**/.reports/unit/coverage/lcov.info" ".reports/lcov.info" --ignore "node_modules" --prepend-source-files --prepend-path-fix "../../.."' + +# Propagate any Jest failure now that all other test suites have completed +if [ "${jest_exit:-0}" -ne 0 ]; then + echo "Jest tests failed with exit code ${jest_exit}" + exit "${jest_exit}" +fi diff --git a/src/cloudevents/tools/builder/__tests__/build-schema.test.ts b/src/cloudevents/tools/builder/__tests__/build-schema.test.ts index f7c35f90..7c7fecf9 100644 --- a/src/cloudevents/tools/builder/__tests__/build-schema.test.ts +++ b/src/cloudevents/tools/builder/__tests__/build-schema.test.ts @@ -9,6 +9,10 @@ import fs from 'fs'; import path from 'path'; import { execSync } from 'child_process'; +// Resolve paths relative to this test file so the suite works whether Jest +// runs from the workspace directory or from the repository root. +const cloudEventsRoot = path.resolve(__dirname, '..', '..', '..'); + describe('build-schema CLI', () => { let testDir: string; let sourceDir: string; @@ -16,7 +20,7 @@ describe('build-schema CLI', () => { beforeAll(() => { // Create test directories - testDir = path.join(process.cwd(), 'test-build-' + Date.now()); + testDir = path.join(cloudEventsRoot, 'test-build-' + Date.now()); sourceDir = path.join(testDir, 'src'); outputDir = path.join(testDir, 'output'); @@ -52,7 +56,7 @@ describe('build-schema CLI', () => { execSync( `tsx tools/builder/build-schema.ts "${inputFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -91,7 +95,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "${inputFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -120,7 +124,7 @@ properties: const result = execSync( `tsx tools/builder/build-schema.ts "${inputFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe', encoding: 'utf-8' } @@ -139,7 +143,7 @@ properties: execSync( 'tsx tools/builder/build-schema.ts', { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -160,7 +164,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "${inputFile}" "${outputDir}" "https://example.com"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -198,7 +202,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "${mainFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -224,7 +228,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "${yamlFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -246,7 +250,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "nonexistent.json" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -266,7 +270,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "${invalidFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -282,7 +286,7 @@ properties: describe('module structure', () => { it('should export expected functions (if any)', () => { // build-schema-cli.ts contains the testable logic - const buildSchemaCliPath = path.join(process.cwd(), 'tools/builder/build-schema-cli.ts'); + const buildSchemaCliPath = path.join(cloudEventsRoot, 'tools/builder/build-schema-cli.ts'); expect(fs.existsSync(buildSchemaCliPath)).toBe(true); // Verify file has expected structure @@ -293,7 +297,7 @@ properties: }); it('should have proper imports', () => { - const buildSchemaCliPath = path.join(process.cwd(), 'tools/builder/build-schema-cli.ts'); + const buildSchemaCliPath = path.join(cloudEventsRoot, 'tools/builder/build-schema-cli.ts'); const content = fs.readFileSync(buildSchemaCliPath, 'utf-8'); expect(content).toContain('import fs from'); From 52c6caf5c9fd334752d7c915813d821932a17450 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Fri, 20 Mar 2026 11:18:55 +0000 Subject: [PATCH 4/5] CCM-14615: run pytets in parrallel --- lambdas/mesh-acknowledge/pytest.ini | 1 + lambdas/mesh-download/pytest.ini | 1 + lambdas/mesh-poll/pytest.ini | 1 + lambdas/report-sender/pytest.ini | 1 + scripts/tests/unit.sh | 106 ++++++++++++-------- src/asyncapigenerator/pytest.ini | 1 + src/cloudeventjekylldocs/pytest.ini | 1 + src/eventcatalogasyncapiimporter/pytest.ini | 1 + src/python-schema-generator/pytest.ini | 1 + utils/py-utils/pytest.ini | 1 + 10 files changed, 71 insertions(+), 44 deletions(-) diff --git a/lambdas/mesh-acknowledge/pytest.ini b/lambdas/mesh-acknowledge/pytest.ini index e19306a7..7f80cf1a 100644 --- a/lambdas/mesh-acknowledge/pytest.ini +++ b/lambdas/mesh-acknowledge/pytest.ini @@ -7,6 +7,7 @@ addopts = -v --tb=short [coverage:run] relative_files = True +data_file = lambdas/mesh-acknowledge/.coverage omit = */mesh_acknowledge/__tests__/* */test_*.py diff --git a/lambdas/mesh-download/pytest.ini b/lambdas/mesh-download/pytest.ini index 303659aa..b2483b3c 100644 --- a/lambdas/mesh-download/pytest.ini +++ b/lambdas/mesh-download/pytest.ini @@ -7,6 +7,7 @@ addopts = -v --tb=short [coverage:run] relative_files = True +data_file = lambdas/mesh-download/.coverage omit = */tests/* */test_*.py diff --git a/lambdas/mesh-poll/pytest.ini b/lambdas/mesh-poll/pytest.ini index 93372031..8657f96d 100644 --- a/lambdas/mesh-poll/pytest.ini +++ b/lambdas/mesh-poll/pytest.ini @@ -7,6 +7,7 @@ addopts = -v --tb=short [coverage:run] relative_files = True +data_file = lambdas/mesh-poll/.coverage omit = */mesh_poll/__tests__/* */test_*.py diff --git a/lambdas/report-sender/pytest.ini b/lambdas/report-sender/pytest.ini index 91879c29..9402fd11 100644 --- a/lambdas/report-sender/pytest.ini +++ b/lambdas/report-sender/pytest.ini @@ -7,6 +7,7 @@ addopts = -v --tb=short [coverage:run] relative_files = True +data_file = lambdas/report-sender/.coverage omit = */report_sender/__tests__/* */test_*.py diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index 67ee6037..63a24fd3 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -63,50 +63,60 @@ run_timed "npm ci" npm ci run_timed "npm run generate-dependencies" npm run generate-dependencies run_timed "npm run test:unit:parallel" npm run test:unit:parallel || jest_exit=$? -# Python projects - asyncapigenerator -echo "Setting up and running asyncapigenerator tests..." -run_timed "asyncapigenerator: install-dev" make -C ./src/asyncapigenerator install-dev -run_timed "asyncapigenerator: coverage" make -C ./src/asyncapigenerator coverage - -# Python projects - cloudeventjekylldocs -echo "Setting up and running cloudeventjekylldocs tests..." -run_timed "cloudeventjekylldocs: install-dev" make -C ./src/cloudeventjekylldocs install-dev -run_timed "cloudeventjekylldocs: coverage" make -C ./src/cloudeventjekylldocs coverage - -# Python projects - eventcatalogasyncapiimporter -echo "Setting up and running eventcatalogasyncapiimporter tests..." -run_timed "eventcatalogasyncapiimporter: install-dev" make -C ./src/eventcatalogasyncapiimporter install-dev -run_timed "eventcatalogasyncapiimporter: coverage" make -C ./src/eventcatalogasyncapiimporter coverage - -# Python utility packages - py-utils -echo "Setting up and running py-utils tests..." -run_timed "py-utils: install-dev" make -C ./utils/py-utils install-dev -run_timed "py-utils: coverage" make -C ./utils/py-utils coverage - -# Python projects - python-schema-generator -echo "Setting up and running python-schema-generator tests..." -run_timed "python-schema-generator: install-dev" make -C ./src/python-schema-generator install-dev -run_timed "python-schema-generator: coverage" make -C ./src/python-schema-generator coverage - -# Python Lambda - mesh-acknowledge -echo "Setting up and running mesh-acknowledge tests..." -run_timed "mesh-acknowledge: install-dev" make -C ./lambdas/mesh-acknowledge install-dev -run_timed "mesh-acknowledge: coverage" make -C ./lambdas/mesh-acknowledge coverage - -# Python Lambda - mesh-poll -echo "Setting up and running mesh-poll tests..." -run_timed "mesh-poll: install-dev" make -C ./lambdas/mesh-poll install-dev -run_timed "mesh-poll: coverage" make -C ./lambdas/mesh-poll coverage - -# Python Lambda - mesh-download -echo "Setting up and running mesh-download tests..." -run_timed "mesh-download: install-dev" make -C ./lambdas/mesh-download install-dev -run_timed "mesh-download: coverage" make -C ./lambdas/mesh-download coverage - -# Python Lambda - report-sender -echo "Setting up and running report-sender tests..." -run_timed "report-sender: install-dev" make -C ./lambdas/report-sender install-dev -run_timed "report-sender: coverage" make -C ./lambdas/report-sender coverage +# Python projects - run all install-dev steps sequentially (they share the same +# pip environment so cannot be parallelised), then run all coverage (pytest) +# steps in parallel since each writes to its own isolated output directory. + +# ---- Phase 1: install all Python dev dependencies (sequential, shared pip env) ---- +echo "Installing Python dev dependencies..." +_python_projects=( + ./src/asyncapigenerator + ./src/cloudeventjekylldocs + ./src/eventcatalogasyncapiimporter + ./utils/py-utils + ./src/python-schema-generator + ./lambdas/mesh-acknowledge + ./lambdas/mesh-poll + ./lambdas/mesh-download + ./lambdas/report-sender +) +for proj in "${_python_projects[@]}"; do + run_timed "${proj}: install-dev" make -C "$proj" install-dev +done + +# ---- Phase 2: run all coverage steps in parallel ---- +# Each job writes output to a temp file; we print them sequentially on +# completion so the log is readable. Non-zero exit codes are all collected and +# the script fails at the end if any job failed. +echo "Running Python coverage in parallel..." + +_py_pids=() +_py_labels=() +_py_logs=() +_py_exits=() + +for proj in "${_python_projects[@]}"; do + label="${proj}: coverage" + logfile=$(mktemp) + make -C "$proj" coverage >"$logfile" 2>&1 & + _py_pids+=("$!") + _py_labels+=("$label") + _py_logs+=("$logfile") +done + +# Collect results in launch order (preserves deterministic output) +_py_start=$(date +%s) +for i in "${!_py_pids[@]}"; do + wait "${_py_pids[$i]}" + _py_exits+=("$?") + echo "" + echo "--- ${_py_labels[$i]} ---" + cat "${_py_logs[$i]}" + rm -f "${_py_logs[$i]}" +done +_py_end=$(date +%s) +_timer_labels+=("Python coverage (parallel)") +_timer_seconds+=("$((_py_end - _py_start))") # merge coverage reports run_timed "lcov-result-merger" \ @@ -117,3 +127,11 @@ if [ "${jest_exit:-0}" -ne 0 ]; then echo "Jest tests failed with exit code ${jest_exit}" exit "${jest_exit}" fi + +# Propagate any Python coverage failure +for i in "${!_py_exits[@]}"; do + if [ "${_py_exits[$i]}" -ne 0 ]; then + echo "${_py_labels[$i]} failed with exit code ${_py_exits[$i]}" + exit "${_py_exits[$i]}" + fi +done diff --git a/src/asyncapigenerator/pytest.ini b/src/asyncapigenerator/pytest.ini index 94bd46ad..fcdb5a3f 100644 --- a/src/asyncapigenerator/pytest.ini +++ b/src/asyncapigenerator/pytest.ini @@ -19,6 +19,7 @@ markers = [coverage:run] relative_files = True +data_file = src/asyncapigenerator/.coverage omit = */tests/* */test_*.py diff --git a/src/cloudeventjekylldocs/pytest.ini b/src/cloudeventjekylldocs/pytest.ini index dba4156c..b79a593f 100644 --- a/src/cloudeventjekylldocs/pytest.ini +++ b/src/cloudeventjekylldocs/pytest.ini @@ -18,6 +18,7 @@ markers = [coverage:run] relative_files = True +data_file = src/cloudeventjekylldocs/.coverage omit = */tests/* */test_*.py diff --git a/src/eventcatalogasyncapiimporter/pytest.ini b/src/eventcatalogasyncapiimporter/pytest.ini index 0b2a4551..41b39ff4 100644 --- a/src/eventcatalogasyncapiimporter/pytest.ini +++ b/src/eventcatalogasyncapiimporter/pytest.ini @@ -20,6 +20,7 @@ testpaths = tests [coverage:run] relative_files = True +data_file = src/eventcatalogasyncapiimporter/.coverage omit = */tests/* */test_*.py diff --git a/src/python-schema-generator/pytest.ini b/src/python-schema-generator/pytest.ini index 0d63d6be..001fba94 100644 --- a/src/python-schema-generator/pytest.ini +++ b/src/python-schema-generator/pytest.ini @@ -9,6 +9,7 @@ addopts = [coverage:run] relative_files = True +data_file = src/python-schema-generator/.coverage omit = */tests/* */test_*.py diff --git a/utils/py-utils/pytest.ini b/utils/py-utils/pytest.ini index f704cd77..b5bbd23b 100644 --- a/utils/py-utils/pytest.ini +++ b/utils/py-utils/pytest.ini @@ -7,6 +7,7 @@ addopts = -v --tb=short [coverage:run] relative_files = True +data_file = utils/py-utils/.coverage omit = */dl_utils/__tests__/* */test_*.py From b6dadac7a67aafbced5581e4cb460b9c667a6fd3 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Fri, 20 Mar 2026 12:10:01 +0000 Subject: [PATCH 5/5] CCM-14615: more parrallels --- src/cloudevents/domains/common.mk | 32 +++++++++++-------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/cloudevents/domains/common.mk b/src/cloudevents/domains/common.mk index e96dcd34..e4891919 100644 --- a/src/cloudevents/domains/common.mk +++ b/src/cloudevents/domains/common.mk @@ -56,31 +56,23 @@ build-no-bundle: @echo "Building $(DOMAIN) schemas to output/..." @if [ -n "$(PROFILE_NAMES)" ]; then \ echo "Building profile schemas..."; \ - for schema in $(PROFILE_NAMES); do \ - echo " - $$schema"; \ - cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/$$schema.schema.yaml $(OUTPUT_DIR) || exit 1; \ - done; \ + printf '%s\n' $(PROFILE_NAMES) | xargs -P 0 -I{} sh -c \ + 'cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/{}.schema.yaml $(OUTPUT_DIR) || exit 1'; \ fi @if [ -n "$(DEFS_NAMES)" ]; then \ echo "Building defs schemas..."; \ - for schema in $(DEFS_NAMES); do \ - echo " - $$schema"; \ - cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/defs/$$schema.yaml $(OUTPUT_DIR)/defs || exit 1; \ - done; \ + printf '%s\n' $(DEFS_NAMES) | xargs -P 0 -I{} sh -c \ + 'cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/defs/{}.yaml $(OUTPUT_DIR)/defs || exit 1'; \ fi @if [ -n "$(DATA_NAMES)" ]; then \ echo "Building data schemas..."; \ - for schema in $(DATA_NAMES); do \ - echo " - $$schema"; \ - cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/data/$$schema.yaml $(OUTPUT_DIR)/data || exit 1; \ - done; \ + printf '%s\n' $(DATA_NAMES) | xargs -P 0 -I{} sh -c \ + 'cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/data/{}.yaml $(OUTPUT_DIR)/data || exit 1'; \ fi @if [ -n "$(EVENT_NAMES)" ]; then \ echo "Building event schemas..."; \ - for schema in $(EVENT_NAMES); do \ - echo " - $$schema"; \ - cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/events/$$schema.schema.yaml $(OUTPUT_DIR)/events || exit 1; \ - done; \ + printf '%s\n' $(EVENT_NAMES) | xargs -P 0 -I{} sh -c \ + 'cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/events/{}.schema.yaml $(OUTPUT_DIR)/events || exit 1'; \ fi publish-json: @@ -138,11 +130,9 @@ publish-json: publish-bundled-json: @if [ -n "$(EVENT_NAMES)" ]; then \ - @echo "Flattening published event schemas..."; \ - for schema in $(EVENT_NAMES); do \ - echo " - $$schema (flatten)"; \ - cd $(CLOUD_EVENTS_DIR) && npm run bundle -- --flatten --root-dir $(ROOT_DIR) --base-url $(SCHEMA_BASE_URL) $(OUTPUT_DIR)/events/$$schema.schema.json $(SCHEMAS_DIR)/events/$$schema.flattened.schema.json || exit 1; \ - done; \ + echo "Flattening published event schemas..."; \ + printf '%s\n' $(EVENT_NAMES) | xargs -P 0 -I{} sh -c \ + 'cd $(CLOUD_EVENTS_DIR) && npm run bundle -- --flatten --root-dir $(ROOT_DIR) --base-url $(SCHEMA_BASE_URL) $(OUTPUT_DIR)/events/{}.schema.json $(SCHEMAS_DIR)/events/{}.flattened.schema.json || exit 1'; \ fi publish-yaml: