@@ -20,11 +20,40 @@ use proptest::{
20
20
strategy:: { Strategy , ValueTree } ,
21
21
test_runner:: { TestCaseError , TestRunner } ,
22
22
} ;
23
- use std:: { cell:: RefCell , collections:: BTreeMap } ;
23
+ use serde:: Serialize ;
24
+ use std:: { collections:: BTreeMap , fmt} ;
24
25
25
26
mod types;
26
27
pub use types:: { CaseOutcome , CounterExampleOutcome , FuzzOutcome } ;
27
28
29
+ #[ derive( Serialize , Default ) ]
30
+ struct FuzzCoverageMetrics {
31
+ // Number of edges seen during the invariant run.
32
+ cumulative_edges_seen : usize ,
33
+ // Number of features (new hitcount bin of previously hit edge) seen during the invariant run.
34
+ cumulative_features_seen : usize ,
35
+ }
36
+
37
+ impl fmt:: Display for FuzzCoverageMetrics {
38
+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
39
+ writeln ! ( f) ?;
40
+ writeln ! ( f, " - cumulative edges seen: {}" , self . cumulative_edges_seen) ?;
41
+ writeln ! ( f, " - cumulative features seen: {}" , self . cumulative_features_seen) ?;
42
+ Ok ( ( ) )
43
+ }
44
+ }
45
+
46
+ impl FuzzCoverageMetrics {
47
+ /// Records number of new edges or features explored during the campaign.
48
+ pub fn update_seen ( & mut self , is_edge : bool ) {
49
+ if is_edge {
50
+ self . cumulative_edges_seen += 1 ;
51
+ } else {
52
+ self . cumulative_features_seen += 1 ;
53
+ }
54
+ }
55
+ }
56
+
28
57
/// Contains data collected during fuzz test runs.
29
58
#[ derive( Default ) ]
30
59
pub struct FuzzTestData {
@@ -64,8 +93,12 @@ pub struct FuzzedExecutor {
64
93
config : FuzzConfig ,
65
94
/// The persisted counterexample to be replayed, if any.
66
95
persisted_failure : Option < BaseCounterExample > ,
96
+ /// History of binned hitcount of edges seen during fuzzing.
97
+ history_map : Vec < u8 > ,
67
98
}
68
99
100
+ const COVERAGE_MAP_SIZE : usize = 65536 ;
101
+
69
102
impl FuzzedExecutor {
70
103
/// Instantiates a fuzzed executor given a testrunner
71
104
pub fn new (
@@ -75,7 +108,14 @@ impl FuzzedExecutor {
75
108
config : FuzzConfig ,
76
109
persisted_failure : Option < BaseCounterExample > ,
77
110
) -> Self {
78
- Self { executor, runner, sender, config, persisted_failure }
111
+ Self {
112
+ executor,
113
+ runner,
114
+ sender,
115
+ config,
116
+ persisted_failure,
117
+ history_map : vec ! [ 0u8 ; COVERAGE_MAP_SIZE ] ,
118
+ }
79
119
}
80
120
81
121
/// Fuzzes the provided function, assuming it is available at the contract at `address`
@@ -93,7 +133,7 @@ impl FuzzedExecutor {
93
133
progress : Option < & ProgressBar > ,
94
134
) -> FuzzTestResult {
95
135
// Stores the fuzz test execution data.
96
- let execution_data = RefCell :: new ( FuzzTestData :: default ( ) ) ;
136
+ let mut execution_data = FuzzTestData :: default ( ) ;
97
137
let state = self . build_fuzz_state ( deployed_libs) ;
98
138
let dictionary_weight = self . config . dictionary . dictionary_weight . min ( 100 ) ;
99
139
let strategy = proptest:: prop_oneof![
@@ -106,19 +146,20 @@ impl FuzzedExecutor {
106
146
107
147
// Start timer for this fuzz test.
108
148
let timer = FuzzTestTimer :: new ( self . config . timeout ) ;
109
-
149
+ let max_runs = self . config . runs ;
110
150
let continue_campaign = |runs : u32 | {
111
151
// If timeout is configured, then perform fuzz runs until expires.
112
- if self . config . timeout . is_some ( ) {
152
+ if timer . is_enabled ( ) {
113
153
return !timer. is_timed_out ( ) ;
114
154
}
115
155
// If no timeout configured then loop until configured runs.
116
- runs < self . config . runs
156
+ runs < max_runs
117
157
} ;
118
158
119
159
let mut runs = 0 ;
120
160
let mut rejects = 0 ;
121
161
let mut run_failure = None ;
162
+ let mut coverage_metrics = FuzzCoverageMetrics :: default ( ) ;
122
163
123
164
' stop: while continue_campaign ( runs) {
124
165
// If counterexample recorded, replay it first, without incrementing runs.
@@ -128,6 +169,10 @@ impl FuzzedExecutor {
128
169
// If running with progress, then increment current run.
129
170
if let Some ( progress) = progress {
130
171
progress. inc ( 1 ) ;
172
+ // Display metrics in progress bar.
173
+ if self . config . show_edge_coverage {
174
+ progress. set_message ( format ! ( "{}" , & coverage_metrics) ) ;
175
+ }
131
176
} ;
132
177
133
178
runs += 1 ;
@@ -140,39 +185,38 @@ impl FuzzedExecutor {
140
185
strategy. current ( )
141
186
} ;
142
187
143
- match self . single_fuzz ( address, input) {
188
+ match self . single_fuzz ( address, input, & mut coverage_metrics ) {
144
189
Ok ( fuzz_outcome) => match fuzz_outcome {
145
190
FuzzOutcome :: Case ( case) => {
146
- let mut data = execution_data. borrow_mut ( ) ;
147
- data. gas_by_case . push ( ( case. case . gas , case. case . stipend ) ) ;
191
+ execution_data. gas_by_case . push ( ( case. case . gas , case. case . stipend ) ) ;
148
192
149
- if data . first_case . is_none ( ) {
150
- data . first_case . replace ( case. case ) ;
193
+ if execution_data . first_case . is_none ( ) {
194
+ execution_data . first_case . replace ( case. case ) ;
151
195
}
152
196
153
197
if let Some ( call_traces) = case. traces {
154
- if data . traces . len ( ) == max_traces_to_collect {
155
- data . traces . pop ( ) ;
198
+ if execution_data . traces . len ( ) == max_traces_to_collect {
199
+ execution_data . traces . pop ( ) ;
156
200
}
157
- data . traces . push ( call_traces) ;
158
- data . breakpoints . replace ( case. breakpoints ) ;
201
+ execution_data . traces . push ( call_traces) ;
202
+ execution_data . breakpoints . replace ( case. breakpoints ) ;
159
203
}
160
204
161
205
if show_logs {
162
- data . logs . extend ( case. logs ) ;
206
+ execution_data . logs . extend ( case. logs ) ;
163
207
}
164
208
165
- HitMaps :: merge_opt ( & mut data . coverage , case. coverage ) ;
166
- data . deprecated_cheatcodes = case. deprecated_cheatcodes ;
209
+ HitMaps :: merge_opt ( & mut execution_data . coverage , case. coverage ) ;
210
+ execution_data . deprecated_cheatcodes = case. deprecated_cheatcodes ;
167
211
}
168
212
FuzzOutcome :: CounterExample ( CounterExampleOutcome {
169
213
exit_reason : status,
170
214
counterexample : outcome,
171
215
..
172
216
} ) => {
173
217
let reason = rd. maybe_decode ( & outcome. 1 . result , status) ;
174
- execution_data. borrow_mut ( ) . logs . extend ( outcome. 1 . logs . clone ( ) ) ;
175
- execution_data. borrow_mut ( ) . counterexample = outcome;
218
+ execution_data. logs . extend ( outcome. 1 . logs . clone ( ) ) ;
219
+ execution_data. counterexample = outcome;
176
220
run_failure = Some ( TestCaseError :: fail ( reason. unwrap_or_default ( ) ) ) ;
177
221
break ' stop;
178
222
}
@@ -198,30 +242,29 @@ impl FuzzedExecutor {
198
242
}
199
243
}
200
244
201
- let fuzz_result = execution_data. into_inner ( ) ;
202
- let ( calldata, call) = fuzz_result. counterexample ;
245
+ let ( calldata, call) = execution_data. counterexample ;
203
246
204
- let mut traces = fuzz_result . traces ;
247
+ let mut traces = execution_data . traces ;
205
248
let ( last_run_traces, last_run_breakpoints) = if run_failure. is_none ( ) {
206
- ( traces. pop ( ) , fuzz_result . breakpoints )
249
+ ( traces. pop ( ) , execution_data . breakpoints )
207
250
} else {
208
251
( call. traces . clone ( ) , call. cheatcodes . map ( |c| c. breakpoints ) )
209
252
} ;
210
253
211
254
let mut result = FuzzTestResult {
212
- first_case : fuzz_result . first_case . unwrap_or_default ( ) ,
213
- gas_by_case : fuzz_result . gas_by_case ,
255
+ first_case : execution_data . first_case . unwrap_or_default ( ) ,
256
+ gas_by_case : execution_data . gas_by_case ,
214
257
success : run_failure. is_none ( ) ,
215
258
skipped : false ,
216
259
reason : None ,
217
260
counterexample : None ,
218
- logs : fuzz_result . logs ,
261
+ logs : execution_data . logs ,
219
262
labeled_addresses : call. labels ,
220
263
traces : last_run_traces,
221
264
breakpoints : last_run_breakpoints,
222
265
gas_report_traces : traces. into_iter ( ) . map ( |a| a. arena ) . collect ( ) ,
223
- line_coverage : fuzz_result . coverage ,
224
- deprecated_cheatcodes : fuzz_result . deprecated_cheatcodes ,
266
+ line_coverage : execution_data . coverage ,
267
+ deprecated_cheatcodes : execution_data . deprecated_cheatcodes ,
225
268
} ;
226
269
227
270
match run_failure {
@@ -258,16 +301,24 @@ impl FuzzedExecutor {
258
301
259
302
/// Granular and single-step function that runs only one fuzz and returns either a `CaseOutcome`
260
303
/// or a `CounterExampleOutcome`
261
- pub fn single_fuzz (
262
- & self ,
304
+ fn single_fuzz (
305
+ & mut self ,
263
306
address : Address ,
264
- calldata : alloy_primitives:: Bytes ,
307
+ calldata : Bytes ,
308
+ coverage_metrics : & mut FuzzCoverageMetrics ,
265
309
) -> Result < FuzzOutcome , TestCaseError > {
266
310
let mut call = self
267
311
. executor
268
312
. call_raw ( self . sender , address, calldata. clone ( ) , U256 :: ZERO )
269
313
. map_err ( |e| TestCaseError :: fail ( e. to_string ( ) ) ) ?;
270
314
315
+ if self . config . show_edge_coverage {
316
+ let ( new_coverage, is_edge) = call. merge_edge_coverage ( & mut self . history_map ) ;
317
+ if new_coverage {
318
+ coverage_metrics. update_seen ( is_edge) ;
319
+ }
320
+ }
321
+
271
322
// Handle `vm.assume`.
272
323
if call. result . as_ref ( ) == MAGIC_ASSUME {
273
324
return Err ( TestCaseError :: reject ( FuzzError :: TooManyRejects (
0 commit comments