1use std::collections::HashSet;
15use std::panic::AssertUnwindSafe;
16
17use crate::{EinsumGraph, EinsumNode, TLExpr, Term};
18
19#[derive(Debug, Clone)]
21pub struct ExprGenConfig {
22 pub max_depth: usize,
24 pub max_arity: usize,
26 pub max_vars: usize,
28 pub max_predicates: usize,
30 pub quantifier_probability: f64,
32 pub arithmetic_probability: f64,
34 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 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 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#[derive(Debug, Clone)]
88pub struct SimpleRng {
89 state: u64,
90}
91
92impl SimpleRng {
93 pub fn new(seed: u64) -> Self {
95 Self { state: seed }
96 }
97
98 pub fn next_u64(&mut self) -> u64 {
100 self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345);
102 self.state
103 }
104
105 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 pub fn gen_f64(&mut self) -> f64 {
115 (self.next_u64() as f64) / (u64::MAX as f64)
116 }
117
118 pub fn gen_bool(&mut self, probability: f64) -> bool {
120 self.gen_f64() < probability
121 }
122
123 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
133pub struct ExprGenerator {
135 config: ExprGenConfig,
136 rng: SimpleRng,
137 var_names: Vec<String>,
138 pred_names: Vec<String>,
139}
140
141impl ExprGenerator {
142 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 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 pub fn gen_const(&mut self) -> Term {
165 let value = format!("c{}", self.rng.gen_range(100));
166 Term::constant(value)
167 }
168
169 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 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 pub fn gen_expr(&mut self, depth: usize) -> TLExpr {
188 if depth == 0 {
189 if self.rng.gen_bool(0.8) {
191 self.gen_predicate()
192 } else {
193 TLExpr::constant(self.rng.gen_f64())
194 }
195 } else {
196 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum MutationKind {
235 Negate,
237 WrapExists,
239 WrapForall,
241 AndWith,
243 OrWith,
245 ReplaceSubexpr,
247 Duplicate,
249}
250
251pub 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 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
284pub 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
298pub 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#[derive(Debug, Clone)]
309pub struct GraphGenConfig {
310 pub max_tensors: usize,
312 pub max_nodes: usize,
314 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
328pub fn gen_random_graph(config: &GraphGenConfig, rng: &mut SimpleRng) -> EinsumGraph {
330 let mut graph = EinsumGraph::new();
331
332 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 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 if rng.gen_bool(config.einsum_probability) && tensors.len() >= 2 {
348 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 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 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#[derive(Debug, Clone, Default)]
382pub struct FuzzStats {
383 pub tests_run: usize,
385 pub tests_passed: usize,
387 pub tests_failed: usize,
389 pub panics_caught: usize,
391 pub unique_errors: HashSet<String>,
393}
394
395impl FuzzStats {
396 pub fn new() -> Self {
398 Self::default()
399 }
400
401 pub fn record_success(&mut self) {
403 self.tests_run += 1;
404 self.tests_passed += 1;
405 }
406
407 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 pub fn record_panic(&mut self) {
416 self.tests_run += 1;
417 self.panics_caught += 1;
418 }
419
420 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 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
448pub fn fuzz_expression_operations(expr: &TLExpr) -> FuzzStats {
453 let mut stats = FuzzStats::new();
454
455 if std::panic::catch_unwind(|| expr.free_vars()).is_ok() {
457 stats.record_success();
458 } else {
459 stats.record_panic();
460 }
461
462 if std::panic::catch_unwind(|| expr.all_predicates()).is_ok() {
464 stats.record_success();
465 } else {
466 stats.record_panic();
467 }
468
469 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 if std::panic::catch_unwind(|| format!("{:?}", expr)).is_ok() {
483 stats.record_success();
484 } else {
485 stats.record_panic();
486 }
487
488 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
503pub fn fuzz_graph_validation(graph: &EinsumGraph) -> FuzzStats {
505 let mut stats = FuzzStats::new();
506
507 if std::panic::catch_unwind(|| graph.validate()).is_ok() {
509 stats.record_success();
510 } else {
511 stats.record_panic();
512 }
513
514 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 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
535pub 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
546pub 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
561pub 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
574pub 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
588pub fn test_expression_edge_cases() -> FuzzStats {
590 let mut stats = FuzzStats::new();
591
592 let test_cases = vec![
593 ("empty_name", TLExpr::pred("", vec![])),
595 ("zero_arity", TLExpr::pred("P", vec![])),
597 (
599 "large_arity",
600 TLExpr::pred(
601 "P",
602 (0..100).map(|i| Term::var(format!("x{}", i))).collect(),
603 ),
604 ),
605 ("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_negation", create_deep_negation(100)),
613 ("wide_and", create_wide_and(100)),
615 ("wide_or", create_wide_or(100)),
616 ("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
637pub fn test_graph_edge_cases() -> FuzzStats {
639 let mut stats = FuzzStats::new();
640
641 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 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 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 let mut invalid_graph = EinsumGraph::new();
676 let t1 = invalid_graph.add_tensor("t1");
677 let result = invalid_graph.add_node(EinsumNode::elem_binary("add", 999, t1, t1));
679 if result.is_err() {
680 stats.record_success(); } else {
682 stats.record_failure("invalid tensor reference not caught");
683 }
684
685 stats
686}
687
688pub fn check_expression_invariants(expr: &TLExpr) -> bool {
692 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 let preds1 = expr.all_predicates();
702 let preds2 = cloned.all_predicates();
703 if preds1 != preds2 {
704 return false;
705 }
706
707 #[allow(clippy::eq_op)]
709 if expr != expr {
710 return false;
711 }
712
713 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 let _ = test_expression_edge_cases();
784 let _ = test_graph_edge_cases();
785 }
787
788 #[test]
789 fn test_simple_rng() {
790 let mut rng = SimpleRng::new(42);
791
792 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 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 for _ in 0..10 {
819 let expr = gen.gen();
820 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 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 { .. } => {} _ => 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 for _ in 0..10 {
893 let mutated = random_mutation(&expr, &mut rng);
894 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 let mutated = multi_mutate(&expr, 5, &mut rng);
907
908 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 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 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); 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 let minimal = ExprGenConfig::minimal();
953 assert_eq!(minimal.max_depth, 2);
954 assert_eq!(minimal.max_arity, 2);
955
956 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 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 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 for _ in 0..10 {
1007 let expr = gen.gen();
1008
1009 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 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 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 let mut seen = HashSet::new();
1063 for _ in 0..100 {
1064 let item = rng.choose(&items).unwrap();
1065 seen.insert(*item);
1066 }
1067
1068 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 for _ in 0..10 {
1078 assert!(!rng.gen_bool(0.0));
1079 }
1080
1081 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 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 if graph.tensor_count() > 0 {
1110 let _ = graph.validate();
1112 }
1113 }
1114 }
1115}