this commit;

- makes interaction plans serializable
- fixes the shadowing bug where non-created tables were assumed to be created in the shadow tables map
- makes small changes to make clippy happy
- reorganizes simulation running flow to remove unnecessary plan regenerations while shrinking and double checking
This commit is contained in:
alpaylan
2025-01-17 01:28:37 +03:00
parent 2aaa981b18
commit 28cde537a8
10 changed files with 154 additions and 112 deletions

14
Cargo.lock generated
View File

@@ -1284,6 +1284,8 @@ dependencies = [
"log",
"rand",
"rand_chacha",
"serde",
"serde_json",
"tempfile",
]
@@ -2128,18 +2130,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.216"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.216"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@@ -2148,9 +2150,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
dependencies = [
"indexmap",
"itoa",

View File

@@ -23,3 +23,5 @@ tempfile = "3.0.7"
env_logger = "0.10.1"
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" }

View File

@@ -1,6 +1,7 @@
use std::{fmt::Display, rc::Rc, vec};
use limbo_core::{Connection, Result, StepResult};
use serde::{Deserialize, Serialize};
use crate::{
model::{
@@ -19,7 +20,7 @@ use super::{
pub(crate) type ResultSet = Result<Vec<Vec<Value>>>;
#[derive(Clone)]
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct InteractionPlan {
pub(crate) plan: Vec<Interactions>,
}
@@ -30,7 +31,7 @@ pub(crate) struct InteractionPlanState {
pub(crate) secondary_pointer: usize,
}
#[derive(Clone)]
#[derive(Clone, Serialize, Deserialize)]
pub(crate) enum Interactions {
Property(Property),
Query(Query),
@@ -178,7 +179,7 @@ pub(crate) struct Assertion {
pub(crate) message: String,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum Fault {
Disconnect,
}
@@ -195,6 +196,29 @@ impl Interactions {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) {
match self {
Interactions::Property(property) => {
match property {
Property::InsertSelect {
insert,
row_index: _,
queries,
select,
} => {
insert.shadow(env);
for query in queries {
query.shadow(env);
}
select.shadow(env);
}
Property::DoubleCreateFailure { create, queries } => {
if env.tables.iter().any(|t| t.name == create.table.name) {
return;
}
create.shadow(env);
for query in queries {
query.shadow(env);
}
}
}
for interaction in property.interactions() {
match interaction {
Interaction::Query(query) => match query {
@@ -220,23 +244,7 @@ impl Interactions {
}
}
}
Interactions::Query(query) => match query {
Query::Create(create) => {
if !env.tables.iter().any(|t| t.name == create.table.name) {
env.tables.push(create.table.clone());
}
}
Query::Insert(insert) => {
let table = env
.tables
.iter_mut()
.find(|t| t.name == insert.table)
.unwrap();
table.rows.extend(insert.values.clone());
}
Query::Delete(_) => todo!(),
Query::Select(_) => {}
},
Interactions::Query(query) => query.shadow(env),
Interactions::Fault(_) => {}
}
}

View File

@@ -1,4 +1,5 @@
use limbo_core::LimboError;
use serde::{Deserialize, Serialize};
use crate::{
model::{
@@ -16,7 +17,7 @@ use super::{
/// Properties are representations of executable specifications
/// about the database behavior.
#[derive(Clone)]
#[derive(Clone, Serialize, Deserialize)]
pub(crate) enum Property {
/// Insert-Select is a property in which the inserted row
/// must be in the resulting rows of a select query that has a
@@ -205,7 +206,7 @@ fn property_insert_select<R: rand::Rng>(
.collect::<Vec<_>>();
// Pick a random row to select
let row_index = pick_index(rows.len(), rng).clone();
let row_index = pick_index(rows.len(), rng);
let row = rows[row_index].clone();
// Insert the rows
@@ -228,7 +229,7 @@ fn property_insert_select<R: rand::Rng>(
predicate,
}) => {
// The inserted row will not be deleted.
if t == &table.name && predicate.test(&row, &table) {
if t == &table.name && predicate.test(&row, table) {
continue;
}
}

View File

@@ -97,32 +97,26 @@ 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(|| {
run_simulation(
seed,
&cli_opts,
&paths.db,
&paths.plan,
last_execution.clone(),
None,
)
run_simulation(env.clone(), &mut plans.clone(), last_execution.clone())
}),
last_execution.clone(),
);
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(
seed,
&cli_opts,
&paths.doublecheck_db,
&paths.plan,
last_execution.clone(),
None,
)
run_simulation(env.clone(), &mut plans.clone(), last_execution.clone())
}),
last_execution.clone(),
);
@@ -202,18 +196,24 @@ fn main() -> Result<(), String> {
if cli_opts.shrink {
log::info!("Starting to shrink");
let shrink = Some(last_execution);
let shrunk_plans = plans
.iter()
.map(|plan| {
let shrunk = plan.shrink_interaction_plan(last_execution);
log::info!("{}", shrunk.stats());
shrunk
})
.collect::<Vec<_>>();
let last_execution = Arc::new(Mutex::new(*last_execution));
let shrunk = SandboxedResult::from(
std::panic::catch_unwind(|| {
run_simulation(
seed,
&cli_opts,
&paths.shrunk_db,
&paths.shrunk_plan,
env.clone(),
&mut shrunk_plans.clone(),
last_execution.clone(),
shrink,
)
}),
last_execution,
@@ -270,28 +270,6 @@ fn main() -> Result<(), String> {
Ok(())
}
fn move_db_and_plan_files(output_dir: &Path) {
let old_db_path = output_dir.join("simulator.db");
let old_plan_path = output_dir.join("simulator.plan");
let new_db_path = output_dir.join("simulator_double.db");
let new_plan_path = output_dir.join("simulator_double.plan");
std::fs::rename(&old_db_path, &new_db_path).unwrap();
std::fs::rename(&old_plan_path, &new_plan_path).unwrap();
}
fn revert_db_and_plan_files(output_dir: &Path) {
let old_db_path = output_dir.join("simulator.db");
let old_plan_path = output_dir.join("simulator.plan");
let new_db_path = output_dir.join("simulator_double.db");
let new_plan_path = output_dir.join("simulator_double.plan");
std::fs::rename(&new_db_path, &old_db_path).unwrap();
std::fs::rename(&new_plan_path, &old_plan_path).unwrap();
}
#[derive(Debug)]
enum SandboxedResult {
Panicked {
@@ -345,14 +323,12 @@ impl SandboxedResult {
}
}
fn run_simulation(
fn setup_simulation(
seed: u64,
cli_opts: &SimulatorCLI,
db_path: &Path,
plan_path: &Path,
last_execution: Arc<Mutex<Execution>>,
shrink: Option<&Execution>,
) -> ExecutionResult {
) -> (SimulatorEnv, Vec<InteractionPlan>) {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let (create_percent, read_percent, write_percent, delete_percent) = {
@@ -403,9 +379,32 @@ fn run_simulation(
};
log::info!("Generating database interaction plan...");
let mut plans = (1..=env.opts.max_connections)
let plans = (1..=env.opts.max_connections)
.map(|_| InteractionPlan::arbitrary_from(&mut env.rng.clone(), &mut env))
.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 mut f = std::fs::File::create(plan_path.with_extension(".json")).unwrap();
f.write_all(serde_json::to_string(&plan).unwrap().as_bytes())
.unwrap();
log::info!("{}", plan.stats());
log::info!("Executing database interaction plan...");
(env, plans)
}
fn run_simulation(
env: Arc<Mutex<SimulatorEnv>>,
plans: &mut [InteractionPlan],
last_execution: Arc<Mutex<Execution>>,
) -> ExecutionResult {
let mut states = plans
.iter()
.map(|_| InteractionPlanState {
@@ -414,27 +413,9 @@ fn run_simulation(
secondary_pointer: 0,
})
.collect::<Vec<_>>();
let result = execute_plans(env.clone(), plans, &mut states, last_execution);
let plan = if let Some(failing_execution) = shrink {
// todo: for now, we only use 1 connection, so it's safe to use the first plan.
println!("Interactions Before: {}", plans[0].plan.len());
let shrunk = plans[0].shrink_interaction_plan(failing_execution);
println!("Interactions After: {}", shrunk.plan.len());
shrunk
} else {
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();
log::info!("{}", plan.stats());
log::info!("Executing database interaction plan...");
let result = execute_plans(&mut env, &mut plans, &mut states, last_execution);
let env = env.lock().unwrap();
env.io.print_stats();
log::info!("Simulation completed");

View File

@@ -1,8 +1,13 @@
use std::fmt::Display;
use crate::model::table::{Table, Value};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq)]
use crate::{
model::table::{Table, Value},
runner::env::SimulatorEnv,
};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) enum Predicate {
And(Vec<Predicate>), // p1 AND p2 AND p3... AND pn
Or(Vec<Predicate>), // p1 OR p2 OR p3... OR pn
@@ -83,7 +88,7 @@ impl Display for Predicate {
}
// This type represents the potential queries on the database.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum Query {
Create(Create),
Select(Select),
@@ -108,30 +113,68 @@ impl Query {
| Query::Delete(Delete { table, .. }) => vec![table.clone()],
}
}
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) {
match self {
Query::Create(create) => create.shadow(env),
Query::Insert(insert) => insert.shadow(env),
Query::Delete(delete) => delete.shadow(env),
Query::Select(select) => select.shadow(env),
}
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Create {
pub(crate) table: Table,
}
#[derive(Clone, Debug, PartialEq)]
impl Create {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) {
if !env.tables.iter().any(|t| t.name == self.table.name) {
env.tables.push(self.table.clone());
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Select {
pub(crate) table: String,
pub(crate) predicate: Predicate,
}
#[derive(Clone, Debug, PartialEq)]
impl Select {
pub(crate) fn shadow(&self, _env: &mut SimulatorEnv) {}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Insert {
pub(crate) table: String,
pub(crate) values: Vec<Vec<Value>>,
}
#[derive(Clone, Debug, PartialEq)]
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());
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Delete {
pub(crate) table: String,
pub(crate) predicate: Predicate,
}
impl Delete {
pub(crate) fn shadow(&self, _env: &mut SimulatorEnv) {
todo!()
}
}
impl Display for Query {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {

View File

@@ -1,5 +1,7 @@
use std::{fmt::Display, ops::Deref};
use serde::{Deserialize, Serialize};
pub(crate) struct Name(pub(crate) String);
impl Deref for Name {
@@ -10,14 +12,14 @@ impl Deref for Name {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Table {
pub(crate) rows: Vec<Vec<Value>>,
pub(crate) name: String,
pub(crate) columns: Vec<Column>,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Column {
pub(crate) name: String,
pub(crate) column_type: ColumnType,
@@ -25,7 +27,7 @@ pub(crate) struct Column {
pub(crate) unique: bool,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum ColumnType {
Integer,
Float,
@@ -44,7 +46,7 @@ impl Display for ColumnType {
}
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) enum Value {
Null,
Integer(i64),

View File

@@ -51,6 +51,7 @@ impl SimulatorCLI {
if self.maximum_size < 1 {
return Err("maximum size must be at least 1".to_string());
}
// todo: fix an issue here where if minimum size is not defined, it prevents setting low maximum sizes.
if self.minimum_size > self.maximum_size {
return Err("Minimum size cannot be greater than maximum size".to_string());
}

View File

@@ -8,6 +8,7 @@ use crate::model::table::Table;
use crate::runner::io::SimulatorIO;
#[derive(Clone)]
pub(crate) struct SimulatorEnv {
pub(crate) opts: SimulatorOpts,
pub(crate) tables: Vec<Table>,

View File

@@ -55,13 +55,14 @@ impl ExecutionResult {
}
pub(crate) fn execute_plans(
env: &mut SimulatorEnv,
env: Arc<Mutex<SimulatorEnv>>,
plans: &mut [InteractionPlan],
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);
@@ -77,7 +78,7 @@ pub(crate) fn execute_plans(
last_execution.interaction_index = state.interaction_pointer;
last_execution.secondary_index = state.secondary_pointer;
// Execute the interaction for the selected connection
match execute_plan(env, connection_index, plans, states) {
match execute_plan(&mut env, connection_index, plans, states) {
Ok(_) => {}
Err(err) => {
return ExecutionResult::new(history, Some(err));