Merge 'simulator: add counterexample minimization' from Alperen Keleş

This PR introduces counterexample minimization(shrinking) in the
simulator. It will require changes to the current structure in various
places, so I've opened it as a draft PR for now, in order to not
overwhelm the reviewers all at once.
- [x] Turn interactions plans into sequences of properties instead of
sequences of interactions, adding a semantic layer.
- [x] Add assumptions to the properties, rendering a property invalid if
its assumptions are not valid.
- [x] Record the failure point in a failing assertion, as shrinking by
definition works when the same assertion fails for a smaller input.
- [ ] Add shrinking at three levels,
  - [x] top level(removing whole properties),
  - [x] property level(removing interactions within properties),
  - [ ] interaction level(shrinking values in interactions to smaller
ones).
- [ ] Add [marauders](https://github.com/alpaylan/marauders) as a dev
dependency, inject custom mutations to a testing branch for evaluating
the simulator performance.
- [ ] Integrate the simulator evaluation with the CI.

Closes #623
This commit is contained in:
Pekka Enberg
2025-01-15 13:12:17 +02:00
13 changed files with 1482 additions and 434 deletions

View File

@@ -4,24 +4,41 @@ use anarchist_readable_name_generator_lib::readable_name_custom;
use rand::{distributions::uniform::SampleUniform, Rng};
pub mod plan;
pub mod property;
pub mod query;
pub mod table;
/// Arbitrary trait for generating random values
/// An implementation of arbitrary is assumed to be a uniform sampling of
/// the possible values of the type, with a bias towards smaller values for
/// practicality.
pub trait Arbitrary {
fn arbitrary<R: Rng>(rng: &mut R) -> Self;
}
/// ArbitraryFrom trait for generating random values from a given value
/// ArbitraryFrom allows for constructing relations, where the generated
/// value is dependent on the given value. These relations could be constraints
/// such as generating an integer within an interval, or a value that fits in a table,
/// or a predicate satisfying a given table row.
pub trait ArbitraryFrom<T> {
fn arbitrary_from<R: Rng>(rng: &mut R, t: &T) -> Self;
fn arbitrary_from<R: Rng>(rng: &mut R, t: T) -> Self;
}
/// Frequency is a helper function for composing different generators with different frequency
/// of occurences.
/// The type signature for the `N` parameter is a bit complex, but it
/// roughly corresponds to a type that can be summed, compared, subtracted and sampled, which are
/// the operations we require for the implementation.
// todo: switch to a simpler type signature that can accomodate all integer and float types, which
// should be enough for our purposes.
pub(crate) fn frequency<
'a,
T,
R: rand::Rng,
N: Sum + PartialOrd + Copy + Default + SampleUniform + SubAssign,
>(
choices: Vec<(N, Box<dyn FnOnce(&mut R) -> T + 'a>)>,
choices: Vec<(N, Box<dyn Fn(&mut R) -> T + 'a>)>,
rng: &mut R,
) -> T {
let total = choices.iter().map(|(weight, _)| *weight).sum::<N>();
@@ -37,6 +54,7 @@ pub(crate) fn frequency<
unreachable!()
}
/// one_of is a helper function for composing different generators with equal probability of occurence.
pub(crate) fn one_of<'a, T, R: rand::Rng>(
choices: Vec<Box<dyn Fn(&mut R) -> T + 'a>>,
rng: &mut R,
@@ -45,15 +63,20 @@ pub(crate) fn one_of<'a, T, R: rand::Rng>(
choices[index](rng)
}
/// pick is a helper function for uniformly picking a random element from a slice
pub(crate) fn pick<'a, T, R: rand::Rng>(choices: &'a [T], rng: &mut R) -> &'a T {
let index = rng.gen_range(0..choices.len());
&choices[index]
}
/// pick_index is typically used for picking an index from a slice to later refer to the element
/// at that index.
pub(crate) fn pick_index<R: rand::Rng>(choices: usize, rng: &mut R) -> usize {
rng.gen_range(0..choices)
}
/// gen_random_text uses `anarchist_readable_name_generator_lib` to generate random
/// readable names for tables, columns, text values etc.
fn gen_random_text<T: Rng>(rng: &mut T) -> String {
let big_text = rng.gen_ratio(1, 1000);
if big_text {

View File

@@ -1,12 +1,10 @@
use std::{fmt::Display, rc::Rc};
use std::{fmt::Display, rc::Rc, vec};
use limbo_core::{Connection, Result, StepResult};
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use crate::{
model::{
query::{Create, Insert, Predicate, Query, Select},
query::{Create, Insert, Query, Select},
table::Value,
},
SimConnection, SimulatorEnv,
@@ -14,25 +12,118 @@ use crate::{
use crate::generation::{frequency, Arbitrary, ArbitraryFrom};
use super::{pick, pick_index};
use super::{
pick,
property::{remaining, Property},
};
pub(crate) type ResultSet = Result<Vec<Vec<Value>>>;
#[derive(Clone)]
pub(crate) struct InteractionPlan {
pub(crate) plan: Vec<Interaction>,
pub(crate) plan: Vec<Interactions>,
}
pub(crate) struct InteractionPlanState {
pub(crate) stack: Vec<ResultSet>,
pub(crate) interaction_pointer: usize,
pub(crate) secondary_pointer: usize,
}
#[derive(Clone)]
pub(crate) enum Interactions {
Property(Property),
Query(Query),
Fault(Fault),
}
impl Interactions {
pub(crate) fn name(&self) -> Option<String> {
match self {
Interactions::Property(property) => Some(property.name()),
Interactions::Query(_) => None,
Interactions::Fault(_) => None,
}
}
pub(crate) fn interactions(&self) -> Vec<Interaction> {
match self {
Interactions::Property(property) => property.interactions(),
Interactions::Query(query) => vec![Interaction::Query(query.clone())],
Interactions::Fault(fault) => vec![Interaction::Fault(fault.clone())],
}
}
}
impl Interactions {
pub(crate) fn dependencies(&self) -> Vec<String> {
match self {
Interactions::Property(property) => {
property
.interactions()
.iter()
.fold(vec![], |mut acc, i| match i {
Interaction::Query(q) => {
acc.extend(q.dependencies());
acc
}
_ => acc,
})
}
Interactions::Query(query) => query.dependencies(),
Interactions::Fault(_) => vec![],
}
}
pub(crate) fn uses(&self) -> Vec<String> {
match self {
Interactions::Property(property) => {
property
.interactions()
.iter()
.fold(vec![], |mut acc, i| match i {
Interaction::Query(q) => {
acc.extend(q.uses());
acc
}
_ => acc,
})
}
Interactions::Query(query) => query.uses(),
Interactions::Fault(_) => vec![],
}
}
}
impl Display for InteractionPlan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for interaction in &self.plan {
match interaction {
Interaction::Query(query) => writeln!(f, "{};", query)?,
Interaction::Assertion(assertion) => {
writeln!(f, "-- ASSERT: {};", assertion.message)?
for interactions in &self.plan {
match interactions {
Interactions::Property(property) => {
let name = property.name();
writeln!(f, "-- begin testing '{}'", name)?;
for interaction in property.interactions() {
write!(f, "\t")?;
match interaction {
Interaction::Query(query) => writeln!(f, "{};", query)?,
Interaction::Assumption(assumption) => {
writeln!(f, "-- ASSUME: {};", assumption.message)?
}
Interaction::Assertion(assertion) => {
writeln!(f, "-- ASSERT: {};", assertion.message)?
}
Interaction::Fault(fault) => writeln!(f, "-- FAULT: {};", fault)?,
}
}
writeln!(f, "-- end testing '{}'", name)?;
}
Interactions::Fault(fault) => {
writeln!(f, "-- FAULT '{}'", fault)?;
}
Interactions::Query(query) => {
writeln!(f, "{};", query)?;
}
Interaction::Fault(fault) => writeln!(f, "-- FAULT: {};", fault)?,
}
}
@@ -40,7 +131,7 @@ impl Display for InteractionPlan {
}
}
#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
pub(crate) struct InteractionStats {
pub(crate) read_count: usize,
pub(crate) write_count: usize,
@@ -60,6 +151,7 @@ impl Display for InteractionStats {
pub(crate) enum Interaction {
Query(Query),
Assumption(Assertion),
Assertion(Assertion),
Fault(Fault),
}
@@ -68,19 +160,25 @@ 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),
}
}
}
type AssertionFunc = dyn Fn(&Vec<ResultSet>) -> bool;
type AssertionFunc = dyn Fn(&Vec<ResultSet>, &SimulatorEnv) -> bool;
enum AssertionAST {
Pick(),
}
pub(crate) struct Assertion {
pub(crate) func: Box<AssertionFunc>,
pub(crate) message: String,
}
#[derive(Debug, Clone)]
pub(crate) enum Fault {
Disconnect,
}
@@ -93,47 +191,60 @@ impl Display for Fault {
}
}
pub(crate) struct Interactions(Vec<Interaction>);
impl Interactions {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) {
for interaction in &self.0 {
match interaction {
Interaction::Query(query) => match query {
Query::Create(create) => {
if !env.tables.iter().any(|t| t.name == create.table.name) {
env.tables.push(create.table.clone());
}
match self {
Interactions::Property(property) => {
for interaction in property.interactions() {
match interaction {
Interaction::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(_) => {}
},
Interaction::Assertion(_) => {}
Interaction::Assumption(_) => {}
Interaction::Fault(_) => {}
}
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(_) => {}
},
Interaction::Assertion(_) => {}
Interaction::Fault(_) => {}
}
}
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::Fault(_) => {}
}
}
}
impl InteractionPlan {
pub(crate) fn new() -> Self {
Self {
plan: Vec::new(),
stack: Vec::new(),
interaction_pointer: 0,
}
}
pub(crate) fn push(&mut self, interaction: Interaction) {
self.plan.push(interaction);
Self { plan: Vec::new() }
}
pub(crate) fn stats(&self) -> InteractionStats {
@@ -142,16 +253,27 @@ impl InteractionPlan {
let mut delete = 0;
let mut create = 0;
for interaction in &self.plan {
match interaction {
Interaction::Query(query) => match query {
for interactions in &self.plan {
match interactions {
Interactions::Property(property) => {
for interaction in &property.interactions() {
if let Interaction::Query(query) = interaction {
match query {
Query::Select(_) => read += 1,
Query::Insert(_) => write += 1,
Query::Delete(_) => delete += 1,
Query::Create(_) => create += 1,
}
}
}
}
Interactions::Query(query) => match query {
Query::Select(_) => read += 1,
Query::Insert(_) => write += 1,
Query::Delete(_) => delete += 1,
Query::Create(_) => create += 1,
},
Interaction::Assertion(_) => {}
Interaction::Fault(_) => {}
Interactions::Fault(_) => {}
}
}
@@ -164,25 +286,18 @@ impl InteractionPlan {
}
}
impl ArbitraryFrom<SimulatorEnv> for InteractionPlan {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
impl ArbitraryFrom<&mut SimulatorEnv> for InteractionPlan {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, env: &mut SimulatorEnv) -> Self {
let mut plan = InteractionPlan::new();
let mut env = SimulatorEnv {
opts: env.opts.clone(),
tables: vec![],
connections: vec![],
io: env.io.clone(),
db: env.db.clone(),
rng: ChaCha8Rng::seed_from_u64(rng.next_u64()),
};
let num_interactions = env.opts.max_interactions;
// First create at least one table
let create_query = Create::arbitrary(rng);
env.tables.push(create_query.table.clone());
plan.push(Interaction::Query(Query::Create(create_query)));
plan.plan
.push(Interactions::Query(Query::Create(create_query)));
while plan.plan.len() < num_interactions {
log::debug!(
@@ -190,10 +305,10 @@ impl ArbitraryFrom<SimulatorEnv> for InteractionPlan {
plan.plan.len(),
num_interactions
);
let interactions = Interactions::arbitrary_from(rng, &(&env, plan.stats()));
interactions.shadow(&mut env);
let interactions = Interactions::arbitrary_from(rng, (env, plan.stats()));
interactions.shadow(env);
plan.plan.extend(interactions.0.into_iter());
plan.plan.push(interactions);
}
log::info!("Generated plan with {} interactions", plan.plan.len());
@@ -203,79 +318,105 @@ impl ArbitraryFrom<SimulatorEnv> for InteractionPlan {
impl Interaction {
pub(crate) fn execute_query(&self, conn: &mut Rc<Connection>) -> ResultSet {
match self {
Self::Query(query) => {
let query_str = query.to_string();
let rows = conn.query(&query_str);
if rows.is_err() {
let err = rows.err();
log::debug!(
"Error running query '{}': {:?}",
&query_str[0..query_str.len().min(4096)],
err
);
return Err(err.unwrap());
}
let rows = rows.unwrap();
assert!(rows.is_some());
let mut rows = rows.unwrap();
let mut out = Vec::new();
while let Ok(row) = rows.next_row() {
match row {
StepResult::Row(row) => {
let mut r = Vec::new();
for el in &row.values {
let v = match el {
limbo_core::Value::Null => Value::Null,
limbo_core::Value::Integer(i) => Value::Integer(*i),
limbo_core::Value::Float(f) => Value::Float(*f),
limbo_core::Value::Text(t) => Value::Text(t.to_string()),
limbo_core::Value::Blob(b) => Value::Blob(b.to_vec()),
};
r.push(v);
}
out.push(r);
if let Self::Query(query) = self {
let query_str = query.to_string();
let rows = conn.query(&query_str);
if rows.is_err() {
let err = rows.err();
log::debug!(
"Error running query '{}': {:?}",
&query_str[0..query_str.len().min(4096)],
err
);
return Err(err.unwrap());
}
let rows = rows.unwrap();
assert!(rows.is_some());
let mut rows = rows.unwrap();
let mut out = Vec::new();
while let Ok(row) = rows.next_row() {
match row {
StepResult::Row(row) => {
let mut r = Vec::new();
for el in &row.values {
let v = match el {
limbo_core::Value::Null => Value::Null,
limbo_core::Value::Integer(i) => Value::Integer(*i),
limbo_core::Value::Float(f) => Value::Float(*f),
limbo_core::Value::Text(t) => Value::Text(t.to_string()),
limbo_core::Value::Blob(b) => Value::Blob(b.to_vec()),
};
r.push(v);
}
StepResult::IO => {}
StepResult::Interrupt => {}
StepResult::Done => {
break;
}
StepResult::Busy => {}
out.push(r);
}
StepResult::IO => {}
StepResult::Interrupt => {}
StepResult::Done => {
break;
}
StepResult::Busy => {}
}
}
Ok(out)
}
Self::Assertion(_) => {
unreachable!("unexpected: this function should only be called on queries")
}
Interaction::Fault(_) => {
unreachable!("unexpected: this function should only be called on queries")
}
Ok(out)
} else {
unreachable!("unexpected: this function should only be called on queries")
}
}
pub(crate) fn execute_assertion(&self, stack: &Vec<ResultSet>) -> Result<()> {
pub(crate) fn execute_assertion(
&self,
stack: &Vec<ResultSet>,
env: &SimulatorEnv,
) -> Result<()> {
match self {
Self::Query(_) => {
unreachable!("unexpected: this function should only be called on assertions")
}
Self::Assertion(assertion) => {
if !assertion.func.as_ref()(stack) {
if !assertion.func.as_ref()(stack, env) {
return Err(limbo_core::LimboError::InternalError(
assertion.message.clone(),
));
}
Ok(())
}
Self::Assumption(_) => {
unreachable!("unexpected: this function should only be called on assertions")
}
Self::Fault(_) => {
unreachable!("unexpected: this function should only be called on assertions")
}
}
}
pub(crate) fn execute_assumption(
&self,
stack: &Vec<ResultSet>,
env: &SimulatorEnv,
) -> Result<()> {
match self {
Self::Query(_) => {
unreachable!("unexpected: this function should only be called on assumptions")
}
Self::Assertion(_) => {
unreachable!("unexpected: this function should only be called on assumptions")
}
Self::Assumption(assumption) => {
if !assumption.func.as_ref()(stack, env) {
return Err(limbo_core::LimboError::InternalError(
assumption.message.clone(),
));
}
Ok(())
}
Self::Fault(_) => {
unreachable!("unexpected: this function should only be called on assumptions")
}
}
}
pub(crate) fn execute_fault(&self, env: &mut SimulatorEnv, conn_index: usize) -> Result<()> {
match self {
Self::Query(_) => {
@@ -284,6 +425,9 @@ impl Interaction {
Self::Assertion(_) => {
unreachable!("unexpected: this function should only be called on faults")
}
Self::Assumption(_) => {
unreachable!("unexpected: this function should only be called on faults")
}
Self::Fault(fault) => {
match fault {
Fault::Disconnect => {
@@ -306,140 +450,57 @@ impl Interaction {
}
}
fn property_insert_select<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
// Get a random table
let table = pick(&env.tables, rng);
// Pick a random column
let column_index = pick_index(table.columns.len(), rng);
let column = &table.columns[column_index].clone();
// Generate a random value of the column type
let value = Value::arbitrary_from(rng, &column.column_type);
// Create a whole new row
let mut row = Vec::new();
for (i, column) in table.columns.iter().enumerate() {
if i == column_index {
row.push(value.clone());
} else {
let value = Value::arbitrary_from(rng, &column.column_type);
row.push(value);
}
}
// Insert the row
let insert_query = Interaction::Query(Query::Insert(Insert {
table: table.name.clone(),
values: vec![row.clone()],
}));
// Select the row
let select_query = Interaction::Query(Query::Select(Select {
table: table.name.clone(),
predicate: Predicate::Eq(column.name.clone(), value.clone()),
}));
// Check that the row is there
let assertion = Interaction::Assertion(Assertion {
message: format!(
"row [{:?}] not found in table {} after inserting ({} = {})",
row.iter().map(|v| v.to_string()).collect::<Vec<String>>(),
table.name,
column.name,
value,
),
func: Box::new(move |stack: &Vec<ResultSet>| {
let rows = stack.last().unwrap();
match rows {
Ok(rows) => rows.iter().any(|r| r == &row),
Err(_) => false,
}
}),
});
Interactions(vec![insert_query, select_query, assertion])
}
fn property_double_create_failure<R: rand::Rng>(rng: &mut R, _env: &SimulatorEnv) -> Interactions {
let create_query = Create::arbitrary(rng);
let table_name = create_query.table.name.clone();
let cq1 = Interaction::Query(Query::Create(create_query.clone()));
let cq2 = Interaction::Query(Query::Create(create_query.clone()));
let assertion = Interaction::Assertion(Assertion {
message:
"creating two tables with the name should result in a failure for the second query"
.to_string(),
func: Box::new(move |stack: &Vec<ResultSet>| {
let last = stack.last().unwrap();
match last {
Ok(_) => false,
Err(e) => e
.to_string()
.contains(&format!("Table {table_name} already exists")),
}
}),
});
Interactions(vec![cq1, cq2, assertion])
}
fn create_table<R: rand::Rng>(rng: &mut R, _env: &SimulatorEnv) -> Interactions {
let create_query = Interaction::Query(Query::Create(Create::arbitrary(rng)));
Interactions(vec![create_query])
Interactions::Query(Query::Create(Create::arbitrary(rng)))
}
fn random_read<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
let select_query = Interaction::Query(Query::Select(Select::arbitrary_from(rng, &env.tables)));
Interactions(vec![select_query])
Interactions::Query(Query::Select(Select::arbitrary_from(rng, &env.tables)))
}
fn random_write<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
let table = pick(&env.tables, rng);
let insert_query = Interaction::Query(Query::Insert(Insert::arbitrary_from(rng, table)));
Interactions(vec![insert_query])
let insert_query = Query::Insert(Insert::arbitrary_from(rng, table));
Interactions::Query(insert_query)
}
fn random_fault<R: rand::Rng>(_rng: &mut R, _env: &SimulatorEnv) -> Interactions {
let fault = Interaction::Fault(Fault::Disconnect);
Interactions(vec![fault])
Interactions::Fault(Fault::Disconnect)
}
impl ArbitraryFrom<(&SimulatorEnv, InteractionStats)> for Interactions {
fn arbitrary_from<R: rand::Rng>(
rng: &mut R,
(env, stats): &(&SimulatorEnv, InteractionStats),
(env, stats): (&SimulatorEnv, InteractionStats),
) -> Self {
let remaining_read = ((env.opts.max_interactions as f64 * env.opts.read_percent / 100.0)
- (stats.read_count as f64))
.max(0.0);
let remaining_write = ((env.opts.max_interactions as f64 * env.opts.write_percent / 100.0)
- (stats.write_count as f64))
.max(0.0);
let remaining_create = ((env.opts.max_interactions as f64 * env.opts.create_percent
/ 100.0)
- (stats.create_count as f64))
.max(0.0);
let remaining_ = remaining(env, &stats);
frequency(
vec![
(
f64::min(remaining_read, remaining_write),
Box::new(|rng: &mut R| property_insert_select(rng, env)),
f64::min(remaining_.read, remaining_.write) + remaining_.create,
Box::new(|rng: &mut R| {
Interactions::Property(Property::arbitrary_from(rng, (env, &stats)))
}),
),
(
remaining_read,
remaining_.read,
Box::new(|rng: &mut R| random_read(rng, env)),
),
(
remaining_write,
remaining_.write,
Box::new(|rng: &mut R| random_write(rng, env)),
),
(
remaining_create,
remaining_.create,
Box::new(|rng: &mut R| create_table(rng, env)),
),
(1.0, Box::new(|rng: &mut R| random_fault(rng, env))),
(
remaining_create / 2.0,
Box::new(|rng: &mut R| property_double_create_failure(rng, env)),
remaining_
.read
.min(remaining_.write)
.min(remaining_.create)
.max(1.0),
Box::new(|rng: &mut R| random_fault(rng, env)),
),
],
rng,

View File

@@ -0,0 +1,319 @@
use crate::{
model::{
query::{Create, Delete, Insert, Predicate, Query, Select},
table::Value,
},
runner::env::SimulatorEnv,
};
use super::{
frequency, pick, pick_index,
plan::{Assertion, Interaction, InteractionStats, ResultSet},
ArbitraryFrom,
};
/// Properties are representations of executable specifications
/// about the database behavior.
#[derive(Clone)]
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
/// where clause that matches the inserted row.
/// The execution of the property is as follows
/// INSERT INTO <t> VALUES (...)
/// I_0
/// I_1
/// ...
/// I_n
/// SELECT * FROM <t> WHERE <predicate>
/// The interactions in the middle has the following constraints;
/// - There will be no errors in the middle interactions.
/// - The inserted row will not be deleted.
/// - The inserted row will not be updated.
/// - The table `t` will not be renamed, dropped, or altered.
InsertSelect {
/// The insert query
insert: Insert,
/// Selected row index
row_index: usize,
/// Additional interactions in the middle of the property
queries: Vec<Query>,
/// The select query
select: Select,
},
/// Double Create Failure is a property in which creating
/// the same table twice leads to an error.
/// The execution of the property is as follows
/// CREATE TABLE <t> (...)
/// I_0
/// I_1
/// ...
/// I_n
/// CREATE TABLE <t> (...) -> Error
/// The interactions in the middle has the following constraints;
/// - There will be no errors in the middle interactions.
/// - Table `t` will not be renamed or dropped.
DoubleCreateFailure {
/// The create query
create: Create,
/// Additional interactions in the middle of the property
queries: Vec<Query>,
},
}
impl Property {
pub(crate) fn name(&self) -> String {
match self {
Property::InsertSelect { .. } => "Insert-Select".to_string(),
Property::DoubleCreateFailure { .. } => "Double-Create-Failure".to_string(),
}
}
/// interactions construct a list of interactions, which is an executable representation of the property.
/// the requirement of property -> vec<interaction> conversion emerges from the need to serialize the property,
/// and `interaction` cannot be serialized directly.
pub(crate) fn interactions(&self) -> Vec<Interaction> {
match self {
Property::InsertSelect {
insert,
row_index,
queries,
select,
} => {
// Check that the insert query has at least 1 value
assert!(
!insert.values.is_empty(),
"insert query should have at least 1 value"
);
// Pick a random row within the insert values
let row = insert.values[*row_index].clone();
// Assume that the table exists
let assumption = Interaction::Assumption(Assertion {
message: format!("table {} exists", insert.table),
func: Box::new({
let table_name = insert.table.clone();
move |_: &Vec<ResultSet>, env: &SimulatorEnv| {
env.tables.iter().any(|t| t.name == table_name)
}
}),
});
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,
),
func: Box::new(move |stack: &Vec<ResultSet>, _: &SimulatorEnv| {
let rows = stack.last().unwrap();
match rows {
Ok(rows) => rows.iter().any(|r| r == &row),
Err(_) => false,
}
}),
});
let mut interactions = Vec::new();
interactions.push(assumption);
interactions.push(Interaction::Query(Query::Insert(insert.clone())));
interactions.extend(queries.clone().into_iter().map(Interaction::Query));
interactions.push(Interaction::Query(Query::Select(select.clone())));
interactions.push(assertion);
interactions
}
Property::DoubleCreateFailure { create, queries } => {
let table_name = create.table.name.clone();
let assumption = Interaction::Assumption(Assertion {
message: "Double-Create-Failure should not be called on an existing table"
.to_string(),
func: Box::new(move |_: &Vec<ResultSet>, env: &SimulatorEnv| {
!env.tables.iter().any(|t| t.name == table_name)
}),
});
let cq1 = Interaction::Query(Query::Create(create.clone()));
let cq2 = Interaction::Query(Query::Create(create.clone()));
let table_name = create.table.name.clone();
let assertion = Interaction::Assertion(Assertion {
message:
"creating two tables with the name should result in a failure for the second query"
.to_string(),
func: Box::new(move |stack: &Vec<ResultSet>, _: &SimulatorEnv| {
let last = stack.last().unwrap();
match last {
Ok(_) => false,
Err(e) => e
.to_string()
.contains(&format!("Table {table_name} already exists")),
}
}),
});
let mut interactions = Vec::new();
interactions.push(assumption);
interactions.push(cq1);
interactions.extend(queries.clone().into_iter().map(Interaction::Query));
interactions.push(cq2);
interactions.push(assertion);
interactions
}
}
}
}
pub(crate) struct Remaining {
pub(crate) read: f64,
pub(crate) write: f64,
pub(crate) create: f64,
}
pub(crate) fn remaining(env: &SimulatorEnv, stats: &InteractionStats) -> Remaining {
let remaining_read = ((env.opts.max_interactions as f64 * env.opts.read_percent / 100.0)
- (stats.read_count as f64))
.max(0.0);
let remaining_write = ((env.opts.max_interactions as f64 * env.opts.write_percent / 100.0)
- (stats.write_count as f64))
.max(0.0);
let remaining_create = ((env.opts.max_interactions as f64 * env.opts.create_percent / 100.0)
- (stats.create_count as f64))
.max(0.0);
Remaining {
read: remaining_read,
write: remaining_write,
create: remaining_create,
}
}
fn property_insert_select<R: rand::Rng>(
rng: &mut R,
env: &SimulatorEnv,
remaining: &Remaining,
) -> Property {
// Get a random table
let table = pick(&env.tables, rng);
// Generate rows to insert
let rows = (0..rng.gen_range(1..=5))
.map(|_| Vec::<Value>::arbitrary_from(rng, table))
.collect::<Vec<_>>();
// Pick a random row to select
let row_index = pick_index(rows.len(), rng).clone();
let row = rows[row_index].clone();
// Insert the rows
let insert_query = Insert {
table: table.name.clone(),
values: rows,
};
// Create random queries respecting the constraints
let mut queries = Vec::new();
// - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort)
// - [x] The inserted row will not be deleted.
// - [ ] The inserted row will not be updated. (todo: add this constraint once UPDATE is implemented)
// - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented)
for _ in 0..rng.gen_range(0..3) {
let query = Query::arbitrary_from(rng, (table, remaining));
match &query {
Query::Delete(Delete {
table: t,
predicate,
}) => {
// The inserted row will not be deleted.
if t == &table.name && predicate.test(&row, &table) {
continue;
}
}
Query::Create(Create { table: t }) => {
// There will be no errors in the middle interactions.
// - Creating the same table is an error
if t.name == table.name {
continue;
}
}
_ => (),
}
queries.push(query);
}
// Select the row
let select_query = Select {
table: table.name.clone(),
predicate: Predicate::arbitrary_from(rng, (table, &row)),
};
Property::InsertSelect {
insert: insert_query,
row_index,
queries,
select: select_query,
}
}
fn property_double_create_failure<R: rand::Rng>(
rng: &mut R,
env: &SimulatorEnv,
remaining: &Remaining,
) -> Property {
// Get a random table
let table = pick(&env.tables, rng);
// Create the table
let create_query = Create {
table: table.clone(),
};
// Create random queries respecting the constraints
let mut queries = Vec::new();
// The interactions in the middle has the following constraints;
// - [x] There will be no errors in the middle interactions.(best effort)
// - [ ] Table `t` will not be renamed or dropped.(todo: add this constraint once ALTER or DROP is implemented)
for _ in 0..rng.gen_range(0..3) {
let query = Query::arbitrary_from(rng, (table, remaining));
match &query {
Query::Create(Create { table: t }) => {
// There will be no errors in the middle interactions.
// - Creating the same table is an error
if t.name == table.name {
continue;
}
}
_ => (),
}
queries.push(query);
}
Property::DoubleCreateFailure {
create: create_query,
queries,
}
}
impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
fn arbitrary_from<R: rand::Rng>(
rng: &mut R,
(env, stats): (&SimulatorEnv, &InteractionStats),
) -> Self {
let remaining_ = remaining(env, stats);
frequency(
vec![
(
f64::min(remaining_.read, remaining_.write),
Box::new(|rng: &mut R| property_insert_select(rng, env, &remaining_)),
),
(
remaining_.create / 2.0,
Box::new(|rng: &mut R| property_double_create_failure(rng, env, &remaining_)),
),
],
rng,
)
}
}

View File

@@ -3,8 +3,10 @@ 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::property::Remaining;
use super::{frequency, pick};
impl Arbitrary for Create {
@@ -15,7 +17,7 @@ impl Arbitrary for Create {
}
}
impl ArbitraryFrom<Vec<Table>> for Select {
impl ArbitraryFrom<&Vec<Table>> for Select {
fn arbitrary_from<R: Rng>(rng: &mut R, tables: &Vec<Table>) -> Self {
let table = pick(tables, rng);
Self {
@@ -25,7 +27,7 @@ impl ArbitraryFrom<Vec<Table>> for Select {
}
}
impl ArbitraryFrom<Vec<&Table>> for Select {
impl ArbitraryFrom<&Vec<&Table>> for Select {
fn arbitrary_from<R: Rng>(rng: &mut R, tables: &Vec<&Table>) -> Self {
let table = pick(tables, rng);
Self {
@@ -35,7 +37,7 @@ impl ArbitraryFrom<Vec<&Table>> for Select {
}
}
impl ArbitraryFrom<Table> for Insert {
impl ArbitraryFrom<&Table> for Insert {
fn arbitrary_from<R: Rng>(rng: &mut R, table: &Table) -> Self {
let num_rows = rng.gen_range(1..10);
let values: Vec<Vec<Value>> = (0..num_rows)
@@ -54,7 +56,7 @@ impl ArbitraryFrom<Table> for Insert {
}
}
impl ArbitraryFrom<Table> for Delete {
impl ArbitraryFrom<&Table> for Delete {
fn arbitrary_from<R: Rng>(rng: &mut R, table: &Table) -> Self {
Self {
table: table.name.clone(),
@@ -63,7 +65,7 @@ impl ArbitraryFrom<Table> for Delete {
}
}
impl ArbitraryFrom<Table> for Query {
impl ArbitraryFrom<&Table> for Query {
fn arbitrary_from<R: Rng>(rng: &mut R, table: &Table) -> Self {
frequency(
vec![
@@ -86,11 +88,37 @@ impl ArbitraryFrom<Table> for Query {
}
}
impl ArbitraryFrom<(&Table, &Remaining)> for Query {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, remaining): (&Table, &Remaining)) -> Self {
frequency(
vec![
(
remaining.create,
Box::new(|rng| Self::Create(Create::arbitrary(rng))),
),
(
remaining.read,
Box::new(|rng| Self::Select(Select::arbitrary_from(rng, &vec![table]))),
),
(
remaining.write,
Box::new(|rng| Self::Insert(Insert::arbitrary_from(rng, table))),
),
(
0.0,
Box::new(|rng| Self::Delete(Delete::arbitrary_from(rng, table))),
),
],
rng,
)
}
}
struct CompoundPredicate(Predicate);
struct SimplePredicate(Predicate);
impl ArbitraryFrom<(&Table, bool)> for SimplePredicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, predicate_value): &(&Table, bool)) -> Self {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self {
// Pick a random column
let column_index = rng.gen_range(0..table.columns.len());
let column = &table.columns[column_index];
@@ -154,61 +182,61 @@ impl ArbitraryFrom<(&Table, bool)> for SimplePredicate {
}
impl ArbitraryFrom<(&Table, bool)> for CompoundPredicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, predicate_value): &(&Table, bool)) -> Self {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self {
// Decide if you want to create an AND or an OR
Self(if rng.gen_bool(0.7) {
// An AND for true requires each of its children to be true
// An AND for false requires at least one of its children to be false
if *predicate_value {
if predicate_value {
Predicate::And(
(0..rng.gen_range(1..=3))
.map(|_| SimplePredicate::arbitrary_from(rng, &(*table, true)).0)
(0..rng.gen_range(0..=3))
.map(|_| SimplePredicate::arbitrary_from(rng, (table, true)).0)
.collect(),
)
} else {
// Create a vector of random booleans
let mut booleans = (0..rng.gen_range(1..=3))
let mut booleans = (0..rng.gen_range(0..=3))
.map(|_| rng.gen_bool(0.5))
.collect::<Vec<_>>();
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;
}
Predicate::And(
booleans
.iter()
.map(|b| SimplePredicate::arbitrary_from(rng, &(*table, *b)).0)
.map(|b| SimplePredicate::arbitrary_from(rng, (table, *b)).0)
.collect(),
)
}
} else {
// An OR for true requires at least one of its children to be true
// An OR for false requires each of its children to be false
if *predicate_value {
if predicate_value {
// Create a vector of random booleans
let mut booleans = (0..rng.gen_range(1..=3))
let mut booleans = (0..rng.gen_range(0..=3))
.map(|_| rng.gen_bool(0.5))
.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;
}
Predicate::Or(
booleans
.iter()
.map(|b| SimplePredicate::arbitrary_from(rng, &(*table, *b)).0)
.map(|b| SimplePredicate::arbitrary_from(rng, (table, *b)).0)
.collect(),
)
} else {
Predicate::Or(
(0..rng.gen_range(1..=3))
.map(|_| SimplePredicate::arbitrary_from(rng, &(*table, false)).0)
(0..rng.gen_range(0..=3))
.map(|_| SimplePredicate::arbitrary_from(rng, (table, false)).0)
.collect(),
)
}
@@ -216,28 +244,28 @@ impl ArbitraryFrom<(&Table, bool)> for CompoundPredicate {
}
}
impl ArbitraryFrom<Table> for Predicate {
impl ArbitraryFrom<&Table> for Predicate {
fn arbitrary_from<R: Rng>(rng: &mut R, table: &Table) -> Self {
let predicate_value = rng.gen_bool(0.5);
CompoundPredicate::arbitrary_from(rng, &(table, predicate_value)).0
CompoundPredicate::arbitrary_from(rng, (table, predicate_value)).0
}
}
impl ArbitraryFrom<(&str, &Value)> for Predicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (column_name, value): &(&str, &Value)) -> Self {
fn arbitrary_from<R: Rng>(rng: &mut R, (column_name, value): (&str, &Value)) -> Self {
one_of(
vec![
Box::new(|_| Predicate::Eq(column_name.to_string(), (*value).clone())),
Box::new(|rng| {
Self::Gt(
column_name.to_string(),
GTValue::arbitrary_from(rng, *value).0,
GTValue::arbitrary_from(rng, value).0,
)
}),
Box::new(|rng| {
Self::Lt(
column_name.to_string(),
LTValue::arbitrary_from(rng, *value).0,
LTValue::arbitrary_from(rng, value).0,
)
}),
],
@@ -245,3 +273,155 @@ impl ArbitraryFrom<(&str, &Value)> for Predicate {
)
}
}
/// 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();
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

@@ -1,8 +1,6 @@
use rand::Rng;
use crate::generation::{
gen_random_text, pick, pick_index, readable_name_custom, Arbitrary, ArbitraryFrom,
};
use crate::generation::{gen_random_text, pick, readable_name_custom, Arbitrary, ArbitraryFrom};
use crate::model::table::{Column, ColumnType, Name, Table, Value};
impl Arbitrary for Name {
@@ -15,7 +13,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 {
@@ -45,7 +43,18 @@ impl Arbitrary for ColumnType {
}
}
impl ArbitraryFrom<Vec<&Value>> for Value {
impl ArbitraryFrom<&Table> for Vec<Value> {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, table: &Table) -> Self {
let mut row = Vec::new();
for column in table.columns.iter() {
let value = Value::arbitrary_from(rng, &column.column_type);
row.push(value);
}
row
}
}
impl ArbitraryFrom<&Vec<&Value>> for Value {
fn arbitrary_from<R: Rng>(rng: &mut R, values: &Vec<&Self>) -> Self {
if values.is_empty() {
return Self::Null;
@@ -55,7 +64,7 @@ impl ArbitraryFrom<Vec<&Value>> for Value {
}
}
impl ArbitraryFrom<ColumnType> for Value {
impl ArbitraryFrom<&ColumnType> for Value {
fn arbitrary_from<R: Rng>(rng: &mut R, column_type: &ColumnType) -> Self {
match column_type {
ColumnType::Integer => Self::Integer(rng.gen_range(i64::MIN..i64::MAX)),
@@ -68,22 +77,22 @@ impl ArbitraryFrom<ColumnType> for Value {
pub(crate) struct LTValue(pub(crate) Value);
impl ArbitraryFrom<Vec<&Value>> for LTValue {
impl ArbitraryFrom<&Vec<&Value>> for LTValue {
fn arbitrary_from<R: Rng>(rng: &mut R, values: &Vec<&Value>) -> Self {
if values.is_empty() {
return Self(Value::Null);
}
let index = pick_index(values.len(), rng);
Self::arbitrary_from(rng, values[index])
let value = pick(values, rng);
Self::arbitrary_from(rng, *value)
}
}
impl ArbitraryFrom<Value> for LTValue {
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();
@@ -128,18 +137,18 @@ impl ArbitraryFrom<Value> for LTValue {
pub(crate) struct GTValue(pub(crate) Value);
impl ArbitraryFrom<Vec<&Value>> for GTValue {
impl ArbitraryFrom<&Vec<&Value>> for GTValue {
fn arbitrary_from<R: Rng>(rng: &mut R, values: &Vec<&Value>) -> Self {
if values.is_empty() {
return Self(Value::Null);
}
let index = pick_index(values.len(), rng);
Self::arbitrary_from(rng, values[index])
let value = pick(values, rng);
Self::arbitrary_from(rng, *value)
}
}
impl ArbitraryFrom<Value> for GTValue {
impl ArbitraryFrom<&Value> for GTValue {
fn arbitrary_from<R: Rng>(rng: &mut R, value: &Value) -> Self {
match value {
Value::Integer(i) => Self(Value::Integer(rng.gen_range(*i..i64::MAX))),

View File

@@ -1,28 +1,68 @@
#![allow(clippy::arc_with_non_send_sync, dead_code)]
use clap::Parser;
use generation::plan::{Interaction, InteractionPlan, ResultSet};
use generation::{pick_index, ArbitraryFrom};
use limbo_core::{Database, Result};
use model::table::Value;
use core::panic;
use generation::plan::{InteractionPlan, InteractionPlanState};
use generation::ArbitraryFrom;
use limbo_core::Database;
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use runner::cli::SimulatorCLI;
use runner::env::{SimConnection, SimulatorEnv, SimulatorOpts};
use runner::execution::{execute_plans, Execution, ExecutionHistory, ExecutionResult};
use runner::io::SimulatorIO;
use std::any::Any;
use std::backtrace::Backtrace;
use std::io::Write;
use std::path::Path;
use std::sync::Arc;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use tempfile::TempDir;
mod generation;
mod model;
mod runner;
mod shrink;
struct Paths {
db: PathBuf,
plan: PathBuf,
shrunk_plan: PathBuf,
history: PathBuf,
doublecheck_db: PathBuf,
shrunk_db: PathBuf,
}
fn main() {
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> {
init_logger();
let cli_opts = SimulatorCLI::parse();
cli_opts.validate()?;
let seed = match cli_opts.seed {
Some(seed) => seed,
@@ -31,19 +71,16 @@ 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 plan_path = output_dir.join("simulator.plan");
banner();
let paths = Paths::new(&output_dir, cli_opts.shrink, cli_opts.doublecheck);
// Print the seed, the locations of the database and the plan file
log::info!("database path: {:?}", db_path);
log::info!("simulator plan path: {:?}", plan_path);
log::info!("seed: {}", seed);
let last_execution = Arc::new(Mutex::new(Execution::new(0, 0, 0)));
std::panic::set_hook(Box::new(move |info| {
log::error!("panic occurred");
@@ -60,83 +97,252 @@ fn main() {
log::error!("captured backtrace:\n{}", bt);
}));
let result = std::panic::catch_unwind(|| run_simulation(seed, &cli_opts, &db_path, &plan_path));
let result = SandboxedResult::from(
std::panic::catch_unwind(|| {
run_simulation(
seed,
&cli_opts,
&paths.db,
&paths.plan,
last_execution.clone(),
None,
)
}),
last_execution.clone(),
);
if cli_opts.doublecheck {
// Move the old database and plan file to a new location
let old_db_path = db_path.with_extension("_old.db");
let old_plan_path = plan_path.with_extension("_old.plan");
std::fs::rename(&db_path, &old_db_path).unwrap();
std::fs::rename(&plan_path, &old_plan_path).unwrap();
// Run the simulation again
let result2 =
std::panic::catch_unwind(|| run_simulation(seed, &cli_opts, &db_path, &plan_path));
let result2 = SandboxedResult::from(
std::panic::catch_unwind(|| {
run_simulation(
seed,
&cli_opts,
&paths.doublecheck_db,
&paths.plan,
last_execution.clone(),
None,
)
}),
last_execution.clone(),
);
match (result, result2) {
(Ok(Ok(_)), Err(_)) => {
(SandboxedResult::Correct, SandboxedResult::Panicked { .. }) => {
log::error!("doublecheck failed! first run succeeded, but second run panicked.");
}
(Ok(Err(_)), Err(_)) => {
(SandboxedResult::FoundBug { .. }, SandboxedResult::Panicked { .. }) => {
log::error!(
"doublecheck failed! first run failed assertion, but second run panicked."
"doublecheck failed! first run failed an assertion, but second run panicked."
);
}
(Err(_), Ok(Ok(_))) => {
(SandboxedResult::Panicked { .. }, SandboxedResult::Correct) => {
log::error!("doublecheck failed! first run panicked, but second run succeeded.");
}
(Err(_), Ok(Err(_))) => {
(SandboxedResult::Panicked { .. }, SandboxedResult::FoundBug { .. }) => {
log::error!(
"doublecheck failed! first run panicked, but second run failed assertion."
"doublecheck failed! first run panicked, but second run failed an assertion."
);
}
(Ok(Ok(_)), Ok(Err(_))) => {
(SandboxedResult::Correct, SandboxedResult::FoundBug { .. }) => {
log::error!(
"doublecheck failed! first run succeeded, but second run failed assertion."
"doublecheck failed! first run succeeded, but second run failed an assertion."
);
}
(Ok(Err(_)), Ok(Ok(_))) => {
(SandboxedResult::FoundBug { .. }, SandboxedResult::Correct) => {
log::error!(
"doublecheck failed! first run failed assertion, but second run succeeded."
"doublecheck failed! first run failed an assertion, but second run succeeded."
);
}
(Err(_), Err(_)) | (Ok(_), Ok(_)) => {
(SandboxedResult::Correct, SandboxedResult::Correct)
| (SandboxedResult::FoundBug { .. }, SandboxedResult::FoundBug { .. })
| (SandboxedResult::Panicked { .. }, SandboxedResult::Panicked { .. }) => {
// Compare the two database files byte by byte
let old_db = std::fs::read(&old_db_path).unwrap();
let new_db = std::fs::read(&db_path).unwrap();
if old_db != new_db {
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.");
}
}
}
// Move the new database and plan file to a new location
let new_db_path = db_path.with_extension("_double.db");
let new_plan_path = plan_path.with_extension("_double.plan");
std::fs::rename(&db_path, &new_db_path).unwrap();
std::fs::rename(&plan_path, &new_plan_path).unwrap();
// Move the old database and plan file back
std::fs::rename(&old_db_path, &db_path).unwrap();
std::fs::rename(&old_plan_path, &plan_path).unwrap();
} else if let Ok(result) = result {
match result {
Ok(_) => {
log::info!("simulation completed successfully");
} else {
// No doublecheck, run shrinking if panicking or found a bug.
match &result {
SandboxedResult::Correct => {
log::info!("simulation succeeded");
}
Err(e) => {
log::error!("simulation failed: {:?}", e);
SandboxedResult::Panicked {
error,
last_execution,
}
| SandboxedResult::FoundBug {
error,
last_execution,
..
} => {
if let SandboxedResult::FoundBug { history, .. } = &result {
// No panic occurred, so write the history to a file
let f = std::fs::File::create(&paths.history).unwrap();
let mut f = std::io::BufWriter::new(f);
for execution in history.history.iter() {
writeln!(
f,
"{} {} {}",
execution.connection_index,
execution.interaction_index,
execution.secondary_index
)
.unwrap();
}
}
log::error!("simulation failed: '{}'", error);
if cli_opts.shrink {
log::info!("Starting to shrink");
let shrink = Some(last_execution);
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,
last_execution.clone(),
shrink,
)
}),
last_execution,
);
match (&shrunk, &result) {
(
SandboxedResult::Panicked { error: e1, .. },
SandboxedResult::Panicked { error: e2, .. },
)
| (
SandboxedResult::FoundBug { error: e1, .. },
SandboxedResult::FoundBug { error: e2, .. },
) => {
if e1 != e2 {
log::error!(
"shrinking failed, the error was not properly reproduced"
);
} else {
log::info!("shrinking succeeded");
}
}
(_, SandboxedResult::Correct) => {
unreachable!("shrinking should never be called on a correct simulation")
}
_ => {
log::error!("shrinking failed, the error was not properly reproduced");
}
}
// Write the shrunk plan to a file
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();
}
}
}
}
// 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!("simulator plan path: {:?}", plan_path);
println!("database path: {:?}", paths.db);
if cli_opts.doublecheck {
println!("doublecheck database path: {:?}", paths.doublecheck_db);
} else if cli_opts.shrink {
println!("shrunk database path: {:?}", paths.shrunk_db);
}
println!("simulator plan path: {:?}", paths.plan);
if cli_opts.shrink {
println!("shrunk plan path: {:?}", paths.shrunk_plan);
}
println!("simulator history path: {:?}", paths.history);
println!("seed: {}", seed);
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 {
error: String,
last_execution: Execution,
},
FoundBug {
error: String,
history: ExecutionHistory,
last_execution: Execution,
},
Correct,
}
impl SandboxedResult {
fn from(
result: Result<ExecutionResult, Box<dyn Any + Send>>,
last_execution: Arc<Mutex<Execution>>,
) -> Self {
match result {
Ok(ExecutionResult { error: None, .. }) => SandboxedResult::Correct,
Ok(ExecutionResult { error: Some(e), .. }) => {
let error = format!("{:?}", e);
let last_execution = last_execution.lock().unwrap();
SandboxedResult::Panicked {
error,
last_execution: *last_execution,
}
}
Err(payload) => {
log::error!("panic occurred");
let err = if let Some(s) = payload.downcast_ref::<&str>() {
log::error!("{}", s);
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
log::error!("{}", s);
s.to_string()
} else {
log::error!("unknown panic payload");
"unknown panic payload".to_string()
};
last_execution.clear_poison();
SandboxedResult::Panicked {
error: err,
last_execution: *last_execution.lock().unwrap(),
}
}
}
}
}
fn run_simulation(
@@ -144,7 +350,9 @@ fn run_simulation(
cli_opts: &SimulatorCLI,
db_path: &Path,
plan_path: &Path,
) -> Result<()> {
last_execution: Arc<Mutex<Execution>>,
shrink: Option<&Execution>,
) -> ExecutionResult {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let (create_percent, read_percent, write_percent, delete_percent) = {
@@ -161,24 +369,6 @@ fn run_simulation(
(create_percent, read_percent, write_percent, delete_percent)
};
if cli_opts.minimum_size < 1 {
return Err(limbo_core::LimboError::InternalError(
"minimum size must be at least 1".to_string(),
));
}
if cli_opts.maximum_size < 1 {
return Err(limbo_core::LimboError::InternalError(
"maximum size must be at least 1".to_string(),
));
}
if cli_opts.maximum_size < cli_opts.minimum_size {
return Err(limbo_core::LimboError::InternalError(
"maximum size must be greater than or equal to minimum size".to_string(),
));
}
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
@@ -214,21 +404,36 @@ fn run_simulation(
log::info!("Generating database interaction plan...");
let mut plans = (1..=env.opts.max_connections)
.map(|_| InteractionPlan::arbitrary_from(&mut env.rng.clone(), &env))
.map(|_| InteractionPlan::arbitrary_from(&mut env.rng.clone(), &mut env))
.collect::<Vec<_>>();
let mut states = plans
.iter()
.map(|_| InteractionPlanState {
stack: vec![],
interaction_pointer: 0,
secondary_pointer: 0,
})
.collect::<Vec<_>>();
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(plans[0].to_string().as_bytes()).unwrap();
f.write_all(plan.to_string().as_bytes()).unwrap();
log::info!("{}", plans[0].stats());
log::info!("{}", plan.stats());
log::info!("Executing database interaction plan...");
let result = execute_plans(&mut env, &mut plans);
if result.is_err() {
log::error!("error executing plans: {:?}", result.as_ref().err());
}
let result = execute_plans(&mut env, &mut plans, &mut states, last_execution);
env.io.print_stats();
@@ -237,98 +442,6 @@ fn run_simulation(
result
}
fn execute_plans(env: &mut SimulatorEnv, plans: &mut [InteractionPlan]) -> Result<()> {
let now = std::time::Instant::now();
// todo: add history here by recording which interaction was executed at which tick
for _tick in 0..env.opts.ticks {
// Pick the connection to interact with
let connection_index = pick_index(env.connections.len(), &mut env.rng);
// Execute the interaction for the selected connection
execute_plan(env, connection_index, plans)?;
// Check if the maximum time for the simulation has been reached
if now.elapsed().as_secs() >= env.opts.max_time_simulation as u64 {
return Err(limbo_core::LimboError::InternalError(
"maximum time for simulation reached".into(),
));
}
}
Ok(())
}
fn execute_plan(
env: &mut SimulatorEnv,
connection_index: usize,
plans: &mut [InteractionPlan],
) -> Result<()> {
let connection = &env.connections[connection_index];
let plan = &mut plans[connection_index];
if plan.interaction_pointer >= plan.plan.len() {
return Ok(());
}
let interaction = &plan.plan[plan.interaction_pointer];
if let SimConnection::Disconnected = connection {
log::trace!("connecting {}", connection_index);
env.connections[connection_index] = SimConnection::Connected(env.db.connect());
} else {
match execute_interaction(env, connection_index, interaction, &mut plan.stack) {
Ok(_) => {
log::debug!("connection {} processed", connection_index);
plan.interaction_pointer += 1;
}
Err(err) => {
log::error!("error {}", err);
return Err(err);
}
}
}
Ok(())
}
fn execute_interaction(
env: &mut SimulatorEnv,
connection_index: usize,
interaction: &Interaction,
stack: &mut Vec<ResultSet>,
) -> Result<()> {
log::trace!("executing: {}", interaction);
match interaction {
generation::plan::Interaction::Query(_) => {
let conn = match &mut env.connections[connection_index] {
SimConnection::Connected(conn) => conn,
SimConnection::Disconnected => unreachable!(),
};
log::debug!("{}", interaction);
let results = interaction.execute_query(conn);
log::debug!("{:?}", results);
stack.push(results);
}
generation::plan::Interaction::Assertion(_) => {
interaction.execute_assertion(stack)?;
stack.clear();
}
Interaction::Fault(_) => {
interaction.execute_fault(env, connection_index)?;
}
}
Ok(())
}
fn compare_equal_rows(a: &[Vec<Value>], b: &[Vec<Value>]) {
assert_eq!(a.len(), b.len(), "lengths are different");
for (r1, r2) in a.iter().zip(b) {
for (v1, v2) in r1.iter().zip(r2) {
assert_eq!(v1, v2, "values are different");
}
}
}
fn init_logger() {
env_logger::Builder::from_env(env_logger::Env::default().filter_or("RUST_LOG", "info"))
.format_timestamp(None)

View File

@@ -12,6 +12,36 @@ 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![])
}
pub(crate) fn test(&self, row: &[Value], table: &Table) -> bool {
let get_value = |name: &str| {
table
.columns
.iter()
.zip(row.iter())
.find(|(column, _)| column.name == name)
.map(|(_, value)| value)
};
match self {
Predicate::And(vec) => vec.iter().all(|p| p.test(row, table)),
Predicate::Or(vec) => vec.iter().any(|p| p.test(row, table)),
Predicate::Eq(column, value) => get_value(column) == Some(value),
Predicate::Neq(column, value) => get_value(column) != Some(value),
Predicate::Gt(column, value) => get_value(column).map(|v| v > value).unwrap_or(false),
Predicate::Lt(column, value) => get_value(column).map(|v| v < value).unwrap_or(false),
}
}
}
impl Display for Predicate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@@ -53,7 +83,7 @@ impl Display for Predicate {
}
// This type represents the potential queries on the database.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) enum Query {
Create(Create),
Select(Select),
@@ -61,6 +91,24 @@ pub(crate) enum Query {
Delete(Delete),
}
impl Query {
pub(crate) fn dependencies(&self) -> Vec<String> {
match self {
Query::Create(_) => vec![],
Query::Select(Select { table, .. })
| Query::Insert(Insert { table, .. })
| Query::Delete(Delete { table, .. }) => vec![table.clone()],
}
}
pub(crate) fn uses(&self) -> Vec<String> {
match self {
Query::Create(Create { table }) => vec![table.name.clone()],
Query::Select(Select { table, .. })
| Query::Insert(Insert { table, .. })
| Query::Delete(Delete { table, .. }) => vec![table.clone()],
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Create {
pub(crate) table: Table,

View File

@@ -53,6 +53,22 @@ pub(crate) enum Value {
Blob(Vec<u8>),
}
impl PartialOrd for Value {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match (self, other) {
(Self::Null, Self::Null) => Some(std::cmp::Ordering::Equal),
(Self::Null, _) => Some(std::cmp::Ordering::Less),
(_, Self::Null) => Some(std::cmp::Ordering::Greater),
(Self::Integer(i1), Self::Integer(i2)) => i1.partial_cmp(i2),
(Self::Float(f1), Self::Float(f2)) => f1.partial_cmp(f2),
(Self::Text(t1), Self::Text(t2)) => t1.partial_cmp(t2),
(Self::Blob(b1), Self::Blob(b2)) => b1.partial_cmp(b2),
// todo: add type coercions here
_ => None,
}
}
}
fn to_sqlite_blob(bytes: &[u8]) -> String {
format!(
"X'{}'",

View File

@@ -35,4 +35,25 @@ pub struct SimulatorCLI {
default_value_t = 60 * 60 // default to 1 hour
)]
pub maximum_time: usize,
#[clap(
short = 'm',
long,
help = "minimize(shrink) the failing counterexample"
)]
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

@@ -0,0 +1,203 @@
use std::sync::{Arc, Mutex};
use limbo_core::{LimboError, Result};
use crate::generation::{
self, pick_index,
plan::{Interaction, InteractionPlan, InteractionPlanState, ResultSet},
};
use super::env::{SimConnection, SimulatorEnv};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Execution {
pub(crate) connection_index: usize,
pub(crate) interaction_index: usize,
pub(crate) secondary_index: usize,
}
impl Execution {
pub(crate) fn new(
connection_index: usize,
interaction_index: usize,
secondary_index: usize,
) -> Self {
Self {
connection_index,
interaction_index,
secondary_index,
}
}
}
#[derive(Debug)]
pub(crate) struct ExecutionHistory {
pub(crate) history: Vec<Execution>,
}
impl ExecutionHistory {
fn new() -> Self {
Self {
history: Vec::new(),
}
}
}
pub(crate) struct ExecutionResult {
pub(crate) history: ExecutionHistory,
pub(crate) error: Option<limbo_core::LimboError>,
}
impl ExecutionResult {
fn new(history: ExecutionHistory, error: Option<LimboError>) -> Self {
Self { history, error }
}
}
pub(crate) fn execute_plans(
env: &mut SimulatorEnv,
plans: &mut [InteractionPlan],
states: &mut [InteractionPlanState],
last_execution: Arc<Mutex<Execution>>,
) -> ExecutionResult {
let mut history = ExecutionHistory::new();
let now = std::time::Instant::now();
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(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 [InteractionPlan],
states: &mut [InteractionPlanState],
) -> Result<()> {
let connection = &env.connections[connection_index];
let plan = &mut plans[connection_index];
let state = &mut states[connection_index];
if state.interaction_pointer >= plan.plan.len() {
return Ok(());
}
let interaction = &plan.plan[state.interaction_pointer].interactions()[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.plan[state.interaction_pointer].interactions().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(())
}
/// The next point of control flow after executing an interaction.
/// `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 {
/// Default continuation, execute the next interaction.
NextInteraction,
/// Typically used in the case of preconditions failures, skip to the next property.
NextProperty,
}
fn execute_interaction(
env: &mut SimulatorEnv,
connection_index: usize,
interaction: &Interaction,
stack: &mut Vec<ResultSet>,
) -> Result<ExecutionContinuation> {
log::info!("executing: {}", interaction);
match interaction {
generation::plan::Interaction::Query(_) => {
let conn = match &mut env.connections[connection_index] {
SimConnection::Connected(conn) => conn,
SimConnection::Disconnected => unreachable!(),
};
log::debug!("{}", interaction);
let results = interaction.execute_query(conn);
log::debug!("{:?}", results);
stack.push(results);
}
generation::plan::Interaction::Assertion(_) => {
interaction.execute_assertion(stack, env)?;
stack.clear();
}
generation::plan::Interaction::Assumption(_) => {
let assumption_result = interaction.execute_assumption(stack, env);
stack.clear();
if assumption_result.is_err() {
log::warn!("assumption failed: {:?}", assumption_result);
return Ok(ExecutionContinuation::NextProperty);
}
}
Interaction::Fault(_) => {
interaction.execute_fault(env, connection_index)?;
}
}
Ok(ExecutionContinuation::NextInteraction)
}

View File

@@ -1,5 +1,6 @@
pub mod cli;
pub mod env;
pub mod execution;
#[allow(dead_code)]
pub mod file;
pub mod io;

1
simulator/shrink/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod plan;

53
simulator/shrink/plan.rs Normal file
View File

@@ -0,0 +1,53 @@
use crate::{
generation::plan::{InteractionPlan, Interactions},
model::query::Query,
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();
let before = self.plan.len();
// Remove all properties after the failing one
plan.plan.truncate(failing_execution.interaction_index + 1);
// Remove all properties that do not use the failing tables
plan.plan
.retain(|p| p.uses().iter().any(|t| depending_tables.contains(t)));
// Remove the extensional parts of the properties
for interaction in plan.plan.iter_mut() {
if let Interactions::Property(p) = interaction {
match p {
crate::generation::property::Property::InsertSelect { queries, .. }
| crate::generation::property::Property::DoubleCreateFailure {
queries, ..
} => {
queries.clear();
}
}
}
}
plan.plan
.retain(|p| !matches!(p, Interactions::Query(Query::Select(_))));
let after = plan.plan.len();
log::info!(
"Shrinking interaction plan from {} to {} properties",
before,
after
);
plan
}
}