pbt — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited pbt (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Rapid (pgregory.net/rapid) is a Go library for property-based testing. Tests integrate with go test via rapid.Check. Rapid generates random inputs for your code and automatically shrinks failing cases to minimal counterexamples. It has zero external dependencies.
Follow these steps when writing property-based tests.
Load references/go.md for full API details and idiomatic patterns.
Before writing any test, understand what you're testing:
The goal is to find evidence for properties, not to invent them.
Look for properties that are:
Write one test per property. Don't cram multiple properties into one test.
Before writing tests from scratch, always check existing tests:
testing/quick, etc.) should beported to rapid. Load references/porting.md for guidance. Don't carry over narrow generator bounds from the old framework — use broader generators unless bounds are justified by the function's contract.
references/evolving-tests.md for guidance. Tests with hardcoded values, table-driven tests, or multiple similar test cases are prime candidates.
the randomness should come from rapid instead so failures produce shrinkable counterexamples.
When you evolve an existing test, modify the existing test file rather than creating a new one. Add rapid tests alongside (or replacing) the existing tests in the same file where the original tests live. Do not create a separate pbt_test.go or similar — property-based tests are tests like any other and belong with the code they're testing.
For each property:
foo_test.go covering the module, add rapid tests there. Only create a new file if no relevant test file exists.
bounds are logically necessary (e.g. if a number has to be non-zero it's fine to force it to be, but slices should not have maxLen set unless there is a compelling correctness reason to set them or poor performance has been observed when actually running the test)
generator.Draw(t, "label")t.Fatalf, t.Errorf, or standard Go assertionsRun the tests. When a test fails, ask:
Use this taxonomy to identify what to test. Not every category applies to every function — pick the ones supported by evidence.
| Category | Description | Example |
|---|---|---|
| Round-trip | encode then decode recovers the original | Unmarshal(Marshal(x)) == x |
| Idempotence | applying twice equals applying once | sort(sort(xs)) == sort(xs) |
| Commutativity | order of operations doesn't matter | a + b == b + a |
| Invariant preservation | an operation maintains a structural property | insert into BST preserves ordering |
| Oracle / reference impl | compare against a known-correct implementation | mySort(xs) == slices.Sort(xs), or comparing against an unoptimised implementation |
| Monotonicity | more input means more (or equal) output | len(append(xs, ys...)) >= len(xs) |
| Bounds / contracts | output stays within documented limits | clamp(x, lo, hi) is in [lo, hi] |
| No-crash / robustness | function handles all valid inputs without panicking | parse(arbitraryString) doesn't panic |
| Equivalence | two implementations produce the same result | iterativeFib(n) == recursiveFib(n) |
| Model-based | operations on real system match a simplified model | custom map ops match built-in map |
| Consistency | related APIs in the same library agree | StringWidth(s) == sum of RuneWidth per rune |
| Precision preservation | numeric values survive format conversions | strconv.Atoi(strconv.Itoa(n)) == n |
These patterns are ranked by how often they found real bugs when tested across many popular Go libraries. See references/field-tested-patterns.md for detailed examples.
For any data structure, the highest-value first test is a model test — run the same operations on the library under test and a known-good reference (usually a standard library type), then assert they agree after every operation.
Choose the right oracle:
[]T for sequential containers (fixed-capacity slices, ring buffers)map[K]V for hash maps (concurrent maps, ordered maps)map[K]struct{} or sorted slice for setsAny normalization, case conversion, or formatting function should be idempotent: f(f(x)) == f(x). Use rapid.String() (not ASCII-only generators) because Unicode edge cases like ß → SS and combining characters are where bugs hide.
Every parsing function should be tested with rapid.String(). The property is simple: it should never panic. Parsers that delegate to constructors which panic on invalid values (instead of returning errors) are a common source of bugs.
parse(format(x)) == x for any serialize/deserialize pair. Test with the full input domain — don't restrict to "reasonable" values. Bugs hide at boundaries like zero (e.g. scientific notation missing the coefficient), large integers (precision loss through float64 intermediaries for values > 2^53), and unusual string content (double slashes in paths, control characters).
Integer operations should be tested with math.MinInt, math.MaxInt, 0, and unconstrained ranges. Negating math.MinInt overflows, and many libraries forget to handle these. Don't add rapid.IntRange(-100, 100) — those bounds hide real bugs.
Properties must be evidence-based. Find evidence in:
func Merge(a, b []T) []T implies the output length might equal the sum of input lengths.Err on the side of creating more properties rather than fewer, and if they fail investigate whether the failure is legitimate behaviour or not.
Beware of properties that seem universal but aren't. Read the docs carefully before asserting a property. Examples from real testing:
≠ "\n\r" because \r\n is one grapheme cluster while \n\r` is two).
Difference might mean symmetric difference (A △ B), not setdifference (A \ B) — check the docs.
When a property fails, investigate whether it's a real bug or a genuine edge case in the domain. A weaker property often still holds.
A common mistake agents make when writing property-based tests is over-constraining generators. This leads to tests that are weaker than they need to be.
If the function accepts any int, use:
rapid.Int() // no min, no maxDo NOT preemptively write:
rapid.IntRange(0, 100) // WRONG unless justifiedDon't narrow ranges to "avoid edge cases." Edge cases are exactly what PBT is for. If a function claims to work on all int values, test it on all int values — including math.MinInt, math.MaxInt, 0, -1, and 1.
minLen to 1 by DefaultUnless the function's contract explicitly requires non-empty input, test with empty collections too. If a function panics on an empty slice, that might be a bug worth knowing about.
Your first reaction should be: is this a real bug?
You should assume that it is unless you have strong evidence that it is not. If in doubt, ask the user.
math.MaxInt, that's a bug in the code, not in your test.Add generator bounds only when:
func Sqrt(x float64) documents that x >= 0 is required.When a constraint involves relationships between multiple generated values, you may use t.Skip:
a := rapid.Int().Draw(t, "a")
b := rapid.Int().Draw(t, "b")
if a == b {
t.Skip("need distinct values")
}This example is perfectly fine, but it is better to avoid Skip altogether when you can:
e.g.
a := rapid.Int().Draw(t, "a")
b := rapid.IntMin(a).Draw(t, "b")is better than
a := rapid.Int().Draw(t, "a")
b := rapid.Int().Draw(t, "b")
if a > b {
t.Skip("need a <= b")
}Even better is:
a := rapid.Int().Draw(t, "a")
b := rapid.Int().Draw(t, "b")
if a > b {
a, b = b, a
}It is particularly important to avoid rejection sampling in cases where the rejection rate is likely to be high.
For example rapid.Map(rapid.Int(), func(n int) int { return n * 2 }) is much better than rapid.Int().Filter(func(n int) bool { return n%2 == 0 }), as the former constructs an even number directly, while the latter throws away around 50% of test cases.
Rapid's default collection size is small. If you need large collections (e.g., to exercise deep tree paths), draw the size separately and use SliceOfN:
// GOOD — can generate large collections, shrinks well
n := rapid.IntRange(0, 300).Draw(t, "n")
keys := rapid.SliceOfN(rapid.Int(), n, -1).Draw(t, "keys")
// BAD — rapid's default size distribution rarely produces 100+ elements
keys := rapid.SliceOf(rapid.Int()).Draw(t, "keys")Setting minLen but not maxLen (using -1) is a shrinking optimization: rapid can shrink n to find the minimal collection size that triggers the bug, while still being able to add extra elements if needed.
SliceOfDistinct for Key GenerationWhen testing maps/sets that need unique keys:
keys := rapid.SliceOfNDistinct(rapid.Int(), -1, 30, rapid.ID[int]).Draw(t, "keys")This avoids confusion about which value wins for duplicate keys.
When the code under test requires an RNG (e.g., func Sample(weights []float64, rng *rand.Rand)), do not create a seeded RNG like rand.New(rand.NewSource(seed)) with a rapid-generated seed. This defeats shrinking — rapid can only shrink the seed integer, not the actual random decisions the RNG makes.
Instead, generate the random decisions you need directly through rapid generators. For example, if the code needs a random index, draw it with rapid.IntRange(0, n-1).
If the code's RNG is deeply embedded and cannot be easily replaced, generating a seed is acceptable as a last resort, but understand that shrinking quality will be reduced.
assert(x == x) or assert(len(s) >= 0) test nothing. Every property should be falsifiable by a buggy implementation..Filter() or t.Skip() rejects most inputs, rapid will give up. Restructure your generators instead (e.g., use rapid.Map or dependent generation).pbt_test.go or properties_test.go — add them to the existing test files.rand.New(rand.NewSource(seed)). Generate the random decisions you need through rapid generators so rapid can shrink them. See "Handling Randomness" above.m[k] = k * 10), your test code itself can overflow before the library has a chance to be buggy. Use smaller intermediate types (draw int16, cast to int for multiplication) to prevent this. Distinguish "this constraint protects the library's contract" (keep it) from "this constraint prevents my test from overflowing" (use a smaller type instead).-rapid.checks rather than restricting the input space. A slow test that finds bugs beats a fast test that can't. Many tree/trie bugs only manifest at 50-200+ elements.// go.mod
require pgregory.net/rapid latestRun with go test. Rapid integrates with go test via rapid.Check(t, prop).
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.