Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
# Long-running property and fuzz runs. The gate runs the same property tests at
# the default case count on every push; this is where they get hours-of-CPU
# depth, on a schedule and on demand, so a pull request never waits for them.
#
# A failure leaves the shrunk counterexample in the job log (proptest) or the
# failing input as an artifact (Go). Add it to the corpus as a vector
# (conformance/...) and fix the cause; do not just re-run.

name: fuzz

on:
schedule:
- cron: "17 3 * * 1" # Mondays 03:17 UTC
workflow_dispatch:
inputs:
proptest_cases:
description: "PROPTEST_CASES per property"
default: "500000"
go_fuzztime:
description: "go test -fuzztime"
default: "10m"

permissions:
contents: read

concurrency:
group: fuzz-${{ github.ref }}
cancel-in-progress: true

jobs:
proptest:
name: property tests (many cases)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Run the property suite
env:
PROPTEST_CASES: ${{ inputs.proptest_cases || '500000' }}
run: cargo test --release -p cic-primitive-engine --test properties

go-fuzz:
name: go fuzz (FuzzToCanonicalJSON)
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: go
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Fuzz
run: go test -fuzz=FuzzToCanonicalJSON -fuzztime=${{ inputs.go_fuzztime || '10m' }} ./canonical
- name: Keep the failing input
if: failure()
uses: actions/upload-artifact@v4
with:
name: go-fuzz-failing-input
path: go/canonical/testdata/fuzz
if-no-files-found: ignore
47 changes: 45 additions & 2 deletions .github/workflows/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
# The conformance suite checks itself as well as the engine: an empty corpus
# fails, and a group with no accepted vector fails. Both were verified by
# breaking them on purpose.
#
# Beyond the fixed corpora: the Rust property tests (engine/tests/properties.rs)
# run in `cargo test`; the same suite writes a generated corpus that the Go peer
# re-derives (cross-language check below); Go runs with the race detector; and
# both languages publish a coverage summary (informational -- no floor yet, see
# the issue for ratcheting). The long fuzz / high-case-count runs are in fuzz.yml.

name: gate

Expand All @@ -30,10 +36,36 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
components: rustfmt, clippy, llvm-tools-preview
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- run: cargo fmt --all -- --check
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo test --workspace
# Release profile too: debug-only assertions are compiled out there, and
# a test that passes only because of one (e.g. Map::push, #53) would
# otherwise never be seen failing.
- run: cargo test --workspace --release
- name: Cross-language check on generated input
# Rust generates documents (fixed seed) and records the canonical bytes
# and digest its pipeline derives; Go re-derives both. The Go test fails
# when the corpus is set but missing or empty, so this cannot pass by
# the input silently vanishing.
run: |
CIC_PROP_CORPUS_OUT="$RUNNER_TEMP/corpus.jsonl" CIC_PROP_CORPUS_CASES=20000 \
cargo test -p cic-primitive-engine --test properties export_cross_language_corpus
test -s "$RUNNER_TEMP/corpus.jsonl"
cd go && CIC_PROP_CORPUS_IN="$RUNNER_TEMP/corpus.jsonl" go test ./canonical -run CrossLanguage -v
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Coverage (Rust, informational)
run: |
{
echo '### Rust coverage (cargo llvm-cov)'
echo '```'
cargo llvm-cov --workspace --summary-only 2>&1 | grep -E '^(Filename|-|cli/|engine/|TOTAL)'
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

gate-go:
name: gate (go)
Expand All @@ -49,4 +81,15 @@ jobs:
go-version: "1.25"
- run: gofmt -l . | tee /dev/stderr | (! read -r)
- run: go vet ./...
- run: go test ./...
# -race: the Go peer is plain code today, but the packages are meant to be
# imported by services; the detector is free here and expensive to add
# after the first concurrent caller.
- run: go test -race -coverprofile=cover.out ./...
- name: Coverage (Go, informational)
run: |
{
echo '### Go coverage (statements)'
echo '```'
go tool cover -func=cover.out | tail -n 1
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
20 changes: 20 additions & 0 deletions conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,23 @@ integer/float forms at both ordinary and extreme magnitudes, A4's escape
table including the characters RFC 8259 doesn't require escaped, a
canonicalization-stage rejection) so a future Go peer has the same fixtures
to agree with, not just this crate's own unit tests.

## Generated input, not only written vectors

The vectors above pin cases someone thought of. Two further checks cover the
rest (#54):

- `engine/tests/properties.rs` (Rust, `proptest`) checks over generated values
and bytes that canonicalization never panics and writes valid JSON with
sorted, unique keys, ignores insertion order, that the digest is a pure
function of the canonical bytes, that the constructors refuse exactly what
they promise to, and that the reader never panics. It runs in the gate at
the default case count; `fuzz.yml` raises it on a schedule.
- The same suite writes a fixed-seed corpus (`export_cross_language_corpus`)
that `go/canonical/crosslang_test.go` re-derives: for each generated
document the Go peer must produce the same canonical bytes and digest. This
is how the float tie divergence (#57) was found; a counterexample from
either side becomes a vector in `canonicalize/` or `differential/`.
- `go/canonical/fuzz_test.go` is a native Go fuzz target (seeds run in
`go test`; the long run is in `fuzz.yml`).

9 changes: 9 additions & 0 deletions engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,12 @@ sha2 = "0.10"
num-bigint = "0.4"
num-rational = "0.4"
num-traits = "0.2"

[dev-dependencies]
# Property tests (tests/properties.rs): generated Values and inputs, checked
# for the invariants the canonicalizer and comparators promise. serde_json is
# only the independent JSON reader the properties check the output against --
# it is not used by the engine.
proptest = "1"
serde = "1"
serde_json = "1"
Loading
Loading