Skip to content

Add E2E test suite CI job with code coverage#129

Merged
mudigal merged 43 commits into
devfrom
feat/e2e-coverage
Jun 5, 2026
Merged

Add E2E test suite CI job with code coverage#129
mudigal merged 43 commits into
devfrom
feat/e2e-coverage

Conversation

@mudigal

@mudigal mudigal commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add a comprehensive E2E test suite (10 workflows, 95 tests) that exercises the full storage pallet lifecycle against a real chain + provider node, with dual JS/Rust code coverage.

Chain infrastructure: dev mode instead of zombienet

The E2E job runs polkadot-omni-node in standalone dev mode (--dev-block-time 2000) instead of spawning a full zombienet relay+parachain network. This is a strategic change:

  • No relay chain needed — blocks are produced and finalized instantly by a single node
  • 2-second block times — each extrinsic finalizes in ~2s vs ~12s with zombienet
  • Deterministic finality — no relay chain consensus delays; blocks finalize immediately
  • ~5x faster E2E runs — full suite completes in ~8 minutes vs ~40+ with zombienet
  • Simpler CI setup — single binary, no relay chain binary, no zombienet config

The start-e2e-chain composite action generates a chain spec via chain-spec-builder and launches polkadot-omni-node --alice --tmp --dev-block-time 2000.

Test execution model

  • Sequential runner (e2e/runner.js): discovers NN-*.js files, runs each as a child process with 10-minute timeout
  • Shared chain state: tests run sequentially on the same chain, so later workflows see state from earlier ones (e.g., test 08 sees agreements created in test 01)
  • Single provider node: only Alice's provider runs (with agreement + checkpoint coordinators), which means tests needing other providers must manually accept agreements
  • JS coverage via c8 (V8 native): measures api.js and common.js coverage
  • Rust coverage via LLVM: provider-node built with -C instrument-coverage, profraw merged after graceful SIGTERM

Test workflows

# Workflow Tests What it covers
01 Provider Registration 12 Register, update settings, add stake, multiaddr, matching, failure cases
02 Agreement Lifecycle 12 Request, accept (auto), reject, withdraw, end (Pay/Burn), failure cases
03 S3 Bucket & Objects 11 Create/delete S3 buckets, put/get/copy/delete objects, upsert, paths
04 Data Upload & Retrieval 12 Small/medium/max chunks, binary data, S3 PUT/GET/HEAD/LIST/DELETE, blake2-256
05 Checkpoint & Challenges 7 Client/provider checkpoints, off-chain/on-chain challenges, rewards
06 Bucket Membership ACL 10 Add/remove members, promote/demote roles, self-demotion, authorization
07 Agreement Extensions 7 Extend duration, top up bytes, block extensions (per-agreement & global)
08 Provider Deregistration 8 Announce/cancel deregistration, active agreements, re-registration
09 Drive Lifecycle 7 Create/share/unshare/delete drives, authorization
10 Edge Cases 9 Balance accounting, capacity tracking, freeze semantics, data integrity

Bug fixes discovered during E2E development

Fixed 8 bugs in test logic and shared utilities:

  1. TDZ in waitForNextBlock/waitForBlock (common.js): ReplaySubject fired callback synchronously before const sub was assigned
  2. Process hang after tests: unhandled rejection skipped papi.destroy(), open WebSocket kept Node alive
  3. Wrong field names in createBucketWithStorage callers: passed max_capacity/max_payment but pallet expects max_bytes/max_price_per_byte
  4. Test 6.4 cross-admin demotion: pallet only allows self-demotion, test had admin demoting another admin
  5. setExtensionsBlocked API: passed extra provider param pallet doesn't accept, and signed as client instead of provider
  6. Test 8.4 auto-accept: relied on Charlie's provider node (not running) — switched to manual acceptAgreement
  7. Test 7.1 extend duration: extend_agreement resets expires_at = current_block + additional_duration (not additive) — extDuration must exceed original duration
  8. Test 2.6 Burn encoding: Enum("Burn") missing required burn_percent field for EndAction::Burn { burn_percent: u8 }

Other changes

  • Removed superseded standalone demo scripts (bucket-membership, bucket-with-storage, checkpoint-rewards, drive-lifecycle, s3-lifecycle)
  • Extracted CI composite actions: start-e2e-chain, start-zombienet, wait-for-parachain, wait-for-provider-health
  • Added just e2e and just e2e-single recipes

Test plan

  • All 10 E2E workflows pass (95 tests total)
  • JS coverage text summary appears in CI logs
  • Rust provider coverage text summary appears in CI logs
  • js-coverage-* and rust-coverage-* artifacts downloadable from Actions run
  • Existing integration-tests and sc-integration-tests jobs still pass unaffected

Closes #126

mudigal added 2 commits June 3, 2026 16:07
Add a dedicated `e2e-integration-tests` CI job that runs all 10 E2E
workflows against an inmemory provider, replacing the individual demo
script invocations that the suite supersedes.

JS coverage uses c8 (V8 native) to measure api.js and common.js
coverage. Rust coverage uses LLVM instrument-coverage on the provider
binary built into a separate target-cov/ directory; profraw data is
merged after graceful SIGTERM shutdown. Both text summaries print to
CI logs and artifacts are uploaded for downstream consumption.

Remove the standalone demo scripts (bucket-membership, bucket-with-
storage, checkpoint-rewards, drive-lifecycle, s3-lifecycle) and their
justfile recipes, as the E2E suite covers these flows.

Closes #126
Add the 10 E2E workflow files, runner, and helpers that the
e2e-integration-tests CI job executes. Also include the new
extrinsic wrappers in api.js that the workflows depend on.
Comment thread examples/papi/e2e/02-agreement-lifecycle.js Fixed
Comment thread examples/papi/e2e/02-agreement-lifecycle.js Fixed
Comment thread examples/papi/e2e/04-data-upload-and-retrieval.js Fixed
Comment thread examples/papi/e2e/04-data-upload-and-retrieval.js Fixed
Comment thread examples/papi/e2e/08-provider-deregistration.js Fixed
Comment thread examples/papi/e2e/08-provider-deregistration.js Fixed
Comment thread examples/papi/e2e/05-checkpoint-and-challenges.js Fixed
Comment thread examples/papi/e2e/07-agreement-extensions-topup.js Fixed
Comment thread examples/papi/e2e/07-agreement-extensions-topup.js Fixed
Comment thread examples/papi/e2e/09-drive-lifecycle.js Fixed
Comment thread .github/workflows/integration-tests.yml Outdated
mudigal added 2 commits June 3, 2026 19:58
…tions

E2E test fixes:
- Fix TDZ crash in waitForNextBlock/waitForBlock (ReplaySubject fires
  synchronously before const sub is assigned)
- Add .catch/.finally to main() in all 10 workflows to ensure clean
  process exit and prevent 10-minute timeout hangs
- Fix wrong field names in createBucketWithStorage callers: max_capacity
  → max_bytes, max_payment → max_price_per_byte
- Fix test 6.4: use self-demotion (pallet enforces ensure!(member==who))
- Fix extend_agreement callers: duration → additional_duration
- Fix setExtensionsBlocked: remove extra provider param, use provider
  signer instead of client signer
- Fix test 8.4: manually accept agreement instead of waiting for
  auto-accept (Charlie has no provider node running)

CI workflow DRY-up:
- Extract start-zombienet composite action (3 copies → 1)
- Extract wait-for-parachain composite action (3 copies → 1)
- Extract wait-for-provider-health composite action (4 copies → 1)
Remote had added wait-for-parachain inline; keep our start-zombienet
composite action and deduplicate the wait-for-parachain step.
Comment thread examples/papi/e2e/08-provider-deregistration.js Fixed
- 02: remove unused UNIT, use balBefore in reject assertion
- 04: remove unused putChunk, providerFetch imports
- 05: remove unused sameAddress import
- 07: remove unused waitForBlock, getFree imports
- 08: remove unused UNIT, completeDeregister, registerProvider
- 09: remove unused fmtRole import
Comment thread examples/papi/e2e/10-edge-cases-and-adversarial.js Fixed
Comment thread examples/papi/e2e/10-edge-cases-and-adversarial.js Fixed
Comment thread examples/papi/e2e/10-edge-cases-and-adversarial.js Fixed
mudigal added 5 commits June 3, 2026 21:16
…5x speedup

- submitTx now delegates to waitForTransaction(TX_MODE_IN_BLOCK) instead
  of signAndSubmit (which waits for finalization ~36s per tx)
- submitTxExpectFailure uses signSubmitAndWatch with txBestBlocksState
- waitForNextBlock/waitForBlock use bestBlocks$ instead of finalizedBlock$
- Fix test 4.7 S3 list: data.objects -> data.contents matching ListResult
Best-block confirmation is too fast — subsequent api.query calls read
from finalized state and don't see the tx's state changes yet. Switch
submitTx back to TX_MODE_FINALIZED_BLOCK (still using the cleaner
observable-based waitForTransaction). Keep submitTxExpectFailure and
waitForNextBlock/waitForBlock at best-block for speed since they don't
depend on post-tx state reads.
Skip the relay chain entirely for E2E tests — run polkadot-omni-node
directly with --dev-block-time 2000. Blocks finalize instantly, cutting
per-tx wait from ~18-36s (relay finalization) to ~2-4s.

- Add start-e2e-chain composite action (generates spec + runs omni-node)
- Add chain_spec_script field to runtimes-matrix.json
- Add just start-e2e-chain for local dev
- Remove zombienet dependency from e2e-integration-tests job
omni-node refuses to start without a stable network key. Since E2E uses
--tmp (ephemeral), pass --unsafe-force-node-key-generation to skip.
- 7.1: increase extDuration to 200 (> original 100) since extend_agreement
  resets expires_at = current_block + additional_duration, not additive
- 8.1-8.3: use Ferdie (fresh provider, no prior agreements) instead of
  Charlie who accumulates committed_bytes from earlier workflows
- 10.2: increase duration to 10,000 so reserved payment dwarfs tx fees
- 10.5: add checkpoint before freeze (pallet requires snapshot) and fix
  frozen field assertion to use frozen_start_seq
Comment thread examples/papi/e2e/10-edge-cases-and-adversarial.js Fixed
mudigal and others added 6 commits June 4, 2026 09:29
- 2.3: compare balance after reject vs after request (not vs before),
  since client paid tx fees for create_bucket + request_primary_agreement
- 2.6: pass burn_percent to Enum("Burn", { burn_percent: 100 }) since
  the pallet's EndAction::Burn variant requires this field
- api.js: add optional actionValue param to endAgreement for enum data
- Run E2E tests against web3-storage-paseo only (storage pallet config
  is identical across runtimes — running both just doubles CI time)
- Add dedicated "Print JS coverage" step so metrics are visible in CI
  logs instead of buried in 500+ lines of test output
- Drop runtime-matrix dependency since E2E job no longer needs it
- Export LLVM_PROFILE_FILE explicitly in shell script instead of relying
  on step-level env (may not propagate to nohup'd background process in
  container environments)
- Add diagnostics: log provider PID at start, list coverage files and
  process state before generating report
- Add else branches with clear warnings when profraw/llvm-tools missing
- Send SIGKILL as fallback if provider doesn't exit after SIGTERM
Test 2.6: payment_locked (price_per_byte × max_bytes × duration) was
10,485,760 with price=1, 1MiB, 10 blocks — below the existential
deposit (1,000,000,000). The treasury transfer fails with
Token::BelowMinimum when the treasury account doesn't exist yet.
Fix: use 1 GiB max_bytes so payment_locked = 10,737,418,240 > ED.

Test 10: remove unused sameAddress import, charlie variable, and
balBefore declaration flagged by CodeQL.
- Use inline env var (LLVM_PROFILE_FILE=... nohup) instead of export
  to guarantee the env var reaches the backgrounded process
- Save provider PID to GITHUB_ENV and use kill -TERM $PID directly
  instead of pkill -f which is unreliable in containers
- Remove %m from profraw filename (unnecessary for single binary)
- Add binary instrumentation verification after build (nm check)
- Verify LLVM_PROFILE_FILE in /proc/PID/environ after start
- Wait up to 15s for graceful exit before SIGKILL
- Search /tmp and cwd for stray profraw files
- Better diagnostics on failure (provider log tail)
…allel

The integration-tests and sc-integration-tests jobs each rebuilt the runtime
and provider before running their demos. Factor the shared compilation into a
single 'build' job and have both consume its artifact:

  build                 Compile the COMMON stuff once: every runtime wasm
                        (looping the runtimes matrix file, for zombienet's
                        chain_spec_command) and the provider node, uploaded as
                        one 'build' artifact.
  integration-tests     Download the artifact, spawn its own chain + inmemory
                        and disk providers, run the L0 / fs / s3 / papi demos.
                        fs/s3 still 'cargo run' their examples (incremental off
                        the restored build cache).
  sc-integration-tests  Download the artifact, install solc/resolc + build the
                        example contracts, spawn its own chain + inmemory
                        provider, run the smart-contract demos.

The two test jobs run in parallel and skip the runtime/provider build. Demos
stay sequential within each job (drain-tx-pool-then preserved).
@mudigal mudigal enabled auto-merge (squash) June 4, 2026 09:39
bkontur added 7 commits June 4, 2026 11:52
Reflect the build-once + sharded-matrix CI refactor onto feat/e2e-coverage:

- New 'build' job compiles every matrix runtime wasm + the provider once and
  publishes them as a single 'build' artifact.
- integration-tests and sc-integration-tests drop their build steps and consume
  the artifact, running in parallel (timeout 30m).
- e2e-integration-tests consumes the artifact's runtime wasm but still builds
  its own coverage-instrumented provider.
- Existing composite actions (start-zombienet, start-e2e-chain,
  wait-for-parachain, wait-for-provider-health) and the e2e coverage job are
  preserved.
Eliminate the local/CI divergence and make the matrix's chain_spec_script
field load-bearing instead of metadata-only:

- justfile start-e2e-chain takes a RUNTIME arg (default web3-storage-paseo,
  matching CI) and reads build_command + chain_spec_script from
  scripts/runtimes-matrix.json, so a local run exercises the same runtime and
  spec as CI instead of hardcoding build-runtime + build-chain-spec.sh.
- The e2e CI job resolves chain_spec_script from the matrix (by runtime name)
  into E2E_CHAIN_SPEC_SCRIPT and passes it to start-e2e-chain, instead of
  hardcoding scripts/build-paseo-chain-spec.sh.
The build-once artifact uploads both target/release/storage-provider-node
and the runtime wasm under target/release/wbuild, so upload-artifact roots
the archive at their least common ancestor (target/release) and strips that
prefix. The test jobs downloaded into '.', landing the binary at
./storage-provider-node, so 'chmod +x target/release/storage-provider-node'
failed with 'No such file or directory'. Download into target/release so the
stripped prefix is restored where chmod, the chain_spec_command scripts, and
the demos expect it.

Also upload only the final *.compact.compressed.wasm per runtime instead of
the entire wbuild Cargo scratch tree, cutting the artifact from ~927 MiB /
3453 files to a handful of files and making the download/extract near-instant.
LCA stays target/release, so the download fix covers both.
The e2e-integration-tests job never consumed the shared build artifact: its
provider is built here with coverage instrumentation (target-cov), and its
runtime wasm is rebuilt by the chain-spec script at chain start. The download
step pulled the artifact into '.' (where nothing read it) and 'needs: build'
serialized the job behind the build for no benefit.

Drop both, and correct the misleading comment. The job now runs in parallel
with build. The build artifact is still consumed by integration-tests and
sc-integration-tests, and build remains in the completion gate's needs.
The header comment claimed the build artifact carries the fs/s3 example
binaries and the Solidity contracts and that the test jobs compile nothing.
Neither is true: the artifact holds only the provider binary and the
compressed runtime wasm, the fs/s3 demos still 'cargo run' their example
crates, and the sc job still installs solc/resolc and builds contracts.
Rewrite both comments to describe what actually happens and note the test
jobs lean on the shared Rust cache.
bkontur added 5 commits June 4, 2026 13:44
…at/e2e-coverage

# Conflicts:
#	.github/workflows/integration-tests.yml
#	examples/papi/common.js
submitTx now resolves at in-block inclusion rather than finalization, so a
demo that reads state right after a write was racing the ~6-block finality
lag: storage reads default to the finalized head, so the just-included write
wasn't visible yet (e.g. full-flow's fetchChallengeProof failing with
"No challenges at deadline").

Point every post-write read in the PAPI demos at the best (non-finalized)
block via a single shared READ_OPTS = { at: "best" } exported from common.js
-- one switch for the whole suite (flip to "finalized" there to revert).
Covers the CI demos (full-flow/api, s3-lifecycle, drive-lifecycle, sc-flow,
sc-coverage, sc-token-gated, common helpers) plus the non-CI demos
(checkpoint-rewards, checkpoint-missed, bucket-membership, provider-discovery)
for consistency.
…at/e2e-coverage

# Conflicts:
#	examples/papi/bucket-membership.js
#	examples/papi/checkpoint-rewards.js
#	examples/papi/drive-lifecycle.js
#	examples/papi/s3-lifecycle.js
The suite submits txs at in-block (best-block) inclusion, not finalization.
State assertions that read right after submitTx via api.query.*.getValue/
getEntries default to finalized state, which lags inclusion and races the
just-applied change. Append READ_OPTS ({ at: "best" }) to all 54 such reads
across the 10 e2e workflow files + helpers.js, matching what 391f8bf did for
the standalone demos. No assertion, submitTx, or HTTP logic changed.
full-flow's verification used api.event...ChallengeDefended.watch(), which
only observes FINALIZED blocks. With in-block tx submission the two defenses
land in best (not-yet-finalized) blocks, so a count taken right after (even
behind a 3s sleep) read 0 -> "Expected 2 ChallengeDefended events, got 0".

The respond_to_challenge dispatch emits ChallengeDefended in the same block
the tx lands in, so extract it from each response's in-block events via
requireOneEvent instead. Deterministic and race-free; drops the sleep.
Comment thread examples/papi/api.js
Comment thread examples/papi/common.js Outdated
bkontur added 3 commits June 4, 2026 16:14
…engeNotFound

A challenge's id embeds the block height it was created at, and
respond_to_challenge references that exact id. With in-block submission the
creating tx sits in a best block that — even with a single collator — can be
reorged before it's relay-backed/finalized, rolling the challenge out from
under the response (seen as 'respond_to_challenge dispatch failed:
ChallengeNotFound' when the response lands a few blocks later, as with the
slower disk provider).

Add submitTxFinalized (waits for a finalized block) and use it only for the
challenge creators: challengeOffchain + challengeCheckpoint (api.js), and the
challengeCheckpoint precompile call in sc-coverage via a new callContract
{ finalized: true } opt. Everything else stays in-block. Two extra finality
waits per affected demo, still far under the 30-min cap.
Smaller requested duration shortens the 'wait for agreement expiry' step
(expires_at = accept_block + duration). 15 stays >= the provider's
min_duration (10), and max_payment scales automatically off duration.
@socket-security

socket-security Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​c8@​10.1.39910010083100

View full report

Provider coverage:
- Use %c (continuous mode) in LLVM_PROFILE_FILE — profraw is mmap'd
  and flushed in real-time, so coverage data survives SIGKILL. The
  previous approach relied on atexit handlers which never ran because
  the provider didn't exit on SIGTERM within 15s.
- Clean up stray build-step profraw files (default_*.profraw from
  proc-macros/build-scripts) that were polluting the coverage report
  with 0% data.

Pallet coverage:
- Add pallet unit test coverage step. The pallet runs as WASM on-chain
  so E2E tests cannot instrument it. Instead, run
  `cargo test -p storage-provider-pallet --lib` with native
  -C instrument-coverage to get pallet/src/ coverage.
- Generate separate pallet coverage report (pallet-lcov.info).
mudigal and others added 6 commits June 4, 2026 18:14
…erage gates

Split the 3,729-line monolithic tests.rs into 12 focused sub-files under
pallet/src/tests/ for maintainability. Move shared test helpers into mock.rs.

Add coverage regression checks to CI:
- check.yml: pallet-coverage job compares PR vs base branch pallet coverage
- integration-tests.yml: extract coverage summary (pallet, provider, JS),
  cache baseline on main/dev pushes, and coverage-gate job fails PRs if
  any metric decreases
…or provider

Pallet tests run without --release, so the test binary is in
target-cov/debug/deps/, not release/deps/. Also send SIGTERM before
SIGKILL when stopping the provider so the LLVM atexit handler can flush
profraw data when continuous mode (%c) is not supported.
ui-e2e.yml filtered at the WORKFLOW level (on.*.paths), so on PRs that touch
none of those paths the whole workflow was skipped and the required status
check 'UI E2E Tests (chain + provider + UIs)' never reported — leaving such PRs
permanently BLOCKED in branch protection (e.g. CI-only / PAPI-demo changes).

Adopt the same shape ui-checks.yml already uses: drop the top-level paths
filter, detect relevant changes in a 'changes' job, gate the heavy e2e job (now
'UI E2E run') on it, and add an always-running aggregate job that carries the
required context name and is green on skip, red on real failure/cancellation.
Condense the comment blocks across the integration-tests/ui-e2e workflows and
the PAPI demos (READ_OPTS, submitTx/submitTxFinalized, challenge-finalize and
event-read notes) to one or two lines each, keeping the 'why' and dropping the
restated mechanics. Comments only; no behaviour change.
…at/e2e-coverage

# Conflicts:
#	.github/workflows/integration-tests.yml
Comment thread .github/workflows/integration-tests.yml
mudigal added 3 commits June 5, 2026 10:27
…path

- Add git safe.directory in pallet-coverage so git stash/checkout works
  inside the CI container (different user owns the workspace)
- Fix pallet test binary path from release/deps to debug/deps (cargo test
  runs without --release)
- Use SIGTERM before SIGKILL for provider shutdown so LLVM atexit handler
  can flush profraw data
# Conflicts:
#	.github/workflows/integration-tests.yml
#	examples/papi/bucket-membership.js
#	examples/papi/checkpoint-rewards.js
#	examples/papi/drive-lifecycle.js
#	examples/papi/s3-lifecycle.js
@mudigal mudigal disabled auto-merge June 5, 2026 09:05
…in E2E

Move pallet + provider-node coverage to Basic Checks using cargo-llvm-cov
unit tests (reliable, fast). Strip all Rust coverage instrumentation from
the E2E job — no more target-cov, RUSTFLAGS, profraw, or SIGTERM dance.
E2E job now only collects JS c8 coverage. Coverage-gate compares JS only.
@mudigal mudigal merged commit 348a3da into dev Jun 5, 2026
32 checks passed
@mudigal mudigal deleted the feat/e2e-coverage branch June 5, 2026 09:58
@bkontur bkontur mentioned this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add E2E test suite CI job with code coverage

3 participants