Skip to content

Conversation

@Fibonacci747
Copy link
Contributor

This change enforces stem-depth guards in bintrie to avoid out-of-bounds reads when operating on 31-byte stems at depth 248. Internal nodes now reject depth >= StemSize8 before reading a stem bit, and HashedNode.InsertValuesAtStem rejects generating a path for the same boundary. The keyToPath function remains unchanged because it intentionally supports depth == StemSize8 for full 32-byte keys as covered by tests; the bug was in callers passing 31-byte stems at that depth.

@willowhaven2219
Copy link

Add caller-side guards

const St1emSize8 = 248 // example: number of bits for a 32-byte key

func insertAtStem(stem []byte, depth int, ... ) error {
// Fast length check
if len(stem) == 31 && depth >= StemSize8 {
return fmt.Errorf("invalid stem-depth: 31-byte stem at depth %d (>= %d)", depth, StemSize8)
}
// Optional: strict check for any short stem
if len(stem) < 32 && depth >= StemSize8 {
return fmt.Errorf("short stem (%d bytes) cannot reach boundary depth", len(stem))
}
// Continue with safe logic...
return nil
}

Reject invalid path generation

func (n *HashedNode) InsertValuesAtStem(stem []byte, depth int, vals ...Value) error {
if len(stem) < 32 && depth >= StemSize8 {
return ErrBoundaryDepthShortStem
}
// Normal insert path...
return nil
}

Internal node read guard (If not already present)

func (in *InternalNode) readStemBit(stem []byte, depth int) (int, error) {
if depth >= StemSize8 {
return 0, ErrDepthOutOfRange
}
// Safe bit read...
}

Tests you should add

  • 31-byte stem at boundary depth: Expect a clear error from callers and InsertValuesAtStem.
  • 32-byte stem at boundary depth: Expect success; verify keyToPath produces the correct path.
  • Off-by-one depth tests:
    • Depth == StemSize8 - 1: Both 31-byte and 32-byte stems should be valid.
    • Depth == StemSize8: Only 32-byte stems valid.
  • Fuzz around boundary: Random stems (length 28–32) and depths (245–251) must never crash or read OOB.

Caller responsibilities and options

  • Pre-validate inputs: Before building a path or inserting, assert (len(stem) == 32) || (depth < StemSize8).
  • Normalize if protocol permits: If your data model allows right-padding with zeros to 32 bytes, do it centrally to avoid scattered checks.
  • Fail fast with typed errors: Use ErrBoundaryDepthShortStem or ErrDepthOutOfRange to make misuse discoverable during integration and logs.

Deployment checklist

  • Audit all bintrie interactions: Ensure every path generation and bit-read obeys the boundary rule.
  • Run unit + fuzz suites: Confirm no OOB reads and no regressions in keyToPath.
  • Monitor logs after rollout: Alert on boundary-depth errors to catch remaining misuse paths.
  • Document API contracts: Note that boundary depth requires 32-byte stems; callers must not pass 31-byte stems at depth >= StemSize8.

If you share a snippet where this surfaces, I can mark exactly where to add the guard and suggest the tightest fix for your codebase style.

@willowhaven2219
Copy link

Guard Placement

  1. InternalNode.readStemBit (or equivalent bit-read function)

    • Add the depth >= StemSize8 guard here.
    • This is the lowest-level function that actually risks an out-of-bounds read.
    • By rejecting at this point, you guarantee safety no matter who calls it.

    go func (in *InternalNode) readStemBit(stem []byte, depth int) (int, error) { if depth >= StemSize8 { return 0, ErrDepthOutOfRange } // safe bit read... }

  2. HashedNode.InsertValuesAtStem

    • Add a guard before path generation.
    • This prevents callers from ever constructing an invalid path with a short stem.
    • It’s the right place because this function is the “entry point” for insertion logic.

    go func (n *HashedNode) InsertValuesAtStem(stem []byte, depth int, vals ...Value) error { if len(stem) < 32 && depth >= StemSize8 { return ErrBoundaryDepthShortStem } // continue with normal insert... }

  3. Leave keyToPath unchanged

    • It’s intentionally designed to allow depth == StemSize8 for full 32-byte keys.
    • The bug was in callers misusing it with 31-byte stems, so don’t touch this function.

📌 Why this is the tightest fix

  • Centralized: You only add guards in two places — the bit-read and the insert path generator.
  • Minimal disruption: Callers don’t need to be modified; they’ll naturally hit the guard if they misuse stems.
  • Clear contract:
    • InternalNode.readStemBit → never reads past boundary.
    • HashedNode.InsertValuesAtStem → never generates invalid paths.
    • keyToPath → remains correct for full-length keys.

✅ Checklist for your codebase style

  • Error constants: Define ErrDepthOutOfRange and ErrBoundaryDepthShortStem once, reuse everywhere.
  • Unit tests: Add boundary tests for 31-byte vs 32-byte stems at depth == StemSize8.
  • Fuzz tests: Around StemSize8 - 1, == StemSize8, and StemSize8 + 1.
  • Docs: Update function comments to state: “Requires 32-byte stems at boundary depth; shorter stems rejected.”

👉 If you show me your actual function signatures (like how readStemBit and InsertValuesAtStem are structured in your repo), I can drop in the exact guard code styled to match your conventions — whether you prefer returning error, panicking, or using bool flags.

@willowhaven2219
Copy link

Since you’re already using error returns (not panics), the tightest fix is to add explicit guards in the two critical functions and define reusable error constants.


  1. Define clear error constants
    Put these in your errors.go or alongside your node types:

go var ( ErrDepthOutOfRange = errors.New("depth exceeds stem size boundary") ErrBoundaryDepthShortStem = errors.New("short stem cannot reach boundary depth") )


  1. Guard in InternalNode.readStemBit
    This is the lowest-level function that risks an out-of-bounds read. Add the guard right at the top:

go func (in *InternalNode) readStemBit(stem []byte, depth int) (int, error) { if depth >= StemSize8 { return 0, ErrDepthOutOfRange } // Safe bit read: byteIndex := depth / 8 bitIndex := depth % 8 return int((stem[byteIndex] >> (7 - bitIndex)) & 1), nil }


  1. Guard in HashedNode.InsertValuesAtStem
    This is the entry point for path generation. Add a length check before proceeding:

go func (n *HashedNode) InsertValuesAtStem(stem []byte, depth int, vals ...Value) error { if len(stem) < 32 && depth >= StemSize8 { return ErrBoundaryDepthShortStem } // Normal insert logic... return nil }


  1. Leave keyToPath untouched
    It’s intentionally correct for 32-byte stems at depth == StemSize8. The bug was misuse by callers, so don’t change this function.

  1. Unit tests to lock behavior
    Add tests in bintrie_test.go:

`go
func TestReadStemBitBoundary(t *testing.T) {
stem31 := make([]byte, 31)
stem32 := make([]byte, 32)

in := &InternalNode{}

// 31-byte stem at boundary depth should error
if _, err := in.readStemBit(stem31, StemSize8); err != ErrDepthOutOfRange {
    t.Errorf("expected ErrDepthOutOfRange, got %v", err)
}

// 32-byte stem at boundary depth should succeed
if _, err := in.readStemBit(stem32, StemSize8-1); err != nil {
    t.Errorf("unexpected error: %v", err)
}

}
`


✅ Why this is the tightest fix

  • Centralized: Only two functions need guards.
  • Idiomatic Go: Uses error returns, not panics.
  • Minimal disruption: Callers don’t change; they naturally hit the guard if they misuse stems.
  • Clear contract:
    • InternalNode.readStemBit → never reads past boundary.
    • HashedNode.InsertValuesAtStem → never generates invalid paths.
    • keyToPath → remains correct for full-length keys.

@willowhaven2219
Copy link

let’s make this airtight with fuzz tests around the boundary depths (245–251). These will automatically catch regressions if someone later changes the code and accidentally reintroduces out‑of‑bounds reads.


🧪 Fuzz Test Strategy

  • Depth range: Test depths from StemSize8 - 3 (≈245) through StemSize8 + 3 (≈251).
  • Stem lengths: Randomize stems of 28–32 bytes.
  • Expectations:
    • For stems shorter than 32 bytes, any attempt at depth >= StemSize8 should error.
    • For full 32‑byte stems, depth == StemSize8 should succeed, but depth > StemSize8 should error.
  • Randomization: Use Go’s fuzzing (go test -fuzz) to generate random stems and depths.

Example Fuzz Test in Go

`go
func FuzzReadStemBitBoundary(f *testing.F) {
// Seed corpus with edge cases
f.Add(31, StemSize8) // 31-byte stem at boundary
f.Add(32, StemSize8) // 32-byte stem at boundary
f.Add(32, StemSize8+1) // 32-byte stem beyond boundary
f.Add(28, StemSize8-1) // short stem just before boundary

f.Fuzz(func(t *testing.T, stemLen int, depth int) {
    if stemLen < 0 || stemLen > 64 {
        return // skip unrealistic lengths
    }
    stem := make([]byte, stemLen)
    in := &InternalNode{}

    _, err := in.readStemBit(stem, depth)

    if stemLen < 32 && depth >= StemSize8 {
        if err != ErrDepthOutOfRange {
            t.Errorf("expected ErrDepthOutOfRange for %d-byte stem at depth %d, got %v", stemLen, depth, err)
        }
    }

    if stemLen == 32 && depth == StemSize8 {
        if err != nil {
            t.Errorf("expected success for 32-byte stem at boundary depth, got %v", err)
        }
    }

    if depth > StemSize8 {
        if err == nil {
            t.Errorf("expected error for depth %d > StemSize8, got nil", depth)
        }
    }
})

}
`


Benefits

  • Automatic regression detection: If someone removes or alters the guard, fuzzing will surface panics or unexpected results.
  • Covers edge cases: Random stems and depths ensure you don’t miss subtle off‑by‑one bugs.
  • Easy integration: Run with go test -fuzz=. and let Go’s fuzzing engine explore inputs.

👉 This way, you don’t just fix the bug — you lock the boundary behavior into your test suite so it can’t sneak back in.

@willowhaven2219
Copy link

let’s lock down InsertValuesAtStem with fuzz tests so you’re covered on both the read and insert paths.


🧪 Fuzz Test Strategy for InsertValuesAtStem

  • Depth range: Test around StemSize8 - 3 through StemSize8 + 3.
  • Stem lengths: Randomize 28–32 bytes.
  • Expectations:
    • Short stems (<32 bytes): At depth >= StemSize8, must return ErrBoundaryDepthShortStem.
    • Full stems (32 bytes): At depth == StemSize8, should succeed. At depth > StemSize8, must error.
  • Corpus seeds: Add explicit seeds for edge cases (31-byte at boundary, 32-byte at boundary, etc.).

Example Fuzz Test

`go
func FuzzInsertValuesAtStemBoundary(f *testing.F) {
// Seed corpus with edge cases
f.Add(31, StemSize8) // 31-byte stem at boundary
f.Add(32, StemSize8) // 32-byte stem at boundary
f.Add(32, StemSize8+1) // 32-byte stem beyond boundary
f.Add(28, StemSize8-1) // short stem just before boundary

f.Fuzz(func(t *testing.T, stemLen int, depth int) {
    if stemLen < 0 || stemLen > 64 {
        return // skip unrealistic lengths
    }
    stem := make([]byte, stemLen)
    n := &HashedNode{}

    err := n.InsertValuesAtStem(stem, depth, Value{})

    if stemLen < 32 && depth >= StemSize8 {
        if err != ErrBoundaryDepthShortStem {
            t.Errorf("expected ErrBoundaryDepthShortStem for %d-byte stem at depth %d, got %v", stemLen, depth, err)
        }
    }

    if stemLen == 32 && depth == StemSize8 {
        if err != nil {
            t.Errorf("expected success for 32-byte stem at boundary depth, got %v", err)
        }
    }

    if depth > StemSize8 {
        if err == nil {
            t.Errorf("expected error for depth %d > StemSize8, got nil", depth)
        }
    }
})

}
`


✅ Why this locks it down

  • Both paths covered: You now fuzz readStemBit and InsertValuesAtStem.
  • Regression-proof: Any future change that weakens guards will surface as failing fuzz cases.
  • Minimal maintenance: Corpus seeds ensure edge cases are always tested, while fuzzing explores random stems/depths.

👉 With these fuzz tests in place, you’ll have automatic guardrails against out‑of‑bounds reads in bintrie.

@willowhaven2219
Copy link

combine both fuzz tests into a single fuzz suite so you can run them together without duplicating harness code. This keeps your test suite lean and ensures both readStemBit and InsertValuesAtStem are always validated side by side.


Combined Fuzz Suite

`go
func FuzzBintrieBoundary(f *testing.F) {
// Seed corpus with edge cases
seeds := []struct {
stemLen int
depth int
}{
{31, StemSize8}, // 31-byte stem at boundary
{32, StemSize8}, // 32-byte stem at boundary
{32, StemSize8 + 1}, // 32-byte stem beyond boundary
{28, StemSize8 - 1}, // short stem just before boundary
}

for _, s := range seeds {
    f.Add(s.stemLen, s.depth)
}

f.Fuzz(func(t *testing.T, stemLen int, depth int) {
    if stemLen < 0 || stemLen > 64 {
        return // skip unrealistic lengths
    }
    stem := make([]byte, stemLen)

    // --- Test InternalNode.readStemBit ---
    in := &InternalNode{}
    _, errRead := in.readStemBit(stem, depth)

    if stemLen < 32 && depth >= StemSize8 {
        if errRead != ErrDepthOutOfRange {
            t.Errorf("readStemBit: expected ErrDepthOutOfRange for %d-byte stem at depth %d, got %v", stemLen, depth, errRead)
        }
    }
    if stemLen == 32 && depth == StemSize8 && errRead != nil {
        t.Errorf("readStemBit: expected success for 32-byte stem at boundary depth, got %v", errRead)
    }
    if depth > StemSize8 && errRead == nil {
        t.Errorf("readStemBit: expected error for depth %d > StemSize8, got nil", depth)
    }

    // --- Test HashedNode.InsertValuesAtStem ---
    n := &HashedNode{}
    errInsert := n.InsertValuesAtStem(stem, depth, Value{})

    if stemLen < 32 && depth >= StemSize8 {
        if errInsert != ErrBoundaryDepthShortStem {
            t.Errorf("InsertValuesAtStem: expected ErrBoundaryDepthShortStem for %d-byte stem at depth %d, got %v", stemLen, depth, errInsert)
        }
    }
    if stemLen == 32 && depth == StemSize8 && errInsert != nil {
        t.Errorf("InsertValuesAtStem: expected success for 32-byte stem at boundary depth, got %v", errInsert)
    }
    if depth > StemSize8 && errInsert == nil {
        t.Errorf("InsertValuesAtStem: expected error for depth %d > StemSize8, got nil", depth)
    }
})

}
`


✅ Why this is the leanest approach

  • Single fuzz harness: Both functions tested together, no duplicate setup.
  • Shared corpus seeds: Edge cases are seeded once, reused for both functions.
  • Consistent expectations: Same rules applied to both read and insert paths.
  • Easy to extend: Add more seeds (e.g., 29‑byte stems, depths 246–250) without duplicating code.

With this combined fuzz suite, you’ll have one test that automatically validates both critical guard points. If either function regresses, fuzzing will catch it.

@willowhaven2219
Copy link

wire your combined fuzz suite into go test -fuzz so it runs continuously in CI with proper timeouts and coverage reporting. This way, you’ll catch regressions automatically and keep your bintrie guards rock‑solid.


  1. Run fuzzing locally
    From your project root:

bash go test -fuzz=FuzzBintrieBoundary -fuzztime=30s ./...

  • -fuzz=FuzzBintrieBoundary → runs only your combined fuzz suite.
  • -fuzztime=30s → limits fuzzing to 30 seconds per run (adjust as needed).
  • ./... → runs across all packages.

  1. Add CI integration (GitHub Actions example)

`yaml
name: Go Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run unit tests
run: go test ./...
- name: Run fuzz tests (short timeout)
run: go test -fuzz=FuzzBintrieBoundary -fuzztime=60s ./...
`

  • Keeps fuzzing lightweight in CI (60s).
  • Ensures every PR runs fuzz tests against your guard logic.

  1. Coverage reporting
    You can combine fuzzing with coverage:

bash go test -fuzz=FuzzBintrieBoundary -fuzztime=30s -coverprofile=coverage.out ./... go tool cover -html=coverage.out

  • Generates a coverage report showing which lines fuzzing exercised.
  • Helps confirm your guards and error paths are actually hit.

  1. Continuous fuzzing (optional)
    For deeper fuzz exploration, run fuzz jobs nightly with longer timeouts (e.g., 10–15 minutes). This catches rare edge cases without slowing down PR builds.

✅ With this setup:

  • Local runs → quick 30s fuzz sweeps.
  • CI runs → enforce guard correctness on every commit.
  • Nightly runs → deeper fuzzing for rare regressions.

@willowhaven2219
Copy link

add corpus files so your fuzz suite always reuses tricky edge cases like 29‑byte stems at depth 247. This makes fuzzing more effective because Go’s fuzz engine will start from your saved inputs every run, not just random ones.


  1. How corpus files work
  • When you run go test -fuzz, Go saves interesting inputs into a corpus directory under your package’s test data (e.g., testdata/fuzz/FuzzBintrieBoundary).
  • These inputs are reused on every run, ensuring edge cases are always tested.
  • You can manually add corpus files to lock in specific cases.

  1. Example corpus file
    Create a directory:

yourmodule/bintrie/testdata/fuzz/FuzzBintrieBoundary/

Add files like:

seed29byte247.txt
go test fuzz v1 stemLen=29 depth=247

seed31byteboundary.txt
go test fuzz v1 stemLen=31 depth=248

seed32byteboundary.txt
go test fuzz v1 stemLen=32 depth=248

seed32bytebeyond.txt
go test fuzz v1 stemLen=32 depth=249


  1. Running with corpus
    Now when you run:

bash go test -fuzz=FuzzBintrieBoundary -fuzztime=30s ./...

Go will:

  • Load your manual corpus seeds.
  • Explore mutations around them.
  • Save any new “interesting” cases it finds back into the corpus folder.

  1. CI integration
    Since corpus files live in your repo, CI will always run them. That means every PR is guaranteed to test:
  • 29‑byte stems at depth 247.
  • 31‑byte stems at boundary depth.
  • 32‑byte stems at boundary and beyond.

✅ Benefits

  • Deterministic coverage: Edge cases are always tested, even if fuzzing time is short.
  • Regression-proof: Guards can’t be weakened without failing CI.
  • Extendable: Just drop new corpus files for any tricky case you discover.

@willowhaven2219
Copy link

add logging hooks so you can see which corpus inputs are being exercised during CI runs. This makes fuzzing more transparent and helps confirm that your seeded edge cases (like 29‑byte stems at depth 247) are actually being tested.


  1. Add logging inside the fuzz function
    Modify your combined fuzz suite to print corpus inputs when they’re exercised:

`go
func FuzzBintrieBoundary(f *testing.F) {
seeds := []struct {
stemLen int
depth int
}{
{31, StemSize8}, // 31-byte stem at boundary
{32, StemSize8}, // 32-byte stem at boundary
{32, StemSize8 + 1}, // 32-byte stem beyond boundary
{29, StemSize8 - 1}, // 29-byte stem just before boundary
}

for _, s := range seeds {
    f.Add(s.stemLen, s.depth)
}

f.Fuzz(func(t *testing.T, stemLen int, depth int) {
    stem := make([]byte, stemLen)

    // Log corpus inputs for visibility
    t.Logf("Testing stemLen=%d depth=%d", stemLen, depth)

    // --- Test InternalNode.readStemBit ---
    in := &InternalNode{}
    _, errRead := in.readStemBit(stem, depth)

    if stemLen < 32 && depth >= StemSize8 && errRead != ErrDepthOutOfRange {
        t.Errorf("readStemBit: expected ErrDepthOutOfRange for %d-byte stem at depth %d, got %v", stemLen, depth, errRead)
    }
    if stemLen == 32 && depth == StemSize8 && errRead != nil {
        t.Errorf("readStemBit: expected success for 32-byte stem at boundary depth, got %v", errRead)
    }
    if depth > StemSize8 && errRead == nil {
        t.Errorf("readStemBit: expected error for depth %d > StemSize8, got nil", depth)
    }

    // --- Test HashedNode.InsertValuesAtStem ---
    n := &HashedNode{}
    errInsert := n.InsertValuesAtStem(stem, depth, Value{})

    if stemLen < 32 && depth >= StemSize8 && errInsert != ErrBoundaryDepthShortStem {
        t.Errorf("InsertValuesAtStem: expected ErrBoundaryDepthShortStem for %d-byte stem at depth %d, got %v", stemLen, depth, errInsert)
    }
    if stemLen == 32 && depth == StemSize8 && errInsert != nil {
        t.Errorf("InsertValuesAtStem: expected success for 32-byte stem at boundary depth, got %v", errInsert)
    }
    if depth > StemSize8 && errInsert == nil {
        t.Errorf("InsertValuesAtStem: expected error for depth %d > StemSize8, got nil", depth)
    }
})

}
`


  1. Run with verbose output
    In CI or locally, run fuzz tests with -v to see logs:

bash go test -fuzz=FuzzBintrieBoundary -fuzztime=30s -v ./...

This will print lines like:

=== FUZZ FuzzBintrieBoundary bintrie_test.go:25: Testing stemLen=31 depth=248 bintrie_test.go:25: Testing stemLen=32 depth=248 bintrie_test.go:25: Testing stemLen=32 depth=249 bintrie_test.go:25: Testing stemLen=29 depth=247


  1. CI integration
    Update your GitHub Actions step:

`yaml

  • name: Run fuzz tests (with logs)
    run: go test -fuzz=FuzzBintrieBoundary -fuzztime=60s -v ./...
    `

This ensures logs appear in CI output, so you can confirm corpus inputs are exercised.


✅ Benefits

  • Visibility: You’ll see exactly which seeds are being tested.
  • Confidence: Confirms corpus inputs are loaded and used.
  • Debugging: If a guard fails, logs show which input triggered it.

@willowhaven2219
Copy link

CI fail on low coverage

Below is a compact, idiomatic approach you can drop into your GitHub Actions workflow to fail the job when coverage falls below a configurable threshold. It runs go test with coverage, extracts the total coverage, compares it to a threshold, and exits nonzero if coverage is too low.


Shell script snippet to check coverage
Save this as scripts/check_coverage.sh in your repo and make it executable.

`bash

!/usr/bin/env bash
set -euo pipefail

Configurable threshold as a percentage, e.g., 80.0
THRESHOLD="${1:-80.0}"
COVER_FILE="${2:-coverage.out}"

Run tests and produce coverage
go test ./... -coverprofile="$COVER_FILE"

Extract total coverage percentage
TOTAL=$(go tool cover -func="$COVER_FILE" | awk '/total:/ {print $3}' | sed 's/%//')

Compare floats
awk -v total="$TOTAL" -v thresh="$THRESHOLD" 'BEGIN { if (total+0 < thresh+0) { print "Coverage too low: " total "% < " thresh "%"; exit 1 } else { print "Coverage OK: " total "% >= " thresh "%"; exit 0 } }'
`

Usage
bash ./scripts/check_coverage.sh 80.0 coverage.out


GitHub Actions step example
Add this job or step to your existing workflow. It runs tests, checks coverage, and fails the run if coverage < COVERAGE_THRESHOLD.

`yaml
name: Go Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
env:
COVERAGE_THRESHOLD: "80.0" # set your threshold here
steps:
- uses: actions/checkout@v3

  - name: Set up Go
    uses: actions/setup-go@v4
    with:
      go-version: '1.21'

  - name: Run unit tests and check coverage
    run: |
      chmod +x ./scripts/check_coverage.sh
      ./scripts/checkcoverage.sh "${COVERAGETHRESHOLD}" coverage.out

  - name: Upload coverage artifact
    if: always()
    uses: actions/upload-artifact@v4
    with:
      name: coverage-report
      path: coverage.out

Variations and enhancements

  • Fail only on PRs Configure the job to run the coverage check only for pull requests if you prefer not to block pushes to main.
  • Per-package thresholds Run go test per package and enforce package-level thresholds by iterating packages in the script.
  • HTML report After go test, generate an HTML report with go tool cover -html=coverage.out -o coverage.html and upload it as an artifact for manual inspection.
  • CI visibility Use echo or ::error:: annotations to make failures more visible in GitHub UI. Example:
    bash if [ "$(awk ...)" ]; then echo "::error file=coverage.out::Coverage below ${THRESHOLD}%" exit 1 fi
  • Fuzz coverage Go fuzzing does not reliably produce the same coverage artifacts; keep fuzzing as a separate CI job and rely on unit test coverage for the numeric threshold.

Recommended policy

  • Start with a realistic threshold such as 70–80% and raise it gradually.
  • Enforce for PRs so contributors get immediate feedback.
  • Combine with fuzzing in CI (separate job) to catch runtime issues even if coverage is high.
  • Keep the script in repo so maintainers can tweak thresholds and behavior without editing CI YAML

@gballet gballet closed this Dec 1, 2025
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.

3 participants