this commit fixes query generation;

- previous query generation method was faulty, producing wrong assertions
- this commit adds a new arbitrary_from implementation for predicates
- new implementation takes a table and a row, and produces a predicate that would evaluate to true for the row
this commit makes small changes to the main for increasing readability
This commit is contained in:
alpaylan
2025-01-13 02:31:19 +03:00
parent 7b2f65f5d0
commit 82fcc27a58
7 changed files with 245 additions and 71 deletions

View File

@@ -184,10 +184,7 @@ fn property_insert_select<R: rand::Rng>(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 {

View File

@@ -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::<Vec<_>>();
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<R: Rng>(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<R: Rng>(rng: &mut R, (t, row): &(&Table, &Vec<Value>)) -> 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<R: Rng>(rng: &mut R, (t, row): &(&Table, &Vec<Value>)) -> 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<Value>)> for Predicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (t, row): &(&Table, &Vec<Value>)) -> 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::<Vec<_>>();
let false_predicates = (0..=rng.gen_range(0..=3))
.map(|_| produce_false_predicate(rng, &(*t, row)))
.collect::<Vec<_>>();
// 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::<Vec<_>>();
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
}
}

View File

@@ -15,7 +15,7 @@ impl Arbitrary for Name {
impl Arbitrary for Table {
fn arbitrary<R: Rng>(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<Value> for LTValue {
fn arbitrary_from<R: Rng>(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();

View File

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

View File

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

View File

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

View File

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