diff --git a/simulator/main.rs b/simulator/main.rs index 9523dbdce..1012a34d3 100644 --- a/simulator/main.rs +++ b/simulator/main.rs @@ -4,7 +4,7 @@ use clap::Parser; use notify::event::{DataChange, ModifyKind}; use notify::{EventKind, RecursiveMode, Watcher}; use rand::prelude::*; -use runner::bugbase::{Bug, BugBase, LoadedBug}; +use runner::bugbase::BugBase; use runner::cli::{SimulatorCLI, SimulatorCommand}; use runner::differential; use runner::env::SimulatorEnv; @@ -43,7 +43,7 @@ fn main() -> anyhow::Result<()> { let profile = Profile::parse_from_type(cli_opts.profile.clone())?; tracing::debug!(sim_profile = ?profile); - if let Some(ref command) = cli_opts.subcommand { + if let Some(command) = cli_opts.subcommand.take() { match command { SimulatorCommand::List => { let mut bugbase = BugBase::load()?; @@ -51,10 +51,10 @@ fn main() -> anyhow::Result<()> { } SimulatorCommand::Loop { n, short_circuit } => { banner(); - for i in 0..*n { + for i in 0..n { println!("iteration {i}"); - let result = testing_main(&cli_opts, &profile); - if result.is_err() && *short_circuit { + let result = testing_main(&mut cli_opts, &profile); + if result.is_err() && short_circuit { println!("short circuiting after {i} iterations"); return result; } else if result.is_err() { @@ -66,7 +66,7 @@ fn main() -> anyhow::Result<()> { Ok(()) } SimulatorCommand::Test { filter } => { - let mut bugbase = BugBase::load()?; + let bugbase = BugBase::load()?; let bugs = bugbase.load_bugs()?; let mut bugs = bugs .into_iter() @@ -75,7 +75,7 @@ fn main() -> anyhow::Result<()> { .runs .into_iter() .filter_map(|run| run.error.clone().map(|_| run)) - .filter(|run| run.error.as_ref().unwrap().contains(filter)) + .filter(|run| run.error.as_ref().unwrap().contains(&filter)) .map(|run| run.cli_options) .collect::>(); @@ -100,7 +100,7 @@ fn main() -> anyhow::Result<()> { let results = bugs .into_iter() - .map(|cli_opts| testing_main(&cli_opts, &profile)) + .map(|mut cli_opts| testing_main(&mut cli_opts, &profile)) .collect::>(); let (successes, failures): (Vec<_>, Vec<_>) = @@ -118,11 +118,11 @@ fn main() -> anyhow::Result<()> { } } else { banner(); - testing_main(&cli_opts, &profile) + testing_main(&mut cli_opts, &profile) } } -fn testing_main(cli_opts: &SimulatorCLI, profile: &Profile) -> anyhow::Result<()> { +fn testing_main(cli_opts: &mut SimulatorCLI, profile: &Profile) -> anyhow::Result<()> { let mut bugbase = if cli_opts.disable_bugbase { None } else { @@ -260,11 +260,6 @@ fn run_simulator( tracing::info!("{}", plan.stats()); std::fs::write(env.get_plan_path(), plan.to_string()).unwrap(); - std::fs::write( - env.get_plan_path().with_extension("json"), - serde_json::to_string_pretty(&*plan).unwrap(), - ) - .unwrap(); // No doublecheck, run shrinking if panicking or found a bug. match &result { @@ -385,7 +380,7 @@ fn run_simulator( ); // Save the shrunk database if let Some(bugbase) = bugbase.as_deref_mut() { - bugbase.make_shrunk( + bugbase.save_shrunk( seed, cli_opts, final_plan.clone(), @@ -471,81 +466,58 @@ impl SandboxedResult { } fn setup_simulation( - bugbase: Option<&mut BugBase>, - cli_opts: &SimulatorCLI, + mut bugbase: Option<&mut BugBase>, + cli_opts: &mut SimulatorCLI, profile: &Profile, ) -> (u64, SimulatorEnv, InteractionPlan) { - if let Some(seed) = &cli_opts.load { - let seed = seed.parse::().expect("seed should be a number"); - let bugbase = bugbase.expect("BugBase must be enabled to load a bug"); - tracing::info!("seed={}", seed); - let bug = bugbase - .get_bug(seed) - .unwrap_or_else(|| panic!("bug '{seed}' not found in bug base")); - + if let Some(seed) = cli_opts.load { + let bugbase = bugbase + .as_mut() + .expect("BugBase must be enabled to load a bug"); 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, - paths, - SimulationType::Default, - profile, - ); - let plan = match bug { - Bug::Loaded(LoadedBug { plan, .. }) => plan.clone(), - Bug::Unloaded { seed } => { - let seed = *seed; - bugbase - .load_bug(seed) - .unwrap_or_else(|_| panic!("could not load bug '{seed}' in bug base")) - .plan - .clone() - } - }; + let bug = bugbase + .get_or_load_bug(seed) + .unwrap() + .unwrap_or_else(|| panic!("bug '{seed}' not found in bug base")); - std::fs::write(env.get_plan_path(), plan.to_string()).unwrap(); - std::fs::write( - env.get_plan_path().with_extension("json"), - serde_json::to_string_pretty(&plan).unwrap(), - ) - .unwrap(); - (seed, env, plan) - } else { - let seed = cli_opts.seed.unwrap_or_else(|| { - let mut rng = rand::rng(); - rng.next_u64() - }); - tracing::info!("seed={}", seed); - - let paths = if let Some(bugbase) = bugbase { - 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:?}")) - .unwrap(); - } - paths - } else { - let dir = std::env::current_dir().unwrap().join("simulator-output"); - std::fs::create_dir_all(&dir).unwrap(); - Paths::new(&dir) - }; - - let mut env = SimulatorEnv::new(seed, cli_opts, paths, SimulationType::Default, profile); - - tracing::info!("Generating database interaction plan..."); - - let plan = InteractionPlan::init_plan(&mut env); - - (seed, env, plan) + // run the simulation with the same CLI options as the loaded bug + *cli_opts = bug.last_cli_opts(); } -} + let seed = cli_opts.seed.unwrap_or_else(|| { + let mut rng = rand::rng(); + rng.next_u64() + }); + tracing::info!("seed={}", seed); + cli_opts.seed = Some(seed); + + let paths = if let Some(bugbase) = bugbase { + 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:?}")) + .unwrap(); + } + paths + } else { + let dir = std::env::current_dir().unwrap().join("simulator-output"); + std::fs::create_dir_all(&dir).unwrap(); + Paths::new(&dir) + }; + + let mut env = SimulatorEnv::new(seed, cli_opts, paths, SimulationType::Default, profile); + + tracing::info!("Generating database interaction plan..."); + + let plan = InteractionPlan::init_plan(&mut env); + + (seed, env, plan) +} fn run_simulation( env: Arc>, plan: impl InteractionPlanIterator, diff --git a/simulator/runner/bugbase.rs b/simulator/runner/bugbase.rs index 89ad25e71..ce8892b04 100644 --- a/simulator/runner/bugbase.rs +++ b/simulator/runner/bugbase.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, env::current_dir, fs::File, - io::{self, Read, Write}, + io::Read, path::{Path, PathBuf}, time::SystemTime, }; @@ -11,60 +11,84 @@ use anyhow::{Context, anyhow}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use crate::{InteractionPlan, Paths}; +use crate::{Paths, model::interactions::InteractionPlan}; use super::cli::SimulatorCLI; +const READABLE_PLAN_PATH: &str = "plan.sql"; +const SHRUNK_READABLE_PLAN_PATH: &str = "shrunk.sql"; +const SEED_PATH: &str = "seed.txt"; +const RUNS_PATH: &str = "runs.json"; + /// A bug is a run that has been identified as buggy. #[derive(Clone)] -pub(crate) enum Bug { - Unloaded { seed: u64 }, - Loaded(LoadedBug), -} - -#[derive(Clone)] -pub struct LoadedBug { +pub struct Bug { /// The seed of the bug. pub seed: u64, + /// The plan of the bug. - pub plan: InteractionPlan, + /// TODO: currently plan is only saved to the .sql file, and that is not deserializable yet + /// so we cannot always store an interaction plan here + pub plan: Option, + /// The shrunk plan of the bug, if any. pub shrunk_plan: Option, + /// The runs of the bug. pub runs: Vec, } #[derive(Clone, Serialize, Deserialize)] -pub(crate) struct BugRun { +pub struct BugRun { /// Commit hash of the current version of Limbo. - pub(crate) hash: String, + pub hash: String, /// Timestamp of the run. #[serde(with = "chrono::serde::ts_seconds")] - pub(crate) timestamp: DateTime, + pub timestamp: DateTime, /// Error message of the run. - pub(crate) error: Option, + pub error: Option, /// Options - pub(crate) cli_options: SimulatorCLI, + pub cli_options: SimulatorCLI, /// Whether the run was a shrunk run. - pub(crate) shrunk: bool, + pub shrunk: bool, } impl Bug { - #[expect(dead_code)] - /// Check if the bug is loaded. - pub(crate) fn is_loaded(&self) -> bool { - match self { - Bug::Unloaded { .. } => false, - Bug::Loaded { .. } => true, + fn save_to_path(&self, path: impl AsRef) -> anyhow::Result<()> { + let path = path.as_ref(); + let bug_path = path.join(self.seed.to_string()); + std::fs::create_dir_all(&bug_path) + .with_context(|| "should be able to create bug directory")?; + + let seed_path = bug_path.join(SEED_PATH); + std::fs::write(&seed_path, self.seed.to_string()) + .with_context(|| "should be able to write seed file")?; + + if let Some(plan) = &self.plan { + let readable_plan_path = bug_path.join(READABLE_PLAN_PATH); + std::fs::write(&readable_plan_path, plan.to_string()) + .with_context(|| "should be able to write readable plan file")?; } + + if let Some(shrunk_plan) = &self.shrunk_plan { + let readable_shrunk_plan_path = bug_path.join(SHRUNK_READABLE_PLAN_PATH); + std::fs::write(&readable_shrunk_plan_path, shrunk_plan.to_string()) + .with_context(|| "should be able to write readable shrunk plan file")?; + } + + let runs_path = bug_path.join(RUNS_PATH); + std::fs::write( + &runs_path, + serde_json::to_string_pretty(&self.runs) + .with_context(|| "should be able to serialize runs")?, + ) + .with_context(|| "should be able to write runs file")?; + + Ok(()) } - /// Get the seed of the bug. - pub(crate) fn seed(&self) -> u64 { - match self { - Bug::Unloaded { seed } => *seed, - Bug::Loaded(LoadedBug { seed, .. }) => *seed, - } + pub fn last_cli_opts(&self) -> SimulatorCLI { + self.runs.last().unwrap().cli_options.clone() } } @@ -73,13 +97,14 @@ pub(crate) struct BugBase { /// Path to the bug base directory. path: PathBuf, /// The list of buggy runs, uniquely identified by their seed - bugs: HashMap, + bugs: HashMap>, } impl BugBase { /// Create a new bug base. fn new(path: PathBuf) -> anyhow::Result { 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() { @@ -95,7 +120,7 @@ impl BugBase { entry.file_name().to_string_lossy() ) })?; - bugs.insert(seed, Bug::Unloaded { seed }); + bugs.insert(seed, None); } } } @@ -105,7 +130,7 @@ impl BugBase { /// Load the bug base from one of the potential paths. pub(crate) fn load() -> anyhow::Result { - let potential_paths = vec![ + let potential_paths = [ // limbo project directory BugBase::get_limbo_project_dir()?, // home directory @@ -132,57 +157,33 @@ impl BugBase { Err(anyhow!("failed to create bug base")) } - #[expect(dead_code)] - /// Load the bug base from one of the potential paths. - pub(crate) fn interactive_load() -> anyhow::Result { - let potential_paths = vec![ - // limbo project directory - BugBase::get_limbo_project_dir()?, - // home directory - dirs::home_dir().with_context(|| "should be able to get home directory")?, - // current directory - std::env::current_dir().with_context(|| "should be able to get current directory")?, - ]; + fn load_bug(&self, seed: u64) -> anyhow::Result { + let path = self.path.join(seed.to_string()).join(RUNS_PATH); - 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::() - .with_context(|| format!("invalid choice {choice}"))?; - let path = match choice { - 1 => BugBase::get_limbo_project_dir()?.join(".bugbase"), - 2 => { - let home = std::env::var("HOME").with_context(|| "failed to get home directory")?; - PathBuf::from(home).join(".bugbase") - } - 3 => PathBuf::from(".bugbase"), - _ => anyhow::bail!(format!("invalid choice {choice}")), + let runs = if !path.exists() { + vec![] + } else { + std::fs::read_to_string(self.path.join(seed.to_string()).join(RUNS_PATH)) + .with_context(|| "should be able to read runs file") + .and_then(|runs| serde_json::from_str(&runs).map_err(|e| anyhow!("{}", e)))? }; - if path.exists() { - unreachable!("bug base already exists at {}", path.display()); - } else { - std::fs::create_dir_all(&path).with_context(|| "failed to create bug base")?; - tracing::info!("bug base created at {}", path.display()); - BugBase::new(path) - } + let bug = Bug { + seed, + plan: None, + shrunk_plan: None, + runs, + }; + Ok(bug) + } + + pub fn load_bugs(&self) -> anyhow::Result> { + let seeds = self.bugs.keys().copied().collect::>(); + + seeds + .iter() + .map(|seed| self.load_bug(*seed)) + .collect::, _>>() } /// Add a new bug to the bug base. @@ -193,12 +194,11 @@ impl BugBase { error: Option, cli_options: &SimulatorCLI, ) -> anyhow::Result<()> { - tracing::debug!("adding bug with seed {}", seed); - let bug = self.get_bug(seed); + let path = self.path.clone(); - if bug.is_some() { - let mut bug = self.load_bug(seed)?; - bug.plan = plan.clone(); + tracing::debug!("adding bug with seed {}", seed); + let bug = self.get_or_load_bug(seed)?; + let bug = if let Some(bug) = bug { bug.runs.push(BugRun { hash: Self::get_current_commit_hash()?, timestamp: SystemTime::now().into(), @@ -206,11 +206,13 @@ impl BugBase { cli_options: cli_options.clone(), shrunk: false, }); - self.bugs.insert(seed, Bug::Loaded(bug.clone())); + bug.plan = Some(plan); + bug } else { - let bug = LoadedBug { + let bug = Bug { seed, - plan: plan.clone(), + plan: Some(plan), + shrunk_plan: None, runs: vec![BugRun { hash: Self::get_current_commit_hash()?, timestamp: SystemTime::now().into(), @@ -218,172 +220,44 @@ impl BugBase { cli_options: cli_options.clone(), shrunk: false, }], - shrunk_plan: None, }; - self.bugs.insert(seed, Bug::Loaded(bug.clone())); - } + + self.bugs.insert(seed, Some(bug.clone())); + self.bugs.get_mut(&seed).unwrap().as_mut().unwrap() + }; + // Save the bug to the bug base. - self.save_bug(seed) + bug.save_to_path(&path) } - /// Get a bug from the bug base. - pub(crate) fn get_bug(&self, seed: u64) -> Option<&Bug> { - self.bugs.get(&seed) - } + pub fn get_or_load_bug(&mut self, seed: u64) -> anyhow::Result> { + // Check if the bug exists and is loaded + let needs_loading = match self.bugs.get(&seed) { + Some(Some(_)) => false, // Already loaded + Some(None) => true, // Exists but unloaded + None => return Ok(None), // Doesn't exist + }; - /// Save a bug to the bug base. - fn save_bug(&self, seed: u64) -> anyhow::Result<()> { - let bug = self.get_bug(seed); - - match bug { - None | Some(Bug::Unloaded { .. }) => { - unreachable!("save should only be called within add_bug"); - } - Some(Bug::Loaded(bug)) => { - let bug_path = self.path.join(seed.to_string()); - std::fs::create_dir_all(&bug_path) - .with_context(|| "should be able to create bug directory")?; - - let seed_path = bug_path.join("seed.txt"); - std::fs::write(&seed_path, seed.to_string()) - .with_context(|| "should be able to write seed file")?; - - let plan_path = bug_path.join("plan.json"); - std::fs::write( - &plan_path, - serde_json::to_string_pretty(&bug.plan) - .with_context(|| "should be able to serialize plan")?, - ) - .with_context(|| "should be able to write plan file")?; - - if let Some(shrunk_plan) = &bug.shrunk_plan { - let shrunk_plan_path = bug_path.join("shrunk.json"); - std::fs::write( - &shrunk_plan_path, - serde_json::to_string_pretty(shrunk_plan) - .with_context(|| "should be able to serialize shrunk plan")?, - ) - .with_context(|| "should be able to write shrunk plan file")?; - - let readable_shrunk_plan_path = bug_path.join("shrunk.sql"); - std::fs::write(&readable_shrunk_plan_path, shrunk_plan.to_string()) - .with_context(|| "should be able to write readable shrunk plan file")?; - } - - let readable_plan_path = bug_path.join("plan.sql"); - std::fs::write(&readable_plan_path, bug.plan.to_string()) - .with_context(|| "should be able to write readable plan file")?; - - let runs_path = bug_path.join("runs.json"); - std::fs::write( - &runs_path, - serde_json::to_string_pretty(&bug.runs) - .with_context(|| "should be able to serialize runs")?, - ) - .with_context(|| "should be able to write runs file")?; - } + if needs_loading { + let bug = self.load_bug(seed)?; + self.bugs.insert(seed, Some(bug)); } - Ok(()) + // Now get the mutable reference + Ok(self.bugs.get_mut(&seed).and_then(|opt| opt.as_mut())) } - pub(crate) fn load_bug(&mut self, seed: u64) -> anyhow::Result { - let seed_match = self.bugs.get(&seed); - - match seed_match { - None => anyhow::bail!("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")) - .with_context(|| { - format!( - "should be able to read plan file at {}", - self.path.join(seed.to_string()).join("plan.json").display() - ) - })?; - let plan: InteractionPlan = serde_json::from_str(&plan) - .with_context(|| "should be able to deserialize plan")?; - - let shrunk_plan: Option = - std::fs::read_to_string(self.path.join(seed.to_string()).join("shrunk.json")) - .with_context(|| "should be able to read shrunk plan file") - .and_then(|shrunk| { - serde_json::from_str(&shrunk).map_err(|e| anyhow!("{}", e)) - }) - .ok(); - - let shrunk_plan: Option = - shrunk_plan.and_then(|shrunk_plan| serde_json::from_str(&shrunk_plan).ok()); - - let runs = - std::fs::read_to_string(self.path.join(seed.to_string()).join("runs.json")) - .with_context(|| "should be able to read runs file") - .and_then(|runs| serde_json::from_str(&runs).map_err(|e| anyhow!("{}", e))) - .unwrap_or_default(); - - let bug = LoadedBug { - seed, - plan: plan.clone(), - runs, - shrunk_plan, - }; - - self.bugs.insert(seed, Bug::Loaded(bug.clone())); - tracing::debug!("Loaded bug with seed {}", seed); - Ok(bug) - } - Some(Bug::Loaded(bug)) => { - tracing::warn!( - "Bug with seed {} is already loaded, returning the existing plan", - seed - ); - Ok(bug.clone()) - } - } - } - - #[expect(dead_code)] - pub(crate) fn mark_successful_run( - &mut self, - seed: u64, - cli_options: &SimulatorCLI, - ) -> anyhow::Result<()> { - let bug = self.get_bug(seed); - match bug { - None => { - tracing::debug!("removing bug base entry for {}", seed); - std::fs::remove_dir_all(self.path.join(seed.to_string())) - .with_context(|| "should be able to remove bug directory")?; - } - Some(_) => { - let mut bug = self.load_bug(seed)?; - bug.runs.push(BugRun { - hash: Self::get_current_commit_hash()?, - timestamp: SystemTime::now().into(), - error: None, - cli_options: cli_options.clone(), - shrunk: false, - }); - self.bugs.insert(seed, Bug::Loaded(bug.clone())); - // Save the bug to the bug base. - self.save_bug(seed) - .with_context(|| "should be able to save bug")?; - tracing::debug!("Updated bug with seed {}", seed); - } - } - - Ok(()) - } - - pub(crate) fn make_shrunk( + pub(crate) fn save_shrunk( &mut self, seed: u64, cli_options: &SimulatorCLI, shrunk_plan: InteractionPlan, error: Option, ) -> anyhow::Result<()> { - let mut bug = self.load_bug(seed)?; - bug.shrunk_plan = Some(shrunk_plan); + let path = self.path.clone(); + let bug = self + .get_or_load_bug(seed)? + .expect("bug should have been loaded"); bug.runs.push(BugRun { hash: Self::get_current_commit_hash()?, timestamp: SystemTime::now().into(), @@ -391,27 +265,18 @@ impl BugBase { cli_options: cli_options.clone(), shrunk: true, }); - self.bugs.insert(seed, Bug::Loaded(bug.clone())); + bug.shrunk_plan = Some(shrunk_plan); + // Save the bug to the bug base. - self.save_bug(seed) + bug.save_to_path(path) .with_context(|| "should be able to save shrunk bug")?; Ok(()) } - pub(crate) fn load_bugs(&mut self) -> anyhow::Result> { - let seeds = self.bugs.keys().copied().collect::>(); - - seeds - .iter() - .map(|seed| self.load_bug(*seed)) - .collect::, _>>() - } - pub(crate) fn list_bugs(&mut self) -> anyhow::Result<()> { let bugs = self.load_bugs()?; for bug in bugs { println!("seed: {}", bug.seed); - println!("plan: {}", bug.plan.stats()); println!("runs:"); println!(" ------------------"); for run in &bug.runs { diff --git a/simulator/runner/cli.rs b/simulator/runner/cli.rs index 97062dd2d..f0b9f7093 100644 --- a/simulator/runner/cli.rs +++ b/simulator/runner/cli.rs @@ -55,7 +55,7 @@ pub struct SimulatorCLI { help = "load plan from the bug base", conflicts_with = "seed" )] - pub load: Option, + pub load: Option, #[clap( short = 'w', long,