Skip to content

Commit 124f134

Browse files
authored
fix(forge): show logs and coverage for table tests (#11575)
1 parent 9828fd5 commit 124f134

File tree

4 files changed

+168
-12
lines changed

4 files changed

+168
-12
lines changed

crates/evm/fuzz/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ impl fmt::Display for BaseCounterExample {
199199
}
200200

201201
/// The outcome of a fuzz test
202-
#[derive(Debug)]
202+
#[derive(Debug, Default)]
203203
pub struct FuzzTestResult {
204204
/// we keep this for the debugger
205205
pub first_case: FuzzCase,

crates/forge/src/result.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,33 @@ impl TestResult {
684684
self.gas_report_traces = gas_report_traces;
685685
}
686686

687+
/// Returns the result for a table test. Merges table test execution results (logs, labeled
688+
/// addresses, traces and coverages) in initial setup results.
689+
pub fn table_result(&mut self, result: FuzzTestResult) {
690+
self.kind = TestKind::Table {
691+
median_gas: result.median_gas(false),
692+
mean_gas: result.mean_gas(false),
693+
runs: result.gas_by_case.len(),
694+
};
695+
696+
// Record logs, labels, traces and merge coverages.
697+
extend!(self, result, TraceKind::Execution);
698+
699+
self.status = if result.skipped {
700+
TestStatus::Skipped
701+
} else if result.success {
702+
TestStatus::Success
703+
} else {
704+
TestStatus::Failure
705+
};
706+
self.reason = result.reason;
707+
self.counterexample = result.counterexample;
708+
self.duration = Duration::default();
709+
self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
710+
self.breakpoints = result.breakpoints.unwrap_or_default();
711+
self.deprecated_cheatcodes = result.deprecated_cheatcodes;
712+
}
713+
687714
/// Returns `true` if this is the result of a fuzz test
688715
pub fn is_fuzz(&self) -> bool {
689716
matches!(self.kind, TestKind::Fuzz { .. })
@@ -724,6 +751,11 @@ pub enum TestKindReport {
724751
metrics: Map<String, InvariantMetrics>,
725752
failed_corpus_replays: usize,
726753
},
754+
Table {
755+
runs: usize,
756+
mean_gas: u64,
757+
median_gas: u64,
758+
},
727759
}
728760

729761
impl fmt::Display for TestKindReport {
@@ -752,6 +784,9 @@ impl fmt::Display for TestKindReport {
752784
write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
753785
}
754786
}
787+
Self::Table { runs, mean_gas, median_gas } => {
788+
write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
789+
}
755790
}
756791
}
757792
}
@@ -762,7 +797,7 @@ impl TestKindReport {
762797
match *self {
763798
Self::Unit { gas } => gas,
764799
// We use the median for comparisons
765-
Self::Fuzz { median_gas, .. } => median_gas,
800+
Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
766801
// We return 0 since it's not applicable
767802
Self::Invariant { .. } => 0,
768803
}
@@ -791,6 +826,8 @@ pub enum TestKind {
791826
metrics: Map<String, InvariantMetrics>,
792827
failed_corpus_replays: usize,
793828
},
829+
/// A table test.
830+
Table { runs: usize, mean_gas: u64, median_gas: u64 },
794831
}
795832

796833
impl Default for TestKind {
@@ -821,6 +858,9 @@ impl TestKind {
821858
failed_corpus_replays: *failed_corpus_replays,
822859
}
823860
}
861+
Self::Table { runs, mean_gas, median_gas } => {
862+
TestKindReport::Table { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
863+
}
824864
}
825865
}
826866
}

crates/forge/src/runner.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
33
use crate::{
44
MultiContractRunner, TestFilter,
5-
fuzz::BaseCounterExample,
5+
coverage::HitMaps,
6+
fuzz::{BaseCounterExample, FuzzTestResult},
67
multi_runner::{TestContract, TestRunnerConfig},
78
progress::{TestsProgress, start_fuzz_progress},
89
result::{SuiteResult, TestResult, TestSetup},
@@ -640,6 +641,8 @@ impl<'a> FunctionRunner<'a> {
640641
fixtures_len as u32,
641642
);
642643

644+
let mut result = FuzzTestResult::default();
645+
643646
for i in 0..fixtures_len {
644647
if self.tcfg.fail_fast.should_stop() {
645648
return self.result;
@@ -671,24 +674,33 @@ impl<'a> FunctionRunner<'a> {
671674
}
672675
};
673676

677+
result.gas_by_case.push((raw_call_result.gas_used, raw_call_result.stipend));
678+
result.logs.extend(raw_call_result.logs.clone());
679+
result.labels.extend(raw_call_result.labels.clone());
680+
HitMaps::merge_opt(&mut result.line_coverage, raw_call_result.line_coverage.clone());
681+
674682
let is_success =
675683
self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false);
676684
// Record counterexample if test fails.
677685
if !is_success {
678-
self.result.counterexample =
686+
result.counterexample =
679687
Some(CounterExample::Single(BaseCounterExample::from_fuzz_call(
680688
Bytes::from(func.abi_encode_input(&args).unwrap()),
681689
args,
682690
raw_call_result.traces.clone(),
683691
)));
684-
self.result.single_result(false, reason, raw_call_result);
692+
result.reason = reason;
693+
result.traces = raw_call_result.traces;
694+
self.result.table_result(result);
685695
return self.result;
686696
}
687697

688698
// If it's the last iteration and all other runs succeeded, then use last call result
689699
// for logs and traces.
690700
if i == fixtures_len - 1 {
691-
self.result.single_result(true, None, raw_call_result);
701+
result.success = true;
702+
result.traces = raw_call_result.traces;
703+
self.result.table_result(result);
692704
return self.result;
693705
}
694706
}

crates/forge/tests/it/table.rs

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,25 +69,25 @@ Compiler run successful!
6969
7070
Ran 8 tests for test/CounterTable.t.sol:CounterTableTest
7171
[FAIL: 2 fixtures defined for diffSwap (expected 10)] tableMultipleParamsDifferentFixturesFail(uint256,bool) ([GAS])
72-
[FAIL: Cannot swap; counterexample: calldata=0x717892ca00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001 args=[1, true]] tableMultipleParamsFail(uint256,bool) ([GAS])
72+
[FAIL: Cannot swap; counterexample: calldata=0x717892ca00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001 args=[1, true]] tableMultipleParamsFail(uint256,bool) (runs: 1, [AVG_GAS])
7373
Traces:
7474
[..] CounterTableTest::tableMultipleParamsFail(1, true)
7575
└─ ← [Revert] Cannot swap
7676
7777
[FAIL: No fixture defined for param noSwap] tableMultipleParamsNoParamFail(uint256,bool) ([GAS])
78-
[PASS] tableMultipleParamsPass(uint256,bool) ([GAS])
78+
[PASS] tableMultipleParamsPass(uint256,bool) (runs: 10, [AVG_GAS])
7979
Traces:
8080
[..] CounterTableTest::tableMultipleParamsPass(10, true)
8181
├─ [..] Counter::increment()
8282
│ └─ ← [Stop]
8383
└─ ← [Stop]
8484
85-
[FAIL: Amount cannot be 10; counterexample: calldata=0x44fa2375000000000000000000000000000000000000000000000000000000000000000a args=[10]] tableSingleParamFail(uint256) ([GAS])
85+
[FAIL: Amount cannot be 10; counterexample: calldata=0x44fa2375000000000000000000000000000000000000000000000000000000000000000a args=[10]] tableSingleParamFail(uint256) (runs: 10, [AVG_GAS])
8686
Traces:
8787
[..] CounterTableTest::tableSingleParamFail(10)
8888
└─ ← [Revert] Amount cannot be 10
8989
90-
[PASS] tableSingleParamPass(uint256) ([GAS])
90+
[PASS] tableSingleParamPass(uint256) (runs: 10, [AVG_GAS])
9191
Traces:
9292
[..] CounterTableTest::tableSingleParamPass(10)
9393
├─ [..] Counter::increment()
@@ -103,13 +103,117 @@ Ran 1 test suite [ELAPSED]: 2 tests passed, 6 failed, 0 skipped (8 total tests)
103103
Failing tests:
104104
Encountered 6 failing tests in test/CounterTable.t.sol:CounterTableTest
105105
[FAIL: 2 fixtures defined for diffSwap (expected 10)] tableMultipleParamsDifferentFixturesFail(uint256,bool) ([GAS])
106-
[FAIL: Cannot swap; counterexample: calldata=0x717892ca00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001 args=[1, true]] tableMultipleParamsFail(uint256,bool) ([GAS])
106+
[FAIL: Cannot swap; counterexample: calldata=0x717892ca00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001 args=[1, true]] tableMultipleParamsFail(uint256,bool) (runs: 1, [AVG_GAS])
107107
[FAIL: No fixture defined for param noSwap] tableMultipleParamsNoParamFail(uint256,bool) ([GAS])
108-
[FAIL: Amount cannot be 10; counterexample: calldata=0x44fa2375000000000000000000000000000000000000000000000000000000000000000a args=[10]] tableSingleParamFail(uint256) ([GAS])
108+
[FAIL: Amount cannot be 10; counterexample: calldata=0x44fa2375000000000000000000000000000000000000000000000000000000000000000a args=[10]] tableSingleParamFail(uint256) (runs: 10, [AVG_GAS])
109109
[FAIL: Table test should have at least one parameter] tableWithNoParamFail() ([GAS])
110110
[FAIL: Table test should have at least one fixture] tableWithParamNoFixtureFail(uint256) ([GAS])
111111
112112
Encountered a total of 6 failing tests, 2 tests succeeded
113113
114114
"#]]);
115115
});
116+
117+
// Table tests should show logs and contribute to coverage.
118+
// <https://github.com/foundry-rs/foundry/issues/11066>
119+
forgetest_init!(should_show_logs_and_add_coverage, |prj, cmd| {
120+
prj.wipe_contracts();
121+
prj.add_source(
122+
"Counter.sol",
123+
r#"
124+
contract Counter {
125+
uint256 public number;
126+
127+
function setNumber(uint256 a, uint256 b) public {
128+
if (a == 1) {
129+
number = b + 1;
130+
} else if (a == 2) {
131+
number = b + 2;
132+
} else if (a == 3) {
133+
number = b + 3;
134+
} else {
135+
number = a + b;
136+
}
137+
}
138+
}
139+
"#,
140+
);
141+
prj.add_test(
142+
"CounterTest.t.sol",
143+
r#"
144+
import "forge-std/Test.sol";
145+
import {Counter} from "../src/Counter.sol";
146+
147+
contract CounterTest is Test {
148+
struct TestCase {
149+
uint256 a;
150+
uint256 b;
151+
uint256 expected;
152+
}
153+
154+
Counter public counter;
155+
156+
function setUp() public {
157+
counter = new Counter();
158+
}
159+
160+
function fixtureNumbers() public pure returns (TestCase[] memory) {
161+
TestCase[] memory entries = new TestCase[](4);
162+
entries[0] = TestCase(1, 5, 6);
163+
entries[1] = TestCase(2, 10, 12);
164+
entries[2] = TestCase(3, 11, 14);
165+
entries[3] = TestCase(4, 11, 15);
166+
return entries;
167+
}
168+
169+
function tableSetNumberTest(TestCase memory numbers) public {
170+
console.log("expected", numbers.expected);
171+
counter.setNumber(numbers.a, numbers.b);
172+
require(counter.number() == numbers.expected, "test failed");
173+
}
174+
}
175+
"#,
176+
);
177+
178+
cmd.args(["test", "-vvv"]).assert_success().stdout_eq(str![[r#"
179+
[COMPILING_FILES] with [SOLC_VERSION]
180+
[SOLC_VERSION] [ELAPSED]
181+
Compiler run successful!
182+
183+
Ran 1 test for test/CounterTest.t.sol:CounterTest
184+
[PASS] tableSetNumberTest((uint256,uint256,uint256)) (runs: 4, [AVG_GAS])
185+
Logs:
186+
expected 6
187+
expected 12
188+
expected 14
189+
expected 15
190+
191+
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]
192+
193+
Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)
194+
195+
"#]]);
196+
197+
cmd.forge_fuse().args(["coverage"]).assert_success().stdout_eq(str![[r#"
198+
[COMPILING_FILES] with [SOLC_VERSION]
199+
[SOLC_VERSION] [ELAPSED]
200+
Compiler run successful!
201+
Analysing contracts...
202+
Running tests...
203+
204+
Ran 1 test for test/CounterTest.t.sol:CounterTest
205+
[PASS] tableSetNumberTest((uint256,uint256,uint256)) (runs: 4, [AVG_GAS])
206+
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]
207+
208+
Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)
209+
210+
╭-----------------+---------------+---------------+---------------+---------------╮
211+
| File | % Lines | % Statements | % Branches | % Funcs |
212+
+=================================================================================+
213+
| src/Counter.sol | 100.00% (8/8) | 100.00% (7/7) | 100.00% (6/6) | 100.00% (1/1) |
214+
|-----------------+---------------+---------------+---------------+---------------|
215+
| Total | 100.00% (8/8) | 100.00% (7/7) | 100.00% (6/6) | 100.00% (1/1) |
216+
╰-----------------+---------------+---------------+---------------+---------------╯
217+
218+
"#]]);
219+
});

0 commit comments

Comments
 (0)