Skip to content

Commit a398def

Browse files
committed
Tests and Nits
1 parent a764891 commit a398def

File tree

7 files changed

+122
-5
lines changed

7 files changed

+122
-5
lines changed

crates/config/src/invariant.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub struct InvariantConfig {
3737
pub timeout: Option<u32>,
3838
/// Display counterexample as solidity calls.
3939
pub show_solidity: bool,
40+
/// Continue invariant run until all invariants declared in current test suite breaks.
41+
pub continuous_run: bool,
4042
}
4143

4244
impl Default for InvariantConfig {
@@ -55,6 +57,7 @@ impl Default for InvariantConfig {
5557
show_metrics: true,
5658
timeout: None,
5759
show_solidity: false,
60+
continuous_run: false,
5861
}
5962
}
6063
}

crates/forge/src/result.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,9 @@ pub struct TestResult {
396396
/// still be successful (i.e self.success == true) when it's expected to fail.
397397
pub reason: Option<String>,
398398

399+
/// This field will be populated if there are additional invariant broken besides the main one.
400+
pub other_failures: Vec<String>,
401+
399402
/// Minimal reproduction test case for failing test
400403
pub counterexample: Option<CounterExample>,
401404

@@ -484,6 +487,12 @@ impl fmt::Display for TestResult {
484487
} else {
485488
s.push(']');
486489
}
490+
if !self.other_failures.is_empty() {
491+
writeln!(s).unwrap();
492+
for failure in &self.other_failures {
493+
writeln!(s, "{failure}").unwrap();
494+
}
495+
}
487496
s.red().wrap().fmt(f)
488497
}
489498
}
@@ -679,6 +688,7 @@ impl TestResult {
679688
gas_report_traces: Vec<Vec<CallTraceArena>>,
680689
success: bool,
681690
reason: Option<String>,
691+
other_failures: Vec<String>,
682692
counterexample: Option<CounterExample>,
683693
cases: Vec<FuzzedCases>,
684694
reverts: usize,
@@ -697,6 +707,7 @@ impl TestResult {
697707
false => TestStatus::Failure,
698708
};
699709
self.reason = reason;
710+
self.other_failures = other_failures;
700711
self.counterexample = counterexample;
701712
self.gas_report_traces = gas_report_traces;
702713
}

crates/forge/src/runner.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -772,10 +772,19 @@ impl<'a> FunctionRunner<'a> {
772772
identified_contracts,
773773
&self.cr.mcr.known_contracts,
774774
);
775+
776+
// Filter out additional invariants to test if we already have a persisted failure.
775777
let invariant_contract = InvariantContract {
776778
address: self.address,
777779
invariant_fn: func,
778-
invariant_fns: invariants,
780+
invariant_fns: invariants
781+
.into_iter()
782+
.filter(|(invariant_fn, _)| {
783+
*invariant_fn == func
784+
|| (invariant_config.continuous_run
785+
&& !canonicalized(failure_dir.join(invariant_fn.name.clone())).exists())
786+
})
787+
.collect(),
779788
call_after_invariant,
780789
abi: &self.cr.contract.abi,
781790
};
@@ -893,6 +902,7 @@ impl<'a> FunctionRunner<'a> {
893902
.errors
894903
.get(&invariant_contract.invariant_fn.name)
895904
.and_then(|err| err.revert_reason());
905+
let mut other_failures = vec![];
896906

897907
if success {
898908
// If invariants ran successfully, replay the last run to collect logs and
@@ -965,12 +975,20 @@ impl<'a> FunctionRunner<'a> {
965975
continue;
966976
}
967977

968-
// Generate counterexamples for other invariants broken.
969-
if let Some(error) = invariant_result.errors.get(&invariant.name)
978+
// Generate counterexamples for broken invariant, if there is no failure persisted
979+
// already.
980+
let persisted_failure = canonicalized(failure_dir.join(invariant.name.clone()));
981+
if !persisted_failure.exists()
982+
&& let Some(error) = invariant_result.errors.get(&invariant.name)
970983
&& let InvariantFuzzError::BrokenInvariant(case_data)
971984
| InvariantFuzzError::Revert(case_data) = error
972985
&& let TestError::Fail(_, ref calls) = case_data.test_error
973986
{
987+
other_failures.push(format!(
988+
"{}: {}",
989+
invariant.name,
990+
error.revert_reason().unwrap_or_default()
991+
));
974992
match generate_counterexample(
975993
self.clone_executor(),
976994
&self.cr.mcr.known_contracts,
@@ -982,7 +1000,7 @@ impl<'a> FunctionRunner<'a> {
9821000
// Persist error in invariant failure dir.
9831001
record_invariant_failure(
9841002
failure_dir.as_path(),
985-
canonicalized(failure_dir.join(invariant.name.clone())).as_path(),
1003+
persisted_failure.as_path(),
9861004
&call_sequence,
9871005
test_bytecode,
9881006
);
@@ -999,6 +1017,7 @@ impl<'a> FunctionRunner<'a> {
9991017
invariant_result.gas_report_traces,
10001018
success,
10011019
reason,
1020+
other_failures,
10021021
counterexample,
10031022
invariant_result.cases,
10041023
invariant_result.reverts,

crates/forge/tests/cli/config.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ show_edge_coverage = false
197197
failure_persist_dir = "cache/invariant"
198198
show_metrics = true
199199
show_solidity = false
200+
continuous_run = false
200201
201202
[labels]
202203
@@ -1272,7 +1273,8 @@ forgetest_init!(test_default_config, |prj, cmd| {
12721273
"failure_persist_dir": "cache/invariant",
12731274
"show_metrics": true,
12741275
"timeout": null,
1275-
"show_solidity": false
1276+
"show_solidity": false,
1277+
"continuous_run": false
12761278
},
12771279
"ffi": false,
12781280
"allow_internal_expect_revert": false,

crates/forge/tests/cli/test_cmd/invariant/mod.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,3 +991,83 @@ Ran 3 test suites [ELAPSED]: 6 tests passed, 0 failed, 0 skipped (6 total tests)
991991
prj.root().join("fuzz_corpus").join("Counter2Test").join("testFuzz_SetNumber").exists()
992992
);
993993
});
994+
995+
forgetest_init!(continous_run, |prj, cmd| {
996+
prj.update_config(|config| {
997+
config.invariant.runs = 10;
998+
config.invariant.depth = 100;
999+
config.invariant.continuous_run = true;
1000+
});
1001+
prj.add_source(
1002+
"Counter.sol",
1003+
r#"
1004+
contract Counter {
1005+
uint256 public cond;
1006+
1007+
function work(uint256 x) public {
1008+
if (x % 2 != 0 && x < 9000) {
1009+
cond++;
1010+
} else {
1011+
revert();
1012+
}
1013+
}
1014+
}
1015+
"#,
1016+
);
1017+
prj.add_test(
1018+
"CounterTest.t.sol",
1019+
r#"
1020+
import {Test} from "forge-std/Test.sol";
1021+
import {Counter} from "../src/Counter.sol";
1022+
1023+
contract CounterTest is Test {
1024+
Counter public counter;
1025+
1026+
function setUp() public {
1027+
counter = new Counter();
1028+
}
1029+
1030+
function invariant_cond1() public view {
1031+
require(counter.cond() < 10, "condition 1 met");
1032+
}
1033+
1034+
function invariant_cond2() public view {
1035+
require(counter.cond() < 15, "condition 2 met");
1036+
}
1037+
1038+
function invariant_cond3() public view {
1039+
require(counter.cond() < 5, "condition 3 met");
1040+
}
1041+
1042+
function invariant_cond4() public view {
1043+
require(counter.cond() < 111111, "condition 4 met");
1044+
}
1045+
1046+
/// forge-config: default.invariant.fail-on-revert = true
1047+
function invariant_cond5() public view {
1048+
require(counter.cond() < 111111, "condition 5 met");
1049+
}
1050+
}
1051+
"#,
1052+
);
1053+
1054+
// Check that running single `invariant_cond3` test continue to run until it breaks all other
1055+
// invariants.
1056+
cmd.args(["test", "--mt", "invariant_cond3"]).assert_failure().stdout_eq(str![[r#"
1057+
[COMPILING_FILES] with [SOLC_VERSION]
1058+
[SOLC_VERSION] [ELAPSED]
1059+
Compiler run successful!
1060+
1061+
Ran 1 test for test/CounterTest.t.sol:CounterTest
1062+
[FAIL: condition 3 met]
1063+
[Sequence] (original: 5, shrunk: 5)
1064+
...
1065+
1066+
invariant_cond1: condition 1 met
1067+
invariant_cond2: condition 2 met
1068+
invariant_cond5: EvmError: Revert
1069+
invariant_cond3() (runs: 10, calls: 1000, reverts: [..])
1070+
...
1071+
1072+
"#]]);
1073+
});

crates/forge/tests/fixtures/SimpleContractTestNonVerbose.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"test()": {
66
"status": "Success",
77
"reason": null,
8+
"other_failures": [],
89
"counterexample": null,
910
"logs": [],
1011
"decoded_logs": [],

crates/forge/tests/fixtures/SimpleContractTestVerbose.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"test()": {
66
"status": "Success",
77
"reason": null,
8+
"other_failures": [],
89
"counterexample": null,
910
"logs": [
1011
{

0 commit comments

Comments
 (0)