use indexmap::IndexSet; use crate::{ SandboxedResult, SimulatorEnv, generation::{ plan::{InteractionPlan, InteractionType, Interactions, InteractionsType}, property::Property, }, model::Query, run_simulation, runner::execution::Execution, }; use std::{ collections::HashMap, sync::{Arc, Mutex}, }; fn retain_relevant_queries( extensional_queries: &mut Vec, depending_tables: &IndexSet, ) { extensional_queries.retain(|query| { query.is_transaction() || (!matches!(query, Query::Select(..)) && query.uses().iter().any(|t| depending_tables.contains(t))) }); } 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 all_interactions = self.interactions_list_with_secondary_index(); let secondary_interactions_index = all_interactions[failing_execution.interaction_index].0; // Index of the parent property where the interaction originated from let failing_property = &self[secondary_interactions_index]; let mut depending_tables = failing_property.dependencies(); { let mut idx = failing_execution.interaction_index; loop { if all_interactions[idx].0 != secondary_interactions_index { // Stop when we reach a different property break; } match &all_interactions[idx].1.interaction { InteractionType::Query(query) | InteractionType::FaultyQuery(query) => { depending_tables = query.dependencies(); break; } // Fault does not depend on InteractionType::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.len(); // Remove all properties after the failing one plan.truncate(secondary_interactions_index + 1); // means we errored in some fault on transaction statement so just maintain the statements from before the failing one if !depending_tables.is_empty() { plan.remove_properties(&depending_tables, secondary_interactions_index); } let after = plan.len(); tracing::info!( "Shrinking interaction plan from {} to {} properties", before, after ); plan } /// Remove all properties that do not use the failing tables fn remove_properties( &mut self, depending_tables: &IndexSet, failing_interaction_index: usize, ) { let mut idx = 0; // Remove all properties that do not use the failing tables self.retain_mut(|interactions| { let retain = if idx == failing_interaction_index { true } else { let mut has_table = interactions .uses() .iter() .any(|t| depending_tables.contains(t)); if has_table { // will contain extensional queries that reference the depending tables let mut extensional_queries = Vec::new(); // Remove the extensional parts of the properties if let InteractionsType::Property(p) = &mut interactions.interactions { match p { Property::InsertValuesSelect { queries, .. } | Property::DoubleCreateFailure { queries, .. } | Property::DeleteSelect { queries, .. } | Property::DropSelect { queries, .. } | Property::Queries { queries } => { // Remove placeholder queries queries.retain(|query| !matches!(query, Query::Placeholder)); extensional_queries.append(queries); } Property::AllTableHaveExpectedContent { tables } => { tables.retain(|table| depending_tables.contains(table)); } Property::FsyncNoWait { .. } | Property::FaultyQuery { .. } => {} Property::SelectLimit { .. } | Property::SelectSelectOptimizer { .. } | Property::WhereTrueFalseNull { .. } | Property::UNIONAllPreservesCardinality { .. } | Property::ReadYourUpdatesBack { .. } | Property::TableHasExpectedContent { .. } => {} } } // Check again after query clear if the interactions still uses the failing table has_table = interactions .uses() .iter() .any(|t| depending_tables.contains(t)); // means the queries in the original property are present in the depending tables regardless of the extensional queries if has_table { if let Some(queries) = interactions.get_extensional_queries() { retain_relevant_queries(&mut extensional_queries, depending_tables); queries.append(&mut extensional_queries); } } else { // original property without extensional queries does not reference the tables so convert the property to // `Property::Queries` if `extensional_queries` is not empty retain_relevant_queries(&mut extensional_queries, depending_tables); if !extensional_queries.is_empty() { has_table = true; *interactions = Interactions::new( interactions.connection_index, InteractionsType::Property(Property::Queries { queries: extensional_queries, }), ); } } } let is_fault = matches!(interactions.interactions, InteractionsType::Fault(..)); let is_transaction = matches!( interactions.interactions, InteractionsType::Query(Query::Begin(..)) | InteractionsType::Query(Query::Commit(..)) | InteractionsType::Query(Query::Rollback(..)) ); is_fault || is_transaction || (has_table && !matches!( interactions.interactions, InteractionsType::Query(Query::Select(_)) | InteractionsType::Property(Property::SelectLimit { .. }) | InteractionsType::Property( Property::SelectSelectOptimizer { .. } ) )) }; idx += 1; retain }); // Comprises of idxs of Begin interactions let mut begin_idx: HashMap> = HashMap::new(); // Comprises of idxs of Commit and Rollback intereactions let mut end_tx_idx: HashMap> = HashMap::new(); for (idx, interactions) in self.iter().enumerate() { match &interactions.interactions { InteractionsType::Query(Query::Begin(..)) => { begin_idx .entry(interactions.connection_index) .or_insert_with(|| vec![idx]); } InteractionsType::Query(Query::Commit(..)) | InteractionsType::Query(Query::Rollback(..)) => { let last_begin = begin_idx .get(&interactions.connection_index) .and_then(|list| list.last()) .unwrap() + 1; if last_begin == idx { end_tx_idx .entry(interactions.connection_index) .or_insert_with(|| vec![idx]); } } _ => {} } } // remove interactions if its just a Begin Commit/Rollback with no queries in the middle let mut range_transactions = end_tx_idx .into_iter() .map(|(conn_index, list)| (conn_index, list.into_iter().peekable())) .collect::>(); let mut idx = 0; self.retain_mut(|interactions| { let mut retain = true; let iter = range_transactions.get_mut(&interactions.connection_index); if let Some(iter) = iter { if let Some(txn_interaction_idx) = iter.peek().copied() { if txn_interaction_idx == idx { iter.next(); } if txn_interaction_idx == idx || txn_interaction_idx.saturating_sub(1) == idx { retain = false; } } } idx += 1; retain }); } /// Create a smaller interaction plan by deleting a property pub(crate) fn brute_shrink_interaction_plan( &self, result: &SandboxedResult, env: Arc>, ) -> InteractionPlan { let failing_execution = match result { SandboxedResult::Panicked { error: _, last_execution: e, } => e, SandboxedResult::FoundBug { error: _, history: _, last_execution: e, } => e, SandboxedResult::Correct => { unreachable!("shrink is never called on correct result") } }; let mut plan = self.clone(); let all_interactions = self.interactions_list_with_secondary_index(); let secondary_interactions_index = all_interactions[failing_execution.interaction_index].0; { let mut idx = failing_execution.interaction_index; loop { if all_interactions[idx].0 != secondary_interactions_index { // Stop when we reach a different property break; } match &all_interactions[idx].1.interaction { // Fault does not depend on InteractionType::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.len(); plan.truncate(secondary_interactions_index + 1); // phase 1: shrink extensions for interaction in &mut plan { if let InteractionsType::Property(property) = &mut interaction.interactions { match property { Property::InsertValuesSelect { queries, .. } | Property::DoubleCreateFailure { queries, .. } | Property::DeleteSelect { queries, .. } | Property::DropSelect { queries, .. } | Property::Queries { queries } => { let mut temp_plan = InteractionPlan::new_with( queries .iter() .map(|q| { Interactions::new( interaction.connection_index, InteractionsType::Query(q.clone()), ) }) .collect(), self.mvcc, ); temp_plan = InteractionPlan::iterative_shrink( temp_plan, failing_execution, result, env.clone(), secondary_interactions_index, ); //temp_plan = Self::shrink_queries(temp_plan, failing_execution, result, env); *queries = temp_plan .into_iter() .filter_map(|i| match i.interactions { InteractionsType::Query(q) => Some(q), _ => None, }) .collect(); } Property::WhereTrueFalseNull { .. } | Property::UNIONAllPreservesCardinality { .. } | Property::SelectLimit { .. } | Property::SelectSelectOptimizer { .. } | Property::FaultyQuery { .. } | Property::FsyncNoWait { .. } | Property::ReadYourUpdatesBack { .. } | Property::TableHasExpectedContent { .. } | Property::AllTableHaveExpectedContent { .. } => {} } } } // phase 2: shrink the entire plan plan = Self::iterative_shrink( plan, failing_execution, result, env, secondary_interactions_index, ); let after = plan.len(); tracing::info!( "Shrinking interaction plan from {} to {} properties", before, after ); plan } /// shrink a plan by removing one interaction at a time (and its deps) while preserving the error fn iterative_shrink( mut plan: InteractionPlan, failing_execution: &Execution, old_result: &SandboxedResult, env: Arc>, secondary_interaction_index: usize, ) -> InteractionPlan { for i in (0..plan.len()).rev() { if i == secondary_interaction_index { continue; } let mut test_plan = plan.clone(); test_plan.remove(i); if Self::test_shrunk_plan(&test_plan, failing_execution, old_result, env.clone()) { plan = test_plan; } } plan } fn test_shrunk_plan( test_plan: &InteractionPlan, failing_execution: &Execution, old_result: &SandboxedResult, env: Arc>, ) -> bool { let last_execution = Arc::new(Mutex::new(*failing_execution)); let result = SandboxedResult::from( std::panic::catch_unwind(|| { let plan = test_plan.static_iterator(); run_simulation(env.clone(), plan, last_execution.clone()) }), last_execution, ); match (old_result, &result) { ( SandboxedResult::Panicked { error: e1, .. }, SandboxedResult::Panicked { error: e2, .. }, ) | ( SandboxedResult::FoundBug { error: e1, .. }, SandboxedResult::FoundBug { error: e2, .. }, ) => e1 == e2, _ => false, } } }