Skip to content

Commit 522d008

Browse files
committed
with loop command you can also produce a report of fuzzer runs and collect bugs
1 parent 19caa1d commit 522d008

File tree

4 files changed

+160
-7
lines changed

4 files changed

+160
-7
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testing/differential-oracle/fuzzer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ proptest = "1.9"
3232
indexmap.workspace = true
3333
parking_lot.workspace = true
3434
comfy-table = "7"
35+
serde = { workspace = true, features = ["derive"] }
3536
serde_json = { workspace = true }
3637

3738
[lints]

testing/differential-oracle/fuzzer/main.rs

Lines changed: 131 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
//! results against SQLite for generated SQL statements.
55
66
use std::io::{IsTerminal, stdin};
7+
use std::path::PathBuf;
78

89
use anyhow::Result;
910
use clap::{Parser, Subcommand};
1011
use differential_fuzzer::{Fuzzer, GeneratorKind, SimConfig, TreeMode};
1112
use 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+
71119
fn 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
}

testing/runner/tests/check_constraint.sqltest

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,6 +1655,32 @@ expect {
16551655
# Bug fix: CHECK constraint type coercion must use column affinity (#5170)
16561656
# TEXT column comparing with integer literal should use TEXT affinity,
16571657
# meaning the integer is coerced to text for comparison.
1658+
# Bug fix: integrity_check must not flag NULL values as CHECK violations.
1659+
# Per SQL standard, NULL does not violate CHECK constraints.
1660+
@skip-if mvcc "MVCC integrity_check does not perform row-level CHECK validation"
1661+
test check_constraint_integrity_check_null_not_violation {
1662+
CREATE TABLE t(id INTEGER PRIMARY KEY, val REAL CHECK (val > 0));
1663+
INSERT INTO t (id, val) VALUES (1, 100.0);
1664+
INSERT INTO t (id, val) VALUES (2, NULL);
1665+
INSERT INTO t (id, val) VALUES (3, 200.0);
1666+
PRAGMA integrity_check;
1667+
}
1668+
expect {
1669+
ok
1670+
}
1671+
1672+
@skip-if mvcc "MVCC integrity_check does not perform row-level CHECK validation"
1673+
test check_constraint_integrity_check_detects_real_violation {
1674+
CREATE TABLE t(id INTEGER PRIMARY KEY, val INTEGER CHECK (val > 0));
1675+
PRAGMA ignore_check_constraints = ON;
1676+
INSERT INTO t VALUES (1, -5);
1677+
PRAGMA ignore_check_constraints = OFF;
1678+
PRAGMA integrity_check;
1679+
}
1680+
expect {
1681+
CHECK constraint failed in t
1682+
}
1683+
16581684
test check_constraint_text_affinity_coercion_violation {
16591685
CREATE TABLE t(val TEXT CHECK(val > 5));
16601686
INSERT INTO t VALUES ('10');

0 commit comments

Comments
 (0)