add bug base, refactor

This commit is contained in:
alpaylan
2025-04-08 17:48:16 -04:00
parent 96ed7c5982
commit 64c2917e81
11 changed files with 435 additions and 179 deletions

1
.gitignore vendored
View File

@@ -34,3 +34,4 @@ dist/
# testing
testing/limbo_output.txt
**/limbo_output.txt
.bugbase

40
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -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(),
));

View File

@@ -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
View 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())?,
))
}
}

View File

@@ -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(())

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -1,3 +1,4 @@
pub mod bugbase;
pub mod cli;
pub mod differential;
pub mod env;