mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-30 05:14:23 +01:00
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:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
319
simulator/generation/property.rs
Normal file
319
simulator/generation/property.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'{}'",
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
203
simulator/runner/execution.rs
Normal file
203
simulator/runner/execution.rs
Normal 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)
|
||||
}
|
||||
@@ -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
1
simulator/shrink/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod plan;
|
||||
53
simulator/shrink/plan.rs
Normal file
53
simulator/shrink/plan.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user