add sometimes assertions#803
Conversation
2bb0428 to
4a0e6f6
Compare
Adds support for "sometimes assertions" - a new test type that validates code paths are reached with sufficient frequency during fuzzing campaigns. This addresses issue #450 by providing "fuzz canaries" that alert when test harnesses become ineffective or regress. Sometimes assertions are methods with a configurable prefix (default: "sometimes_") that take no arguments and should succeed (not revert) at least a minimum percentage of the time. If the success rate falls below the configured threshold after sufficient executions, the test fails. Key features: - Configurable success rate threshold (default: 5%) - Minimum execution count before evaluation (default: 100) - Thread-safe statistics tracking across parallel workers - End-of-campaign evaluation to ensure statistical significance - Per-method execution and success counts in test output Implementation: - New SometimesTestCaseProvider following the existing provider pattern - Configuration in FuzzingConfig.Testing.SometimesTesting - Statistics tracked per test: executionCount, successCount - Tests executed after each call in a sequence (no shrinking) - Success determined by: successRate >= minSuccessRate Example usage: ```solidity function doSomething(uint256 x) public { if (x > 100) { counter++; } } // Passes if counter increments at least 5% of the time function sometimes_counterIncremented() public view { require(counter > 0); } ``` Includes comprehensive test coverage validating success rate calculations, minimum execution enforcement, and multi-contract scenarios. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Replace the per-worker contract tracking maps and event handlers (onWorkerCreated, onWorkerDeployedContractAdded, onWorkerDeployedContractDeleted) with direct iteration over worker.DeployedContracts() in callSequencePostCallTest. Move the shared testCasesLock onto each SometimesTestCase so workers testing different methods don't contend with each other. Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
6fb144a to
38edabe
Compare
|
yeah this should be good |
P1 — Insufficient executions silently marked as PASSED:
- Log a warning via GlobalLogger when a sometimes test does not reach
minExecutionCount and is marked as passed by default. Users now see
which tests were not fully evaluated.
P2 — Race condition on SometimesTestCase field reads:
- Add lock acquisition to Status() and LogMessage() so concurrent
workers cannot produce torn reads of executionCount/successCount.
- Rename SuccessRate() to unexported successRate() (caller-holds-lock
contract) and update all call sites.
P2 — StopOnFailedTest limitation undocumented:
- Add doc comment on SometimesTestingConfig noting that sometimes tests
are evaluated post-campaign, so StopOnFailedTest has no effect.
P2 — StopOnNoTests error message omits "sometimes":
- Update both error strings in fuzzer.go to include "sometimes".
P3 — Test improvements:
- TestSometimesMultipleContracts: assert zero failures, add explicit
StopOnFailedTest = false for consistency with TestSometimesMode.
- TestSometimesMode: use GreaterOrEqual to match the >= semantics in
onFuzzerStopping.
- Fix staticcheck QF1004: use strings.ReplaceAll in ID().
Dismissed findings:
- P2 #4 (self-destructed contracts inflate success rate): real concern
but low-probability edge case; fixing requires adopting per-worker
contract lifecycle tracking which is a larger refactor best done in a
follow-up.
- P2 #5 (default config enables sometimes testing): consistent with
property/optimization defaults; flagging for release notes instead.
- P3 #8 (prefix overlap uses exact match): pre-existing limitation
across all providers, not introduced by this PR.
- P3 #9 (IsSometimesTest allows return values): by design — return
values are harmless and not read. Documenting would add noise.
- P3 #11 (failedSequences metric not incremented): inherent to
post-campaign evaluation; no worker exists to attribute the metric to.
- P4 findings: informational, no changes needed.
Verification: build, lint (zero new issues), go vet, and both
TestSometimesMode and TestSometimesMultipleContracts pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Review SummaryFindingsP1 — Blocks Merge
P2 — Important
P3 — Nice to Have
P4 — Informational
Verification
Commit
|
Add per-test-case locks to PropertyTestCase, AssertionTestCase, and
OptimizationTestCase, matching the pattern already applied to
SometimesTestCase. All three types had data races where Status(),
LogMessage(), and Value() read fields concurrently written by workers.
PropertyTestCase and AssertionTestCase:
- Add lock to Status() and LogMessage()
- Protect status/callSequence writes in FinishedCallback and
onWorkerDeployedContractAdded
OptimizationTestCase (most severe pre-existing race):
- Add lock to Status(), LogMessage(), and Value()
- Protect value/callSequence comparison and update in
callSequencePostCallTest (resolves the acknowledged TODO:
"Should we allow for races here?")
- Protect callSequence/trace writes in FinishedCallback
- Protect shrinkCallSequenceRequest update
- Value() now returns a copy to prevent callers from racing
on the underlying big.Int
All 23 fuzzing tests pass. No lock is taken in onFuzzerStopping
since workers are already destroyed at that point.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Follow-up: Fix pre-existing data races in all test case providersThe race condition found in
Changes per typeAll three types:
OptimizationTestCase additionally:
VerificationAll 23 fuzzing tests pass (property, assertion, optimization, sometimes, chain behavior, hooks, corpus, deployment order, verbosity, filtering, external library). |
| for _, contract := range t.fuzzer.ContractDefinitions() { | ||
| // If we're not testing all contracts, verify the current contract is one we specified in our target contracts. | ||
| if !t.fuzzer.config.Fuzzing.Testing.TestAllContracts && !slices.Contains(t.fuzzer.config.Fuzzing.TargetContracts, contract.Name()) { | ||
| continue | ||
| } |
There was a problem hiding this comment.
by iterating through all the contract definitions it doesn't matter whether testAllContracts is true or false.
The best route imo is to:
- Iterate through
targetContractsand only add test cases there - Add OnFuzzWorkerDeployedContract/Deleted you should check if
testAllContractsis true/false. If true, then you can add the test cases from the newly deployed contract.
There is one caveat that I don't remember is that the property test provider maintain workerState per worker while assertion test does not. I can't remember which model you should use. I think this provider is the amalgamation of both.
This reverts commit 4aac1bf.
This reverts commit 1b7c368.
- Default SometimesTesting.Enabled to false for safe upgrades - Add "sometimes" to StopOnNoTests error messages - Log warning when sometimes tests don't reach minExecutionCount - Use %w for error wrapping in executeSometimesTest Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
VIBE CODED PROTOTYPE. Still need to check whether this is correct, but it at least looks plausibleAlso not completely sure that this interface is the best way to go, rather than adding assert statementsresolves #450
Adds support for "sometimes assertions" - a new test type that validates code paths are reached with sufficient frequency during fuzzing campaigns. This addresses issue #450 by providing "fuzz canaries" that alert when test harnesses become ineffective or regress.
Sometimes assertions are methods with a configurable prefix (default: "sometimes_") that take no arguments and should succeed (not revert) at least a minimum percentage of the time. If the success rate falls below the configured threshold after sufficient executions, the test fails.
Key features:
Implementation:
Example usage:
Includes comprehensive test coverage validating success rate calculations, minimum execution enforcement, and multi-contract scenarios.