diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index 39956210b..358b5be17 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -184,10 +184,7 @@ fn property_insert_select(rng: &mut R, env: &SimulatorEnv) -> Prop // Select the row let select_query = Select { table: table.name.clone(), - predicate: Predicate::arbitrary_from( - rng, - &(table, &Predicate::Eq(column.name.clone(), value.clone())), - ), + predicate: Predicate::arbitrary_from(rng, &(table, &row)), }; Property::InsertSelect { diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index c99638f6d..bc71515c1 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -3,6 +3,7 @@ use crate::generation::{one_of, Arbitrary, ArbitraryFrom}; use crate::model::query::{Create, Delete, Insert, Predicate, Query, Select}; use crate::model::table::{Table, Value}; +use rand::seq::SliceRandom as _; use rand::Rng; use super::{frequency, pick}; @@ -174,7 +175,7 @@ impl ArbitraryFrom<(&Table, bool)> for CompoundPredicate { let len = booleans.len(); // Make sure at least one of them is false - if booleans.iter().all(|b| *b) { + if !booleans.is_empty() && booleans.iter().all(|b| *b) { booleans[rng.gen_range(0..len)] = false; } @@ -195,7 +196,7 @@ impl ArbitraryFrom<(&Table, bool)> for CompoundPredicate { .collect::>(); let len = booleans.len(); // Make sure at least one of them is true - if booleans.iter().all(|b| !*b) { + if !booleans.is_empty() && booleans.iter().all(|b| !*b) { booleans[rng.gen_range(0..len)] = true; } @@ -246,16 +247,155 @@ impl ArbitraryFrom<(&str, &Value)> for Predicate { } } -impl ArbitraryFrom<(&Table, &Predicate)> for Predicate { - fn arbitrary_from(rng: &mut R, (t, p): &(&Table, &Predicate)) -> Self { - if rng.gen_bool(0.5) { - // produce a true predicate - let p_t = CompoundPredicate::arbitrary_from(rng, &(*t, true)).0; - Predicate::And(vec![p_t, (*p).clone()]) - } else { - // produce a false predicate - let p_f = CompoundPredicate::arbitrary_from(rng, &(*t, false)).0; - Predicate::Or(vec![p_f, (*p).clone()]) +/// Produces a predicate that is true for the provided row in the given table +fn produce_true_predicate(rng: &mut R, (t, row): &(&Table, &Vec)) -> Predicate { + // Pick a column + let column_index = rng.gen_range(0..t.columns.len()); + let column = &t.columns[column_index]; + let value = &row[column_index]; + one_of( + vec![ + Box::new(|_| Predicate::Eq(column.name.clone(), value.clone())), + Box::new(|rng| { + let v = loop { + let v = Value::arbitrary_from(rng, &column.column_type); + if &v != value { + break v; + } + }; + Predicate::Neq(column.name.clone(), v) + }), + Box::new(|rng| { + Predicate::Gt(column.name.clone(), LTValue::arbitrary_from(rng, value).0) + }), + Box::new(|rng| { + Predicate::Lt(column.name.clone(), GTValue::arbitrary_from(rng, value).0) + }), + ], + rng, + ) +} + +/// Produces a predicate that is false for the provided row in the given table +fn produce_false_predicate(rng: &mut R, (t, row): &(&Table, &Vec)) -> Predicate { + // Pick a column + let column_index = rng.gen_range(0..t.columns.len()); + let column = &t.columns[column_index]; + let value = &row[column_index]; + one_of( + vec![ + Box::new(|_| Predicate::Neq(column.name.clone(), value.clone())), + Box::new(|rng| { + let v = loop { + let v = Value::arbitrary_from(rng, &column.column_type); + if &v != value { + break v; + } + }; + Predicate::Eq(column.name.clone(), v) + }), + Box::new(|rng| { + Predicate::Gt(column.name.clone(), GTValue::arbitrary_from(rng, value).0) + }), + Box::new(|rng| { + Predicate::Lt(column.name.clone(), LTValue::arbitrary_from(rng, value).0) + }), + ], + rng, + ) +} + +impl ArbitraryFrom<(&Table, &Vec)> for Predicate { + fn arbitrary_from(rng: &mut R, (t, row): &(&Table, &Vec)) -> Self { + // We want to produce a predicate that is true for the row + // We can do this by creating several predicates that + // are true, some that are false, combiend them in ways that correspond to the creation of a true predicate + + // Produce some true and false predicates + let mut true_predicates = (1..=rng.gen_range(1..=4)) + .map(|_| produce_true_predicate(rng, &(*t, row))) + .collect::>(); + + let false_predicates = (0..=rng.gen_range(0..=3)) + .map(|_| produce_false_predicate(rng, &(*t, row))) + .collect::>(); + + // Start building a top level predicate from a true predicate + let mut result = true_predicates.pop().unwrap(); + println!("True predicate: {:?}", result); + + let mut predicates = true_predicates + .iter() + .map(|p| (true, p.clone())) + .chain(false_predicates.iter().map(|p| (false, p.clone()))) + .collect::>(); + + predicates.shuffle(rng); + + while !predicates.is_empty() { + // Create a new predicate from at least 1 and at most 3 predicates + let context = + predicates[0..rng.gen_range(0..=usize::min(3, predicates.len()))].to_vec(); + // Shift `predicates` to remove the predicates in the context + predicates = predicates[context.len()..].to_vec(); + + // `result` is true, so we have the following three options to make a true predicate: + // T or F + // T or T + // T and T + + result = one_of( + vec![ + // T or (X1 or X2 or ... or Xn) + Box::new(|_| { + Predicate::Or(vec![ + result.clone(), + Predicate::Or(context.iter().map(|(_, p)| p.clone()).collect()), + ]) + }), + // T or (T1 and T2 and ... and Tn) + Box::new(|_| { + Predicate::Or(vec![ + result.clone(), + Predicate::And(context.iter().map(|(_, p)| p.clone()).collect()), + ]) + }), + // T and T + Box::new(|_| { + // Check if all the predicates in the context are true + if context.iter().all(|(b, _)| *b) { + // T and (X1 or X2 or ... or Xn) + Predicate::And(vec![ + result.clone(), + Predicate::And(context.iter().map(|(_, p)| p.clone()).collect()), + ]) + } + // Check if there is at least one true predicate + else if context.iter().any(|(b, _)| *b) { + // T and (X1 or X2 or ... or Xn) + Predicate::And(vec![ + result.clone(), + Predicate::Or(context.iter().map(|(_, p)| p.clone()).collect()), + ]) + } else { + // T and (X1 or X2 or ... or Xn or TRUE) + Predicate::And(vec![ + result.clone(), + Predicate::Or( + context + .iter() + .map(|(_, p)| p.clone()) + .chain(std::iter::once(Predicate::true_())) + .collect(), + ), + ]) + } + }), + ], + rng, + ); } + + result } } diff --git a/simulator/generation/table.rs b/simulator/generation/table.rs index 179c53436..b5b898eeb 100644 --- a/simulator/generation/table.rs +++ b/simulator/generation/table.rs @@ -15,7 +15,7 @@ impl Arbitrary for Name { impl Arbitrary for Table { fn arbitrary(rng: &mut R) -> Self { let name = Name::arbitrary(rng).0; - let columns = (1..=rng.gen_range(1..5)) + let columns = (1..=rng.gen_range(1..10)) .map(|_| Column::arbitrary(rng)) .collect(); Table { @@ -83,7 +83,7 @@ impl ArbitraryFrom for LTValue { fn arbitrary_from(rng: &mut R, value: &Value) -> Self { match value { Value::Integer(i) => Self(Value::Integer(rng.gen_range(i64::MIN..*i - 1))), - Value::Float(f) => Self(Value::Float(rng.gen_range(-1e10..*f - 1.0))), + Value::Float(f) => Self(Value::Float(f - rng.gen_range(0.0..1e10))), Value::Text(t) => { // Either shorten the string, or make at least one character smaller and mutate the rest let mut t = t.clone(); diff --git a/simulator/main.rs b/simulator/main.rs index a4e99f6ea..1728744af 100644 --- a/simulator/main.rs +++ b/simulator/main.rs @@ -12,7 +12,7 @@ use runner::io::SimulatorIO; use std::any::Any; use std::backtrace::Backtrace; use std::io::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use tempfile::TempDir; @@ -21,10 +21,48 @@ mod model; mod runner; mod shrink; -fn main() { +struct Paths { + db: PathBuf, + plan: PathBuf, + shrunk_plan: PathBuf, + history: PathBuf, + doublecheck_db: PathBuf, + shrunk_db: PathBuf, +} + +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); + } + log::info!("simulator plan path: {:?}", paths.plan); + if shrink { + log::info!("shrunk plan path: {:?}", paths.shrunk_plan); + } + log::info!("simulator history path: {:?}", paths.history); + + paths + } +} + +fn main() -> Result<(), String> { let _ = env_logger::try_init(); let cli_opts = SimulatorCLI::parse(); + cli_opts.validate()?; let seed = match cli_opts.seed { Some(seed) => seed, @@ -33,30 +71,10 @@ fn main() { let output_dir = match &cli_opts.output_dir { Some(dir) => Path::new(dir).to_path_buf(), - None => TempDir::new().unwrap().into_path(), + None => TempDir::new().map_err(|e| format!("{:?}", e))?.into_path(), }; - let db_path = output_dir.join("simulator.db"); - let doublecheck_db_path = db_path.with_extension("_doublecheck.db"); - let shrunk_db_path = db_path.with_extension("_shrink.db"); - - let plan_path = output_dir.join("simulator.plan"); - let shrunk_plan_path = plan_path.with_extension("_shrunk.plan"); - - let history_path = output_dir.join("simulator.history"); - - // Print the seed, the locations of the database and the plan file - log::info!("database path: {:?}", db_path); - if cli_opts.doublecheck { - log::info!("doublecheck database path: {:?}", doublecheck_db_path); - } else if cli_opts.shrink { - log::info!("shrunk database path: {:?}", shrunk_db_path); - } - log::info!("simulator plan path: {:?}", plan_path); - if cli_opts.shrink { - log::info!("shrunk plan path: {:?}", shrunk_plan_path); - } - log::info!("simulator history path: {:?}", history_path); + let paths = Paths::new(&output_dir, cli_opts.shrink, cli_opts.doublecheck); log::info!("seed: {}", seed); let last_execution = Arc::new(Mutex::new(Execution::new(0, 0, 0))); @@ -82,8 +100,8 @@ fn main() { run_simulation( seed, &cli_opts, - &db_path, - &plan_path, + &paths.db, + &paths.plan, last_execution.clone(), None, ) @@ -98,8 +116,8 @@ fn main() { run_simulation( seed, &cli_opts, - &doublecheck_db_path, - &plan_path, + &paths.doublecheck_db, + &paths.plan, last_execution.clone(), None, ) @@ -138,8 +156,8 @@ fn main() { | (SandboxedResult::FoundBug { .. }, SandboxedResult::FoundBug { .. }) | (SandboxedResult::Panicked { .. }, SandboxedResult::Panicked { .. }) => { // Compare the two database files byte by byte - let db_bytes = std::fs::read(&db_path).unwrap(); - let doublecheck_db_bytes = std::fs::read(&doublecheck_db_path).unwrap(); + 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 { @@ -164,7 +182,7 @@ fn main() { } => { if let SandboxedResult::FoundBug { history, .. } = &result { // No panic occurred, so write the history to a file - let f = std::fs::File::create(&history_path).unwrap(); + let f = std::fs::File::create(&paths.history).unwrap(); let mut f = std::io::BufWriter::new(f); for execution in history.history.iter() { writeln!( @@ -190,8 +208,8 @@ fn main() { run_simulation( seed, &cli_opts, - &shrunk_db_path, - &shrunk_plan_path, + &paths.shrunk_db, + &paths.shrunk_plan, last_execution.clone(), shrink, ) @@ -225,8 +243,8 @@ fn main() { } // Write the shrunk plan to a file - let shrunk_plan = std::fs::read(&shrunk_plan_path).unwrap(); - let mut f = std::fs::File::create(&shrunk_plan_path).unwrap(); + let shrunk_plan = std::fs::read(&paths.shrunk_plan).unwrap(); + let mut f = std::fs::File::create(&paths.shrunk_plan).unwrap(); f.write_all(&shrunk_plan).unwrap(); } } @@ -234,18 +252,20 @@ fn main() { } // Print the seed, the locations of the database and the plan file at the end again for easily accessing them. - println!("database path: {:?}", db_path); + println!("database path: {:?}", paths.db); if cli_opts.doublecheck { - println!("doublecheck database path: {:?}", doublecheck_db_path); + println!("doublecheck database path: {:?}", paths.doublecheck_db); } else if cli_opts.shrink { - println!("shrunk database path: {:?}", shrunk_db_path); + println!("shrunk database path: {:?}", paths.shrunk_db); } - println!("simulator plan path: {:?}", plan_path); + println!("simulator plan path: {:?}", paths.plan); if cli_opts.shrink { - println!("shrunk plan path: {:?}", shrunk_plan_path); + println!("shrunk plan path: {:?}", paths.shrunk_plan); } - println!("simulator history path: {:?}", history_path); + println!("simulator history path: {:?}", paths.history); println!("seed: {}", seed); + + Ok(()) } fn move_db_and_plan_files(output_dir: &Path) { @@ -346,18 +366,6 @@ fn run_simulation( (create_percent, read_percent, write_percent, delete_percent) }; - if cli_opts.minimum_size < 1 { - panic!("minimum size must be at least 1"); - } - - if cli_opts.maximum_size < 1 { - panic!("maximum size must be at least 1"); - } - - if cli_opts.maximum_size < cli_opts.minimum_size { - panic!("maximum size must be greater than or equal to minimum size"); - } - let opts = SimulatorOpts { ticks: rng.gen_range(cli_opts.minimum_size..=cli_opts.maximum_size), max_connections: 1, // TODO: for now let's use one connection as we didn't implement diff --git a/simulator/model/query.rs b/simulator/model/query.rs index 40d7b7c89..a111a508f 100644 --- a/simulator/model/query.rs +++ b/simulator/model/query.rs @@ -12,6 +12,16 @@ pub(crate) enum Predicate { Lt(String, Value), // column < Value } +impl Predicate { + pub(crate) fn true_() -> Self { + Self::And(vec![]) + } + + pub(crate) fn false_() -> Self { + Self::Or(vec![]) + } +} + impl Display for Predicate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/simulator/runner/cli.rs b/simulator/runner/cli.rs index 6b56b9b82..365ad6a77 100644 --- a/simulator/runner/cli.rs +++ b/simulator/runner/cli.rs @@ -42,3 +42,18 @@ pub struct SimulatorCLI { )] pub shrink: bool, } + +impl SimulatorCLI { + pub fn validate(&self) -> Result<(), String> { + if self.minimum_size < 1 { + return Err("minimum size must be at least 1".to_string()); + } + if self.maximum_size < 1 { + return Err("maximum size must be at least 1".to_string()); + } + if self.minimum_size > self.maximum_size { + return Err("Minimum size cannot be greater than maximum size".to_string()); + } + Ok(()) + } +} diff --git a/simulator/shrink/plan.rs b/simulator/shrink/plan.rs index 87eb90248..01fe18f48 100644 --- a/simulator/shrink/plan.rs +++ b/simulator/shrink/plan.rs @@ -3,6 +3,10 @@ use crate::{generation::plan::InteractionPlan, runner::execution::Execution}; impl InteractionPlan { /// Create a smaller interaction plan by deleting a property pub(crate) fn shrink_interaction_plan(&self, failing_execution: &Execution) -> InteractionPlan { + // todo: this is a very naive implementation, next steps are; + // - Shrink to multiple values by removing random interactions + // - Shrink properties by removing their extensions, or shrinking their values + let mut plan = self.clone(); let failing_property = &self.plan[failing_execution.interaction_index]; let depending_tables = failing_property.dependencies();