mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-23 17:05:36 +01:00
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:
107
Cargo.lock
generated
107
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>),
|
||||
|
||||
@@ -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))?;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
133
simulator/runner/watch.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user