Skip to content

Commit 0c3ff70

Browse files
committed
test(contracts): parallelise integration tests and shard CI coverage
Shard the mega-asset integration suite for parallel mocha workers (shared suiteDiscovery + atsShardRunner; ats.test.ts delegates, shards/ats.shard.1..8 are the local parallel entries) and split solidity-coverage across a weight-balanced 4-way CI matrix that merges per-shard lcov into a single Codecov upload via an in-repo merger preserving function coverage. Add local parallel coverage via ephemeral git worktrees (test:coverage:parallel) and skip the redundant test-phase recompile under coverage. Guard the split with a disjoint+complete partition assertion plus unit tests, and a CI verify-test-count job that reconciles the sharded test count against a sequential pass so no suite can be silently dropped. Raise the coverage gas limit (gasLimitOverride 30M->100M) and route the orchestrator-library deploys through it so v8.0.0's heavier instrumented deploy fits under solidity-coverage. Cache CI deps and make the workflow contracts-only. Documented in scripts/tools/coverage-shard/README.md. Signed-off-by: Miguel_LZPF <miguel.carpena@io.builders>
1 parent 884d217 commit 0c3ff70

26 files changed

Lines changed: 1344 additions & 134 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hashgraph/asset-tokenization-contracts": patch
3+
---
4+
5+
Shard contracts solidity-coverage across parallel CI runners (one shared mega-asset deploy per shard plus a weight-balanced split of the standalone suites) and merge the per-shard lcov reports into a single Codecov upload matching the local single-run report. Test/CI only — no contract behaviour change; also fixes a test-isolation leak in the registry unit suite by adding a `resetOrchestratorLibraryAddresses()` helper that clears the shared orchestrator-library singleton between runs.

.github/workflows/100-flow-ats-test.yaml

Lines changed: 244 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ on:
55
branches: [main]
66
pull_request:
77
paths:
8-
- "packages/ats/**"
9-
- "apps/ats/**"
8+
- "packages/ats/contracts/**"
109
- "package.json"
1110
- ".github/workflows/*ats*.yaml"
1211
workflow_dispatch:
@@ -20,34 +19,206 @@ permissions:
2019

2120
jobs:
2221
test-ats:
23-
name: testing
22+
name: contracts scripts tests
2423
runs-on: token-studio-linux-large
2524
timeout-minutes: 45
2625
env:
2726
CONTRACT_SIZER_RUN_ON_COMPILE: "false"
2827
REPORT_GAS: "false"
29-
CLIENT_PRIVATE_KEY_ECDSA_1: ${{ secrets.CLIENT_PRIVATE_KEY_ECDSA_1 }}
30-
CLIENT_PUBLIC_KEY_ECDSA_1: ${{ secrets.CLIENT_PUBLIC_KEY_ECDSA_1 }}
31-
CLIENT_ACCOUNT_ID_ECDSA_1: "0.0.1328"
32-
CLIENT_EVM_ADDRESS_ECDSA_1_CORRECT: "0x97C50bb12E1C6284cF2855cdba95c5D60AEE44CF"
33-
CLIENT_EVM_ADDRESS_ECDSA_1: "0x0000000000000000000000000000000000000530"
34-
CLIENT_PRIVATE_KEY_ECDSA_2: ${{ secrets.CLIENT_PRIVATE_KEY_ECDSA_2 }}
35-
CLIENT_PUBLIC_KEY_ECDSA_2: ${{ secrets.CLIENT_PUBLIC_KEY_ECDSA_2 }}
36-
CLIENT_ACCOUNT_ID_ECDSA_2: "0.0.2168740"
37-
CLIENT_EVM_ADDRESS_ECDSA_2: "0x00000000000000000000000000000000002117A4"
38-
FACTORY_ADDRESS: "0.0.5480051"
39-
RESOLVER_ADDRESS: "0.0.5479997"
40-
FIREBLOCKS_HEDERA_ACCOUNT_ID: "0.0.2168740"
41-
FIREBLOCKS_HEDERA_PUBLIC_KEY: ${{ secrets.CLIENT_PUBLIC_KEY_ECDSA_2 }}
42-
DFNS_HEDERA_ACCOUNT_ID: "0.0.2168740"
43-
DFNS_WALLET_PUBLIC_KEY: ${{ secrets.CLIENT_PUBLIC_KEY_ECDSA_2 }}
44-
AWS_KMS_HEDERA_ACCOUNT_ID: "0.0.4394946"
45-
AWS_KMS_HEDERA_PUBLIC_KEY: "302d300706052b8104000a03220003ee815bb9b5e53f5dbe7264a77e586127dfcb75da8c1246f5aa6ededdb13e6c21"
46-
REACT_APP_MIRROR_NODE: "https://testnet.mirrornode.hedera.com/api/v1/"
47-
REACT_APP_RPC_NODE: "https://testnet.hashio.io/api"
48-
REACT_APP_RESOLVER: "0.0.5479997"
49-
REACT_APP_FACTORY: "0.0.5480051"
50-
REACT_APP_SHOW_DISCLAIMER: "true"
28+
29+
steps:
30+
- name: Harden Runner
31+
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
32+
with:
33+
egress-policy: audit
34+
35+
- name: Checkout repository
36+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
37+
38+
- name: Setup NodeJS Environment
39+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
40+
with:
41+
node-version-file: .nvmrc
42+
cache: "npm"
43+
44+
# Shared dependency cache across every job in this workflow (and matched by the other ATS
45+
# workflows on the same key), so node_modules is restored instead of reinstalled. On a hit
46+
# the install below is skipped entirely.
47+
- name: Restore dependencies from cache
48+
id: deps-cache
49+
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
50+
with:
51+
path: |
52+
node_modules
53+
*/*/node_modules
54+
*/*/*/node_modules
55+
~/.npm
56+
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}
57+
58+
# --ignore-scripts skips the contracts `prepare` hook (a *production* `hardhat compile`).
59+
# Every job here recompiles in test/coverage mode anyway and regenerates the gitignored
60+
# resolver registry as part of that compile, so the prepare compile is pure duplicated work.
61+
- name: Install dependencies
62+
if: steps.deps-cache.outputs.cache-hit != 'true'
63+
run: npm ci --ignore-scripts
64+
65+
# Contracts integration tests run under coverage in the parallel `coverage-ats` matrix below
66+
# (which doubles as their gate), so this job only runs the scripts/* suite that coverage does
67+
# not exercise. SDK and web are deprecated and no longer tested here.
68+
#
69+
# The scripts suite deploys the system and exercises test-only facets (EvmAccessors,
70+
# MockDiamondCut, …), so it needs a TEST-MODE compile — which also regenerates the test-mode
71+
# resolver registry — before the run. `--force` is mandatory: without it Hardhat sees no
72+
# source change and skips, leaving a registry with no key for EvmAccessorsFacet.
73+
- name: Run contracts scripts tests
74+
working-directory: packages/ats/contracts
75+
env:
76+
ATS_TEST_MODE: "true"
77+
run: |
78+
npm run compile:force
79+
npm run test:scripts
80+
81+
# Contracts coverage is sharded across parallel runners: solidity-coverage re-instruments and
82+
# recompiles per shard (unavoidable on Hardhat 2), but each shard runs on its own runner so the
83+
# ~3 min compile overlaps instead of serializing. Each shard runs the mega-asset entry once for
84+
# its `i % SHARD_TOTAL` slice plus a round-robined share of the standalone suites, computed by
85+
# scripts/tools/coverage-shard/planShard.ts. Shards upload their lcov as artifacts; the
86+
# merge-coverage job below combines them into a single report and uploads it once to Codecov.
87+
coverage-ats:
88+
name: coverage (shard ${{ matrix.shard }})
89+
runs-on: token-studio-linux-large
90+
timeout-minutes: 45
91+
strategy:
92+
fail-fast: false
93+
matrix:
94+
# Keep this list's length equal to SHARD_TOTAL below (indices 0..N-1).
95+
shard: [0, 1, 2, 3]
96+
env:
97+
CONTRACT_SIZER_RUN_ON_COMPILE: "false"
98+
REPORT_GAS: "false"
99+
SHARD_INDEX: ${{ matrix.shard }}
100+
SHARD_TOTAL: "4"
101+
102+
steps:
103+
- name: Harden Runner
104+
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
105+
with:
106+
egress-policy: audit
107+
108+
- name: Checkout repository
109+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
110+
111+
- name: Setup NodeJS Environment
112+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
113+
with:
114+
node-version-file: .nvmrc
115+
cache: "npm"
116+
117+
- name: Restore dependencies from cache
118+
id: deps-cache
119+
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
120+
with:
121+
path: |
122+
node_modules
123+
*/*/node_modules
124+
*/*/*/node_modules
125+
~/.npm
126+
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}
127+
128+
- name: Install dependencies
129+
if: steps.deps-cache.outputs.cache-hit != 'true'
130+
run: npm ci --ignore-scripts
131+
132+
- name: Run contracts coverage shard
133+
run: |
134+
npm run test:coverage:shard --workspace=packages/ats/contracts 2>&1 | tee shard.log
135+
# Record this shard's passing-test count (ANSI-stripped) for the merge-time reconciliation
136+
# that proves the sharded runs executed every test — see the merge-coverage job.
137+
sed -E 's/\x1b\[[0-9;]*m//g' shard.log | grep -oE '[0-9]+ passing' | head -1 | grep -oE '^[0-9]+' \
138+
> packages/ats/contracts/coverage/shard-count.txt
139+
echo "shard ${SHARD_INDEX} passing: $(cat packages/ats/contracts/coverage/shard-count.txt)"
140+
141+
- name: Upload coverage shard artifact
142+
if: ${{ !cancelled() }}
143+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
144+
with:
145+
name: coverage-shard-${{ matrix.shard }}
146+
path: |
147+
packages/ats/contracts/coverage/lcov.info
148+
packages/ats/contracts/coverage/shard-count.txt
149+
if-no-files-found: error
150+
retention-days: 1
151+
152+
# Sequential oracle for the trust check: run the integration suite in ONE pass and record its
153+
# passing-test count. The merge job compares this against the sum of the coverage shards' counts to
154+
# prove the sharding/balancing dropped no test. Integration-only (NOT the scripts suite, which the
155+
# coverage shards don't run), and excludes the local-parallel `ats.shard.*` files. Runs in parallel
156+
# with the coverage matrix, so it adds no critical-path time.
157+
verify-test-count:
158+
name: verify test count
159+
runs-on: token-studio-linux-large
160+
timeout-minutes: 45
161+
env:
162+
CONTRACT_SIZER_RUN_ON_COMPILE: "false"
163+
REPORT_GAS: "false"
164+
165+
steps:
166+
- name: Harden Runner
167+
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
168+
with:
169+
egress-policy: audit
170+
171+
- name: Checkout repository
172+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
173+
174+
- name: Setup NodeJS Environment
175+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
176+
with:
177+
node-version-file: .nvmrc
178+
cache: "npm"
179+
180+
- name: Restore dependencies from cache
181+
id: deps-cache
182+
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
183+
with:
184+
path: |
185+
node_modules
186+
*/*/node_modules
187+
*/*/*/node_modules
188+
~/.npm
189+
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}
190+
191+
- name: Install dependencies
192+
if: steps.deps-cache.outputs.cache-hit != 'true'
193+
run: npm ci --ignore-scripts
194+
195+
- name: Run the integration suite sequentially (oracle)
196+
working-directory: packages/ats/contracts
197+
run: |
198+
npm run test:integration 2>&1 | tee oracle.log
199+
sed -E 's/\x1b\[[0-9;]*m//g' oracle.log | grep -oE '[0-9]+ passing' | head -1 | grep -oE '^[0-9]+' \
200+
> oracle-count.txt
201+
echo "sequential integration passing: $(cat oracle-count.txt)"
202+
203+
- name: Upload oracle count
204+
if: ${{ !cancelled() }}
205+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
206+
with:
207+
name: oracle-count
208+
path: packages/ats/contracts/oracle-count.txt
209+
if-no-files-found: error
210+
retention-days: 1
211+
212+
# Merge the per-shard lcov reports into a single report and upload it once to Codecov — matching
213+
# the local `npm run test:coverage` single report, instead of uploading partial per-shard pieces.
214+
# Also reconciles the sharded test count against the sequential oracle. Runs only when every shard
215+
# and the oracle succeeded, so the merged report is complete.
216+
merge-coverage:
217+
name: merge coverage + upload
218+
needs: [coverage-ats, verify-test-count]
219+
runs-on: token-studio-linux-large
220+
timeout-minutes: 15
221+
env:
51222
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
52223

53224
steps:
@@ -65,33 +236,58 @@ jobs:
65236
node-version-file: .nvmrc
66237
cache: "npm"
67238

239+
- name: Restore dependencies from cache
240+
id: deps-cache
241+
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
242+
with:
243+
path: |
244+
node_modules
245+
*/*/node_modules
246+
*/*/*/node_modules
247+
~/.npm
248+
key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}
249+
68250
- name: Install dependencies
69-
run: npm run ats:install
70-
71-
# BBND-1766: `atsRegistry.generated.ts` is gitignored and regenerated
72-
# by the contracts package's `prepare` script during `npm ci`. This
73-
# step is the clean-checkout guard for that contract — if the file is
74-
# missing after install, `prepare` regressed and every downstream
75-
# build / test step would fail with a confusing module-not-found.
76-
- name: Verify auto-generated registry produced by prepare hook
251+
if: steps.deps-cache.outputs.cache-hit != 'true'
252+
run: npm ci --ignore-scripts
253+
254+
- name: Download all coverage shard artifacts
255+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
256+
with:
257+
pattern: coverage-shard-*
258+
path: coverage-shards
259+
260+
- name: Download sequential oracle count
261+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
262+
with:
263+
name: oracle-count
264+
path: oracle
265+
266+
# Trust check: the sum of the coverage shards' passing-test counts must equal the sequential
267+
# single-pass count, proving the sharding/balancing ran every test (none dropped or doubled).
268+
- name: Reconcile sharded vs sequential test count
77269
run: |
78-
REGISTRY_PATH="packages/ats/contracts/scripts/domain/atsRegistry.generated.ts"
79-
if [[ ! -f "${REGISTRY_PATH}" ]]; then
80-
echo "::error::${REGISTRY_PATH} missing after npm ci. The contracts package's 'prepare' script (hardhat compile) likely regressed."
81-
exit 1
82-
fi
83-
if ! grep -q "^export const FACET_REGISTRY" "${REGISTRY_PATH}"; then
84-
echo "::error::${REGISTRY_PATH} missing FACET_REGISTRY export — generator produced a malformed file."
270+
oracle=$(cat oracle/oracle-count.txt)
271+
sharded=$(find coverage-shards -name shard-count.txt -exec cat {} + | awk '{s += $1} END {print s + 0}')
272+
echo "sequential integration : ${oracle} passing"
273+
echo "Σ coverage shards : ${sharded} passing"
274+
if [[ "${oracle}" != "${sharded}" ]]; then
275+
echo "::error::Test-count mismatch — sequential ${oracle} vs sharded ${sharded}. A suite may be dropped or double-run by the sharding."
85276
exit 1
86277
fi
87-
echo "✓ ${REGISTRY_PATH} auto-regenerated by prepare hook"
278+
echo "✅ all tests accounted for (${oracle})"
88279
89-
- name: Build ATS packages
90-
run: npm run ats:build
280+
# Sum line/function/branch hits across the disjoint shards into one lcov, written to the same
281+
# path a local single-run coverage produces, so the Codecov upload is identical to local. Uses
282+
# the in-repo merger (preserves function coverage, which lcov-result-merger drops; no
283+
# runtime-fetched dependency).
284+
- name: Merge shard lcov reports
285+
working-directory: packages/ats/contracts
286+
run: npm run coverage:merge -- coverage/lcov.info "${GITHUB_WORKSPACE}/coverage-shards"
91287

92-
- name: Run ATS tests
93-
run: npm run ats:test:ci
94-
95-
- name: Upload coverage report
96-
if: ${{ !cancelled() && always() }}
288+
- name: Upload merged coverage to Codecov
97289
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v5.4.0
290+
with:
291+
token: ${{ secrets.CODECOV_TOKEN }}
292+
files: packages/ats/contracts/coverage/lcov.info
293+
disable_search: true

packages/ats/contracts/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,10 @@
127127
"compile:forceBuild": "npx hardhat compile --force && npm run build",
128128
"typechain": "npx hardhat typechain",
129129
"clean:cache": "npx hardhat clean",
130-
"test": "bash -c 'npx hardhat test $(find test/contracts/integration test/scripts -name \"*.test.ts\")'",
131-
"test:parallel": "bash -c 'NODE_OPTIONS=\"--import tsx\" npx hardhat test --parallel $(find test/contracts/integration test/scripts -name \"*.test.ts\")'",
130+
"test": "bash -c 'npx hardhat test $(find test/contracts/integration test/scripts -name \"*.test.ts\" -not -name \"ats.shard.*.test.ts\")'",
131+
"test:parallel": "bash -c 'ATS_TEST_MODE=true NODE_OPTIONS=\"--import tsx\" npx hardhat test --parallel $(find test/contracts/integration test/scripts -name \"*.test.ts\" -not -name \"ats.test.ts\")'",
132+
"test:parallel:ats": "bash -c 'ATS_TEST_MODE=true NODE_OPTIONS=\"--import tsx\" npx hardhat test --parallel $(find test/contracts/integration -name \"ats.shard.*.test.ts\")'",
133+
"test:integration": "bash -c 'npx hardhat test $(find test/contracts/integration -name \"*.test.ts\" -not -name \"ats.shard.*.test.ts\")'",
132134
"test:contracts": "npx hardhat test",
133135
"test:contracts:parallel": "NODE_OPTIONS='--import tsx' npx hardhat test --parallel",
134136
"test:scripts": "bash -c 'npx hardhat test --no-compile $(find test/scripts -name \"*.test.ts\")'",
@@ -143,11 +145,14 @@
143145
"test:factory": "npx hardhat test test/contracts/integration/factory/factory.test.ts",
144146
"test:resolver": "npx hardhat test test/contracts/integration/resolver/BusinessLogicResolver.test.ts",
145147
"test:coverage": "npm run test:coverage:all",
146-
"test:coverage:all": "npx hardhat coverage --testfiles 'test/contracts/integration/**/*.ts'",
148+
"test:coverage:all": "SHARD_TOTAL=1 SHARD_INDEX=0 npm run test:coverage:shard",
147149
"test:coverage:layer1": "npx hardhat coverage --testfiles 'test/contracts/integration/layer_1/**/*.ts'",
148150
"test:coverage:factory": "npx hardhat coverage --testfiles 'test/contracts/integration/factory/**/*.ts'",
149151
"test:coverage:resolver": "npx hardhat coverage --testfiles 'test/contracts/integration/resolver/**/*.ts'",
150152
"test:coverage:resolverProxy": "npx hardhat coverage --testfiles 'test/contracts/integration/resolverProxy/**/*.ts'",
153+
"test:coverage:shard": "npx tsx scripts/tools/coverage-shard/runShardCoverage.ts",
154+
"test:coverage:parallel": "npx tsx scripts/tools/coverage-shard/runParallelCoverage.ts",
155+
"coverage:merge": "npx tsx scripts/tools/coverage-shard/mergeLcov.ts",
151156
"test:ci": "npm run test",
152157
"size": "npx hardhat size-contracts",
153158
"keccak256": "npx hardhat keccak256",

packages/ats/contracts/scripts/domain/orchestratorLibraries.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import { Signer } from "ethers";
2020
import {
2121
GAS_LIMIT,
22+
gasLimitOverride,
2223
hederaGasOverrides,
2324
info,
2425
retryTransaction,
@@ -328,7 +329,9 @@ export async function deployOrchestratorLibraries(
328329
});
329330

330331
// Phase 1: ScheduledTasksDispatchOps and ClearingReadOps have no library dependencies.
331-
const gasOverrides = { ...hederaGasOverrides(), gasLimit: GAS_LIMIT.high };
332+
// gasLimitOverride (not a fixed gasLimit) so instrumented library deploys under solidity-coverage
333+
// get the higher coverage limit — their bytecode grows past GAS_LIMIT.high when instrumented.
334+
const gasOverrides = { ...hederaGasOverrides(), ...gasLimitOverride(GAS_LIMIT.high) };
332335
const scheduledTasksDispatchOpsAddr = await deployLib("ScheduledTasksDispatchOps", () =>
333336
new ScheduledTasksDispatchOps__factory(signer)
334337
.deploy(gasOverrides)

packages/ats/contracts/scripts/infrastructure/utils/transaction.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ import { info, warn, debug } from "./logging";
2525
* Under solidity-coverage (`COVERAGE=true`), contracts are instrumented and grow
2626
* substantially, so we use a higher fixed limit instead of auto-estimation — the
2727
* auto-estimate is based on the unmodified bytecode and will be too low.
28-
* 30M sits well within the 300M blockGasLimit configured for coverage runs.
28+
* 100M gives headroom for the heaviest instrumented deploy tx (the v8.0.0 system
29+
* deploy exceeded the previous 30M) while staying well under the 300M coverage blockGasLimit.
2930
*/
3031
export function gasLimitOverride(limit: number): { gasLimit: number } {
31-
return { gasLimit: process.env.COVERAGE ? 30_000_000 : limit };
32+
return { gasLimit: process.env.COVERAGE ? 100_000_000 : limit };
3233
}
3334

3435
/**

0 commit comments

Comments
 (0)