- modify bugbase to not save interaction plan in a plan.json. As we

will track `Interaction` instead of `Interactions` in the Plan, this
  change will impossibilitate the serialization of the InteractionPlan with Serde Json. 
- make --load just load the previous cli args
This commit is contained in:
pedrocarlo
2025-10-18 11:55:19 -03:00
parent 8f50154db2
commit 4f143f385a
3 changed files with 169 additions and 332 deletions

View File

@@ -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::<Vec<_>>();
@@ -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::<Vec<_>>();
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::<u64>().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<Mutex<SimulatorEnv>>,
plan: impl InteractionPlanIterator,

View File

@@ -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<InteractionPlan>,
/// The shrunk plan of the bug, if any.
pub shrunk_plan: Option<InteractionPlan>,
/// The runs of the bug.
pub runs: Vec<BugRun>,
}
#[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<Utc>,
pub timestamp: DateTime<Utc>,
/// Error message of the run.
pub(crate) error: Option<String>,
pub error: Option<String>,
/// 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<Path>) -> 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<u64, Bug>,
bugs: HashMap<u64, Option<Bug>>,
}
impl BugBase {
/// Create a new bug base.
fn new(path: PathBuf) -> anyhow::Result<Self> {
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<Self> {
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<Self> {
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<Bug> {
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::<u32>()
.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<Vec<Bug>> {
let seeds = self.bugs.keys().copied().collect::<Vec<_>>();
seeds
.iter()
.map(|seed| self.load_bug(*seed))
.collect::<Result<Vec<_>, _>>()
}
/// Add a new bug to the bug base.
@@ -193,12 +194,11 @@ impl BugBase {
error: Option<String>,
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<Option<&mut Bug>> {
// 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<LoadedBug> {
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<String> =
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<InteractionPlan> =
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<String>,
) -> 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<Vec<LoadedBug>> {
let seeds = self.bugs.keys().copied().collect::<Vec<_>>();
seeds
.iter()
.map(|seed| self.load_bug(*seed))
.collect::<Result<Vec<_>, _>>()
}
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 {

View File

@@ -55,7 +55,7 @@ pub struct SimulatorCLI {
help = "load plan from the bug base",
conflicts_with = "seed"
)]
pub load: Option<String>,
pub load: Option<u64>,
#[clap(
short = 'w',
long,