Skip to main content

tensorlogic_ir/
fuzzing.rs

1//! Fuzzing and stress testing infrastructure for the IR.
2//!
3//! This module provides tools for testing the robustness of the TensorLogic IR through:
4//! - Stress testing with deeply nested expressions
5//! - Edge case testing (empty, large, boundary values)
6//! - Invariant checking across operations
7//! - Malformed input handling
8//! - Random expression generation for property-based testing
9//! - Mutation-based fuzzing
10//!
11//! The module supports both deterministic stress tests and random property-based testing
12//! through proptest integration.
13
14use std::collections::HashSet;
15use std::panic::AssertUnwindSafe;
16
17use crate::{EinsumGraph, EinsumNode, TLExpr, Term};
18
19/// Configuration for random expression generation.
20#[derive(Debug, Clone)]
21pub struct ExprGenConfig {
22    /// Maximum depth of nested expressions
23    pub max_depth: usize,
24    /// Maximum number of arguments for predicates
25    pub max_arity: usize,
26    /// Maximum number of variables to use
27    pub max_vars: usize,
28    /// Maximum number of predicates to use
29    pub max_predicates: usize,
30    /// Probability of generating a quantifier (0.0 - 1.0)
31    pub quantifier_probability: f64,
32    /// Probability of generating an arithmetic operation
33    pub arithmetic_probability: f64,
34    /// Domains to use for quantifiers
35    pub domains: Vec<String>,
36}
37
38impl Default for ExprGenConfig {
39    fn default() -> Self {
40        Self {
41            max_depth: 5,
42            max_arity: 3,
43            max_vars: 10,
44            max_predicates: 5,
45            quantifier_probability: 0.2,
46            arithmetic_probability: 0.1,
47            domains: vec!["Entity".to_string(), "Int".to_string(), "Bool".to_string()],
48        }
49    }
50}
51
52impl ExprGenConfig {
53    /// Create a minimal config for quick tests
54    pub fn minimal() -> Self {
55        Self {
56            max_depth: 2,
57            max_arity: 2,
58            max_vars: 3,
59            max_predicates: 2,
60            quantifier_probability: 0.1,
61            arithmetic_probability: 0.05,
62            domains: vec!["Entity".to_string()],
63        }
64    }
65
66    /// Create a stress test config with deep nesting
67    pub fn stress() -> Self {
68        Self {
69            max_depth: 10,
70            max_arity: 5,
71            max_vars: 20,
72            max_predicates: 10,
73            quantifier_probability: 0.3,
74            arithmetic_probability: 0.2,
75            domains: vec![
76                "Entity".to_string(),
77                "Int".to_string(),
78                "Bool".to_string(),
79                "Real".to_string(),
80            ],
81        }
82    }
83}
84
85/// Simple deterministic random number generator for reproducible tests.
86/// Uses a linear congruential generator.
87#[derive(Debug, Clone)]
88pub struct SimpleRng {
89    state: u64,
90}
91
92impl SimpleRng {
93    /// Create a new RNG with a seed
94    pub fn new(seed: u64) -> Self {
95        Self { state: seed }
96    }
97
98    /// Generate next random u64
99    pub fn next_u64(&mut self) -> u64 {
100        // LCG parameters (same as glibc)
101        self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345);
102        self.state
103    }
104
105    /// Generate a random number in range [0, max)
106    pub fn gen_range(&mut self, max: usize) -> usize {
107        if max == 0 {
108            return 0;
109        }
110        (self.next_u64() as usize) % max
111    }
112
113    /// Generate a random f64 in [0, 1)
114    pub fn gen_f64(&mut self) -> f64 {
115        (self.next_u64() as f64) / (u64::MAX as f64)
116    }
117
118    /// Generate a random bool with given probability of true
119    pub fn gen_bool(&mut self, probability: f64) -> bool {
120        self.gen_f64() < probability
121    }
122
123    /// Choose a random element from a slice
124    pub fn choose<'a, T>(&mut self, items: &'a [T]) -> Option<&'a T> {
125        if items.is_empty() {
126            None
127        } else {
128            Some(&items[self.gen_range(items.len())])
129        }
130    }
131}
132
133/// Random expression generator.
134pub struct ExprGenerator {
135    config: ExprGenConfig,
136    rng: SimpleRng,
137    var_names: Vec<String>,
138    pred_names: Vec<String>,
139}
140
141impl ExprGenerator {
142    /// Create a new generator with config and seed
143    pub fn new(config: ExprGenConfig, seed: u64) -> Self {
144        let var_names: Vec<String> = (0..config.max_vars).map(|i| format!("x{}", i)).collect();
145        let pred_names: Vec<String> = (0..config.max_predicates)
146            .map(|i| format!("P{}", i))
147            .collect();
148
149        Self {
150            config,
151            rng: SimpleRng::new(seed),
152            var_names,
153            pred_names,
154        }
155    }
156
157    /// Generate a random variable term
158    pub fn gen_var(&mut self) -> Term {
159        let name = self
160            .rng
161            .choose(&self.var_names)
162            .expect("non-empty var_names slice must have a chooseable element")
163            .clone();
164        Term::var(name)
165    }
166
167    /// Generate a random constant term
168    pub fn gen_const(&mut self) -> Term {
169        let value = format!("c{}", self.rng.gen_range(100));
170        Term::constant(value)
171    }
172
173    /// Generate a random term
174    pub fn gen_term(&mut self) -> Term {
175        if self.rng.gen_bool(0.7) {
176            self.gen_var()
177        } else {
178            self.gen_const()
179        }
180    }
181
182    /// Generate a random predicate expression
183    pub fn gen_predicate(&mut self) -> TLExpr {
184        let name = self
185            .rng
186            .choose(&self.pred_names)
187            .expect("non-empty pred_names slice must have a chooseable element")
188            .clone();
189        let arity = self.rng.gen_range(self.config.max_arity) + 1;
190        let args: Vec<Term> = (0..arity).map(|_| self.gen_term()).collect();
191        TLExpr::pred(name, args)
192    }
193
194    /// Generate a random expression with given depth limit
195    pub fn gen_expr(&mut self, depth: usize) -> TLExpr {
196        if depth == 0 {
197            // Base case: generate atomic expression
198            if self.rng.gen_bool(0.8) {
199                self.gen_predicate()
200            } else {
201                TLExpr::constant(self.rng.gen_f64())
202            }
203        } else {
204            // Choose expression type
205            let choice = self.rng.gen_range(10);
206            match choice {
207                0 => self.gen_predicate(),
208                1 => TLExpr::negate(self.gen_expr(depth - 1)),
209                2 => TLExpr::and(self.gen_expr(depth - 1), self.gen_expr(depth - 1)),
210                3 => TLExpr::or(self.gen_expr(depth - 1), self.gen_expr(depth - 1)),
211                4 => TLExpr::imply(self.gen_expr(depth - 1), self.gen_expr(depth - 1)),
212                5 if self.rng.gen_bool(self.config.quantifier_probability) => {
213                    let var = self
214                        .rng
215                        .choose(&self.var_names)
216                        .expect("non-empty var_names slice must have a chooseable element")
217                        .clone();
218                    let domain = self
219                        .rng
220                        .choose(&self.config.domains)
221                        .expect("non-empty domains slice must have a chooseable element")
222                        .clone();
223                    TLExpr::exists(var, domain, self.gen_expr(depth - 1))
224                }
225                6 if self.rng.gen_bool(self.config.quantifier_probability) => {
226                    let var = self
227                        .rng
228                        .choose(&self.var_names)
229                        .expect("non-empty var_names slice must have a chooseable element")
230                        .clone();
231                    let domain = self
232                        .rng
233                        .choose(&self.config.domains)
234                        .expect("non-empty domains slice must have a chooseable element")
235                        .clone();
236                    TLExpr::forall(var, domain, self.gen_expr(depth - 1))
237                }
238                7 if self.rng.gen_bool(self.config.arithmetic_probability) => {
239                    TLExpr::add(self.gen_expr(depth - 1), self.gen_expr(depth - 1))
240                }
241                8 if self.rng.gen_bool(self.config.arithmetic_probability) => {
242                    TLExpr::mul(self.gen_expr(depth - 1), self.gen_expr(depth - 1))
243                }
244                _ => self.gen_predicate(),
245            }
246        }
247    }
248
249    /// Generate a random expression with default depth
250    pub fn gen(&mut self) -> TLExpr {
251        let depth = self.rng.gen_range(self.config.max_depth) + 1;
252        self.gen_expr(depth)
253    }
254}
255
256/// Mutation operations for expressions.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum MutationKind {
259    /// Negate the expression
260    Negate,
261    /// Wrap in existential quantifier
262    WrapExists,
263    /// Wrap in universal quantifier
264    WrapForall,
265    /// Add conjunction with random expression
266    AndWith,
267    /// Add disjunction with random expression
268    OrWith,
269    /// Replace a subexpression
270    ReplaceSubexpr,
271    /// Duplicate expression (wrap in AND with self)
272    Duplicate,
273}
274
275/// Mutate an expression
276pub fn mutate_expr(expr: &TLExpr, mutation: MutationKind, rng: &mut SimpleRng) -> TLExpr {
277    match mutation {
278        MutationKind::Negate => TLExpr::negate(expr.clone()),
279        MutationKind::WrapExists => {
280            let var = format!("mut_x{}", rng.gen_range(100));
281            TLExpr::exists(var, "Entity", expr.clone())
282        }
283        MutationKind::WrapForall => {
284            let var = format!("mut_x{}", rng.gen_range(100));
285            TLExpr::forall(var, "Entity", expr.clone())
286        }
287        MutationKind::AndWith => {
288            let mut gen = ExprGenerator::new(ExprGenConfig::minimal(), rng.next_u64());
289            TLExpr::and(expr.clone(), gen.gen_predicate())
290        }
291        MutationKind::OrWith => {
292            let mut gen = ExprGenerator::new(ExprGenConfig::minimal(), rng.next_u64());
293            TLExpr::or(expr.clone(), gen.gen_predicate())
294        }
295        MutationKind::ReplaceSubexpr => {
296            // For simplicity, just wrap in a new operation
297            let mut gen = ExprGenerator::new(ExprGenConfig::minimal(), rng.next_u64());
298            if rng.gen_bool(0.5) {
299                TLExpr::and(expr.clone(), gen.gen_predicate())
300            } else {
301                TLExpr::or(expr.clone(), gen.gen_predicate())
302            }
303        }
304        MutationKind::Duplicate => TLExpr::and(expr.clone(), expr.clone()),
305    }
306}
307
308/// Apply a random mutation to an expression
309pub fn random_mutation(expr: &TLExpr, rng: &mut SimpleRng) -> TLExpr {
310    let mutations = [
311        MutationKind::Negate,
312        MutationKind::WrapExists,
313        MutationKind::WrapForall,
314        MutationKind::AndWith,
315        MutationKind::OrWith,
316        MutationKind::Duplicate,
317    ];
318    let mutation = *rng
319        .choose(&mutations)
320        .expect("non-empty mutations slice must have a chooseable element");
321    mutate_expr(expr, mutation, rng)
322}
323
324/// Apply multiple random mutations to an expression
325pub fn multi_mutate(expr: &TLExpr, num_mutations: usize, rng: &mut SimpleRng) -> TLExpr {
326    let mut result = expr.clone();
327    for _ in 0..num_mutations {
328        result = random_mutation(&result, rng);
329    }
330    result
331}
332
333/// Configuration for graph generation
334#[derive(Debug, Clone)]
335pub struct GraphGenConfig {
336    /// Maximum number of tensors
337    pub max_tensors: usize,
338    /// Maximum number of nodes
339    pub max_nodes: usize,
340    /// Probability of creating an einsum operation
341    pub einsum_probability: f64,
342}
343
344impl Default for GraphGenConfig {
345    fn default() -> Self {
346        Self {
347            max_tensors: 10,
348            max_nodes: 5,
349            einsum_probability: 0.3,
350        }
351    }
352}
353
354/// Generate a random graph for testing
355pub fn gen_random_graph(config: &GraphGenConfig, rng: &mut SimpleRng) -> EinsumGraph {
356    let mut graph = EinsumGraph::new();
357
358    // Add random tensors
359    let num_tensors = rng.gen_range(config.max_tensors) + 1;
360    let mut tensors = Vec::new();
361    for i in 0..num_tensors {
362        tensors.push(graph.add_tensor(format!("t{}", i)));
363    }
364
365    // Add random nodes
366    let num_nodes = rng.gen_range(config.max_nodes);
367    for _ in 0..num_nodes {
368        if tensors.len() < 2 {
369            break;
370        }
371
372        // Pick random operation
373        if rng.gen_bool(config.einsum_probability) && tensors.len() >= 2 {
374            // Einsum operation
375            let idx1 = rng.gen_range(tensors.len());
376            let idx2 = rng.gen_range(tensors.len());
377            if idx1 != idx2 {
378                let out = graph.add_tensor(format!("out_{}", graph.tensor_count()));
379                let _ = graph.add_node(EinsumNode::einsum(
380                    "ij,jk->ik",
381                    vec![tensors[idx1], tensors[idx2]],
382                    vec![out],
383                ));
384                tensors.push(out);
385            }
386        } else if !tensors.is_empty() {
387            // Element-wise unary operation
388            let idx = rng.gen_range(tensors.len());
389            let out = graph.add_tensor(format!("out_{}", graph.tensor_count()));
390            let ops = ["neg", "exp", "log", "relu"];
391            let op = *rng
392                .choose(&ops)
393                .expect("non-empty ops slice must have a chooseable element");
394            let _ = graph.add_node(EinsumNode::elem_unary(op, tensors[idx], out));
395            tensors.push(out);
396        }
397    }
398
399    // Set output
400    if !tensors.is_empty() {
401        let output_idx = rng.gen_range(tensors.len());
402        let _ = graph.add_output(tensors[output_idx]);
403    }
404
405    graph
406}
407
408/// Fuzz testing statistics.
409#[derive(Debug, Clone, Default)]
410pub struct FuzzStats {
411    /// Number of tests run
412    pub tests_run: usize,
413    /// Number of tests that passed
414    pub tests_passed: usize,
415    /// Number of tests that failed
416    pub tests_failed: usize,
417    /// Number of panics caught
418    pub panics_caught: usize,
419    /// Unique error messages
420    pub unique_errors: HashSet<String>,
421}
422
423impl FuzzStats {
424    /// Create new stats.
425    pub fn new() -> Self {
426        Self::default()
427    }
428
429    /// Record a successful test.
430    pub fn record_success(&mut self) {
431        self.tests_run += 1;
432        self.tests_passed += 1;
433    }
434
435    /// Record a failure.
436    pub fn record_failure(&mut self, error: impl Into<String>) {
437        self.tests_run += 1;
438        self.tests_failed += 1;
439        self.unique_errors.insert(error.into());
440    }
441
442    /// Record a panic.
443    pub fn record_panic(&mut self) {
444        self.tests_run += 1;
445        self.panics_caught += 1;
446    }
447
448    /// Get success rate.
449    pub fn success_rate(&self) -> f64 {
450        if self.tests_run == 0 {
451            return 1.0;
452        }
453        self.tests_passed as f64 / self.tests_run as f64
454    }
455
456    /// Print summary.
457    pub fn summary(&self) -> String {
458        format!(
459            "Fuzz Stats:\n\
460             - Tests run: {}\n\
461             - Passed: {}\n\
462             - Failed: {}\n\
463             - Panics: {}\n\
464             - Unique errors: {}\n\
465             - Success rate: {:.2}%",
466            self.tests_run,
467            self.tests_passed,
468            self.tests_failed,
469            self.panics_caught,
470            self.unique_errors.len(),
471            self.success_rate() * 100.0
472        )
473    }
474}
475
476/// Test expression operations for robustness.
477///
478/// This function applies various operations to an expression and checks
479/// that they don't panic and maintain invariants.
480pub fn fuzz_expression_operations(expr: &TLExpr) -> FuzzStats {
481    let mut stats = FuzzStats::new();
482
483    // Test free_vars
484    if std::panic::catch_unwind(|| expr.free_vars()).is_ok() {
485        stats.record_success();
486    } else {
487        stats.record_panic();
488    }
489
490    // Test all_predicates
491    if std::panic::catch_unwind(|| expr.all_predicates()).is_ok() {
492        stats.record_success();
493    } else {
494        stats.record_panic();
495    }
496
497    // Test clone + equality
498    if std::panic::catch_unwind(|| {
499        let cloned = expr.clone();
500        assert!(cloned == *expr);
501    })
502    .is_ok()
503    {
504        stats.record_success();
505    } else {
506        stats.record_panic();
507    }
508
509    // Test Debug formatting
510    if std::panic::catch_unwind(|| format!("{:?}", expr)).is_ok() {
511        stats.record_success();
512    } else {
513        stats.record_panic();
514    }
515
516    // Test serialization (serde is always enabled)
517    if std::panic::catch_unwind(|| {
518        let json = serde_json::to_string(expr).expect("serialization of TLExpr should succeed");
519        let _deserialized: TLExpr = serde_json::from_str(&json)
520            .expect("deserialization of just-serialized TLExpr should succeed");
521    })
522    .is_ok()
523    {
524        stats.record_success();
525    } else {
526        stats.record_panic();
527    }
528
529    stats
530}
531
532/// Test graph validation for robustness.
533pub fn fuzz_graph_validation(graph: &EinsumGraph) -> FuzzStats {
534    let mut stats = FuzzStats::new();
535
536    // Test validation
537    if std::panic::catch_unwind(|| graph.validate()).is_ok() {
538        stats.record_success();
539    } else {
540        stats.record_panic();
541    }
542
543    // Test clone
544    if std::panic::catch_unwind(|| {
545        let _cloned = graph.clone();
546    })
547    .is_ok()
548    {
549        stats.record_success();
550    } else {
551        stats.record_panic();
552    }
553
554    // Test Debug formatting
555    if std::panic::catch_unwind(|| format!("{:?}", graph)).is_ok() {
556        stats.record_success();
557    } else {
558        stats.record_panic();
559    }
560
561    stats
562}
563
564/// Create a deeply nested expression for stress testing.
565///
566/// Creates an expression of the form: ¬¬¬...¬P (depth negations)
567pub fn create_deep_negation(depth: usize) -> TLExpr {
568    let mut expr = TLExpr::pred("P", vec![Term::var("x")]);
569    for _ in 0..depth {
570        expr = TLExpr::negate(expr);
571    }
572    expr
573}
574
575/// Create a wide AND expression for stress testing.
576///
577/// Creates: P1 ∧ P2 ∧ P3 ∧ ... ∧ Pn
578pub fn create_wide_and(width: usize) -> TLExpr {
579    if width == 0 {
580        return TLExpr::constant(1.0);
581    }
582
583    let mut expr = TLExpr::pred(format!("P{}", 0), vec![Term::var("x")]);
584    for i in 1..width {
585        expr = TLExpr::and(expr, TLExpr::pred(format!("P{}", i), vec![Term::var("x")]));
586    }
587    expr
588}
589
590/// Create a wide OR expression for stress testing.
591pub fn create_wide_or(width: usize) -> TLExpr {
592    if width == 0 {
593        return TLExpr::constant(0.0);
594    }
595
596    let mut expr = TLExpr::pred(format!("P{}", 0), vec![Term::var("x")]);
597    for i in 1..width {
598        expr = TLExpr::or(expr, TLExpr::pred(format!("P{}", i), vec![Term::var("x")]));
599    }
600    expr
601}
602
603/// Create nested quantifiers for stress testing.
604pub fn create_nested_quantifiers(depth: usize) -> TLExpr {
605    let mut expr = TLExpr::pred("P", vec![Term::var(format!("x{}", depth))]);
606    for i in (0..depth).rev() {
607        let var = format!("x{}", i);
608        if i % 2 == 0 {
609            expr = TLExpr::exists(var, "Entity", expr);
610        } else {
611            expr = TLExpr::forall(var, "Entity", expr);
612        }
613    }
614    expr
615}
616
617/// Test edge cases for expressions.
618pub fn test_expression_edge_cases() -> FuzzStats {
619    let mut stats = FuzzStats::new();
620
621    let test_cases = vec![
622        // Empty predicate name
623        ("empty_name", TLExpr::pred("", vec![])),
624        // Zero-arity predicate
625        ("zero_arity", TLExpr::pred("P", vec![])),
626        // Large arity predicate
627        (
628            "large_arity",
629            TLExpr::pred(
630                "P",
631                (0..100).map(|i| Term::var(format!("x{}", i))).collect(),
632            ),
633        ),
634        // Extreme constants
635        ("max_float", TLExpr::constant(f64::MAX)),
636        ("min_float", TLExpr::constant(f64::MIN)),
637        ("inf", TLExpr::constant(f64::INFINITY)),
638        ("neg_inf", TLExpr::constant(f64::NEG_INFINITY)),
639        ("nan", TLExpr::constant(f64::NAN)),
640        // Deep nesting
641        ("deep_negation", create_deep_negation(100)),
642        // Wide expressions
643        ("wide_and", create_wide_and(100)),
644        ("wide_or", create_wide_or(100)),
645        // Nested quantifiers
646        ("nested_quantifiers", create_nested_quantifiers(20)),
647    ];
648
649    for (name, expr) in test_cases {
650        let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
651            let _ = expr.free_vars();
652            let _ = expr.all_predicates();
653            let _ = expr.clone();
654        }));
655
656        if result.is_ok() {
657            stats.record_success();
658        } else {
659            stats.record_failure(format!("Edge case '{}' caused panic", name));
660        }
661    }
662
663    stats
664}
665
666/// Test edge cases for graphs.
667pub fn test_graph_edge_cases() -> FuzzStats {
668    let mut stats = FuzzStats::new();
669
670    // Empty graph
671    let empty_graph = EinsumGraph::new();
672    if std::panic::catch_unwind(AssertUnwindSafe(|| empty_graph.validate())).is_ok() {
673        stats.record_success();
674    } else {
675        stats.record_failure("empty graph validation panicked");
676    }
677
678    // Graph with single tensor, no operations
679    let mut single_tensor_graph = EinsumGraph::new();
680    let t1 = single_tensor_graph.add_tensor("t1");
681    single_tensor_graph.add_output(t1).ok();
682    if std::panic::catch_unwind(AssertUnwindSafe(|| single_tensor_graph.validate())).is_ok() {
683        stats.record_success();
684    } else {
685        stats.record_failure("single tensor graph validation panicked");
686    }
687
688    // Graph with many tensors
689    let mut many_tensors_graph = EinsumGraph::new();
690    let mut tensors = Vec::new();
691    for i in 0..1000 {
692        tensors.push(many_tensors_graph.add_tensor(format!("t{}", i)));
693    }
694    if !tensors.is_empty() {
695        many_tensors_graph.add_output(tensors[0]).ok();
696    }
697    if std::panic::catch_unwind(AssertUnwindSafe(|| many_tensors_graph.validate())).is_ok() {
698        stats.record_success();
699    } else {
700        stats.record_failure("many tensors graph validation panicked");
701    }
702
703    // Graph with out-of-bounds tensor reference
704    let mut invalid_graph = EinsumGraph::new();
705    let t1 = invalid_graph.add_tensor("t1");
706    // Try to add node with invalid tensor ID (999 doesn't exist)
707    let result = invalid_graph.add_node(EinsumNode::elem_binary("add", 999, t1, t1));
708    if result.is_err() {
709        stats.record_success(); // Should fail gracefully
710    } else {
711        stats.record_failure("invalid tensor reference not caught");
712    }
713
714    stats
715}
716
717/// Check invariants for an expression.
718///
719/// Returns true if all invariants hold.
720pub fn check_expression_invariants(expr: &TLExpr) -> bool {
721    // Invariant 1: Free vars of cloned expression should be the same
722    let free_vars1 = expr.free_vars();
723    let cloned = expr.clone();
724    let free_vars2 = cloned.free_vars();
725    if free_vars1 != free_vars2 {
726        return false;
727    }
728
729    // Invariant 2: Predicates should be consistent across clones
730    let preds1 = expr.all_predicates();
731    let preds2 = cloned.all_predicates();
732    if preds1 != preds2 {
733        return false;
734    }
735
736    // Invariant 3: Equality should be reflexive
737    #[allow(clippy::eq_op)]
738    if expr != expr {
739        return false;
740    }
741
742    // Invariant 4: Clone should be equal to original
743    if expr != &cloned {
744        return false;
745    }
746
747    true
748}
749
750#[cfg(test)]
751mod tests {
752    use super::*;
753
754    #[test]
755    fn test_fuzz_expression_operations() {
756        let expr = TLExpr::pred("P", vec![Term::var("x")]);
757        let stats = fuzz_expression_operations(&expr);
758        assert!(stats.success_rate() > 0.9);
759        assert_eq!(stats.panics_caught, 0);
760    }
761
762    #[test]
763    fn test_fuzz_graph_validation() {
764        let mut graph = EinsumGraph::new();
765        let t1 = graph.add_tensor("t1");
766        graph.add_output(t1).ok();
767
768        let stats = fuzz_graph_validation(&graph);
769        assert!(stats.success_rate() > 0.9);
770        assert_eq!(stats.panics_caught, 0);
771    }
772
773    #[test]
774    fn test_deep_negation() {
775        let expr = create_deep_negation(50);
776        let stats = fuzz_expression_operations(&expr);
777        assert_eq!(stats.panics_caught, 0);
778    }
779
780    #[test]
781    fn test_wide_expressions() {
782        let and_expr = create_wide_and(50);
783        let or_expr = create_wide_or(50);
784
785        let and_stats = fuzz_expression_operations(&and_expr);
786        let or_stats = fuzz_expression_operations(&or_expr);
787
788        assert_eq!(and_stats.panics_caught, 0);
789        assert_eq!(or_stats.panics_caught, 0);
790    }
791
792    #[test]
793    fn test_nested_quantifiers() {
794        let expr = create_nested_quantifiers(10);
795        let stats = fuzz_expression_operations(&expr);
796        assert_eq!(stats.panics_caught, 0);
797    }
798
799    #[test]
800    fn test_expression_invariants() {
801        let expr = TLExpr::and(
802            TLExpr::pred("P", vec![Term::var("x")]),
803            TLExpr::pred("Q", vec![Term::var("y")]),
804        );
805
806        assert!(check_expression_invariants(&expr));
807    }
808
809    #[test]
810    fn test_stress_edge_cases_compile() {
811        // Just test that the edge case functions compile and run
812        let _ = test_expression_edge_cases();
813        let _ = test_graph_edge_cases();
814        // Success if we get here without panicking
815    }
816
817    #[test]
818    fn test_simple_rng() {
819        let mut rng = SimpleRng::new(42);
820
821        // Test determinism
822        let vals: Vec<u64> = (0..5).map(|_| rng.next_u64()).collect();
823
824        let mut rng2 = SimpleRng::new(42);
825        let vals2: Vec<u64> = (0..5).map(|_| rng2.next_u64()).collect();
826
827        assert_eq!(vals, vals2, "RNG should be deterministic with same seed");
828    }
829
830    #[test]
831    fn test_rng_gen_range() {
832        let mut rng = SimpleRng::new(123);
833
834        // Test that gen_range produces values in range
835        for _ in 0..100 {
836            let val = rng.gen_range(10);
837            assert!(val < 10, "gen_range should produce values < max");
838        }
839    }
840
841    #[test]
842    fn test_expr_generator_basic() {
843        let config = ExprGenConfig::minimal();
844        let mut gen = ExprGenerator::new(config, 42);
845
846        // Generate several expressions
847        for _ in 0..10 {
848            let expr = gen.gen();
849            // Just verify no panic and operations work
850            let _ = expr.free_vars();
851            let _ = expr.all_predicates();
852        }
853    }
854
855    #[test]
856    fn test_expr_generator_deterministic() {
857        let config = ExprGenConfig::minimal();
858
859        let mut gen1 = ExprGenerator::new(config.clone(), 42);
860        let expr1 = gen1.gen();
861
862        let mut gen2 = ExprGenerator::new(config, 42);
863        let expr2 = gen2.gen();
864
865        assert_eq!(expr1, expr2, "Same seed should produce same expression");
866    }
867
868    #[test]
869    fn test_expr_generator_stress() {
870        let config = ExprGenConfig::stress();
871        let mut gen = ExprGenerator::new(config, 12345);
872
873        // Generate complex expressions
874        for _ in 0..5 {
875            let expr = gen.gen();
876            let stats = fuzz_expression_operations(&expr);
877            assert_eq!(
878                stats.panics_caught, 0,
879                "Stress-generated expressions should not panic"
880            );
881        }
882    }
883
884    #[test]
885    fn test_mutation_negate() {
886        let mut rng = SimpleRng::new(42);
887        let expr = TLExpr::pred("P", vec![Term::var("x")]);
888
889        let mutated = mutate_expr(&expr, MutationKind::Negate, &mut rng);
890
891        match mutated {
892            TLExpr::Not { .. } => {} // Expected
893            _ => panic!("Negate should produce Not expression"),
894        }
895    }
896
897    #[test]
898    fn test_mutation_wrap_quantifiers() {
899        let mut rng = SimpleRng::new(42);
900        let expr = TLExpr::pred("P", vec![Term::var("x")]);
901
902        let exists = mutate_expr(&expr, MutationKind::WrapExists, &mut rng);
903        match exists {
904            TLExpr::Exists { .. } => {}
905            _ => panic!("WrapExists should produce Exists expression"),
906        }
907
908        let forall = mutate_expr(&expr, MutationKind::WrapForall, &mut rng);
909        match forall {
910            TLExpr::ForAll { .. } => {}
911            _ => panic!("WrapForall should produce ForAll expression"),
912        }
913    }
914
915    #[test]
916    fn test_random_mutation() {
917        let mut rng = SimpleRng::new(42);
918        let expr = TLExpr::pred("P", vec![Term::var("x")]);
919
920        // Apply random mutations
921        for _ in 0..10 {
922            let mutated = random_mutation(&expr, &mut rng);
923            // Just verify it produces valid expressions
924            let stats = fuzz_expression_operations(&mutated);
925            assert_eq!(stats.panics_caught, 0);
926        }
927    }
928
929    #[test]
930    fn test_multi_mutate() {
931        let mut rng = SimpleRng::new(42);
932        let expr = TLExpr::pred("P", vec![Term::var("x")]);
933
934        // Apply multiple mutations
935        let mutated = multi_mutate(&expr, 5, &mut rng);
936
937        // Should still be a valid expression
938        let stats = fuzz_expression_operations(&mutated);
939        assert_eq!(stats.panics_caught, 0);
940    }
941
942    #[test]
943    fn test_gen_random_graph() {
944        let config = GraphGenConfig::default();
945        let mut rng = SimpleRng::new(42);
946
947        // Generate several graphs
948        for _ in 0..10 {
949            let graph = gen_random_graph(&config, &mut rng);
950            let stats = fuzz_graph_validation(&graph);
951            assert_eq!(stats.panics_caught, 0);
952        }
953    }
954
955    #[test]
956    fn test_graph_gen_config_variations() {
957        let mut rng = SimpleRng::new(42);
958
959        // Small graph
960        let small_config = GraphGenConfig {
961            max_tensors: 3,
962            max_nodes: 2,
963            einsum_probability: 0.5,
964        };
965        let small_graph = gen_random_graph(&small_config, &mut rng);
966        assert!(small_graph.tensor_count() <= 10); // Including generated outputs
967
968        // Larger graph
969        let large_config = GraphGenConfig {
970            max_tensors: 20,
971            max_nodes: 10,
972            einsum_probability: 0.5,
973        };
974        let large_graph = gen_random_graph(&large_config, &mut rng);
975        let _ = large_graph.validate();
976    }
977
978    #[test]
979    fn test_expr_gen_config_presets() {
980        // Test minimal config
981        let minimal = ExprGenConfig::minimal();
982        assert_eq!(minimal.max_depth, 2);
983        assert_eq!(minimal.max_arity, 2);
984
985        // Test stress config
986        let stress = ExprGenConfig::stress();
987        assert_eq!(stress.max_depth, 10);
988        assert_eq!(stress.max_arity, 5);
989    }
990
991    #[test]
992    fn test_generated_expressions_have_valid_free_vars() {
993        let config = ExprGenConfig::default();
994        let mut gen = ExprGenerator::new(config, 99);
995
996        for _ in 0..20 {
997            let expr = gen.gen();
998            let free_vars = expr.free_vars();
999
1000            // All free vars should be from our variable pool
1001            for var in &free_vars {
1002                let is_valid = var.starts_with('x') || var.starts_with("mut_x");
1003                assert!(is_valid, "Free var '{}' should be from generator pool", var);
1004            }
1005        }
1006    }
1007
1008    #[test]
1009    fn test_generated_predicates_have_valid_names() {
1010        let config = ExprGenConfig::default();
1011        let mut gen = ExprGenerator::new(config, 77);
1012
1013        for _ in 0..20 {
1014            let expr = gen.gen();
1015            let predicates = expr.all_predicates();
1016
1017            // All predicates should be from our predicate pool
1018            for pred in predicates.keys() {
1019                assert!(
1020                    pred.starts_with('P'),
1021                    "Predicate '{}' should be from generator pool",
1022                    pred
1023                );
1024            }
1025        }
1026    }
1027
1028    #[test]
1029    fn test_mutation_preserves_expression_validity() {
1030        let mut rng = SimpleRng::new(555);
1031        let config = ExprGenConfig::default();
1032        let mut gen = ExprGenerator::new(config, 666);
1033
1034        // Generate expressions and mutate them
1035        for _ in 0..10 {
1036            let expr = gen.gen();
1037
1038            // Apply each mutation type
1039            for mutation in [
1040                MutationKind::Negate,
1041                MutationKind::WrapExists,
1042                MutationKind::WrapForall,
1043                MutationKind::AndWith,
1044                MutationKind::OrWith,
1045                MutationKind::Duplicate,
1046            ] {
1047                let mutated = mutate_expr(&expr, mutation, &mut rng);
1048
1049                // Verify invariants still hold
1050                assert!(
1051                    check_expression_invariants(&mutated),
1052                    "Mutation {:?} broke invariants",
1053                    mutation
1054                );
1055            }
1056        }
1057    }
1058
1059    #[test]
1060    fn test_fuzz_many_random_expressions() {
1061        let config = ExprGenConfig::default();
1062        let mut gen = ExprGenerator::new(config, 12345);
1063        let mut total_stats = FuzzStats::new();
1064
1065        // Generate and fuzz 100 expressions
1066        for _ in 0..100 {
1067            let expr = gen.gen();
1068            let stats = fuzz_expression_operations(&expr);
1069            total_stats.tests_run += stats.tests_run;
1070            total_stats.tests_passed += stats.tests_passed;
1071            total_stats.tests_failed += stats.tests_failed;
1072            total_stats.panics_caught += stats.panics_caught;
1073        }
1074
1075        assert_eq!(
1076            total_stats.panics_caught, 0,
1077            "Random expressions should not cause panics"
1078        );
1079        assert!(
1080            total_stats.success_rate() > 0.95,
1081            "Success rate should be > 95%"
1082        );
1083    }
1084
1085    #[test]
1086    fn test_rng_choose() {
1087        let mut rng = SimpleRng::new(42);
1088        let items = vec!["a", "b", "c", "d"];
1089
1090        // Choose many times
1091        let mut seen = HashSet::new();
1092        for _ in 0..100 {
1093            let item = rng.choose(&items).expect("unwrap");
1094            seen.insert(*item);
1095        }
1096
1097        // Should eventually see all items (probabilistically)
1098        assert!(seen.len() >= 2, "Should see multiple distinct items");
1099    }
1100
1101    #[test]
1102    fn test_rng_gen_bool() {
1103        let mut rng = SimpleRng::new(42);
1104
1105        // With probability 0, should always return false
1106        for _ in 0..10 {
1107            assert!(!rng.gen_bool(0.0));
1108        }
1109
1110        // With probability 1, should always return true
1111        for _ in 0..10 {
1112            assert!(rng.gen_bool(1.0));
1113        }
1114    }
1115
1116    #[test]
1117    fn test_expr_generator_specific_depth() {
1118        let config = ExprGenConfig::default();
1119        let mut gen = ExprGenerator::new(config, 42);
1120
1121        // Generate expression with depth 0 (should be atomic)
1122        let atomic = gen.gen_expr(0);
1123        match atomic {
1124            TLExpr::Pred { .. } | TLExpr::Constant { .. } => {}
1125            _ => panic!("Depth 0 should produce atomic expression"),
1126        }
1127    }
1128
1129    #[test]
1130    fn test_graph_output_is_valid() {
1131        let config = GraphGenConfig::default();
1132        let mut rng = SimpleRng::new(42);
1133
1134        for _ in 0..10 {
1135            let graph = gen_random_graph(&config, &mut rng);
1136
1137            // Should have at least one output (if tensors exist)
1138            if graph.tensor_count() > 0 {
1139                // Validate should catch invalid outputs
1140                let _ = graph.validate();
1141            }
1142        }
1143    }
1144}