44//! results against SQLite for generated SQL statements.
55
66use std:: io:: { IsTerminal , stdin} ;
7+ use std:: path:: PathBuf ;
78
89use anyhow:: Result ;
910use clap:: { Parser , Subcommand } ;
1011use differential_fuzzer:: { Fuzzer , GeneratorKind , SimConfig , TreeMode } ;
1112use rand:: RngCore ;
13+ use serde:: Serialize ;
1214
1315/// SQLancer-style differential testing fuzzer for Turso.
1416#[ derive( Parser , Debug ) ]
@@ -65,9 +67,55 @@ enum Commands {
6567 /// Number of iterations to run (0 for infinite).
6668 #[ arg( default_value_t = 0 ) ]
6769 iterations : u64 ,
70+
71+ /// Collect errors and write a JSON report to this path instead of stopping on first failure.
72+ #[ arg( long) ]
73+ report : Option < PathBuf > ,
6874 } ,
6975}
7076
77+ /// A single failure recorded during a loop run.
78+ #[ derive( Debug , Serialize ) ]
79+ struct FailureRecord {
80+ iteration : u64 ,
81+ seed : u64 ,
82+ error : String ,
83+ statements_executed : usize ,
84+ oracle_failures : usize ,
85+ warnings : usize ,
86+ config : ConfigRecord ,
87+ }
88+
89+ /// Serializable snapshot of the run configuration.
90+ #[ derive( Debug , Serialize ) ]
91+ struct ConfigRecord {
92+ num_tables : usize ,
93+ columns_per_table : usize ,
94+ num_statements : usize ,
95+ generator : String ,
96+ mvcc : bool ,
97+ }
98+
99+ /// Summary written to the JSON report file.
100+ #[ derive( Debug , Serialize ) ]
101+ struct LoopReport {
102+ total_iterations : u64 ,
103+ total_failures : u64 ,
104+ failures : Vec < FailureRecord > ,
105+ }
106+
107+ impl ConfigRecord {
108+ fn from_args ( args : & Args ) -> Self {
109+ Self {
110+ num_tables : args. num_tables ,
111+ columns_per_table : args. columns_per_table ,
112+ num_statements : args. num_statements ,
113+ generator : format ! ( "{:?}" , args. generator) ,
114+ mvcc : args. mvcc ,
115+ }
116+ }
117+ }
118+
71119fn main ( ) -> Result < ( ) > {
72120 // Initialize tracing
73121 let mut subscriber = tracing_subscriber:: fmt ( ) . with_env_filter (
@@ -83,26 +131,100 @@ fn main() -> Result<()> {
83131 let mut args = Args :: parse ( ) ;
84132
85133 match args. command {
86- Some ( Commands :: Loop { iterations } ) => {
134+ Some ( Commands :: Loop {
135+ iterations,
136+ ref report,
137+ } ) => {
87138 let mut iteration = 0u64 ;
139+ let mut failures: Vec < FailureRecord > = Vec :: new ( ) ;
140+ let collecting = report. is_some ( ) ;
141+
88142 loop {
89143 args. seed = rand:: rng ( ) . next_u64 ( ) ;
90144 tracing:: info!( "Iteration {}: seed {}" , iteration + 1 , args. seed) ;
91- run_single ( & args) ?;
145+
146+ match run_single_inner ( & args) {
147+ Ok ( stats) if stats. oracle_failures > 0 => {
148+ let record = FailureRecord {
149+ iteration : iteration + 1 ,
150+ seed : args. seed ,
151+ error : format ! ( "{} oracle failure(s) detected" , stats. oracle_failures) ,
152+ statements_executed : stats. statements_executed ,
153+ oracle_failures : stats. oracle_failures ,
154+ warnings : stats. warnings ,
155+ config : ConfigRecord :: from_args ( & args) ,
156+ } ;
157+ tracing:: error!(
158+ "Iteration {} failed (seed {}): {}" ,
159+ iteration + 1 ,
160+ args. seed,
161+ record. error
162+ ) ;
163+ failures. push ( record) ;
164+ if !collecting {
165+ break ;
166+ }
167+ }
168+ Err ( e) => {
169+ let record = FailureRecord {
170+ iteration : iteration + 1 ,
171+ seed : args. seed ,
172+ error : format ! ( "{e:#}" ) ,
173+ statements_executed : 0 ,
174+ oracle_failures : 0 ,
175+ warnings : 0 ,
176+ config : ConfigRecord :: from_args ( & args) ,
177+ } ;
178+ tracing:: error!(
179+ "Iteration {} errored (seed {}): {e:#}" ,
180+ iteration + 1 ,
181+ args. seed
182+ ) ;
183+ failures. push ( record) ;
184+ if !collecting {
185+ break ;
186+ }
187+ }
188+ Ok ( _) => { }
189+ }
92190
93191 iteration += 1 ;
94192 if iterations > 0 && iteration >= iterations {
95- tracing:: info!( "Completed {} iterations successfully " , iterations) ;
193+ tracing:: info!( "Completed {} iterations" , iterations) ;
96194 break ;
97195 }
98196 }
197+
198+ if let Some ( path) = report {
199+ let report = LoopReport {
200+ total_iterations : iteration,
201+ total_failures : failures. len ( ) as u64 ,
202+ failures,
203+ } ;
204+ let json = serde_json:: to_string_pretty ( & report) ?;
205+ std:: fs:: write ( path, & json) ?;
206+ tracing:: info!(
207+ "Wrote report ({} failures / {} iterations) to {}" ,
208+ report. total_failures,
209+ report. total_iterations,
210+ path. display( )
211+ ) ;
212+ if report. total_failures > 0 {
213+ std:: process:: exit ( 1 ) ;
214+ }
215+ } else if !failures. is_empty ( ) {
216+ std:: process:: exit ( 1 ) ;
217+ }
218+
99219 Ok ( ( ) )
100220 }
101221 None => run_single ( & args) ,
102222 }
103223}
104224
105- fn run_single ( args : & Args ) -> Result < ( ) > {
225+ /// Run a single fuzzer iteration, returning stats on success.
226+ /// Does NOT call `process::exit` — the caller decides what to do with failures.
227+ fn run_single_inner ( args : & Args ) -> Result < differential_fuzzer:: SimStats > {
106228 let config = SimConfig {
107229 seed : args. seed ,
108230 num_tables : args. num_tables ,
@@ -123,7 +245,6 @@ fn run_single(args: &Args) -> Result<()> {
123245 tracing:: info!( "Starting differential_fuzzer with config: {:?}" , config) ;
124246
125247 let fuzzer = Fuzzer :: new ( config) ?;
126-
127248 let stats = fuzzer. run ( ) ?;
128249
129250 if args. keep_files {
@@ -144,9 +265,13 @@ fn run_single(args: &Args) -> Result<()> {
144265 }
145266 }
146267
268+ Ok ( stats)
269+ }
270+
271+ fn run_single ( args : & Args ) -> Result < ( ) > {
272+ let stats = run_single_inner ( args) ?;
147273 if stats. oracle_failures > 0 {
148274 std:: process:: exit ( 1 ) ;
149275 }
150-
151276 Ok ( ( ) )
152277}
0 commit comments