Merge 'Simulator: Better Shrinking' from Pedro Muniz

This PR attempts to get the specific query that failed in the simulator
and get the correct tables that were used in the Query. Also, implements
a fix where after clearing miscellaneous queries, we did not check again
if the the interaction still referenced any of the tables that were
involved in the failure.

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #1712
This commit is contained in:
Jussi Saurio
2025-06-11 09:45:48 +03:00
4 changed files with 81 additions and 31 deletions

View File

@@ -1,4 +1,4 @@
use std::{fmt::Display, path::Path, rc::Rc, vec};
use std::{collections::HashSet, fmt::Display, path::Path, rc::Rc, vec};
use limbo_core::{Connection, Result, StepResult, IO};
use serde::{Deserialize, Serialize};
@@ -97,7 +97,7 @@ pub(crate) struct InteractionPlanState {
pub(crate) secondary_pointer: usize,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum Interactions {
Property(Property),
Query(Query),
@@ -123,13 +123,13 @@ impl Interactions {
}
impl Interactions {
pub(crate) fn dependencies(&self) -> Vec<String> {
pub(crate) fn dependencies(&self) -> HashSet<String> {
match self {
Interactions::Property(property) => {
property
.interactions()
.iter()
.fold(vec![], |mut acc, i| match i {
.fold(HashSet::new(), |mut acc, i| match i {
Interaction::Query(q) => {
acc.extend(q.dependencies());
acc
@@ -138,7 +138,7 @@ impl Interactions {
})
}
Interactions::Query(query) => query.dependencies(),
Interactions::Fault(_) => vec![],
Interactions::Fault(_) => HashSet::new(),
}
}

View File

@@ -20,7 +20,7 @@ use super::{
/// Properties are representations of executable specifications
/// about the database behavior.
#[derive(Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
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

View File

@@ -1,4 +1,4 @@
use std::fmt::Display;
use std::{collections::HashSet, fmt::Display};
pub(crate) use create::Create;
pub(crate) use create_index::CreateIndex;
@@ -32,16 +32,18 @@ pub(crate) enum Query {
}
impl Query {
pub(crate) fn dependencies(&self) -> Vec<String> {
pub(crate) fn dependencies(&self) -> HashSet<String> {
match self {
Query::Create(_) => vec![],
Query::Create(_) => HashSet::new(),
Query::Select(Select { table, .. })
| Query::Insert(Insert::Select { table, .. })
| Query::Insert(Insert::Values { table, .. })
| Query::Delete(Delete { table, .. })
| Query::Update(Update { table, .. })
| Query::Drop(Drop { table, .. }) => vec![table.clone()],
Query::CreateIndex(CreateIndex { table_name, .. }) => vec![table_name.clone()],
| Query::Drop(Drop { table, .. }) => HashSet::from_iter([table.clone()]),
Query::CreateIndex(CreateIndex { table_name, .. }) => {
HashSet::from_iter([table_name.clone()])
}
}
}
pub(crate) fn uses(&self) -> Vec<String> {

View File

@@ -5,6 +5,7 @@ use crate::{
},
model::query::Query,
runner::execution::Execution,
Interaction,
};
impl InteractionPlan {
@@ -13,36 +14,83 @@ impl 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 mut depending_tables = failing_property.dependencies();
let interactions = failing_property.interactions();
{
let mut idx = failing_execution.secondary_index;
loop {
match &interactions[idx] {
Interaction::Query(query) => {
depending_tables = query.dependencies();
break;
}
// Fault does not depend on
Interaction::Fault(..) => break,
_ => {
// In principle we should never fail this checked_sub.
// But if there is a bug in how we count the secondary index
// we may panic if we do not use a checked_sub.
if let Some(new_idx) = idx.checked_sub(1) {
idx = new_idx;
} else {
tracing::warn!("failed to find error query");
break;
}
}
}
}
}
let before = self.plan.len();
// Remove all properties after the failing one
plan.plan.truncate(failing_execution.interaction_index + 1);
let mut idx = 0;
// 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 {
Property::InsertValuesSelect { queries, .. }
| Property::DoubleCreateFailure { queries, .. }
| Property::DeleteSelect { queries, .. }
| Property::DropSelect { queries, .. } => {
queries.clear();
plan.plan.retain_mut(|interactions| {
let retain = if idx == failing_execution.interaction_index {
true
} else {
let mut has_table = interactions
.uses()
.iter()
.any(|t| depending_tables.contains(t));
if has_table {
// Remove the extensional parts of the properties
if let Interactions::Property(p) = interactions {
match p {
Property::InsertValuesSelect { queries, .. }
| Property::DoubleCreateFailure { queries, .. }
| Property::DeleteSelect { queries, .. }
| Property::DropSelect { queries, .. } => {
queries.clear();
}
Property::SelectLimit { .. }
| Property::SelectSelectOptimizer { .. } => {}
}
}
Property::SelectLimit { .. } | Property::SelectSelectOptimizer { .. } => {}
// Check again after query clear if the interactions still uses the failing table
has_table = interactions
.uses()
.iter()
.any(|t| depending_tables.contains(t));
}
}
}
plan.plan
.retain(|p| !matches!(p, Interactions::Query(Query::Select(_))));
has_table
&& !matches!(
interactions,
Interactions::Query(Query::Select(_))
| Interactions::Property(Property::SelectLimit { .. })
| Interactions::Property(Property::SelectSelectOptimizer { .. })
)
};
idx += 1;
retain
});
let after = plan.plan.len();