adding better shrinking and cli opts

This commit is contained in:
echoumcp1
2025-07-07 21:21:21 -04:00
parent 75b86d1ae2
commit c5d719dafa
4 changed files with 201 additions and 30 deletions

View File

@@ -324,7 +324,7 @@ fn run_simulator(
tracing::error!("simulation failed: '{}'", error);
if cli_opts.disable_shrinking {
if cli_opts.disable_heuristic_shrinking && !cli_opts.brute_force_shrink {
tracing::info!("shrinking is disabled, skipping shrinking");
if let Some(bugbase) = bugbase.as_deref_mut() {
bugbase
@@ -337,8 +337,8 @@ fn run_simulator(
tracing::info!("Starting to shrink");
let shrunk_plans = plans
let (shrunk_plans, shrunk) = if !cli_opts.disable_heuristic_shrinking {
let shrunk_plans = plans
.iter()
.map(|plan| {
let shrunk = plan.shrink_interaction_plan(last_execution);
@@ -346,26 +346,30 @@ fn run_simulator(
shrunk
})
.collect::<Vec<_>>();
// Write the shrunk plan to a file
let mut f = std::fs::File::create(&paths.shrunk_plan).unwrap();
tracing::trace!("writing shrunk plan to {}", paths.shrunk_plan.display());
f.write_all(shrunk_plans[0].to_string().as_bytes()).unwrap();
let last_execution = Arc::new(Mutex::new(*last_execution));
let env = SimulatorEnv::new(seed, cli_opts, &paths.shrunk_db);
// Write the shrunk plan to a file
let mut f = std::fs::File::create(&paths.shrunk_plan).unwrap();
tracing::trace!("writing shrunk plan to {}", paths.shrunk_plan.display());
f.write_all(shrunk_plans[0].to_string().as_bytes()).unwrap();
let env = Arc::new(Mutex::new(env));
let shrunk = SandboxedResult::from(
std::panic::catch_unwind(|| {
run_simulation(
env.clone(),
&mut shrunk_plans.clone(),
last_execution.clone(),
)
}),
last_execution,
);
(shrunk_plans, shrunk)
} else {
(plans.to_vec(), result.clone())
};
let last_execution = Arc::new(Mutex::new(*last_execution));
let env = SimulatorEnv::new(seed, cli_opts, &paths.shrunk_db);
let env = Arc::new(Mutex::new(env));
let shrunk = SandboxedResult::from(
std::panic::catch_unwind(|| {
run_simulation(
env.clone(),
&mut shrunk_plans.clone(),
last_execution.clone(),
)
}),
last_execution,
);
match (&shrunk, &result) {
(
@@ -396,17 +400,28 @@ fn run_simulator(
);
Err(anyhow!("failed with error: '{}'", error))
} else {
let final_plans = if cli_opts.brute_force_shrink {
let brute_shrunk_plans = shrunk_plans
.iter()
.map(|plan| plan.brute_shrink_interaction_plan(&shrunk))
.collect::<Vec<_>>();
tracing::info!("Brute force shrinking completed");
brute_shrunk_plans
} else {
shrunk_plans
};
tracing::info!(
"shrinking succeeded, reduced the plan from {} to {}",
plans[0].plan.len(),
shrunk_plans[0].plan.len()
final_plans[0].plan.len()
);
// Save the shrunk database
if let Some(bugbase) = bugbase.as_deref_mut() {
bugbase.make_shrunk(
seed,
cli_opts,
shrunk_plans[0].clone(),
final_plans[0].clone(),
Some(e1.clone()),
)?;
}
@@ -557,6 +572,7 @@ fn differential_testing(
}
#[derive(Debug)]
#[derive(Clone)]
enum SandboxedResult {
Panicked {
error: String,

View File

@@ -44,12 +44,14 @@ pub struct SimulatorCLI {
pub watch: bool,
#[clap(long, help = "run differential testing between sqlite and Limbo")]
pub differential: bool,
#[clap(long, help = "run brute force shrink (warning: takes a long time)")]
pub brute_force_shrink: bool,
#[clap(subcommand)]
pub subcommand: Option<SimulatorCommand>,
#[clap(long, help = "disable BugBase", default_value_t = false)]
pub disable_bugbase: bool,
#[clap(long, help = "disable shrinking", default_value_t = false)]
pub disable_shrinking: bool,
#[clap(long, help = "disable heuristic shrinking", default_value_t = false)]
pub disable_heuristic_shrinking: bool,
#[clap(long, help = "disable UPDATE Statement", default_value_t = false)]
pub disable_update: bool,
#[clap(long, help = "disable DELETE Statement", default_value_t = false)]

View File

@@ -31,7 +31,7 @@ impl Execution {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct ExecutionHistory {
pub(crate) history: Vec<Execution>,
}

View File

@@ -1,12 +1,16 @@
use crate::model::query::{Query};
use crate::{
generation::{
plan::{InteractionPlan, Interactions},
plan::{Interaction, InteractionPlan, Interactions},
property::Property,
},
model::query::Query,
runner::execution::Execution,
Interaction,
run_simulation,
runner::{cli::SimulatorCLI, execution::Execution},
SandboxedResult, SimulatorEnv,
};
use clap::Parser;
use std::path::Path;
use std::sync::{Arc, Mutex};
impl InteractionPlan {
/// Create a smaller interaction plan by deleting a property
@@ -105,4 +109,153 @@ impl InteractionPlan {
plan
}
/// Create a smaller interaction plan by deleting a property
pub(crate) fn brute_shrink_interaction_plan(
&self,
result: &SandboxedResult,
) -> InteractionPlan {
let failing_execution = match result {
SandboxedResult::Panicked { error: _, last_execution: e } => e,
SandboxedResult::FoundBug { error: _, history: _, last_execution: e } => e,
SandboxedResult::Correct => {
unreachable!("shrink is never called on correct result")
}
};
let mut plan = self.clone();
let failing_property = &self.plan[failing_execution.interaction_index];
let interactions = failing_property.interactions();
{
let mut idx = failing_execution.secondary_index;
loop {
match &interactions[idx] {
// Fault does not depend on
Interaction::Fault(..) => break,
_ => {
// In principle we should never fail this checked_sub.
// But if there is a bug in how we count the secondary index
// we may panic if we do not use a checked_sub.
if let Some(new_idx) = idx.checked_sub(1) {
idx = new_idx;
} else {
tracing::warn!("failed to find error query");
break;
}
}
}
}
}
let before = self.plan.len();
plan.plan.truncate(failing_execution.interaction_index + 1);
// phase 1: shrink extensions
for interaction in &mut plan.plan {
if let Interactions::Property(property) = interaction {
match property {
Property::InsertValuesSelect { queries, .. }
| Property::DoubleCreateFailure { queries, .. }
| Property::DeleteSelect { queries, .. }
| Property::DropSelect { queries, .. } => {
let mut temp_plan = InteractionPlan {
plan: queries
.iter()
.map(|q| Interactions::Query(q.clone()))
.collect(),
};
temp_plan =
InteractionPlan::iterative_shrink(temp_plan, failing_execution, result);
//temp_plan = Self::shrink_queries(temp_plan, failing_execution, result, env);
*queries = temp_plan
.plan
.into_iter()
.filter_map(|i| match i {
Interactions::Query(q) => Some(q),
_ => None,
})
.collect();
}
Property::WhereTrueFalseNull { .. } | Property::SelectLimit { .. } | Property::SelectSelectOptimizer { .. } | Property::FaultyQuery { .. } | Property::FsyncNoWait { .. }=> {}
}
}
}
// phase 2: shrink the entire plan
plan = Self::iterative_shrink(plan, failing_execution, result);
let after = plan.plan.len();
tracing::info!(
"Shrinking interaction plan from {} to {} properties",
before,
after
);
plan
}
/// shrink a plan by removing one interaction at a time (and its deps) while preserving the error
fn iterative_shrink(
mut plan: InteractionPlan,
failing_execution: &Execution,
old_result: &SandboxedResult,
) -> InteractionPlan {
for i in (0..plan.plan.len()).rev() {
if i == failing_execution.interaction_index {
continue;
}
let mut test_plan = plan.clone();
test_plan.plan.remove(i);
if Self::test_shrunk_plan(&test_plan, failing_execution, old_result) {
plan = test_plan;
}
}
plan
}
fn test_shrunk_plan(
test_plan: &InteractionPlan,
failing_execution: &Execution,
old_result: &SandboxedResult,
) -> bool {
let cli_opts = SimulatorCLI {
seed: Some(0),
..SimulatorCLI::parse()
};
let env = Arc::new(Mutex::new(SimulatorEnv::new(
0,
&cli_opts,
Path::new("test.db"),
)));
let last_execution = Arc::new(Mutex::new(*failing_execution));
let result = SandboxedResult::from(
std::panic::catch_unwind(|| {
run_simulation(
env.clone(),
&mut [test_plan.clone()],
last_execution.clone(),
)
}),
last_execution.clone(),
);
match (old_result, &result) {
(
SandboxedResult::Panicked { error: e1, .. },
SandboxedResult::Panicked { error: e2, .. },
)
| (
SandboxedResult::FoundBug { error: e1, .. },
SandboxedResult::FoundBug { error: e2, .. },
) => e1 == e2,
_ => false,
}
}
}