implement watch mode

- add `--watch` flag
- start saving seeds in persistent storage
- make a separate version of execution functions that use `vector of interaction` instead of `InteractionPlan`
This commit is contained in:
alpaylan
2025-01-18 23:54:03 +03:00
parent c30e2757b4
commit e476b9f697
12 changed files with 521 additions and 91 deletions

107
Cargo.lock generated
View File

@@ -183,9 +183,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "block-buffer"
@@ -735,6 +735,18 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]]
name = "findshlibs"
version = "0.10.2"
@@ -753,6 +765,15 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "futures"
version = "0.3.31"
@@ -1009,6 +1030,26 @@ dependencies = [
"str_stack",
]
[[package]]
name = "inotify"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.8.0",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "io-uring"
version = "0.6.4"
@@ -1125,6 +1166,26 @@ dependencies = [
"chrono",
]
[[package]]
name = "kqueue"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -1163,8 +1224,9 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"libc",
"redox_syscall",
]
[[package]]
@@ -1282,6 +1344,7 @@ dependencies = [
"env_logger 0.10.2",
"limbo_core",
"log",
"notify",
"rand",
"rand_chacha",
"serde",
@@ -1416,6 +1479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.52.0",
]
@@ -1472,7 +1536,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -1488,6 +1552,31 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "notify"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
dependencies = [
"bitflags 2.8.0",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"notify-types",
"walkdir",
"windows-sys 0.59.0",
]
[[package]]
name = "notify-types"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "num-format"
version = "0.4.4"
@@ -1949,7 +2038,7 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
]
[[package]]
@@ -2042,7 +2131,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"fallible-iterator 0.2.0",
"fallible-streaming-iterator",
"hashlink",
@@ -2071,7 +2160,7 @@ version = "0.38.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"errno",
"libc",
"linux-raw-sys",
@@ -2084,7 +2173,7 @@ version = "12.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"cfg-if",
"clipboard-win",
"fd-lock",
@@ -2228,7 +2317,7 @@ dependencies = [
name = "sqlite3-parser"
version = "0.13.0"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.8.0",
"cc",
"env_logger 0.11.5",
"fallible-iterator 0.3.0",

View File

@@ -25,3 +25,4 @@ anarchist-readable-name-generator-lib = "0.1.2"
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
notify = "8.0.0"

View File

@@ -1,4 +1,4 @@
use std::{fmt::Display, rc::Rc, vec};
use std::{fmt::Display, path::Path, rc::Rc, vec};
use limbo_core::{Connection, Result, StepResult};
use serde::{Deserialize, Serialize};
@@ -26,6 +26,69 @@ pub(crate) struct InteractionPlan {
pub(crate) plan: Vec<Interactions>,
}
impl InteractionPlan {
/// Compute via diff computes a a plan from a given `.plan` file without the need to parse
/// sql. This is possible because there are two versions of the plan file, one that is human
/// readable and one that is serialized as JSON. Under watch mode, the users will be able to
/// delete interactions from the human readable file, and this function uses the JSON file as
/// a baseline to detect with interactions were deleted and constructs the plan from the
/// remaining interactions.
pub(crate) fn compute_via_diff(plan_path: &Path) -> Vec<Vec<Interaction>> {
let interactions = std::fs::read_to_string(plan_path).unwrap();
let interactions = interactions.lines().collect::<Vec<_>>();
let plan: InteractionPlan = serde_json::from_str(
std::fs::read_to_string(plan_path.with_extension("plan.json"))
.unwrap()
.as_str(),
)
.unwrap();
let mut plan = plan
.plan
.into_iter()
.map(|i| i.interactions())
.collect::<Vec<_>>();
let (mut i, mut j1, mut j2) = (0, 0, 0);
while i < interactions.len() && j1 < plan.len() {
if interactions[i].starts_with("-- begin")
|| interactions[i].starts_with("-- end")
|| interactions[i].is_empty()
{
i += 1;
continue;
}
if interactions[i].contains(plan[j1][j2].to_string().as_str()) {
i += 1;
if j2 + 1 < plan[j1].len() {
j2 += 1;
} else {
j1 += 1;
j2 = 0;
}
} else {
plan[j1].remove(j2);
if plan[j1].is_empty() {
plan.remove(j1);
j2 = 0;
}
}
}
if j1 < plan.len() {
if j2 < plan[j1].len() {
let _ = plan[j1].split_off(j2);
}
let _ = plan.split_off(j1);
}
plan
}
}
pub(crate) struct InteractionPlanState {
pub(crate) stack: Vec<ResultSet>,
pub(crate) interaction_pointer: usize,
@@ -110,12 +173,12 @@ impl Display for InteractionPlan {
match interaction {
Interaction::Query(query) => writeln!(f, "{};", query)?,
Interaction::Assumption(assumption) => {
writeln!(f, "-- ASSUME: {};", assumption.message)?
writeln!(f, "-- ASSUME {};", assumption.message)?
}
Interaction::Assertion(assertion) => {
writeln!(f, "-- ASSERT: {};", assertion.message)?
writeln!(f, "-- ASSERT {};", assertion.message)?
}
Interaction::Fault(fault) => writeln!(f, "-- FAULT: {};", fault)?,
Interaction::Fault(fault) => writeln!(f, "-- FAULT '{}';", fault)?,
}
}
writeln!(f, "-- end testing '{}'", name)?;
@@ -162,9 +225,9 @@ impl Display for Interaction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Query(query) => write!(f, "{}", query),
Self::Assumption(assumption) => write!(f, "ASSUME: {}", assumption.message),
Self::Assertion(assertion) => write!(f, "ASSERT: {}", assertion.message),
Self::Fault(fault) => write!(f, "FAULT: {}", fault),
Self::Assumption(assumption) => write!(f, "ASSUME {}", assumption.message),
Self::Assertion(assertion) => write!(f, "ASSERT {}", assertion.message),
Self::Fault(fault) => write!(f, "FAULT '{}'", fault),
}
}
}
@@ -326,6 +389,14 @@ impl ArbitraryFrom<&mut SimulatorEnv> for InteractionPlan {
}
impl Interaction {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) {
match self {
Self::Query(query) => query.shadow(env),
Self::Assumption(_) => {}
Self::Assertion(_) => {}
Self::Fault(_) => {}
}
}
pub(crate) fn execute_query(&self, conn: &mut Rc<Connection>) -> ResultSet {
if let Self::Query(query) = self {
let query_str = query.to_string();

View File

@@ -104,7 +104,6 @@ impl Property {
let assertion = Interaction::Assertion(Assertion {
message: format!(
// todo: add the part inserting ({} = {})",
"row [{:?}] not found in table {}",
row.iter().map(|v| v.to_string()).collect::<Vec<String>>(),
insert.table,

View File

@@ -1,17 +1,20 @@
#![allow(clippy::arc_with_non_send_sync, dead_code)]
use clap::Parser;
use generation::plan::{InteractionPlan, InteractionPlanState};
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::cli::SimulatorCLI;
use runner::env::SimulatorEnv;
use runner::execution::{execute_plans, Execution, ExecutionHistory, ExecutionResult};
use runner::watch;
use std::any::Any;
use std::backtrace::Backtrace;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::sync::{mpsc, Arc, Mutex};
use tempfile::TempDir;
mod generation;
@@ -46,6 +49,10 @@ impl Paths {
log::info!("shrunk database path: {:?}", paths.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);
}
@@ -77,7 +84,85 @@ fn main() -> Result<(), String> {
log::info!("seed: {}", seed);
let last_execution = Arc::new(Mutex::new(Execution::new(0, 0, 0)));
let (env, plans) = setup_simulation(seed, &cli_opts, &paths.db, &paths.plan);
if cli_opts.watch {
watch_mode(seed, &cli_opts, &paths, last_execution.clone()).unwrap();
} else {
run_simulator(seed, &cli_opts, &paths, env, plans, last_execution.clone());
}
Ok(())
}
fn watch_mode(
seed: u64,
cli_opts: &SimulatorCLI,
paths: &Paths,
last_execution: Arc<Mutex<Execution>>,
) -> notify::Result<()> {
let (tx, rx) = mpsc::channel::<notify::Result<notify::Event>>();
println!("watching {:?}", paths.plan);
// Use recommended_watcher() to automatically select the best implementation
// for your platform. The `EventHandler` passed to this constructor can be a
// closure, a `std::sync::mpsc::Sender`, a `crossbeam_channel::Sender`, or
// another type the trait is implemented for.
let mut watcher = notify::recommended_watcher(tx)?;
// Add a path to be watched. All files and directories at that path and
// below will be monitored for changes.
watcher.watch(&paths.plan, RecursiveMode::NonRecursive)?;
// Block forever, printing out events as they come in
for res in rx {
match res {
Ok(event) => {
if let EventKind::Modify(ModifyKind::Data(DataChange::Content)) = event.kind {
log::info!("plan file modified, rerunning simulation");
let result = SandboxedResult::from(
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| {
i.shadow(&mut env);
});
});
let env = Arc::new(Mutex::new(env.clone()));
watch::run_simulation(env, &mut [plan], last_execution.clone())
}),
last_execution.clone(),
);
match result {
SandboxedResult::Correct => {
log::info!("simulation succeeded");
println!("simulation succeeded");
}
SandboxedResult::Panicked { error, .. }
| SandboxedResult::FoundBug { error, .. } => {
log::error!("simulation failed: '{}'", error);
println!("simulation failed: '{}'", error);
}
}
}
}
Err(e) => println!("watch error: {:?}", e),
}
}
Ok(())
}
fn run_simulator(
seed: u64,
cli_opts: &SimulatorCLI,
paths: &Paths,
env: SimulatorEnv,
plans: Vec<InteractionPlan>,
last_execution: Arc<Mutex<Execution>>,
) {
std::panic::set_hook(Box::new(move |info| {
log::error!("panic occurred");
@@ -94,8 +179,6 @@ fn main() -> Result<(), String> {
log::error!("captured backtrace:\n{}", bt);
}));
let (env, plans) = setup_simulation(seed, &cli_opts, &paths.db, &paths.plan);
let env = Arc::new(Mutex::new(env));
let result = SandboxedResult::from(
std::panic::catch_unwind(|| {
@@ -105,65 +188,13 @@ fn main() -> Result<(), String> {
);
if cli_opts.doublecheck {
{
let mut env_ = env.lock().unwrap();
env_.db = Database::open_file(env_.io.clone(), paths.doublecheck_db.to_str().unwrap())
.unwrap();
}
// Run the simulation again
let result2 = SandboxedResult::from(
std::panic::catch_unwind(|| {
run_simulation(env.clone(), &mut plans.clone(), last_execution.clone())
}),
last_execution.clone(),
);
match (result, result2) {
(SandboxedResult::Correct, SandboxedResult::Panicked { .. }) => {
log::error!("doublecheck failed! first run succeeded, but second run panicked.");
}
(SandboxedResult::FoundBug { .. }, SandboxedResult::Panicked { .. }) => {
log::error!(
"doublecheck failed! first run failed an assertion, but second run panicked."
);
}
(SandboxedResult::Panicked { .. }, SandboxedResult::Correct) => {
log::error!("doublecheck failed! first run panicked, but second run succeeded.");
}
(SandboxedResult::Panicked { .. }, SandboxedResult::FoundBug { .. }) => {
log::error!(
"doublecheck failed! first run panicked, but second run failed an assertion."
);
}
(SandboxedResult::Correct, SandboxedResult::FoundBug { .. }) => {
log::error!(
"doublecheck failed! first run succeeded, but second run failed an assertion."
);
}
(SandboxedResult::FoundBug { .. }, SandboxedResult::Correct) => {
log::error!(
"doublecheck failed! first run failed an assertion, but second run succeeded."
);
}
(SandboxedResult::Correct, SandboxedResult::Correct)
| (SandboxedResult::FoundBug { .. }, SandboxedResult::FoundBug { .. })
| (SandboxedResult::Panicked { .. }, SandboxedResult::Panicked { .. }) => {
// Compare the two database files byte by byte
let db_bytes = std::fs::read(&paths.db).unwrap();
let doublecheck_db_bytes = std::fs::read(&paths.doublecheck_db).unwrap();
if db_bytes != doublecheck_db_bytes {
log::error!("doublecheck failed! database files are different.");
} else {
log::info!("doublecheck succeeded! database files are the same.");
}
}
}
doublecheck(env.clone(), 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");
}
SandboxedResult::Panicked {
error,
@@ -191,6 +222,7 @@ fn main() -> Result<(), String> {
}
log::error!("simulation failed: '{}'", error);
println!("simulation failed: '{}'", error);
if cli_opts.shrink {
log::info!("Starting to shrink");
@@ -268,8 +300,69 @@ fn main() -> Result<(), String> {
}
println!("simulator history path: {:?}", paths.history);
println!("seed: {}", seed);
}
Ok(())
fn doublecheck(
env: Arc<Mutex<SimulatorEnv>>,
paths: &Paths,
plans: &[InteractionPlan],
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()).unwrap();
}
// Run the simulation again
let result2 = SandboxedResult::from(
std::panic::catch_unwind(|| {
run_simulation(env.clone(), &mut plans.to_owned(), last_execution.clone())
}),
last_execution.clone(),
);
match (result, result2) {
(SandboxedResult::Correct, SandboxedResult::Panicked { .. }) => {
log::error!("doublecheck failed! first run succeeded, but second run panicked.");
}
(SandboxedResult::FoundBug { .. }, SandboxedResult::Panicked { .. }) => {
log::error!(
"doublecheck failed! first run failed an assertion, but second run panicked."
);
}
(SandboxedResult::Panicked { .. }, SandboxedResult::Correct) => {
log::error!("doublecheck failed! first run panicked, but second run succeeded.");
}
(SandboxedResult::Panicked { .. }, SandboxedResult::FoundBug { .. }) => {
log::error!(
"doublecheck failed! first run panicked, but second run failed an assertion."
);
}
(SandboxedResult::Correct, SandboxedResult::FoundBug { .. }) => {
log::error!(
"doublecheck failed! first run succeeded, but second run failed an assertion."
);
}
(SandboxedResult::FoundBug { .. }, SandboxedResult::Correct) => {
log::error!(
"doublecheck failed! first run failed an assertion, but second run succeeded."
);
}
(SandboxedResult::Correct, SandboxedResult::Correct)
| (SandboxedResult::FoundBug { .. }, SandboxedResult::FoundBug { .. })
| (SandboxedResult::Panicked { .. }, SandboxedResult::Panicked { .. }) => {
// Compare the two database files byte by byte
let db_bytes = std::fs::read(&paths.db).unwrap();
let doublecheck_db_bytes = std::fs::read(&paths.doublecheck_db).unwrap();
if db_bytes != doublecheck_db_bytes {
log::error!("doublecheck failed! database files are different.");
} else {
log::info!("doublecheck succeeded! database files are the same.");
}
}
}
}
#[derive(Debug)]
@@ -326,11 +419,17 @@ impl SandboxedResult {
}
fn setup_simulation(
seed: u64,
mut seed: u64,
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();
}
let mut env = SimulatorEnv::new(seed, cli_opts, db_path);
// todo: the loading works correctly because of a hacky decision
@@ -357,14 +456,16 @@ fn setup_simulation(
// 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 json_path = plan_path.with_extension("plan.json");
let mut f = std::fs::File::create(&json_path).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())
.unwrap();
log::info!("{}", plan.stats());
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!("Executing database interaction plan...");
log::info!("{}", plan.stats());
(env, plans)
}
@@ -373,6 +474,8 @@ fn run_simulation(
plans: &mut [InteractionPlan],
last_execution: Arc<Mutex<Execution>>,
) -> ExecutionResult {
log::info!("Executing database interaction plan...");
let mut states = plans
.iter()
.map(|_| InteractionPlanState {

View File

@@ -154,12 +154,9 @@ pub(crate) struct Insert {
impl Insert {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) {
let table = env
.tables
.iter_mut()
.find(|t| t.name == self.table)
.unwrap();
table.rows.extend(self.values.clone());
if let Some(t) = env.tables.iter_mut().find(|t| t.name == self.table) {
t.rows.extend(self.values.clone());
}
}
}

View File

@@ -46,10 +46,30 @@ impl Display for ColumnType {
}
}
fn float_to_string<S>(float: &f64, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("{}", float))
}
fn string_to_float<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) enum Value {
Null,
Integer(i64),
// we use custom serialization to preserve float precision
#[serde(
serialize_with = "float_to_string",
deserialize_with = "string_to_float"
)]
Float(f64),
Text(String),
Blob(Vec<u8>),

View File

@@ -43,6 +43,12 @@ pub struct SimulatorCLI {
pub shrink: bool,
#[clap(short = 'l', long, help = "load plan from a file")]
pub load: Option<String>,
#[clap(
short = 'w',
long,
help = "enable watch mode that reruns the simulation on file changes"
)]
pub watch: bool,
}
impl SimulatorCLI {
@@ -58,6 +64,11 @@ impl SimulatorCLI {
return Err("Minimum size cannot be greater than maximum size".to_string());
}
// Make sure uncompatible 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))?;

View File

@@ -56,6 +56,11 @@ impl SimulatorEnv {
let io = Arc::new(SimulatorIO::new(seed, opts.page_size).unwrap());
// Remove existing database file if it exists
if db_path.exists() {
std::fs::remove_file(db_path).unwrap();
}
let db = match Database::open_file(io.clone(), db_path.to_str().unwrap()) {
Ok(db) => db,
Err(e) => {

View File

@@ -36,7 +36,7 @@ pub(crate) struct ExecutionHistory {
}
impl ExecutionHistory {
fn new() -> Self {
pub(crate) fn new() -> Self {
Self {
history: Vec::new(),
}
@@ -49,7 +49,7 @@ pub(crate) struct ExecutionResult {
}
impl ExecutionResult {
fn new(history: ExecutionHistory, error: Option<LimboError>) -> Self {
pub(crate) fn new(history: ExecutionHistory, error: Option<LimboError>) -> Self {
Self { history, error }
}
}
@@ -156,14 +156,14 @@ fn execute_plan(
/// `execute_interaction` uses this type in conjunction with a result, where
/// the `Err` case indicates a full-stop due to a bug, and the `Ok` case
/// indicates the next step in the plan.
enum ExecutionContinuation {
pub(crate) enum ExecutionContinuation {
/// Default continuation, execute the next interaction.
NextInteraction,
/// Typically used in the case of preconditions failures, skip to the next property.
NextProperty,
}
fn execute_interaction(
pub(crate) fn execute_interaction(
env: &mut SimulatorEnv,
connection_index: usize,
interaction: &Interaction,

View File

@@ -4,3 +4,4 @@ pub mod execution;
#[allow(dead_code)]
pub mod file;
pub mod io;
pub mod watch;

133
simulator/runner/watch.rs Normal file
View File

@@ -0,0 +1,133 @@
use std::sync::{Arc, Mutex};
use crate::{
generation::{
pick_index,
plan::{Interaction, InteractionPlanState},
},
runner::execution::ExecutionContinuation,
};
use super::{
env::{SimConnection, SimulatorEnv},
execution::{execute_interaction, Execution, ExecutionHistory, ExecutionResult},
};
pub(crate) fn run_simulation(
env: Arc<Mutex<SimulatorEnv>>,
plans: &mut [Vec<Vec<Interaction>>],
last_execution: Arc<Mutex<Execution>>,
) -> ExecutionResult {
let mut states = plans
.iter()
.map(|_| InteractionPlanState {
stack: vec![],
interaction_pointer: 0,
secondary_pointer: 0,
})
.collect::<Vec<_>>();
let result = execute_plans(env.clone(), plans, &mut states, last_execution);
let env = env.lock().unwrap();
env.io.print_stats();
log::info!("Simulation completed");
result
}
pub(crate) fn execute_plans(
env: Arc<Mutex<SimulatorEnv>>,
plans: &mut [Vec<Vec<Interaction>>],
states: &mut [InteractionPlanState],
last_execution: Arc<Mutex<Execution>>,
) -> ExecutionResult {
let mut history = ExecutionHistory::new();
let now = std::time::Instant::now();
let mut env = env.lock().unwrap();
for _tick in 0..env.opts.ticks {
// Pick the connection to interact with
let connection_index = pick_index(env.connections.len(), &mut env.rng);
let state = &mut states[connection_index];
history.history.push(Execution::new(
connection_index,
state.interaction_pointer,
state.secondary_pointer,
));
let mut last_execution = last_execution.lock().unwrap();
last_execution.connection_index = connection_index;
last_execution.interaction_index = state.interaction_pointer;
last_execution.secondary_index = state.secondary_pointer;
// Execute the interaction for the selected connection
match execute_plan(&mut env, connection_index, plans, states) {
Ok(_) => {}
Err(err) => {
return ExecutionResult::new(history, Some(err));
}
}
// Check if the maximum time for the simulation has been reached
if now.elapsed().as_secs() >= env.opts.max_time_simulation as u64 {
return ExecutionResult::new(
history,
Some(limbo_core::LimboError::InternalError(
"maximum time for simulation reached".into(),
)),
);
}
}
ExecutionResult::new(history, None)
}
fn execute_plan(
env: &mut SimulatorEnv,
connection_index: usize,
plans: &mut [Vec<Vec<Interaction>>],
states: &mut [InteractionPlanState],
) -> limbo_core::Result<()> {
let connection = &env.connections[connection_index];
let plan = &mut plans[connection_index];
let state = &mut states[connection_index];
if state.interaction_pointer >= plan.len() {
return Ok(());
}
let interaction = &plan[state.interaction_pointer][state.secondary_pointer];
if let SimConnection::Disconnected = connection {
log::info!("connecting {}", connection_index);
env.connections[connection_index] = SimConnection::Connected(env.db.connect());
} else {
match execute_interaction(env, connection_index, interaction, &mut state.stack) {
Ok(next_execution) => {
log::debug!("connection {} processed", connection_index);
// Move to the next interaction or property
match next_execution {
ExecutionContinuation::NextInteraction => {
if state.secondary_pointer + 1 >= plan[state.interaction_pointer].len() {
// If we have reached the end of the interactions for this property, move to the next property
state.interaction_pointer += 1;
state.secondary_pointer = 0;
} else {
// Otherwise, move to the next interaction
state.secondary_pointer += 1;
}
}
ExecutionContinuation::NextProperty => {
// Skip to the next property
state.interaction_pointer += 1;
state.secondary_pointer = 0;
}
}
}
Err(err) => {
log::error!("error {}", err);
return Err(err);
}
}
}
Ok(())
}