Skip to content

Make unit tests actually execute (and fix the bugs it surfaced)#84

Merged
koko1123 merged 2 commits into
mainfrom
fix/test-harness-execution
Jun 10, 2026
Merged

Make unit tests actually execute (and fix the bugs it surfaced)#84
koko1123 merged 2 commits into
mainfrom
fix/test-harness-execution

Conversation

@koko1123

@koko1123 koko1123 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

The problem

zig build test has never run a single unit-test assertion. The test step aggregated module tests via _ = eth.module field access, which only forces semantic analysis -- it does not collect the per-module test blocks into the test binary. Proof: injecting test { try expect(1 == 2); } into src/hex.zig still exits 0.

So CI has been green-on-compile, not green-on-pass. The 0.16 migration, @Splat work, etc. were validated only insofar as they compiled.

The fix

Root the test artifact at src/root.zig, whose test block direct-imports every module file (_ = @import("hex.zig") -- the form that does collect tests), re-attaching the XKCP/secp256k1 C backends. Delete the now-dead tests/unit_tests.zig.

Turning the suite on revealed 58 problems (20 assertion failures + 38 Debug-only crashes). Breakdown:

Real defects fixed

  • abi_json still used the 0.15 ArrayList(...).init API -- ContractAbi.fromJson was broken on 0.16, only reachable via its never-run test. Migrated to the unmanaged API.
  • rlp.bytesToUint: << 8 didn't compile for small int instantiations -> comptime-guarded the >64-bit slow path.
  • mnemonic.entropyToMnemonic: comptime-value resolution bug + entropy_bytes * 8 u8 overflow (32*8=256) for 24-word phrases.
  • abi_json parseInputs/parseParam: inferred-error-set dependency loop -> explicit ComponentError.
  • rlp struct reflection: Type.Struct.fields was restructured in 0.17-dev -> std.meta.fieldNames (portable across 0.16 and master).
  • Stale test expectations (written, never run, never validated): formatHash/log-address hex-length typos, a static-tuple ABI length (static tuples encode inline = 64, not 96), fixed-size hex length-error mapping.
  • 38 Debug crashes: vendored XKCP/secp256k1 C now built with -fno-sanitize=undefined -- their intentional unaligned u64 loads were tripping the Debug UBSan runtime (the issue reverted in d369bd0). ReleaseFast/production was never affected.

Quarantined (visible skips + tracking issues, not silent passes)

Four genuine pre-existing correctness bugs, marked error.SkipZigTest:

Result

  • 0.16.0: 749 pass, 16 skip, 0 fail (was: assertions never executed)
  • 0.17-dev master: passes (the rlp reflection fix keeps it portable)
  • integration tests still compile; zig fmt clean

This is the highest-leverage correctness fix in the repo: every future test will now actually run.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed ABI tuple size calculation, ArrayList usage, RLP integer/field handling, mnemonic entropy computation, hex-decoding error reporting, and several test-vector expectations.
  • Tests

    • Rewired unit test aggregation to a rooted harness; several flaky/known-broken tests quarantined or skipped and four pre-existing bugs documented.
  • Chores

    • Adjusted build configuration to avoid undefined-behavior instrumentation in vendored cryptographic code.

The test step aggregated module tests via `_ = eth.module` field
access, which only forces semantic analysis -- it never collects the
per-module `test` blocks. zig build test compiled assertions but ran
zero of them (a deliberately injected `expect(1 == 2)` passed). CI has
been green-on-compile, not green-on-pass, the whole time.

Root the test artifact at src/root.zig, whose test block direct-imports
every module file (the form that does collect tests), re-attaching the
XKCP/secp256k1 C backends. Delete the dead tests/unit_tests.zig.

Real defects this surfaced, now fixed:
- abi_json: still used the 0.15 ArrayList(...).init API -> migrated to
  the 0.16 unmanaged API (fromJson was broken on 0.16)
- rlp.bytesToUint: `<< 8` failed to compile for small int types ->
  comptime-guarded the big-int slow path
- mnemonic.entropyToMnemonic: comptime-value resolution + u8 overflow
  (32*8=256) for 24-word phrases -> widen + comptime
- abi_json parseInputs/parseParam: inferred-error-set dependency loop
  -> explicit ComponentError
- rlp struct reflection: Type.Struct.fields changed in 0.17-dev ->
  std.meta.fieldNames (portable across 0.16 and 0.17-dev master)
- stale test expectations: formatHash/log-address length typos, a
  static-tuple ABI length (static tuples encode inline = 64 not 96),
  fixed-size hex length error mapping
- vendored XKCP/secp256k1 C built with -fno-sanitize=undefined so their
  intentional unaligned u64 loads don't trap Debug UBSan (38 crashes)

Quarantined 4 pre-existing correctness bugs as visible skips with
tracking issues: broken pure-Zig Keccak fallback (#80), mulDiv u256
boundary (#81), BIP-39 passphrase seed vector (#82), DEX round-trip
assumption (#83).

Result: 749 pass, 16 skip on 0.16.0; passes on 0.17-dev master.
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eth-zig Ready Ready Preview, Comment Jun 10, 2026 5:38pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8ec5fd2c-4c00-4c3e-8f73-b384dce43715

📥 Commits

Reviewing files that changed from the base of the PR and between 74baf4f and f008fcb.

📒 Files selected for processing (1)
  • src/rlp.zig
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/rlp.zig

📝 Walkthrough

Walkthrough

Reroot unit-test artifact at src/root.zig and reattach vendored C backends with UBSan undefined instrumentation disabled; apply allocator/error-set fixes to ABI JSON parsing, switch RLP struct reflection and add wide-integer decoding support, correct test expectations, and quarantine several flaky/failing tests.

Changes

Unit test infrastructure rework and defect remediation

Layer / File(s) Summary
Build system reworking and sanitizer configuration
build.zig, CHANGELOG.md
Unit-test artifact rerooted to src/root.zig; XKCP and secp256k1 C backends reattached to the fresh module; vendored C sources compiled with -fno-sanitize=undefined.
ABI JSON parsing allocator and error set fixes
src/abi_json.zig
ContractAbi.fromJson now uses allocator-aware std.ArrayList init/append/toOwnedSlice; parseInputs/parseParam use explicit ComponentError to avoid inferred error-set cycles.
RLP struct reflection and bytesToUint wide-integer support
src/rlp.zig
Struct field enumeration switched to std.meta.fieldNames(@typeof(value)); bytesToUint adds a compile-time branch assembling values byte-by-byte when @bitSizeOf(T) > 64.
Supporting semantic fixes
src/hex.zig, src/mnemonic.zig, src/mev_share.zig
hexToBytesFixed maps OutputTooSmall -> InvalidHexLength; entropyToMnemonic makes checksum/word counts comptime; mev_share test uses refAllDecls instead of recursive variant.
Test expectation corrections
src/abi_encode.zig, src/subscription.zig
ABI tuple static encoding expected length changed from 96 to 64 bytes; two subscription test hex-string literals adjusted to match actual formatting.
Pre-existing bugs quarantined
src/dex/v2.zig, src/uint256.zig, src/mnemonic.zig, src/keccak_optimized.zig
Multiple known failing tests are short-circuited to return error.SkipZigTest, quarantining dex inverse, uint256 mulDiv boundary, BIP-39 TREZOR seed-vector, and many keccak reference tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • StrobeLabs/eth.zig#31: Related vendored libsecp256k1 build wiring and sanitizer flag changes in build.zig.
  • StrobeLabs/eth.zig#5: Related hex/rlp behavior and tests that exercise similar parsing/encoding paths.

Poem

🐰 I hopped through tests to find the light,
Rooted the suite and nudged flags right.
Fields enumerated, big ints tamed,
Some skipped for now, their bugs named.
I'll nibble regressions till they're bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and accurately summarizes the main objective: enabling unit tests to execute and fixing the bugs uncovered by that change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/test-harness-execution

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/rlp.zig`:
- Around line 85-90: The .@"struct" branch of encodeInto still iterates using
s.fields; update that loop to use std.meta.fieldNames(`@TypeOf`(value)) like the
other struct path so reflection works on Zig 0.16/0.17. Replace occurrences of
inline for (s.fields) { ... `@field`(value, field_name) ... } with inline for
(comptime std.meta.fieldNames(`@TypeOf`(value))) |field_name| { ... } and ensure
any uses of field lookup or length computation (e.g., encodedLength/@field
calls) use the new field_name variable from std.meta.fieldNames.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f27aa4b-7e06-4a05-9a98-72f16cae1c09

📥 Commits

Reviewing files that changed from the base of the PR and between ecffc74 and 74baf4f.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • build.zig
  • src/abi_encode.zig
  • src/abi_json.zig
  • src/dex/v2.zig
  • src/hex.zig
  • src/keccak_optimized.zig
  • src/mev_share.zig
  • src/mnemonic.zig
  • src/rlp.zig
  • src/subscription.zig
  • src/uint256.zig
  • tests/unit_tests.zig
💤 Files with no reviewable changes (1)
  • tests/unit_tests.zig

Comment thread src/rlp.zig
encodeInto's struct branch still used the pre-0.17 Type.Struct.fields
layout; CI missed it because no running test instantiates that path
with a struct. Use std.meta.fieldNames like the other two sites for
0.16/0.17-dev portability.
@koko1123
koko1123 merged commit 0498fd7 into main Jun 10, 2026
14 checks passed
@koko1123
koko1123 deleted the fix/test-harness-execution branch June 10, 2026 17:40
koko1123 added a commit that referenced this pull request Jun 10, 2026
NonceManager seeds lazily (no RPC in init) from the pending tx count,
then hands out strictly increasing nonces via a lock-free atomic
fetch-and-add. Seeding uses double-checked locking so exactly one
thread performs the RPC; the hot path (next after seeding) takes no
lock. onFailure(nonce) rolls the counter back only when nonce is the
high-water mark (CAS), a documented no-op otherwise since a lower nonce
cannot be safely reused once a higher one is in flight. Optional
Wallet integration via a nullable nonce_manager field; existing
behavior unchanged when null.

Uses std.Io.Mutex (Zig 0.16 moved concurrency under std.Io) via the
default runtime io.

New docs page nonce-manager.mdx. 10 unit tests covering sequencing,
onFailure semantics, peek, and an 8-thread concurrency check -- these
now actually execute under the revived test harness (#84), which
caught two compile errors in the original draft (std.Thread.Mutex and
refAllDeclsRecursive, both gone in 0.16).
koko1123 added a commit that referenced this pull request Jun 10, 2026
The lane-complementing chi optimization (from #22) was wrong: it kept
lanes {1,2,8,12,17,20} complemented across rounds but the chiLane
operation-selection did not correctly maintain that invariant, so
keccak_optimized.keccak256 produced incorrect digests for all inputs.
This was a pure-Zig fallback never wired into keccak.zig (which uses
XKCP), so it went undetected until the test harness was fixed (#84).

Replace the entire lane-complement scheme with the standard chi step
a[i] = b[i] ^ (~b[i+1] & b[i+2]). Modern LLVM lowers ~a & b to a single
ANDN, so there is no meaningful speed loss on the (non-default) fallback
path, and the code is now obviously correct.

All 13 tests pass: pinned keccak256 vectors plus cross-validation
against std.crypto across sizes through 64KB.
koko1123 added a commit that referenced this pull request Jun 10, 2026
…87)

mulDiv(MAX, MAX, MAX) should equal MAX but returned a slightly smaller
value. The overflow path used a limb-native Knuth Algorithm D divider
(divWide) that mis-handled a full multi-limb divisor. Isolated:
mulDiv(MAX, 2, 2) was correct (small divisor) but the all-limbs-set
divisor case was not.

Replace the overflow path's mulWide+divWide with native u512:
product = a*b in u512, quotient = product/denominator, null if it
exceeds u256. Simple and provably correct. The fast u128 path (the
benchmarked 18ns hot path) and the medium u256 path are unchanged --
only the rare a*b-overflows-u256 branch is affected. Removed the now
dead, buggy divWide.

Surfaced by the test-harness fix (#84).
koko1123 added a commit that referenced this pull request Jun 10, 2026
mnemonic.toSeed was correct all along -- it reproduces the canonical
Trezor BIP-39 seed for all-zero entropy + "TREZOR":
c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04
(byte-identical to a direct std.crypto.pwhash.pbkdf2 call).

The quarantined test's expected_first32/last32 were simply wrong (the
second half of first32 read 7e24052f0b7c87c2 instead of the real
9efa3708e5349553). Fixed the expected values and un-quarantined.

So #82 was a bad test, not a code bug. Surfaced by the test-harness
fix (#84) like the rest.
koko1123 added a commit that referenced this pull request Jun 10, 2026
#89)

* Fix getAmountIn round-trip test to use realistic reserves (closes #83)

The getAmountIn/getAmountOut implementations are correct standard
Uniswap V2. The quarantined round-trip test used reserve_out=200e9
(tiny vs reserve_in=100e18): getAmountOut floors a small output,
discarding ~4e8 wei of input-equivalent, which the +1-unit ceiling in
getAmountIn cannot recover -- so recovered_input legitimately falls
~4e8 below amount_in. That is correct AMM behaviour; the test's
within-2-units assumption was wrong for those parameters.

Use balanced reserves (reserve_out=200e18) where the floored output is
large; the round-trip is then exact (diff 0). Documented why the
invariant only holds for adequately-large outputs.

#83 was a bad test, not a code bug -- the last of the four issues the
test-harness fix (#84) surfaced. The unit suite now runs with zero
skips (775/775).

* Address review: reconcile changelog quarantine note (all four resolved)
@koko1123 koko1123 mentioned this pull request Jun 10, 2026
koko1123 added a commit that referenced this pull request Jun 10, 2026
Consolidate the Unreleased section into a clean 0.6.0 entry:
- Added: nonce_manager (#75), mev_share (#34), eth_sendPrivateTransaction
  (#40), MEV-Share backrunner example (#38)
- Changed: benchmark refresh vs alloy 1.6/2.0 (18/26)
- Fixed: the test-harness fix (#84) plus the four issues it surfaced
  (#80-83) and the sse_transport feedLine break

Align versions: build.zig.zon 0.5.0 -> 0.6.0, README/docs install URLs,
SECURITY.md supported versions -> 0.6.x.
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.

1 participant