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