mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-07 01:04:26 +01:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user