Skip to main content

protest_extras/
shrinking.rs

1//! Enhanced shrinking strategies for Protest
2//!
3//! This module provides advanced shrinking strategies that go beyond basic shrinking:
4//! - **Smart Shrinking**: Preserve invariants while shrinking
5//! - **Delta Debugging**: Binary search for minimal failing subsets
6//! - **Targeted Shrinking**: Shrink toward specific target values
7//!
8//! # Examples
9//!
10//! ## Smart Shrinking with Invariants
11//!
12//! ```rust
13//! use protest_extras::shrinking::SmartShrink;
14//!
15//! let mut sorted_vec = vec![1, 3, 5, 7, 9];
16//!
17//! // Shrink while keeping the vector sorted
18//! let shrunk = sorted_vec.shrink_preserving(|v| {
19//!     v.windows(2).all(|w| w[0] <= w[1])
20//! });
21//!
22//! for candidate in shrunk {
23//!     // All candidates remain sorted
24//!     assert!(candidate.windows(2).all(|w| w[0] <= w[1]));
25//! }
26//! ```
27//!
28//! ## Delta Debugging
29//!
30//! ```rust
31//! use protest_extras::shrinking::DeltaDebugShrinker;
32//!
33//! let operations = vec![1, 2, 3, 4, 5, 6, 7, 8];
34//!
35//! // Find minimal subset that fails a test
36//! let shrinker = DeltaDebugShrinker::new(operations);
37//! let minimal = shrinker.find_minimal(|ops| {
38//!     // Only fails if contains both 3 and 7
39//!     ops.contains(&3) && ops.contains(&7)
40//! });
41//!
42//! assert_eq!(minimal, vec![3, 7]); // Minimal failing case
43//! ```
44
45use std::fmt::Debug;
46
47// ============================================================================
48// Smart Shrinking with Invariants
49// ============================================================================
50
51/// Trait for shrinking values while preserving invariants
52///
53/// This allows you to shrink a value while ensuring that certain properties
54/// (invariants) are maintained throughout the shrinking process.
55pub trait SmartShrink: Clone {
56    /// Shrink this value while preserving the given invariant
57    ///
58    /// The invariant function should return `true` if the value is valid.
59    /// Only shrunk values that satisfy the invariant will be yielded.
60    ///
61    /// # Examples
62    ///
63    /// ```rust
64    /// use protest_extras::shrinking::SmartShrink;
65    ///
66    /// let mut vec = vec![2, 4, 6, 8];
67    ///
68    /// // Shrink while keeping all elements even
69    /// let shrunk: Vec<_> = vec.shrink_preserving(|v| {
70    ///     v.iter().all(|x| x % 2 == 0)
71    /// }).collect();
72    ///
73    /// // All shrunk candidates have even elements
74    /// for candidate in shrunk {
75    ///     assert!(candidate.iter().all(|x| x % 2 == 0));
76    /// }
77    /// ```
78    fn shrink_preserving<F>(&self, invariant: F) -> Box<dyn Iterator<Item = Self>>
79    where
80        F: Fn(&Self) -> bool + 'static;
81}
82
83// Implement SmartShrink for Vec
84impl<T: Clone + Debug + 'static> SmartShrink for Vec<T> {
85    fn shrink_preserving<F>(&self, invariant: F) -> Box<dyn Iterator<Item = Self>>
86    where
87        F: Fn(&Self) -> bool + 'static,
88    {
89        let original = self.clone();
90        let invariant = std::rc::Rc::new(invariant);
91
92        let mut candidates = Vec::new();
93
94        // Try empty vector
95        if !original.is_empty() {
96            let v = Vec::new();
97            if invariant(&v) {
98                candidates.push(v);
99            }
100        }
101
102        // Try removing elements from different positions
103        for i in 0..original.len().min(3) {
104            if original.len() > 1 {
105                let mut v = original.clone();
106                v.remove(i);
107                if invariant(&v) {
108                    candidates.push(v);
109                }
110            }
111        }
112
113        // Try half the length
114        if original.len() > 1 {
115            let v = original[..original.len() / 2].to_vec();
116            if invariant(&v) {
117                candidates.push(v);
118            }
119        }
120
121        Box::new(candidates.into_iter())
122    }
123}
124
125// Implement SmartShrink for integers
126impl SmartShrink for i32 {
127    fn shrink_preserving<F>(&self, invariant: F) -> Box<dyn Iterator<Item = Self>>
128    where
129        F: Fn(&Self) -> bool + 'static,
130    {
131        let current = *self;
132        let invariant = std::rc::Rc::new(invariant);
133
134        let candidates: Vec<i32> = (0..current)
135            .rev()
136            .filter(|x| invariant(x))
137            .take(10)
138            .collect();
139
140        Box::new(candidates.into_iter())
141    }
142}
143
144impl SmartShrink for u32 {
145    fn shrink_preserving<F>(&self, invariant: F) -> Box<dyn Iterator<Item = Self>>
146    where
147        F: Fn(&Self) -> bool + 'static,
148    {
149        let current = *self;
150        let invariant = std::rc::Rc::new(invariant);
151
152        let candidates: Vec<u32> = (0..current)
153            .rev()
154            .filter(|x| invariant(x))
155            .take(10)
156            .collect();
157
158        Box::new(candidates.into_iter())
159    }
160}
161
162// ============================================================================
163// Delta Debugging Shrinker
164// ============================================================================
165
166/// Delta debugging shrinker for finding minimal failing subsets
167///
168/// Uses binary search to efficiently find the smallest subset of a collection
169/// that still fails a test. This is particularly useful for reducing sequences
170/// of operations or large inputs to their minimal failing case.
171///
172/// # Examples
173///
174/// ```rust
175/// use protest_extras::shrinking::DeltaDebugShrinker;
176///
177/// let items = vec!["a", "b", "c", "d", "e"];
178/// let shrinker = DeltaDebugShrinker::new(items);
179///
180/// // Find minimal subset that contains "b" and "d"
181/// let minimal = shrinker.find_minimal(|subset| {
182///     subset.contains(&"b") && subset.contains(&"d")
183/// });
184///
185/// assert_eq!(minimal, vec!["b", "d"]);
186/// ```
187#[derive(Debug, Clone)]
188pub struct DeltaDebugShrinker<T> {
189    items: Vec<T>,
190}
191
192impl<T: Clone + Debug + PartialEq> DeltaDebugShrinker<T> {
193    /// Create a new delta debugging shrinker
194    pub fn new(items: Vec<T>) -> Self {
195        Self { items }
196    }
197
198    /// Find the minimal subset that satisfies the predicate
199    ///
200    /// The predicate should return `true` for failing test cases.
201    pub fn find_minimal<F>(self, predicate: F) -> Vec<T>
202    where
203        F: Fn(&[T]) -> bool,
204    {
205        let mut current = self.items;
206
207        // If the full set doesn't fail, return it
208        if !predicate(&current) {
209            return current;
210        }
211
212        let mut changed = true;
213
214        while changed {
215            changed = false;
216            let n = current.len();
217
218            if n <= 1 {
219                break;
220            }
221
222            // Try removing chunks of size n/2
223            let chunk_size = n / 2;
224
225            for i in 0..=(n - chunk_size) {
226                let candidate: Vec<T> = current
227                    .iter()
228                    .enumerate()
229                    .filter(|(idx, _)| *idx < i || *idx >= i + chunk_size)
230                    .map(|(_, item)| item.clone())
231                    .collect();
232
233                if !candidate.is_empty() && predicate(&candidate) {
234                    current = candidate;
235                    changed = true;
236                    break;
237                }
238            }
239
240            // If no chunk removal worked, try removing individual elements
241            if !changed {
242                for i in 0..current.len() {
243                    let mut candidate = current.clone();
244                    candidate.remove(i);
245
246                    if !candidate.is_empty() && predicate(&candidate) {
247                        current = candidate;
248                        changed = true;
249                        break;
250                    }
251                }
252            }
253        }
254
255        current
256    }
257
258    /// Shrink to minimal subset, yielding intermediate results
259    ///
260    /// This is useful if you want to see the shrinking process or
261    /// stop early.
262    pub fn shrink_steps<F>(self, predicate: F) -> DeltaDebugIterator<T, F>
263    where
264        F: Fn(&[T]) -> bool,
265    {
266        DeltaDebugIterator {
267            current: self.items,
268            predicate,
269            phase: 0,
270            chunk_size: 0,
271            index: 0,
272        }
273    }
274}
275
276/// Iterator that yields shrinking steps
277pub struct DeltaDebugIterator<T, F>
278where
279    F: Fn(&[T]) -> bool,
280{
281    current: Vec<T>,
282    predicate: F,
283    phase: usize,
284    chunk_size: usize,
285    index: usize,
286}
287
288impl<T: Clone + Debug, F: Fn(&[T]) -> bool> Iterator for DeltaDebugIterator<T, F> {
289    type Item = Vec<T>;
290
291    fn next(&mut self) -> Option<Self::Item> {
292        if self.current.len() <= 1 {
293            return None;
294        }
295
296        loop {
297            match self.phase {
298                0 => {
299                    // Initialize chunk size
300                    self.chunk_size = self.current.len() / 2;
301                    self.index = 0;
302                    self.phase = 1;
303                }
304                1 => {
305                    // Try removing chunks
306                    if self.index + self.chunk_size <= self.current.len() {
307                        let candidate: Vec<T> = self
308                            .current
309                            .iter()
310                            .enumerate()
311                            .filter(|(idx, _)| {
312                                *idx < self.index || *idx >= self.index + self.chunk_size
313                            })
314                            .map(|(_, item)| item.clone())
315                            .collect();
316
317                        self.index += 1;
318
319                        if !candidate.is_empty() && (self.predicate)(&candidate) {
320                            self.current = candidate.clone();
321                            self.phase = 0; // Restart with new current
322                            return Some(candidate);
323                        }
324                    } else {
325                        self.phase = 2;
326                        self.index = 0;
327                    }
328                }
329                2 => {
330                    // Try removing individual elements
331                    if self.index < self.current.len() {
332                        let mut candidate = self.current.clone();
333                        candidate.remove(self.index);
334                        self.index += 1;
335
336                        if !candidate.is_empty() && (self.predicate)(&candidate) {
337                            self.current = candidate.clone();
338                            self.phase = 0; // Restart
339                            return Some(candidate);
340                        }
341                    } else {
342                        return None; // No more shrinking possible
343                    }
344                }
345                _ => return None,
346            }
347        }
348    }
349}
350
351// ============================================================================
352// Targeted Shrinking
353// ============================================================================
354
355/// Shrinker that shrinks toward a specific target value
356///
357/// Instead of shrinking toward a "simple" value (like 0 or empty),
358/// this shrinker tries to shrink toward a specific target value.
359///
360/// # Examples
361///
362/// ```rust
363/// use protest_extras::shrinking::TargetedShrinker;
364///
365/// // Shrink toward 42
366/// let shrinker = TargetedShrinker::new_int(100, 42);
367///
368/// let shrunk: Vec<_> = shrinker.shrink().take(5).collect();
369/// // Yields values getting closer to 42
370/// assert!(shrunk.iter().all(|&x| x >= 42 && x <= 100));
371/// ```
372#[derive(Debug, Clone)]
373pub struct TargetedShrinker<T> {
374    current: T,
375    target: T,
376}
377
378impl TargetedShrinker<i32> {
379    /// Create a new targeted shrinker for integers
380    pub fn new_int(current: i32, target: i32) -> Self {
381        Self { current, target }
382    }
383
384    /// Generate shrinking steps toward the target
385    pub fn shrink(self) -> TargetedShrinkIterator<i32> {
386        TargetedShrinkIterator {
387            current: self.current,
388            target: self.target,
389            step: 0,
390        }
391    }
392}
393
394impl TargetedShrinker<f64> {
395    /// Create a new targeted shrinker for floats
396    pub fn new_float(current: f64, target: f64) -> Self {
397        Self { current, target }
398    }
399
400    /// Generate shrinking steps toward the target
401    pub fn shrink(self) -> TargetedShrinkIterator<f64> {
402        TargetedShrinkIterator {
403            current: self.current,
404            target: self.target,
405            step: 0,
406        }
407    }
408}
409
410/// Iterator for targeted shrinking
411pub struct TargetedShrinkIterator<T> {
412    current: T,
413    target: T,
414    step: usize,
415}
416
417impl Iterator for TargetedShrinkIterator<i32> {
418    type Item = i32;
419
420    fn next(&mut self) -> Option<Self::Item> {
421        if self.current == self.target {
422            return None;
423        }
424
425        self.step += 1;
426
427        // Binary search toward target
428        let diff = self.current - self.target;
429        let step_size = (diff.abs() / 2).max(1);
430
431        self.current = if diff > 0 {
432            (self.current - step_size).max(self.target)
433        } else {
434            (self.current + step_size).min(self.target)
435        };
436
437        Some(self.current)
438    }
439}
440
441impl Iterator for TargetedShrinkIterator<f64> {
442    type Item = f64;
443
444    fn next(&mut self) -> Option<Self::Item> {
445        if (self.current - self.target).abs() < f64::EPSILON {
446            return None;
447        }
448
449        self.step += 1;
450
451        // Binary search toward target
452        let diff = self.current - self.target;
453        self.current = self.target + diff / 2.0;
454
455        Some(self.current)
456    }
457}
458
459// ============================================================================
460// Cascading Shrinking Strategy
461// ============================================================================
462
463/// Cascading shrinker that applies multiple shrinking strategies in sequence
464///
465/// This strategy tries multiple shrinking approaches and combines their results,
466/// allowing for more thorough exploration of the shrink space.
467///
468/// # Examples
469///
470/// ```rust
471/// use protest_extras::shrinking::CascadingShrinker;
472///
473/// let value = vec![10, 20, 30, 40, 50];
474///
475/// // Try removing elements, then halving, then targeted shrinking
476/// let shrinker = CascadingShrinker::new(value);
477/// let shrunk: Vec<_> = shrinker.shrink().take(20).collect();
478///
479/// // Will explore many different shrinking paths
480/// assert!(!shrunk.is_empty());
481/// ```
482#[derive(Debug, Clone)]
483pub struct CascadingShrinker<T> {
484    value: T,
485}
486
487impl<T: Clone> CascadingShrinker<T> {
488    /// Create a new cascading shrinker
489    pub fn new(value: T) -> Self {
490        Self { value }
491    }
492
493    /// Get the original value
494    pub fn value(&self) -> &T {
495        &self.value
496    }
497}
498
499impl<T: Clone + Debug + 'static> CascadingShrinker<Vec<T>> {
500    /// Shrink using multiple strategies
501    pub fn shrink(&self) -> Box<dyn Iterator<Item = Vec<T>>> {
502        let original = self.value.clone();
503        let mut candidates = Vec::new();
504
505        // Strategy 1: Remove single elements
506        for i in 0..original.len() {
507            let mut v = original.clone();
508            v.remove(i);
509            candidates.push(v);
510        }
511
512        // Strategy 2: Remove chunks (halves, thirds, quarters)
513        if original.len() > 1 {
514            // Remove first half
515            candidates.push(original[original.len() / 2..].to_vec());
516            // Remove second half
517            candidates.push(original[..original.len() / 2].to_vec());
518
519            // Remove first third
520            if original.len() >= 3 {
521                candidates.push(original[original.len() / 3..].to_vec());
522            }
523
524            // Remove middle third
525            if original.len() >= 3 {
526                let third = original.len() / 3;
527                let mut v = original[..third].to_vec();
528                v.extend_from_slice(&original[2 * third..]);
529                candidates.push(v);
530            }
531        }
532
533        // Strategy 3: Keep only sorted subsequences
534        if original.len() > 2 {
535            for i in 0..original.len() - 1 {
536                candidates.push(original[i..i + 2].to_vec());
537            }
538        }
539
540        // Strategy 4: Empty vector
541        candidates.push(Vec::new());
542
543        Box::new(candidates.into_iter())
544    }
545}
546
547impl CascadingShrinker<i32> {
548    /// Shrink using multiple strategies for integers
549    pub fn shrink(&self) -> Box<dyn Iterator<Item = i32>> {
550        let current = self.value;
551        let mut candidates = Vec::new();
552
553        if current == 0 {
554            return Box::new(std::iter::empty());
555        }
556
557        // Strategy 1: Move toward zero
558        let half = current / 2;
559        if half != current {
560            candidates.push(half);
561        }
562
563        // Strategy 2: Subtract powers of 2
564        for i in 0..10 {
565            let pow = 1 << i;
566            if pow < current.abs() {
567                let next = if current > 0 {
568                    current - pow
569                } else {
570                    current + pow
571                };
572                if next != current {
573                    candidates.push(next);
574                }
575            }
576        }
577
578        // Strategy 3: Zero
579        candidates.push(0);
580
581        // Deduplicate
582        candidates.sort_unstable();
583        candidates.dedup();
584
585        Box::new(candidates.into_iter())
586    }
587}
588
589// ============================================================================
590// Guided Shrinking with Test Feedback
591// ============================================================================
592
593/// Guided shrinker that uses test feedback to prioritize shrinking directions
594///
595/// This shrinker runs the test on shrink candidates and uses pass/fail information
596/// to guide the search toward minimal failing cases.
597///
598/// # Examples
599///
600/// ```rust
601/// use protest_extras::shrinking::GuidedShrinker;
602///
603/// let value = vec![1, 2, 3, 4, 5, 6, 7, 8];
604///
605/// // Find minimal subset that sums to > 10
606/// let shrinker = GuidedShrinker::new(value);
607/// let minimal = shrinker.find_minimal(|v| v.iter().sum::<i32>() > 10);
608///
609/// // Will find a small subset that still sums to > 10
610/// assert!(minimal.iter().sum::<i32>() > 10);
611/// assert!(minimal.len() < 8);
612/// ```
613#[derive(Debug, Clone)]
614pub struct GuidedShrinker<T> {
615    value: T,
616    max_iterations: usize,
617}
618
619impl<T: Clone> GuidedShrinker<T> {
620    /// Create a new guided shrinker with default max iterations
621    pub fn new(value: T) -> Self {
622        Self {
623            value,
624            max_iterations: 1000,
625        }
626    }
627
628    /// Create a guided shrinker with custom max iterations
629    pub fn with_max_iterations(value: T, max_iterations: usize) -> Self {
630        Self {
631            value,
632            max_iterations,
633        }
634    }
635}
636
637impl<T: Clone + Debug + PartialEq + 'static> GuidedShrinker<Vec<T>> {
638    /// Find minimal failing case using test feedback
639    ///
640    /// The test function should return `true` if the test fails (i.e., we want to keep shrinking).
641    /// Returns the smallest value that still fails the test.
642    pub fn find_minimal<F>(&self, test: F) -> Vec<T>
643    where
644        F: Fn(&Vec<T>) -> bool,
645    {
646        let mut current = self.value.clone();
647
648        if !test(&current) {
649            // Original doesn't fail, return as-is
650            return current;
651        }
652
653        for _ in 0..self.max_iterations {
654            let mut found_smaller = false;
655
656            // Try removing each element
657            for i in (0..current.len()).rev() {
658                let mut candidate = current.clone();
659                candidate.remove(i);
660
661                if test(&candidate) {
662                    // Still fails with this element removed
663                    current = candidate;
664                    found_smaller = true;
665                    break;
666                }
667            }
668
669            if !found_smaller {
670                // Try removing halves
671                if current.len() > 1 {
672                    let half = current.len() / 2;
673
674                    // Try first half
675                    let first_half = current[..half].to_vec();
676                    if test(&first_half) {
677                        current = first_half;
678                        found_smaller = true;
679                    } else {
680                        // Try second half
681                        let second_half = current[half..].to_vec();
682                        if test(&second_half) {
683                            current = second_half;
684                            found_smaller = true;
685                        }
686                    }
687                }
688            }
689
690            if !found_smaller {
691                // Can't shrink further
692                break;
693            }
694        }
695
696        current
697    }
698
699    /// Find minimal failing case with detailed feedback
700    ///
701    /// Returns both the minimal value and the number of iterations used.
702    pub fn find_minimal_with_stats<F>(&self, test: F) -> (Vec<T>, usize)
703    where
704        F: Fn(&Vec<T>) -> bool,
705    {
706        let mut current = self.value.clone();
707        let mut iterations = 0;
708
709        if !test(&current) {
710            return (current, iterations);
711        }
712
713        for _ in 0..self.max_iterations {
714            iterations += 1;
715            let mut found_smaller = false;
716
717            for i in (0..current.len()).rev() {
718                let mut candidate = current.clone();
719                candidate.remove(i);
720
721                if test(&candidate) {
722                    current = candidate;
723                    found_smaller = true;
724                    break;
725                }
726            }
727
728            if !found_smaller && current.len() > 1 {
729                let half = current.len() / 2;
730
731                let first_half = current[..half].to_vec();
732                if test(&first_half) {
733                    current = first_half;
734                    found_smaller = true;
735                } else {
736                    let second_half = current[half..].to_vec();
737                    if test(&second_half) {
738                        current = second_half;
739                        found_smaller = true;
740                    }
741                }
742            }
743
744            if !found_smaller {
745                break;
746            }
747        }
748
749        (current, iterations)
750    }
751}
752
753// ============================================================================
754// Breadth-First vs Depth-First Shrinking Control
755// ============================================================================
756
757/// Search strategy for shrinking
758#[derive(Debug, Clone, Copy, PartialEq, Eq)]
759pub enum ShrinkStrategy {
760    /// Breadth-first: Try all shrinks at current depth before going deeper
761    BreadthFirst,
762    /// Depth-first: Follow first successful shrink as far as possible
763    DepthFirst,
764}
765
766/// Configurable shrinker with search strategy control
767///
768/// Allows choosing between breadth-first and depth-first search strategies
769/// when exploring the shrink space.
770///
771/// # Examples
772///
773/// ```rust
774/// use protest_extras::shrinking::{ConfigurableShrinker, ShrinkStrategy};
775///
776/// let value = vec![1, 2, 3, 4, 5];
777///
778/// // Depth-first: aggressive shrinking
779/// let shrinker = ConfigurableShrinker::new(value.clone(), ShrinkStrategy::DepthFirst);
780/// let depth_result = shrinker.find_minimal(|v| v.len() >= 2);
781///
782/// // Breadth-first: explore more evenly
783/// let shrinker = ConfigurableShrinker::new(value, ShrinkStrategy::BreadthFirst);
784/// let breadth_result = shrinker.find_minimal(|v| v.len() >= 2);
785///
786/// assert!(depth_result.len() >= 2);
787/// assert!(breadth_result.len() >= 2);
788/// ```
789#[derive(Debug, Clone)]
790pub struct ConfigurableShrinker<T> {
791    value: T,
792    strategy: ShrinkStrategy,
793    max_depth: usize,
794}
795
796impl<T: Clone> ConfigurableShrinker<T> {
797    /// Create a new configurable shrinker
798    pub fn new(value: T, strategy: ShrinkStrategy) -> Self {
799        Self {
800            value,
801            strategy,
802            max_depth: 100,
803        }
804    }
805
806    /// Set maximum search depth
807    pub fn with_max_depth(mut self, max_depth: usize) -> Self {
808        self.max_depth = max_depth;
809        self
810    }
811
812    /// Get the current strategy
813    pub fn strategy(&self) -> ShrinkStrategy {
814        self.strategy
815    }
816}
817
818impl<T: Clone + Debug + PartialEq + 'static> ConfigurableShrinker<Vec<T>> {
819    /// Find minimal case using the configured strategy
820    pub fn find_minimal<F>(&self, test: F) -> Vec<T>
821    where
822        F: Fn(&Vec<T>) -> bool,
823    {
824        match self.strategy {
825            ShrinkStrategy::DepthFirst => self.find_minimal_dfs(&test, self.value.clone(), 0),
826            ShrinkStrategy::BreadthFirst => self.find_minimal_bfs(&test),
827        }
828    }
829
830    fn find_minimal_dfs<F>(&self, test: &F, current: Vec<T>, depth: usize) -> Vec<T>
831    where
832        F: Fn(&Vec<T>) -> bool,
833    {
834        if depth >= self.max_depth || !test(&current) {
835            return current;
836        }
837
838        // Try removing elements one by one (depth-first)
839        for i in (0..current.len()).rev() {
840            let mut candidate = current.clone();
841            candidate.remove(i);
842
843            if test(&candidate) {
844                // Recursively shrink this candidate
845                return self.find_minimal_dfs(test, candidate, depth + 1);
846            }
847        }
848
849        // Try halving
850        if current.len() > 1 {
851            let half = current.len() / 2;
852
853            let first_half = current[..half].to_vec();
854            if test(&first_half) {
855                return self.find_minimal_dfs(test, first_half, depth + 1);
856            }
857
858            let second_half = current[half..].to_vec();
859            if test(&second_half) {
860                return self.find_minimal_dfs(test, second_half, depth + 1);
861            }
862        }
863
864        current
865    }
866
867    fn find_minimal_bfs<F>(&self, test: &F) -> Vec<T>
868    where
869        F: Fn(&Vec<T>) -> bool,
870    {
871        use std::collections::VecDeque;
872
873        let mut queue = VecDeque::new();
874        queue.push_back((self.value.clone(), 0));
875
876        let mut best = self.value.clone();
877        let mut visited = std::collections::HashSet::new();
878
879        while let Some((current, depth)) = queue.pop_front() {
880            if depth >= self.max_depth {
881                continue;
882            }
883
884            if !test(&current) {
885                continue;
886            }
887
888            // This is a valid failing case
889            if current.len() < best.len() {
890                best = current.clone();
891            }
892
893            // Generate candidates for next level
894            for i in 0..current.len() {
895                let mut candidate = current.clone();
896                candidate.remove(i);
897
898                // Use a simple hash to avoid revisiting
899                let hash = format!("{:?}", candidate);
900                if visited.insert(hash) {
901                    queue.push_back((candidate, depth + 1));
902                }
903            }
904
905            // Add halving candidates
906            if current.len() > 1 {
907                let half = current.len() / 2;
908
909                let first_half = current[..half].to_vec();
910                let hash = format!("{:?}", first_half);
911                if visited.insert(hash.clone()) {
912                    queue.push_back((first_half, depth + 1));
913                }
914
915                let second_half = current[half..].to_vec();
916                let hash = format!("{:?}", second_half);
917                if visited.insert(hash) {
918                    queue.push_back((second_half, depth + 1));
919                }
920            }
921        }
922
923        best
924    }
925}
926
927#[cfg(test)]
928mod tests {
929    use super::*;
930
931    #[test]
932    fn test_smart_shrink_vec_preserving_sorted() {
933        let vec = vec![1, 3, 5, 7, 9];
934
935        let shrunk: Vec<_> = vec
936            .shrink_preserving(|v| v.windows(2).all(|w| w[0] <= w[1]))
937            .collect();
938
939        // All shrunk vectors should be sorted
940        for candidate in shrunk {
941            assert!(candidate.windows(2).all(|w| w[0] <= w[1]));
942        }
943    }
944
945    #[test]
946    fn test_smart_shrink_vec_preserving_even() {
947        let vec = vec![2, 4, 6, 8];
948
949        let shrunk: Vec<_> = vec
950            .shrink_preserving(|v| v.iter().all(|x| x % 2 == 0))
951            .collect();
952
953        // All shrunk vectors should have even elements
954        for candidate in shrunk {
955            assert!(candidate.iter().all(|x| x % 2 == 0));
956        }
957    }
958
959    #[test]
960    fn test_smart_shrink_int_preserving_positive() {
961        let num = 100;
962
963        let shrunk: Vec<_> = num.shrink_preserving(|x| *x > 0).take(10).collect();
964
965        // All shrunk values should be positive
966        for candidate in shrunk {
967            assert!(candidate > 0);
968        }
969    }
970
971    #[test]
972    fn test_delta_debug_minimal_subset() {
973        let items = vec![1, 2, 3, 4, 5, 6, 7, 8];
974        let shrinker = DeltaDebugShrinker::new(items);
975
976        // Find minimal subset containing 3 and 7
977        let minimal = shrinker.find_minimal(|subset| subset.contains(&3) && subset.contains(&7));
978
979        assert!(minimal.contains(&3));
980        assert!(minimal.contains(&7));
981        assert!(minimal.len() <= 2);
982    }
983
984    #[test]
985    fn test_delta_debug_single_element() {
986        let items = vec![1, 2, 3, 4, 5];
987        let shrinker = DeltaDebugShrinker::new(items);
988
989        // Find minimal subset containing 3
990        let minimal = shrinker.find_minimal(|subset| subset.contains(&3));
991
992        assert_eq!(minimal, vec![3]);
993    }
994
995    #[test]
996    fn test_delta_debug_iterator() {
997        let items = vec![1, 2, 3, 4, 5, 6];
998        let shrinker = DeltaDebugShrinker::new(items);
999
1000        let steps: Vec<_> = shrinker
1001            .shrink_steps(|subset| subset.contains(&2) && subset.contains(&5))
1002            .collect();
1003
1004        // Should yield progressively smaller subsets
1005        assert!(!steps.is_empty());
1006        let last = steps.last().unwrap();
1007        assert!(last.contains(&2));
1008        assert!(last.contains(&5));
1009    }
1010
1011    #[test]
1012    fn test_targeted_shrink_int_toward_zero() {
1013        let shrinker = TargetedShrinker::new_int(100, 0);
1014        let shrunk: Vec<_> = shrinker.shrink().collect();
1015
1016        assert!(!shrunk.is_empty());
1017        assert_eq!(*shrunk.last().unwrap(), 0);
1018
1019        // Should be monotonically decreasing
1020        for window in shrunk.windows(2) {
1021            assert!(window[0] > window[1]);
1022        }
1023    }
1024
1025    #[test]
1026    fn test_targeted_shrink_int_toward_target() {
1027        let shrinker = TargetedShrinker::new_int(100, 42);
1028        let shrunk: Vec<_> = shrinker.shrink().collect();
1029
1030        assert!(!shrunk.is_empty());
1031        assert_eq!(*shrunk.last().unwrap(), 42);
1032
1033        // Should be getting closer to 42
1034        for value in shrunk {
1035            assert!((42..=100).contains(&value));
1036        }
1037    }
1038
1039    #[test]
1040    fn test_targeted_shrink_float() {
1041        let shrinker = TargetedShrinker::new_float(100.0, 42.0);
1042        let shrunk: Vec<_> = shrinker.shrink().take(10).collect();
1043
1044        assert!(!shrunk.is_empty());
1045
1046        // Should be getting closer to 42.0
1047        let mut prev = 100.0_f64;
1048        for value in shrunk {
1049            let prev_diff = (prev - 42.0_f64).abs();
1050            let curr_diff = (value - 42.0_f64).abs();
1051            assert!(curr_diff < prev_diff);
1052            prev = value;
1053        }
1054    }
1055
1056    #[test]
1057    fn test_cascading_shrinker_vec() {
1058        let value = vec![1, 2, 3, 4, 5, 6, 7, 8];
1059        let shrinker = CascadingShrinker::new(value);
1060
1061        let shrunk: Vec<_> = shrinker.shrink().collect();
1062
1063        assert!(!shrunk.is_empty());
1064
1065        // Should include various shrinking strategies
1066        // - Single element removals
1067        // - Halves
1068        // - Empty vec
1069        assert!(shrunk.contains(&vec![]));
1070
1071        // Check that some element removals are present
1072        assert!(shrunk.iter().any(|v| v.len() == 7));
1073    }
1074
1075    #[test]
1076    fn test_cascading_shrinker_int() {
1077        let shrinker = CascadingShrinker::new(100);
1078        let shrunk: Vec<_> = shrinker.shrink().collect();
1079
1080        assert!(!shrunk.is_empty());
1081
1082        // Should include zero
1083        assert!(shrunk.contains(&0));
1084
1085        // Should include half
1086        assert!(shrunk.contains(&50));
1087
1088        // All values should be smaller than original
1089        for value in &shrunk {
1090            assert!(value.abs() <= 100);
1091        }
1092    }
1093
1094    #[test]
1095    fn test_guided_shrinker_finds_minimal() {
1096        let value = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
1097        let shrinker = GuidedShrinker::new(value);
1098
1099        // Find minimal subset that sums to > 15
1100        let minimal = shrinker.find_minimal(|v| v.iter().sum::<i32>() > 15);
1101
1102        // Should find something smaller than original
1103        assert!(minimal.len() < 10);
1104
1105        // Should still sum to > 15
1106        assert!(minimal.iter().sum::<i32>() > 15);
1107
1108        // Should be reasonably small (e.g., sum of 16 would be minimal)
1109        assert!(minimal.len() <= 6); // At most [10, 6] or similar
1110    }
1111
1112    #[test]
1113    fn test_guided_shrinker_with_stats() {
1114        let value = vec![1, 2, 3, 4, 5];
1115        let shrinker = GuidedShrinker::new(value);
1116
1117        let (minimal, iterations) = shrinker.find_minimal_with_stats(|v| v.len() >= 2);
1118
1119        // Should find something of length 2
1120        assert_eq!(minimal.len(), 2);
1121
1122        // Should have taken some iterations
1123        assert!(iterations > 0);
1124        assert!(iterations < 100); // Reasonable bound
1125    }
1126
1127    #[test]
1128    fn test_guided_shrinker_no_shrink_if_passes() {
1129        let value = vec![1, 2, 3];
1130        let shrinker = GuidedShrinker::new(value.clone());
1131
1132        // Test always fails (returns false), so no shrinking
1133        let minimal = shrinker.find_minimal(|_v| false);
1134
1135        // Should return original since test never fails
1136        assert_eq!(minimal, value);
1137    }
1138
1139    #[test]
1140    fn test_configurable_shrinker_depth_first() {
1141        let value = vec![1, 2, 3, 4, 5, 6, 7, 8];
1142        let shrinker = ConfigurableShrinker::new(value, ShrinkStrategy::DepthFirst);
1143
1144        let minimal = shrinker.find_minimal(|v| v.len() >= 3);
1145
1146        // Should find something of length 3
1147        assert_eq!(minimal.len(), 3);
1148    }
1149
1150    #[test]
1151    fn test_configurable_shrinker_breadth_first() {
1152        let value = vec![1, 2, 3, 4, 5, 6, 7, 8];
1153        let shrinker = ConfigurableShrinker::new(value, ShrinkStrategy::BreadthFirst);
1154
1155        let minimal = shrinker.find_minimal(|v| v.len() >= 3);
1156
1157        // Should find something of length 3
1158        assert_eq!(minimal.len(), 3);
1159    }
1160
1161    #[test]
1162    fn test_configurable_shrinker_with_max_depth() {
1163        let value = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
1164        let shrinker =
1165            ConfigurableShrinker::new(value, ShrinkStrategy::DepthFirst).with_max_depth(5);
1166
1167        let minimal = shrinker.find_minimal(|v| v.len() >= 2);
1168
1169        // With max_depth=5, DFS can remove at most 5 elements: 10 - 5 = 5
1170        // This demonstrates that max_depth limits how many steps we can take
1171        assert_eq!(minimal.len(), 5);
1172    }
1173
1174    #[test]
1175    fn test_shrink_strategy_enum() {
1176        // Test enum properties
1177        assert_eq!(ShrinkStrategy::DepthFirst, ShrinkStrategy::DepthFirst);
1178        assert_eq!(ShrinkStrategy::BreadthFirst, ShrinkStrategy::BreadthFirst);
1179        assert_ne!(ShrinkStrategy::DepthFirst, ShrinkStrategy::BreadthFirst);
1180    }
1181
1182    #[test]
1183    fn test_cascading_shrinker_aggressive() {
1184        let value = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
1185        let shrinker = CascadingShrinker::new(value);
1186
1187        let shrunk: Vec<_> = shrinker.shrink().collect();
1188
1189        // Should produce many candidates
1190        assert!(shrunk.len() > 15);
1191
1192        // Should include thirds (remove first third)
1193        assert!(shrunk.iter().any(|v| v.len() == 8));
1194
1195        // Should include adjacent pairs
1196        assert!(shrunk.iter().any(|v| v.len() == 2));
1197    }
1198}