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