Skip to content

feat(test): [Test] Add mutation testing suite for Soroban Smart Contracts using Cargo Mutants - #1053

Merged
Ceejaytech25 merged 2 commits into
ceejaylaboratory:mainfrom
ZuLu0890:feat/issue-1021-test-add-mutation-testing-suite
Sep 2, 2026
Merged

feat(test): [Test] Add mutation testing suite for Soroban Smart Contracts using Cargo Mutants#1053
Ceejaytech25 merged 2 commits into
ceejaylaboratory:mainfrom
ZuLu0890:feat/issue-1021-test-add-mutation-testing-suite

Conversation

@ZuLu0890

Copy link
Copy Markdown
Contributor

Summary

Closes #1021

This PR turns the existing (dormant) mutants.toml into a real, CI-enforced mutation testing pipeline using cargo-mutants for the core Soroban contracts: sep41-token (src/token), staking (src/staking), and liquid_staking (contracts/liquid_staking).

The repo already shipped a mutants.toml, but it was written for an old cargo-mutants config schema and was never auto-discovered (cargo-mutants only reads .cargo/mutants.toml), and no CI ran it. This PR makes mutation testing executable locally and enforced in CI, and hardens the test suites so that no mutants survive.

Mutation testing results

Workspace Mutants tested Caught Unviable Missed Excluded (equivalent)
Root (sep41-token + staking) 101 92 9 0 2
Contracts (liquid_staking) 95 89 6 0 4

Before this PR, the same suites left 18 missed mutants in the root workspace and 14 missed in liquid_staking.

What changed

1. Configuration (mutants.toml, contracts/mutants.toml)

  • Rewrote mutants.toml for the current cargo-mutants schema: examine_globs (file-path globs) instead of the removed string-form examine_re, minimum_test_timeout instead of the removed timeout, and cap_lints = true so deny-level lints don't make mutants unviable.
  • Added contracts/mutants.toml for the separate contracts workspace, scoped to liquid_staking/**.
  • Documented known behaviorally equivalent mutants in each config's exclude_re list (e.g. reward > 0>= 0 guards where a zero-value Stellar Asset Contract transfer is a no-op, and a n < 0 vs n <= 0 sign guard that only differs at n == 0, which early-returns). Each exclusion is commented with its justification so it stays honest.

2. Edge-case tests added to kill surviving mutants

src/token/lib.rs (was 6 missed → 0)

  • test_get_past_balance_no_transfer_history — fallback-to-current-balance and zero-balance paths for get_past_balance
  • test_get_past_balance_snapshots — checkpointing across two transfers at controlled ledger sequences (kills _write_checkpoint removal, !===, >=<, and the return-value substitutions)
  • test_operator_approval — added negative assertions (!is_approved_for_all) before approval is granted

src/staking/lib.rs (was 12 missed → 0)

  • test_pending_rewards_zero_without_stake / test_pending_rewards_after_time — cover the previously untested pending_rewards query (kills return-value and arithmetic mutants)
  • test_stake_accumulates_rewards — second stake after a time advance banks accrued rewards into accumulated_rewards (kills +=-= / *=)
  • test_stake_same_lock_different_tier_keeps_multiplier — staking a second tier with an identical lock duration must not extend the lock or swap the multiplier (kills >>= on the lock-extension guard)
  • test_withdraw_at_exact_lock_end_no_penalty — withdrawing exactly at lock_end applies no penalty (kills <<=)

contracts/liquid_staking/src/lib.rs (was 14 missed → 0)

  • test_i128_to_string_helper / test_u64_to_string_helper — direct unit tests for the string-conversion helpers including negative numbers and zero (kills the negative-path and digit-loop mutants)
  • test_deposit_rewards_with_no_stake — depositing rewards with zero total staked must not touch the rpt accumulator (kills total_staked > 0>= 0, which would divide by zero)
  • test_claim_zero_rewards_emits_no_claim_event — a zero-reward claim must not emit a claimed event (kills reward > 0>= 0 in claim)
  • test_unstake_with_zero_rewards, test_emergency_withdraw_with_zero_fee — zero-amount edge-case behavior tests

3. CI (.github/workflows/rust.yml)

Added a dedicated mutation-testing job that runs both suites and fails the build if any mutant is missed:

  • Root: cargo mutants --config mutants.toml -p sep41-token -p staking -j 2
  • Contracts: cd contracts && cargo mutants --config mutants.toml -p liquid_staking --in-place (--in-place is required because the contracts workspace has a path dependency pointing outside itself)

4. Documentation (CONTRIBUTING.md)

Added a Mutation Testing section under Testing: what it is, how to install cargo-mutants, exact commands for both workspaces, how to interpret missed/unviable, and guidelines for killing mutants with edge-case tests vs. documenting equivalent ones.

Pre-existing CI breakage fixed along the way

Running the full CI suite locally surfaced unrelated breakage on main (all introduced by the "Resolve merge conflicts with main" commit) that was blocking every pipeline. Fixed so CI is green on this PR:

  • src/utils/events.rs — removed a duplicated use soroban_sdk::{…} line that failed clippy with -D warnings
  • src/security_registry/lib.rs — removed two duplicated test functions and replaced assert_eq!(…, true/false) with assert!/assert!(!…) (clippy bool_assert_comparison under -D warnings)
  • src/liquidation/src/lib.rs — fixed create_vault, which did not compile (cannot assign twice to immutable variable): removed the duplicate vault store and the double increment of NextVaultId, restoring the original pre-merge semantics (store at id, bump counter to id + 1, return id)
  • contracts/Cargo.toml — pinned ed25519-dalek = "2.2" in the contracts workspace. The workspace's Cargo.lock is gitignored, so fresh resolution pulls ed25519-dalek 3.x, which is API-incompatible with soroban-env-host 22.x testutils (ChaCha20Rng no longer satisfies the CryptoRng bound) and breaks cargo test -p liquid_staking. Also added the missing testutils::Events trait import that the liquid_staking test module needed to compile.

Evidence of local test runs

  • cargo mutants --config mutants.toml -p sep41-token -p staking -j 2101 mutants: 92 caught, 9 unviable, 0 missed (exit 0)
  • cd contracts && cargo mutants --config mutants.toml -p liquid_staking --in-place95 mutants: 89 caught, 6 unviable, 0 missed (exit 0)
  • cargo test -p sep41-token → 33 passed; cargo test -p staking → 15 passed; cd contracts && cargo test -p liquid_staking → 23 passed
  • cargo clippy -p anchorpoint-utils -p anchorpoint-security-registry -p event-hub --all-targets --all-features -- -D warnings → clean
  • cargo build --target wasm32v1-none --release → clean
  • cargo test -p upgradeable and cargo test -p anchorpoint-utils -p anchorpoint-security-registry -p event-hub --all-features → all passed
  • ./scripts/security-audit.sh --warn-only → completed successfully

Note

  • package-lock.json has unrelated pre-existing working-tree modifications from before this PR and is intentionally not included.
  • cargo test -p liquidation has 4 pre-existing failures in liquidate/partial_liquidate caused by the contract invoking the oracle's get_price with no arguments while the mock oracle expects an asset argument. This is unrelated to the compile fix included here (which only restores create_vault's intended behavior) and is not part of the CI test steps; it is left for a separate fix.

…racts

Add automated mutation testing with cargo-mutants for the core Soroban
contracts (sep41-token, staking, liquid_staking) and enforce it in CI.
The dormant mutants.toml used an outdated cargo-mutants schema and was
never run; rewrite it for the current schema and add a contracts-workspace
config, a CI job that fails on any missed mutant, and CONTRIBUTING
guidelines. Add edge-case tests (balance history, zero amounts, negative
string inputs, lock/withdraw boundaries) so no mutants survive: 101 root
workspace mutants and 95 liquid_staking mutants all caught. Also fix
pre-existing merge-artifact breakage blocking CI: duplicate imports in
src/utils/events.rs, duplicated tests and bool asserts in
src/security_registry/lib.rs, an uncompilable create_vault in
src/liquidation, and pin ed25519-dalek 2.x in the contracts workspace.

Closes ceejaylaboratory#1021

Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
@Ceejaytech25

Copy link
Copy Markdown
Contributor

Nice implementation, LGTM!

…add-mutation-testing-suite

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	contracts/liquid_staking/src/lib.rs
#	src/liquidation/src/lib.rs
@Ceejaytech25
Ceejaytech25 merged commit 0cee176 into ceejaylaboratory:main Sep 2, 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.

[Test] Add mutation testing suite for Soroban Smart Contracts using Cargo Mutants

2 participants