Skip to content

Commit f76e251

Browse files
committed
ci: add integration test coverage job
1 parent ae502a7 commit f76e251

4 files changed

Lines changed: 201 additions & 7 deletions

File tree

.github/actions/ci-status-gate/action.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ runs:
9696
prepare_ci_js_deps_result="$(jq -nr 'env.NEEDS_JSON | fromjson | .["prepare-ci-js-deps"].result // ""')"
9797
unit_tests_result="$(jq -nr 'env.NEEDS_JSON | fromjson | .["unit-tests"].result // ""')"
9898
component_view_tests_result="$(jq -nr 'env.NEEDS_JSON | fromjson | .["component-view-tests"].result // ""')"
99+
integration_tests_result="$(jq -nr 'env.NEEDS_JSON | fromjson | .["integration-tests"].result // ""')"
99100
100101
if [[ "$block_merge_for_e2e_readiness" == "true" ]]; then
101102
echo "::error::The 'pr-not-ready-for-e2e' label is still applied. Remove it to trigger E2E tests before merging."
@@ -142,7 +143,8 @@ runs:
142143
elif [[ "$job_name" == "sonar-cloud-quality-gate-status" &&
143144
( "$prepare_ci_js_deps_result" =~ ^(failure|cancelled)$ ||
144145
"$unit_tests_result" =~ ^(failure|cancelled)$ ||
145-
"$component_view_tests_result" =~ ^(failure|cancelled)$ ) ]]; then
146+
"$component_view_tests_result" =~ ^(failure|cancelled)$ ||
147+
"$integration_tests_result" =~ ^(failure|cancelled)$ ) ]]; then
146148
add_summary_row "$job_name" "$result" "pass" "downstream coverage/Sonar skip is allowed after upstream dependency/test failure or cancellation"
147149
else
148150
mark_failure "$job_name was skipped unexpectedly"

.github/scripts/collect-qa-stats.mjs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* {
3030
* "unit": { "total_tests_run": 41957, "total_tests_skipped": 17, "bridge_tests_run": 5000, "other_tests_run": 1000 },
3131
* "component_view":{ "total_tests_run": 94, "total_tests_skipped": 0 },
32+
* "integration": { "total_tests_run": 11, "total_tests_skipped": 0 },
3233
* "e2e": { "total_tests_run": 420, "total_tests_skipped": 27,
3334
* "main_tests_run": 276, "main_android_tests_run": 276, "main_ios_tests_run": 276,
3435
* "flask_tests_run": 144, "confirmations_tests_run": 62 },
@@ -70,6 +71,7 @@ const PATH_ONBOARDING_EVENTS = 'tests/helpers/analytics/helpers.ts';
7071
const SCAN_PERFORMANCE_DIR = 'tests/performance';
7172

7273
const PATTERN_CV_TEST_FILE = /\.view(?:\..+)?\.test\.[jt]sx?$/;
74+
const PATTERN_INTEGRATION_TEST_FILE = /\.integration\.test\.[jt]sx?$/;
7375
const PATTERN_UNIT_TEST_FILE = /\.test\.[jt]sx?$/;
7476
const PATTERN_E2E_SPEC_FILE = /\.spec\.[jt]sx?$/;
7577
const PATTERN_PERF_SPEC_FILE = /\.spec\.js$/;
@@ -834,16 +836,64 @@ async function collectComponentViewTestCount() {
834836
return result;
835837
}
836838

839+
async function collectIntegrationTestCount() {
840+
console.log(
841+
'[integration] collecting per-suite counts from shard artifacts...',
842+
);
843+
const result = await collectShardCounts(
844+
/^coverage-integration-\d+$/,
845+
'integration',
846+
);
847+
if (Object.keys(result).length === 0) return result;
848+
849+
const isIntegrationTestFile = (name) =>
850+
PATTERN_INTEGRATION_TEST_FILE.test(name);
851+
const files = await walkFiles(SCAN_APP_DIR, isIntegrationTestFile);
852+
let defined = 0, skips = 0;
853+
for (const f of files) {
854+
const source = await readFile(f, 'utf8');
855+
defined += countDefinedTests(source);
856+
skips += countSkips(source);
857+
}
858+
result.total_tests_defined = defined;
859+
result.total_tests_skipped = skips;
860+
861+
// Coverage from the pre-computed nyc json-summary report produced by
862+
// the merge-unit-and-component-view-tests job in ci.yml.
863+
try {
864+
const destDir = await downloadArtifact('integration-test-coverage-summary');
865+
const summary = JSON.parse(
866+
await readFile(join(destDir, 'coverage-summary.json'), 'utf8'),
867+
);
868+
const { lines, statements, branches, functions } = summary.total;
869+
result.coverage_line = Math.round(lines.pct * 10) / 10;
870+
result.coverage_statement = Math.round(statements.pct * 10) / 10;
871+
result.coverage_branch = Math.round(branches.pct * 10) / 10;
872+
result.coverage_function = Math.round(functions.pct * 10) / 10;
873+
console.log(
874+
`[integration] coverage — line: ${result.coverage_line}%, stmt: ${result.coverage_statement}%, branch: ${result.coverage_branch}%, fn: ${result.coverage_function}%`,
875+
);
876+
} catch (err) {
877+
console.warn(
878+
`[integration] coverage summary not available, skipping: ${err.message}`,
879+
);
880+
}
881+
882+
return result;
883+
}
884+
837885
async function collectUnitTestCount() {
838886
console.log('[unit] collecting per-suite counts from shard artifacts...');
839887
// minFolderCount=200: buckets individual component-level folders into `other`,
840888
// keeping only meaningful team-level categories (bridge, perps, confirmations, etc.)
841889
const result = await collectShardCounts(/^coverage-unit-\d+$/, 'unit', 200);
842890
if (Object.keys(result).length === 0) return result;
843891

844-
// Unit test files: *.test.{ts,tsx,js} excluding *.view[.*].test.*
892+
// Unit test files: *.test.{ts,tsx,js} excluding view and integration suites.
845893
const isUnitTestFile = (name) =>
846-
PATTERN_UNIT_TEST_FILE.test(name) && !PATTERN_CV_TEST_FILE.test(name);
894+
PATTERN_UNIT_TEST_FILE.test(name) &&
895+
!PATTERN_CV_TEST_FILE.test(name) &&
896+
!PATTERN_INTEGRATION_TEST_FILE.test(name);
847897
const files = await walkFiles(SCAN_APP_DIR, isUnitTestFile);
848898
let defined = 0, skips = 0;
849899
for (const f of files) {
@@ -1220,6 +1270,7 @@ async function main() {
12201270
const collectors = [
12211271
{ namespace: 'unit', collect: collectUnitTestCount },
12221272
{ namespace: 'component_view', collect: collectComponentViewTestCount },
1273+
{ namespace: 'integration', collect: collectIntegrationTestCount },
12231274
{ namespace: 'e2e', collect: collectE2ECounts },
12241275
{ namespace: 'e2e_test_times', collect: collectE2ETestTimes },
12251276
{ namespace: 'metametrics', collect: collectMetametricsQaStats },

.github/workflows/ci.yml

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -746,11 +746,15 @@ jobs:
746746
echo "No changes detected"
747747
fi
748748
749-
# We need to merge both unit and component view tests into a single coverage report so the PR coverage
750-
# threshold calculation is accurate.
749+
# We need to merge unit, component view, and integration tests into a
750+
# single coverage report so the PR coverage threshold calculation is accurate.
751751
merge-unit-and-component-view-tests:
752752
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
753-
needs: [prepare-ci-js-deps, unit-tests, component-view-tests]
753+
needs:
754+
- prepare-ci-js-deps
755+
- unit-tests
756+
- component-view-tests
757+
- integration-tests
754758
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
755759
if: ${{ github.event_name != 'merge_group' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' }}
756760
steps:
@@ -807,11 +811,25 @@ jobs:
807811
echo "{\"component_view_test_number\": $cv_total}" > cv-test-stats.json
808812
echo "CV test count: $cv_total"
809813
814+
integration_total=0
815+
for file in ./tests/coverage/coverage-integration-*/count.json; do
816+
[ -f "$file" ] || continue
817+
count=$(jq '.count // 0' "$file")
818+
integration_total=$((integration_total + count))
819+
done
820+
echo "{\"integration_test_number\": $integration_total}" > integration-test-stats.json
821+
echo "Integration test count: $integration_total"
822+
810823
mkdir -p tests/coverage-cv-merged
811824
for file in ./tests/coverage/coverage-cv-*/coverage-cv-*.json; do
812825
[ -f "$file" ] && cp "$file" ./tests/coverage-cv-merged/
813826
done
814827
828+
mkdir -p tests/coverage-integration-merged
829+
for file in ./tests/coverage/coverage-integration-*/coverage-integration-*.json; do
830+
[ -f "$file" ] && cp "$file" ./tests/coverage-integration-merged/
831+
done
832+
815833
mkdir -p tests/coverage-unit-merged
816834
for file in ./tests/coverage/coverage-unit-*/coverage-unit-*.json; do
817835
[ -f "$file" ] && cp "$file" ./tests/coverage-unit-merged/
@@ -852,6 +870,22 @@ jobs:
852870
path: ./cv-test-stats.json
853871
if-no-files-found: error
854872
retention-days: 7
873+
- name: Upload integration-test-stats (Namespace)
874+
if: ${{ inputs.runner_provider == 'namespace' }}
875+
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
876+
with:
877+
name: integration-test-stats
878+
path: ./integration-test-stats.json
879+
if-no-files-found: error
880+
retention-days: 7
881+
- name: Upload integration-test-stats (current)
882+
if: ${{ inputs.runner_provider != 'namespace' }}
883+
uses: actions/upload-artifact@v4
884+
with:
885+
name: integration-test-stats
886+
path: ./integration-test-stats.json
887+
if-no-files-found: error
888+
retention-days: 7
855889
- name: Upload unit-test-stats (Namespace)
856890
if: ${{ inputs.runner_provider == 'namespace' }}
857891
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
@@ -902,6 +936,40 @@ jobs:
902936
path: ./tests/coverage-cv-lcov/coverage-summary.json
903937
if-no-files-found: error
904938
retention-days: 7
939+
- name: Generate integration test coverage report
940+
run: yarn nyc report --temp-dir ./tests/coverage-integration-merged --report-dir ./tests/coverage-integration-lcov --reporter html --reporter json-summary
941+
- name: Upload integration-test-coverage-html (Namespace)
942+
if: ${{ inputs.runner_provider == 'namespace' }}
943+
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
944+
with:
945+
name: integration-test-coverage-html
946+
path: ./tests/coverage-integration-lcov/
947+
if-no-files-found: error
948+
retention-days: 7
949+
- name: Upload integration-test-coverage-html (current)
950+
if: ${{ inputs.runner_provider != 'namespace' }}
951+
uses: actions/upload-artifact@v4
952+
with:
953+
name: integration-test-coverage-html
954+
path: ./tests/coverage-integration-lcov/
955+
if-no-files-found: error
956+
retention-days: 7
957+
- name: Upload integration-test-coverage-summary (Namespace)
958+
if: ${{ inputs.runner_provider == 'namespace' }}
959+
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
960+
with:
961+
name: integration-test-coverage-summary
962+
path: ./tests/coverage-integration-lcov/coverage-summary.json
963+
if-no-files-found: error
964+
retention-days: 7
965+
- name: Upload integration-test-coverage-summary (current)
966+
if: ${{ inputs.runner_provider != 'namespace' }}
967+
uses: actions/upload-artifact@v4
968+
with:
969+
name: integration-test-coverage-summary
970+
path: ./tests/coverage-integration-lcov/coverage-summary.json
971+
if-no-files-found: error
972+
retention-days: 7
905973
- name: Generate unit test coverage summary
906974
run: yarn nyc report --temp-dir ./tests/coverage-unit-merged --report-dir ./tests/coverage-unit-lcov --reporter json-summary
907975
- name: Upload unit-test-coverage-summary (Namespace)
@@ -995,6 +1063,70 @@ jobs:
9951063
if-no-files-found: error
9961064
retention-days: 7
9971065

1066+
integration-tests:
1067+
name: Integration tests
1068+
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
1069+
if: ${{ !cancelled() && needs.get_requirements.result == 'success' && needs.get_requirements.outputs.skip_everything != 'true' }}
1070+
needs:
1071+
- get_requirements
1072+
- prepare-ci-js-deps
1073+
strategy:
1074+
matrix:
1075+
shard: [1, 2]
1076+
steps:
1077+
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
1078+
if: ${{ inputs.runner_provider == 'namespace' }}
1079+
- uses: actions/checkout@v6
1080+
if: ${{ inputs.runner_provider != 'namespace' }}
1081+
# TEMP: artifact fallback for non-Namespace runners.
1082+
# Remove these two steps once Namespace passes the trial and becomes the default.
1083+
- name: Download CI JS deps artifact
1084+
id: download-ci-js-deps
1085+
if: ${{ inputs.runner_provider != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }}
1086+
continue-on-error: true
1087+
uses: actions/download-artifact@v4
1088+
with:
1089+
name: ci-js-deps
1090+
path: .
1091+
- name: Extract CI JS deps
1092+
if: ${{ inputs.runner_provider != 'namespace' && needs.prepare-ci-js-deps.result == 'success' && steps.download-ci-js-deps.outcome == 'success' }}
1093+
run: tar -xzf ci-js-deps.tar.gz && rm ci-js-deps.tar.gz
1094+
- uses: ./.github/actions/setup-ci-js-deps
1095+
with:
1096+
runner_provider: ${{ inputs.runner_provider }}
1097+
- name: Prepare results directory
1098+
run: mkdir -p tests/results
1099+
- run: |
1100+
yarn test:integration:ci \
1101+
--shard=${{ matrix.shard }}/2 \
1102+
--json \
1103+
--outputFile=tests/results/integration-test-results-${{ matrix.shard }}.json
1104+
env:
1105+
NODE_OPTIONS: ${{ inputs.runner_provider == 'namespace' && '--max-old-space-size=12288' || '--max-old-space-size=20480' }}
1106+
- name: Rename coverage report and extract test count for this shard
1107+
shell: bash
1108+
run: |
1109+
mv ./tests/coverage/coverage-final.json ./tests/coverage/coverage-integration-${{ matrix.shard }}.json
1110+
cp tests/results/integration-test-results-${{ matrix.shard }}.json ./tests/coverage/jest-results.json
1111+
count=$(jq '(.numPassedTests // 0) + (.numFailedTests // 0)' tests/results/integration-test-results-${{ matrix.shard }}.json)
1112+
echo "{\"count\": $count}" > ./tests/coverage/count.json
1113+
- name: Upload coverage integration shard (Namespace)
1114+
if: ${{ inputs.runner_provider == 'namespace' }}
1115+
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
1116+
with:
1117+
name: coverage-integration-${{ matrix.shard }}
1118+
path: ./tests/coverage/
1119+
if-no-files-found: error
1120+
retention-days: 7
1121+
- name: Upload coverage integration shard (current)
1122+
if: ${{ inputs.runner_provider != 'namespace' }}
1123+
uses: actions/upload-artifact@v4
1124+
with:
1125+
name: coverage-integration-${{ matrix.shard }}
1126+
path: ./tests/coverage/
1127+
if-no-files-found: error
1128+
retention-days: 7
1129+
9981130
smart-e2e-selection:
9991131
name: 'Smart E2E Selection'
10001132
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
@@ -1515,6 +1647,7 @@ jobs:
15151647
needs:
15161648
- unit-tests
15171649
- component-view-tests
1650+
- integration-tests
15181651
- merge-unit-and-component-view-tests
15191652
permissions:
15201653
actions: write
@@ -1554,6 +1687,8 @@ jobs:
15541687
- scripts
15551688
- unit-tests
15561689
- component-view-tests
1690+
- integration-tests
1691+
- merge-unit-and-component-view-tests
15571692
- check-workflows
15581693
- prepare-ci-js-deps
15591694
- js-bundle-size-check

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,19 @@
9090
"build:ios:pre-flask": "export METAMASK_BUILD_TYPE='flask' && ./scripts/build.sh ios flask --pre",
9191
"build:attribution": "./scripts/generate-attributions.sh",
9292
"test": "yarn test:unit",
93-
"test:unit": "jest ./app/ ./locales/ ./tests/**/*.test.ts .github/**/*.test.ts ./scripts/**/*.test.ts --testPathIgnorePatterns='.*/tests/(smoke|regression)/.*\\.spec\\.(ts|tsx|js)$|.*/e2e/.*\\.spec\\.(ts|tsx|js)$|.*\\.view(\\..*)?\\.test\\.(ts|tsx|js|jsx)$'",
93+
"test:unit": "jest ./app/ ./locales/ ./tests/**/*.test.ts .github/**/*.test.ts ./scripts/**/*.test.ts --testPathIgnorePatterns='.*/tests/(smoke|regression)/.*\\.spec\\.(ts|tsx|js)$|.*/e2e/.*\\.spec\\.(ts|tsx|js)$|.*\\.integration\\.test\\.(ts|tsx|js|jsx)$|.*\\.view(\\..*)?\\.test\\.(ts|tsx|js|jsx)$'",
9494
"test:view:base": "jest -c jest.config.view.js --no-watchman --silent",
9595
"test:view": "yarn test:view:base --coverage=false --testPathPattern='.*\\.view(\\..*)?\\.test\\.(ts|tsx|js|jsx)$'",
9696
"test:view:one": "yarn test:view:base --verbose --coverage=false --testTimeout=30000",
9797
"test:view:coverage": "yarn test:view:base --coverage --testPathPattern='.*\\.view(\\..*)?\\.test\\.(ts|tsx|js|jsx)$'",
9898
"test:view:coverage:folder": "sh -c 'yarn test:view:base --coverage --testPathPattern=\"$1.*\\\\.view\\\\.test\\\\.(ts|tsx|js|jsx)$\" --collectCoverageFrom=\"$1/**/*.{ts,tsx,js,jsx}\" --coverageReporters=text-summary --coverageReporters=lcov' --",
9999
"test:view:ci": "TEST_OS=ios yarn test:view:coverage --forceExit --silent --coverageReporters=json",
100+
"test:integration:base": "jest -c jest.config.integration.js --no-watchman --silent",
101+
"test:integration": "yarn test:integration:base --coverage=false --testPathPattern='.*\\.integration\\.test\\.(ts|tsx|js|jsx)$'",
102+
"test:integration:one": "yarn test:integration:base --verbose --coverage=false --testTimeout=30000",
103+
"test:integration:coverage": "yarn test:integration:base --coverage --testPathPattern='.*\\.integration\\.test\\.(ts|tsx|js|jsx)$'",
104+
"test:integration:coverage:folder": "sh -c 'yarn test:integration:base --coverage --testPathPattern=\"$1.*\\\\.integration\\\\.test\\\\.(ts|tsx|js|jsx)$\" --collectCoverageFrom=\"$1/**/*.{ts,tsx,js,jsx}\" --coverageReporters=text-summary --coverageReporters=lcov' --",
105+
"test:integration:ci": "TEST_OS=ios yarn test:integration:coverage --forceExit --silent --coverageReporters=json",
100106
"test:unit:update": "time jest -u ./app/",
101107
"test:api-specs": "detox reset-lock-file && detox test -c ios.sim.apiSpecs",
102108
"test:e2e:android:main:ci": "HAS_TEST_OVERRIDES='true' NODE_OPTIONS='--experimental-vm-modules' detox test -c android.emu.main.ci --headless",

0 commit comments

Comments
 (0)