mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-07 01:04:26 +01:00
add bug base, refactor
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,3 +34,4 @@ dist/
|
||||
# testing
|
||||
testing/limbo_output.txt
|
||||
**/limbo_output.txt
|
||||
.bugbase
|
||||
|
||||
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -723,7 +723,16 @@ version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -734,10 +743,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"redox_users 0.4.6",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
@@ -1669,7 +1690,7 @@ dependencies = [
|
||||
"comfy-table",
|
||||
"csv",
|
||||
"ctrlc",
|
||||
"dirs",
|
||||
"dirs 5.0.1",
|
||||
"env_logger 0.10.2",
|
||||
"limbo_core",
|
||||
"miette",
|
||||
@@ -1836,6 +1857,7 @@ version = "0.0.19-pre.4"
|
||||
dependencies = [
|
||||
"anarchist-readable-name-generator-lib",
|
||||
"clap",
|
||||
"dirs 6.0.0",
|
||||
"env_logger 0.10.2",
|
||||
"limbo_core",
|
||||
"log",
|
||||
@@ -1847,7 +1869,6 @@ dependencies = [
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2782,6 +2803,17 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"libredox",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
|
||||
@@ -19,7 +19,6 @@ limbo_core = { path = "../core" }
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
log = "0.4.20"
|
||||
tempfile = "3.0.7"
|
||||
env_logger = "0.10.1"
|
||||
regex = "1.11.1"
|
||||
regex-syntax = { version = "0.8.5", default-features = false, features = [
|
||||
@@ -31,3 +30,4 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0" }
|
||||
notify = "8.0.0"
|
||||
rusqlite = { version = "0.34", features = ["bundled"] }
|
||||
dirs = "6.0.0"
|
||||
|
||||
@@ -38,7 +38,7 @@ impl InteractionPlan {
|
||||
let interactions = interactions.lines().collect::<Vec<_>>();
|
||||
|
||||
let plan: InteractionPlan = serde_json::from_str(
|
||||
std::fs::read_to_string(plan_path.with_extension("plan.json"))
|
||||
std::fs::read_to_string(plan_path.with_extension("json"))
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
)
|
||||
@@ -71,7 +71,6 @@ impl InteractionPlan {
|
||||
let _ = plan[j].split_off(k);
|
||||
break;
|
||||
}
|
||||
|
||||
if interactions[i].contains(plan[j][k].to_string().as_str()) {
|
||||
i += 1;
|
||||
k += 1;
|
||||
@@ -86,7 +85,7 @@ impl InteractionPlan {
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = plan.split_off(j);
|
||||
plan
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,7 +407,7 @@ impl Property {
|
||||
match (select_predicate, select_star) {
|
||||
(Ok(rows1), Ok(rows2)) => {
|
||||
// If rows1 results have more than 1 column, there is a problem
|
||||
if rows1.iter().find(|vs| vs.len() > 1).is_some() {
|
||||
if rows1.iter().any(|vs| vs.len() > 1) {
|
||||
return Err(LimboError::InternalError(
|
||||
"Select query without the star should return only one column".to_string(),
|
||||
));
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
use clap::Parser;
|
||||
use generation::plan::{Interaction, InteractionPlan, InteractionPlanState};
|
||||
use generation::ArbitraryFrom;
|
||||
use limbo_core::Database;
|
||||
use notify::event::{DataChange, ModifyKind};
|
||||
use notify::{EventKind, RecursiveMode, Watcher};
|
||||
use rand::prelude::*;
|
||||
use runner::bugbase::{Bug, BugBase};
|
||||
use runner::cli::SimulatorCLI;
|
||||
use runner::env::SimulatorEnv;
|
||||
use runner::execution::{execute_plans, Execution, ExecutionHistory, ExecutionResult};
|
||||
@@ -15,13 +15,13 @@ use std::backtrace::Backtrace;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use tempfile::TempDir;
|
||||
|
||||
mod generation;
|
||||
mod model;
|
||||
mod runner;
|
||||
mod shrink;
|
||||
struct Paths {
|
||||
base: PathBuf,
|
||||
db: PathBuf,
|
||||
plan: PathBuf,
|
||||
shrunk_plan: PathBuf,
|
||||
@@ -31,34 +31,16 @@ struct Paths {
|
||||
}
|
||||
|
||||
impl Paths {
|
||||
fn new(output_dir: &Path, shrink: bool, doublecheck: bool) -> Self {
|
||||
let paths = Paths {
|
||||
db: PathBuf::from(output_dir).join("simulator.db"),
|
||||
plan: PathBuf::from(output_dir).join("simulator.plan"),
|
||||
shrunk_plan: PathBuf::from(output_dir).join("simulator_shrunk.plan"),
|
||||
history: PathBuf::from(output_dir).join("simulator.history"),
|
||||
doublecheck_db: PathBuf::from(output_dir).join("simulator_double.db"),
|
||||
shrunk_db: PathBuf::from(output_dir).join("simulator_shrunk.db"),
|
||||
};
|
||||
|
||||
// Print the seed, the locations of the database and the plan file
|
||||
log::info!("database path: {:?}", paths.db);
|
||||
if doublecheck {
|
||||
log::info!("doublecheck database path: {:?}", paths.doublecheck_db);
|
||||
} else if shrink {
|
||||
log::info!("shrunk database path: {:?}", paths.shrunk_db);
|
||||
fn new(output_dir: &Path) -> Self {
|
||||
Paths {
|
||||
base: output_dir.to_path_buf(),
|
||||
db: PathBuf::from(output_dir).join("test.db"),
|
||||
plan: PathBuf::from(output_dir).join("plan.sql"),
|
||||
shrunk_plan: PathBuf::from(output_dir).join("shrunk.sql"),
|
||||
history: PathBuf::from(output_dir).join("history.txt"),
|
||||
doublecheck_db: PathBuf::from(output_dir).join("double.db"),
|
||||
shrunk_db: PathBuf::from(output_dir).join("shrunk.db"),
|
||||
}
|
||||
log::info!("simulator plan path: {:?}", paths.plan);
|
||||
log::info!(
|
||||
"simulator plan serialized path: {:?}",
|
||||
paths.plan.with_extension("plan.json")
|
||||
);
|
||||
if shrink {
|
||||
log::info!("shrunk plan path: {:?}", paths.shrunk_plan);
|
||||
}
|
||||
log::info!("simulator history path: {:?}", paths.history);
|
||||
|
||||
paths
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,45 +50,37 @@ fn main() -> Result<(), String> {
|
||||
let cli_opts = SimulatorCLI::parse();
|
||||
cli_opts.validate()?;
|
||||
|
||||
let seed = cli_opts.seed.unwrap_or_else(|| thread_rng().next_u64());
|
||||
|
||||
let output_dir = match &cli_opts.output_dir {
|
||||
Some(dir) => Path::new(dir).to_path_buf(),
|
||||
None => TempDir::new().map_err(|e| format!("{:?}", e))?.into_path(),
|
||||
};
|
||||
|
||||
let mut bugbase = BugBase::load().map_err(|e| format!("{:?}", e))?;
|
||||
banner();
|
||||
let paths = Paths::new(&output_dir, cli_opts.shrink, cli_opts.doublecheck);
|
||||
|
||||
log::info!("seed: {}", seed);
|
||||
// let paths = Paths::new(&output_dir, cli_opts.doublecheck);
|
||||
|
||||
let last_execution = Arc::new(Mutex::new(Execution::new(0, 0, 0)));
|
||||
let (env, plans) = setup_simulation(seed, &cli_opts, &paths.db, &paths.plan);
|
||||
let (seed, env, plans) = setup_simulation(&mut bugbase, &cli_opts, |p| &p.plan, |p| &p.db);
|
||||
|
||||
let paths = bugbase.paths(seed);
|
||||
|
||||
// Create the output directory if it doesn't exist
|
||||
if !paths.base.exists() {
|
||||
std::fs::create_dir_all(&paths.base).map_err(|e| format!("{:?}", e))?;
|
||||
}
|
||||
|
||||
if cli_opts.watch {
|
||||
watch_mode(seed, &cli_opts, &paths, last_execution.clone()).unwrap();
|
||||
} else if cli_opts.differential {
|
||||
differential_testing(env, plans, last_execution.clone())
|
||||
} else {
|
||||
run_simulator(&cli_opts, &paths, env, plans, last_execution.clone());
|
||||
run_simulator(
|
||||
seed,
|
||||
&mut bugbase,
|
||||
&cli_opts,
|
||||
&paths,
|
||||
env,
|
||||
plans,
|
||||
last_execution.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
// Print the seed, the locations of the database and the plan file at the end again for easily accessing them.
|
||||
println!("database path: {:?}", paths.db);
|
||||
if cli_opts.doublecheck {
|
||||
println!("doublecheck database path: {:?}", paths.doublecheck_db);
|
||||
} else if cli_opts.shrink {
|
||||
println!("shrunk database path: {:?}", paths.shrunk_db);
|
||||
}
|
||||
println!("simulator plan path: {:?}", paths.plan);
|
||||
println!(
|
||||
"simulator plan serialized path: {:?}",
|
||||
paths.plan.with_extension("plan.json")
|
||||
);
|
||||
if cli_opts.shrink {
|
||||
println!("shrunk plan path: {:?}", paths.shrunk_plan);
|
||||
}
|
||||
println!("simulator history path: {:?}", paths.history);
|
||||
println!("seed: {}", seed);
|
||||
|
||||
Ok(())
|
||||
@@ -140,7 +114,6 @@ fn watch_mode(
|
||||
std::panic::catch_unwind(|| {
|
||||
let plan: Vec<Vec<Interaction>> =
|
||||
InteractionPlan::compute_via_diff(&paths.plan);
|
||||
|
||||
let mut env = SimulatorEnv::new(seed, cli_opts, &paths.db);
|
||||
plan.iter().for_each(|is| {
|
||||
is.iter().for_each(|i| {
|
||||
@@ -173,6 +146,8 @@ fn watch_mode(
|
||||
}
|
||||
|
||||
fn run_simulator(
|
||||
seed: u64,
|
||||
bugbase: &mut BugBase,
|
||||
cli_opts: &SimulatorCLI,
|
||||
paths: &Paths,
|
||||
env: SimulatorEnv,
|
||||
@@ -204,13 +179,17 @@ fn run_simulator(
|
||||
);
|
||||
|
||||
if cli_opts.doublecheck {
|
||||
doublecheck(env.clone(), paths, &plans, last_execution.clone(), result);
|
||||
let env = SimulatorEnv::new(seed, cli_opts, &paths.doublecheck_db);
|
||||
let env = Arc::new(Mutex::new(env));
|
||||
doublecheck(env, paths, &plans, last_execution.clone(), result);
|
||||
} else {
|
||||
// No doublecheck, run shrinking if panicking or found a bug.
|
||||
match &result {
|
||||
SandboxedResult::Correct => {
|
||||
log::info!("simulation succeeded");
|
||||
println!("simulation succeeded");
|
||||
// remove the bugbase entry
|
||||
bugbase.remove_bug(seed).unwrap();
|
||||
}
|
||||
SandboxedResult::Panicked {
|
||||
error,
|
||||
@@ -240,59 +219,62 @@ fn run_simulator(
|
||||
log::error!("simulation failed: '{}'", error);
|
||||
println!("simulation failed: '{}'", error);
|
||||
|
||||
if cli_opts.shrink {
|
||||
log::info!("Starting to shrink");
|
||||
log::info!("Starting to shrink");
|
||||
|
||||
let shrunk_plans = plans
|
||||
.iter()
|
||||
.map(|plan| {
|
||||
let shrunk = plan.shrink_interaction_plan(last_execution);
|
||||
log::info!("{}", shrunk.stats());
|
||||
shrunk
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let shrunk_plans = plans
|
||||
.iter()
|
||||
.map(|plan| {
|
||||
let shrunk = plan.shrink_interaction_plan(last_execution);
|
||||
log::info!("{}", shrunk.stats());
|
||||
shrunk
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Write the shrunk plan to a file
|
||||
let mut f = std::fs::File::create(&paths.shrunk_plan).unwrap();
|
||||
f.write_all(shrunk_plans[0].to_string().as_bytes()).unwrap();
|
||||
// Write the shrunk plan to a file
|
||||
let mut f = std::fs::File::create(&paths.shrunk_plan).unwrap();
|
||||
f.write_all(shrunk_plans[0].to_string().as_bytes()).unwrap();
|
||||
|
||||
let last_execution = Arc::new(Mutex::new(*last_execution));
|
||||
let last_execution = Arc::new(Mutex::new(*last_execution));
|
||||
let env = SimulatorEnv::new(seed, cli_opts, &paths.shrunk_db);
|
||||
|
||||
let shrunk = SandboxedResult::from(
|
||||
std::panic::catch_unwind(|| {
|
||||
run_simulation(
|
||||
env.clone(),
|
||||
&mut shrunk_plans.clone(),
|
||||
last_execution.clone(),
|
||||
)
|
||||
}),
|
||||
last_execution,
|
||||
);
|
||||
|
||||
match (&shrunk, &result) {
|
||||
(
|
||||
SandboxedResult::Panicked { error: e1, .. },
|
||||
SandboxedResult::Panicked { error: e2, .. },
|
||||
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(),
|
||||
)
|
||||
| (
|
||||
SandboxedResult::FoundBug { error: e1, .. },
|
||||
SandboxedResult::FoundBug { error: e2, .. },
|
||||
) => {
|
||||
if e1 != e2 {
|
||||
log::error!(
|
||||
"shrinking failed, the error was not properly reproduced"
|
||||
);
|
||||
} else {
|
||||
log::info!("shrinking succeeded");
|
||||
}
|
||||
}
|
||||
(_, SandboxedResult::Correct) => {
|
||||
unreachable!("shrinking should never be called on a correct simulation")
|
||||
}
|
||||
_ => {
|
||||
}),
|
||||
last_execution,
|
||||
);
|
||||
|
||||
match (&shrunk, &result) {
|
||||
(
|
||||
SandboxedResult::Panicked { error: e1, .. },
|
||||
SandboxedResult::Panicked { error: e2, .. },
|
||||
)
|
||||
| (
|
||||
SandboxedResult::FoundBug { error: e1, .. },
|
||||
SandboxedResult::FoundBug { error: e2, .. },
|
||||
) => {
|
||||
if e1 != e2 {
|
||||
log::error!("shrinking failed, the error was not properly reproduced");
|
||||
bugbase.add_bug(seed, plans[0].clone()).unwrap();
|
||||
} else {
|
||||
log::info!("shrinking succeeded");
|
||||
println!("shrinking succeeded");
|
||||
// Save the shrunk database
|
||||
bugbase.add_bug(seed, shrunk_plans[0].clone()).unwrap();
|
||||
}
|
||||
}
|
||||
(_, SandboxedResult::Correct) => {
|
||||
unreachable!("shrinking should never be called on a correct simulation")
|
||||
}
|
||||
_ => {
|
||||
log::error!("shrinking failed, the error was not properly reproduced");
|
||||
bugbase.add_bug(seed, plans[0].clone()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,16 +288,6 @@ fn doublecheck(
|
||||
last_execution: Arc<Mutex<Execution>>,
|
||||
result: SandboxedResult,
|
||||
) {
|
||||
{
|
||||
let mut env_ = env.lock().unwrap();
|
||||
env_.db = Database::open_file(
|
||||
env_.io.clone(),
|
||||
paths.doublecheck_db.to_str().unwrap(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Run the simulation again
|
||||
let result2 = SandboxedResult::from(
|
||||
std::panic::catch_unwind(|| {
|
||||
@@ -443,54 +415,71 @@ impl SandboxedResult {
|
||||
}
|
||||
|
||||
fn setup_simulation(
|
||||
mut seed: u64,
|
||||
bugbase: &mut BugBase,
|
||||
cli_opts: &SimulatorCLI,
|
||||
db_path: &Path,
|
||||
plan_path: &Path,
|
||||
) -> (SimulatorEnv, Vec<InteractionPlan>) {
|
||||
if let Some(load) = &cli_opts.load {
|
||||
let seed_path = PathBuf::from(load).with_extension("seed");
|
||||
let seed_str = std::fs::read_to_string(&seed_path).unwrap();
|
||||
seed = seed_str.parse().unwrap();
|
||||
}
|
||||
plan_path: fn(&Paths) -> &Path,
|
||||
db_path: fn(&Paths) -> &Path,
|
||||
) -> (u64, SimulatorEnv, Vec<InteractionPlan>) {
|
||||
if let Some(seed) = &cli_opts.load {
|
||||
let seed = seed.parse::<u64>().expect("seed should be a number");
|
||||
let bug = bugbase
|
||||
.get_bug(seed)
|
||||
.unwrap_or_else(|| panic!("bug '{}' not found in bug base", seed));
|
||||
|
||||
let mut env = SimulatorEnv::new(seed, cli_opts, db_path);
|
||||
let paths = bugbase.paths(seed);
|
||||
if !paths.base.exists() {
|
||||
std::fs::create_dir_all(&paths.base).unwrap();
|
||||
}
|
||||
let env = SimulatorEnv::new(bug.seed(), cli_opts, db_path(&paths));
|
||||
|
||||
// todo: the loading works correctly because of a hacky decision
|
||||
// Right now, the plan generation is the only point we use the rng, so the environment doesn't
|
||||
// even need it. In the future, especially with multi-connections and multi-threading, we might
|
||||
// use the RNG for more things such as scheduling, so this assumption will fail. When that happens,
|
||||
// we'll need to reachitect this logic by saving and loading RNG state.
|
||||
let plans = if let Some(load) = &cli_opts.load {
|
||||
log::info!("Loading database interaction plan...");
|
||||
let plan = std::fs::read_to_string(load).unwrap();
|
||||
let plan: InteractionPlan = serde_json::from_str(&plan).unwrap();
|
||||
vec![plan]
|
||||
let plan = match bug {
|
||||
Bug::Loaded { plan, .. } => plan.clone(),
|
||||
Bug::Unloaded { seed } => {
|
||||
let seed = *seed;
|
||||
bugbase
|
||||
.load_bug(seed)
|
||||
.unwrap_or_else(|_| panic!("could not load bug '{}' in bug base", seed))
|
||||
}
|
||||
};
|
||||
|
||||
std::fs::write(plan_path(&paths), plan.to_string()).unwrap();
|
||||
std::fs::write(
|
||||
plan_path(&paths).with_extension("json"),
|
||||
serde_json::to_string_pretty(&plan).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let plans = vec![plan];
|
||||
(seed, env, plans)
|
||||
} else {
|
||||
let seed = cli_opts.seed.unwrap_or_else(|| {
|
||||
let mut rng = rand::thread_rng();
|
||||
rng.next_u64()
|
||||
});
|
||||
|
||||
let paths = bugbase.paths(seed);
|
||||
if !paths.base.exists() {
|
||||
std::fs::create_dir_all(&paths.base).unwrap();
|
||||
}
|
||||
let mut env = SimulatorEnv::new(seed, cli_opts, &paths.db);
|
||||
|
||||
log::info!("Generating database interaction plan...");
|
||||
(1..=env.opts.max_connections)
|
||||
|
||||
let plans = (1..=env.opts.max_connections)
|
||||
.map(|_| InteractionPlan::arbitrary_from(&mut env.rng.clone(), &mut env))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// todo: for now, we only use 1 connection, so it's safe to use the first plan.
|
||||
let plan = plans[0].clone();
|
||||
|
||||
let mut f = std::fs::File::create(plan_path).unwrap();
|
||||
// todo: create a detailed plan file with all the plans. for now, we only use 1 connection, so it's safe to use the first plan.
|
||||
f.write_all(plan.to_string().as_bytes()).unwrap();
|
||||
|
||||
let serialized_plan_path = plan_path.with_extension("plan.json");
|
||||
let mut f = std::fs::File::create(&serialized_plan_path).unwrap();
|
||||
f.write_all(serde_json::to_string(&plan).unwrap().as_bytes())
|
||||
// todo: for now, we only use 1 connection, so it's safe to use the first plan.
|
||||
let plan = &plans[0];
|
||||
log::info!("{}", plan.stats());
|
||||
std::fs::write(plan_path(&paths), plan.to_string()).unwrap();
|
||||
std::fs::write(
|
||||
plan_path(&paths).with_extension("json"),
|
||||
serde_json::to_string_pretty(&plan).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let seed_path = plan_path.with_extension("seed");
|
||||
let mut f = std::fs::File::create(&seed_path).unwrap();
|
||||
f.write_all(seed.to_string().as_bytes()).unwrap();
|
||||
|
||||
log::info!("{}", plan.stats());
|
||||
(env, plans)
|
||||
(seed, env, plans)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_simulation(
|
||||
|
||||
241
simulator/runner/bugbase.rs
Normal file
241
simulator/runner/bugbase.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{self, Write},
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::{InteractionPlan, Paths};
|
||||
|
||||
/// A bug is a run that has been identified as buggy.
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Bug {
|
||||
Unloaded { seed: u64 },
|
||||
Loaded { seed: u64, plan: InteractionPlan },
|
||||
}
|
||||
|
||||
impl Bug {
|
||||
/// Check if the bug is loaded.
|
||||
pub(crate) fn is_loaded(&self) -> bool {
|
||||
match self {
|
||||
Bug::Unloaded { .. } => false,
|
||||
Bug::Loaded { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the seed of the bug.
|
||||
pub(crate) fn seed(&self) -> u64 {
|
||||
match self {
|
||||
Bug::Unloaded { seed } => *seed,
|
||||
Bug::Loaded { seed, .. } => *seed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bug Base is a local database of buggy runs.
|
||||
pub(crate) struct BugBase {
|
||||
/// Path to the bug base directory.
|
||||
path: PathBuf,
|
||||
/// The list of buggy runs, uniquely identified by their seed
|
||||
bugs: HashMap<u64, Bug>,
|
||||
}
|
||||
|
||||
impl BugBase {
|
||||
/// Create a new bug base.
|
||||
fn new(path: PathBuf) -> Result<Self, String> {
|
||||
let mut bugs = HashMap::new();
|
||||
// list all the bugs in the path as directories
|
||||
if let Ok(entries) = std::fs::read_dir(&path) {
|
||||
for entry in entries.flatten() {
|
||||
if entry.file_type().is_ok_and(|ft| ft.is_dir()) {
|
||||
let seed = entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.parse::<u64>()
|
||||
.or(Err(format!(
|
||||
"failed to parse seed from directory name {}",
|
||||
entry.file_name().to_string_lossy()
|
||||
)))?;
|
||||
bugs.insert(seed, Bug::Unloaded { seed });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { path, bugs })
|
||||
}
|
||||
|
||||
/// Load the bug base from one of the potential paths.
|
||||
pub(crate) fn load() -> Result<Self, String> {
|
||||
let potential_paths = vec![
|
||||
// limbo project directory
|
||||
BugBase::get_limbo_project_dir()?,
|
||||
// home directory
|
||||
dirs::home_dir().ok_or("should be able to get home directory".to_string())?,
|
||||
// current directory
|
||||
std::env::current_dir()
|
||||
.or(Err("should be able to get current directory".to_string()))?,
|
||||
];
|
||||
|
||||
for path in potential_paths {
|
||||
let path = path.join(".bugbase");
|
||||
if path.exists() {
|
||||
return BugBase::new(path);
|
||||
}
|
||||
}
|
||||
|
||||
println!("select bug base location:");
|
||||
println!("1. limbo project directory");
|
||||
println!("2. home directory");
|
||||
println!("3. current directory");
|
||||
print!("> ");
|
||||
io::stdout().flush().unwrap();
|
||||
let mut choice = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut choice)
|
||||
.expect("failed to read line");
|
||||
|
||||
let choice = choice
|
||||
.trim()
|
||||
.parse::<u32>()
|
||||
.or(Err(format!("invalid choice {choice}")))?;
|
||||
let path = match choice {
|
||||
1 => BugBase::get_limbo_project_dir()?.join(".bugbase"),
|
||||
2 => {
|
||||
let home = std::env::var("HOME").or(Err("failed to get home directory"))?;
|
||||
PathBuf::from(home).join(".bugbase")
|
||||
}
|
||||
3 => PathBuf::from(".bugbase"),
|
||||
_ => return Err(format!("invalid choice {choice}")),
|
||||
};
|
||||
|
||||
if path.exists() {
|
||||
unreachable!("bug base already exists at {}", path.display());
|
||||
} else {
|
||||
std::fs::create_dir_all(&path).or(Err("failed to create bug base"))?;
|
||||
log::info!("bug base created at {}", path.display());
|
||||
BugBase::new(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new bug to the bug base.
|
||||
pub(crate) fn add_bug(&mut self, seed: u64, plan: InteractionPlan) -> Result<(), String> {
|
||||
log::debug!("adding bug with seed {}", seed);
|
||||
if self.bugs.contains_key(&seed) {
|
||||
return Err(format!("Bug with hash {} already exists", seed));
|
||||
}
|
||||
self.save_bug(seed, &plan)?;
|
||||
self.bugs.insert(seed, Bug::Loaded { seed, plan });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a bug from the bug base.
|
||||
pub(crate) fn get_bug(&self, seed: u64) -> Option<&Bug> {
|
||||
self.bugs.get(&seed)
|
||||
}
|
||||
|
||||
/// Save a bug to the bug base.
|
||||
pub(crate) fn save_bug(&self, seed: u64, plan: &InteractionPlan) -> Result<(), String> {
|
||||
let bug_path = self.path.join(seed.to_string());
|
||||
std::fs::create_dir_all(&bug_path)
|
||||
.or(Err("should be able to create bug directory".to_string()))?;
|
||||
|
||||
let seed_path = bug_path.join("seed.txt");
|
||||
std::fs::write(&seed_path, seed.to_string())
|
||||
.or(Err("should be able to write seed file".to_string()))?;
|
||||
|
||||
// At some point we might want to save the commit hash of the current
|
||||
// version of Limbo.
|
||||
// let commit_hash = Self::get_current_commit_hash()?;
|
||||
// let commit_hash_path = bug_path.join("commit_hash.txt");
|
||||
// std::fs::write(&commit_hash_path, commit_hash)
|
||||
// .or(Err("should be able to write commit hash file".to_string()))?;
|
||||
|
||||
let plan_path = bug_path.join("plan.json");
|
||||
std::fs::write(
|
||||
&plan_path,
|
||||
serde_json::to_string(plan).or(Err("should be able to serialize plan".to_string()))?,
|
||||
)
|
||||
.or(Err("should be able to write plan file".to_string()))?;
|
||||
|
||||
let readable_plan_path = bug_path.join("plan.sql");
|
||||
std::fs::write(&readable_plan_path, plan.to_string())
|
||||
.or(Err("should be able to write readable plan file".to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn load_bug(&mut self, seed: u64) -> Result<InteractionPlan, String> {
|
||||
let seed_match = self.bugs.get(&seed);
|
||||
|
||||
match seed_match {
|
||||
None => Err(format!("No bugs found for seed {}", seed)),
|
||||
Some(Bug::Unloaded { .. }) => {
|
||||
let plan =
|
||||
std::fs::read_to_string(self.path.join(seed.to_string()).join("plan.json"))
|
||||
.or(Err("should be able to read plan file".to_string()))?;
|
||||
let plan: InteractionPlan = serde_json::from_str(&plan)
|
||||
.or(Err("should be able to deserialize plan".to_string()))?;
|
||||
|
||||
let bug = Bug::Loaded {
|
||||
seed,
|
||||
plan: plan.clone(),
|
||||
};
|
||||
self.bugs.insert(seed, bug);
|
||||
log::debug!("Loaded bug with seed {}", seed);
|
||||
Ok(plan)
|
||||
}
|
||||
Some(Bug::Loaded { plan, .. }) => {
|
||||
log::warn!(
|
||||
"Bug with seed {} is already loaded, returning the existing plan",
|
||||
seed
|
||||
);
|
||||
Ok(plan.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_bug(&mut self, seed: u64) -> Result<(), String> {
|
||||
self.bugs.remove(&seed);
|
||||
std::fs::remove_dir_all(self.path.join(seed.to_string()))
|
||||
.or(Err("should be able to remove bug directory".to_string()))?;
|
||||
|
||||
log::debug!("Removed bug with seed {}", seed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BugBase {
|
||||
/// Get the path to the bug base directory.
|
||||
pub(crate) fn path(&self) -> &PathBuf {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Get the path to the database file for a given seed.
|
||||
pub(crate) fn db_path(&self, seed: u64) -> PathBuf {
|
||||
self.path.join(format!("{}/test.db", seed))
|
||||
}
|
||||
|
||||
/// Get paths to all the files for a given seed.
|
||||
pub(crate) fn paths(&self, seed: u64) -> Paths {
|
||||
let base = self.path.join(format!("{}/", seed));
|
||||
Paths::new(&base)
|
||||
}
|
||||
}
|
||||
|
||||
impl BugBase {
|
||||
pub(crate) fn get_limbo_project_dir() -> Result<PathBuf, String> {
|
||||
Ok(PathBuf::from(
|
||||
String::from_utf8(
|
||||
Command::new("git")
|
||||
.args(["rev-parse", "--git-dir"])
|
||||
.output()
|
||||
.or(Err("should be able to get the git path".to_string()))?
|
||||
.stdout,
|
||||
)
|
||||
.or(Err("commit hash should be valid utf8".to_string()))?
|
||||
.trim()
|
||||
.strip_suffix(".git")
|
||||
.ok_or("should be able to strip .git suffix".to_string())?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ use clap::{command, Parser};
|
||||
pub struct SimulatorCLI {
|
||||
#[clap(short, long, help = "set seed for reproducible runs", default_value = None)]
|
||||
pub seed: Option<u64>,
|
||||
#[clap(short, long, help = "set custom output directory for produced files", default_value = None)]
|
||||
pub output_dir: Option<String>,
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
@@ -35,13 +33,7 @@ pub struct SimulatorCLI {
|
||||
default_value_t = 60 * 60 // default to 1 hour
|
||||
)]
|
||||
pub maximum_time: usize,
|
||||
#[clap(
|
||||
short = 'm',
|
||||
long,
|
||||
help = "minimize(shrink) the failing counterexample"
|
||||
)]
|
||||
pub shrink: bool,
|
||||
#[clap(short = 'l', long, help = "load plan from a file")]
|
||||
#[clap(short = 'l', long, help = "load plan from the bug base")]
|
||||
pub load: Option<String>,
|
||||
#[clap(
|
||||
short = 'w',
|
||||
@@ -66,14 +58,8 @@ impl SimulatorCLI {
|
||||
return Err("Minimum size cannot be greater than maximum size".to_string());
|
||||
}
|
||||
|
||||
// Make sure incompatible options are not set
|
||||
if self.shrink && self.doublecheck {
|
||||
return Err("Cannot use shrink and doublecheck at the same time".to_string());
|
||||
}
|
||||
|
||||
if let Some(plan_path) = &self.load {
|
||||
std::fs::File::open(plan_path)
|
||||
.map_err(|_| format!("Plan file '{}' could not be opened", plan_path))?;
|
||||
if self.seed.is_some() && self.load.is_some() {
|
||||
return Err("Cannot set seed and load plan at the same time".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -85,6 +85,7 @@ impl SimulatorEnv {
|
||||
// Remove existing database file if it exists
|
||||
if db_path.exists() {
|
||||
std::fs::remove_file(db_path).unwrap();
|
||||
std::fs::remove_file(db_path.with_extension("db-wal")).unwrap();
|
||||
}
|
||||
|
||||
let db = match Database::open_file(io.clone(), db_path.to_str().unwrap(), false) {
|
||||
|
||||
@@ -68,7 +68,12 @@ pub(crate) fn execute_plans(
|
||||
// Pick the connection to interact with
|
||||
let connection_index = pick_index(env.connections.len(), &mut env.rng);
|
||||
let state = &mut states[connection_index];
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(
|
||||
std::env::var("TICK_SLEEP")
|
||||
.unwrap_or("0".into())
|
||||
.parse()
|
||||
.unwrap_or(0),
|
||||
));
|
||||
history.history.push(Execution::new(
|
||||
connection_index,
|
||||
state.interaction_pointer,
|
||||
@@ -121,6 +126,7 @@ fn execute_plan(
|
||||
} else {
|
||||
match execute_interaction(env, connection_index, interaction, &mut state.stack) {
|
||||
Ok(next_execution) => {
|
||||
interaction.shadow(env);
|
||||
log::debug!("connection {} processed", connection_index);
|
||||
// Move to the next interaction or property
|
||||
match next_execution {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod bugbase;
|
||||
pub mod cli;
|
||||
pub mod differential;
|
||||
pub mod env;
|
||||
|
||||
Reference in New Issue
Block a user