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
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 pub fn gen_const(&mut self) -> Term {
169 let value = format!("c{}", self.rng.gen_range(100));
170 Term::constant(value)
171 }
172
173 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 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 pub fn gen_expr(&mut self, depth: usize) -> TLExpr {
196 if depth == 0 {
197 if self.rng.gen_bool(0.8) {
199 self.gen_predicate()
200 } else {
201 TLExpr::constant(self.rng.gen_f64())
202 }
203 } else {
204 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum MutationKind {
259 Negate,
261 WrapExists,
263 WrapForall,
265 AndWith,
267 OrWith,
269 ReplaceSubexpr,
271 Duplicate,
273}
274
275pub 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 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
308pub 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
324pub 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#[derive(Debug, Clone)]
335pub struct GraphGenConfig {
336 pub max_tensors: usize,
338 pub max_nodes: usize,
340 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
354pub fn gen_random_graph(config: &GraphGenConfig, rng: &mut SimpleRng) -> EinsumGraph {
356 let mut graph = EinsumGraph::new();
357
358 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 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 if rng.gen_bool(config.einsum_probability) && tensors.len() >= 2 {
374 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 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 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#[derive(Debug, Clone, Default)]
410pub struct FuzzStats {
411 pub tests_run: usize,
413 pub tests_passed: usize,
415 pub tests_failed: usize,
417 pub panics_caught: usize,
419 pub unique_errors: HashSet<String>,
421}
422
423impl FuzzStats {
424 pub fn new() -> Self {
426 Self::default()
427 }
428
429 pub fn record_success(&mut self) {
431 self.tests_run += 1;
432 self.tests_passed += 1;
433 }
434
435 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 pub fn record_panic(&mut self) {
444 self.tests_run += 1;
445 self.panics_caught += 1;
446 }
447
448 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 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
476pub fn fuzz_expression_operations(expr: &TLExpr) -> FuzzStats {
481 let mut stats = FuzzStats::new();
482
483 if std::panic::catch_unwind(|| expr.free_vars()).is_ok() {
485 stats.record_success();
486 } else {
487 stats.record_panic();
488 }
489
490 if std::panic::catch_unwind(|| expr.all_predicates()).is_ok() {
492 stats.record_success();
493 } else {
494 stats.record_panic();
495 }
496
497 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 if std::panic::catch_unwind(|| format!("{:?}", expr)).is_ok() {
511 stats.record_success();
512 } else {
513 stats.record_panic();
514 }
515
516 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
532pub fn fuzz_graph_validation(graph: &EinsumGraph) -> FuzzStats {
534 let mut stats = FuzzStats::new();
535
536 if std::panic::catch_unwind(|| graph.validate()).is_ok() {
538 stats.record_success();
539 } else {
540 stats.record_panic();
541 }
542
543 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 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
564pub 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
575pub 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
590pub 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
603pub 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
617pub fn test_expression_edge_cases() -> FuzzStats {
619 let mut stats = FuzzStats::new();
620
621 let test_cases = vec![
622 ("empty_name", TLExpr::pred("", vec![])),
624 ("zero_arity", TLExpr::pred("P", vec![])),
626 (
628 "large_arity",
629 TLExpr::pred(
630 "P",
631 (0..100).map(|i| Term::var(format!("x{}", i))).collect(),
632 ),
633 ),
634 ("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_negation", create_deep_negation(100)),
642 ("wide_and", create_wide_and(100)),
644 ("wide_or", create_wide_or(100)),
645 ("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
666pub fn test_graph_edge_cases() -> FuzzStats {
668 let mut stats = FuzzStats::new();
669
670 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 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 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 let mut invalid_graph = EinsumGraph::new();
705 let t1 = invalid_graph.add_tensor("t1");
706 let result = invalid_graph.add_node(EinsumNode::elem_binary("add", 999, t1, t1));
708 if result.is_err() {
709 stats.record_success(); } else {
711 stats.record_failure("invalid tensor reference not caught");
712 }
713
714 stats
715}
716
717pub fn check_expression_invariants(expr: &TLExpr) -> bool {
721 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 let preds1 = expr.all_predicates();
731 let preds2 = cloned.all_predicates();
732 if preds1 != preds2 {
733 return false;
734 }
735
736 #[allow(clippy::eq_op)]
738 if expr != expr {
739 return false;
740 }
741
742 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 let _ = test_expression_edge_cases();
813 let _ = test_graph_edge_cases();
814 }
816
817 #[test]
818 fn test_simple_rng() {
819 let mut rng = SimpleRng::new(42);
820
821 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 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 for _ in 0..10 {
848 let expr = gen.gen();
849 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 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 { .. } => {} _ => 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 for _ in 0..10 {
922 let mutated = random_mutation(&expr, &mut rng);
923 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 let mutated = multi_mutate(&expr, 5, &mut rng);
936
937 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 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 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); 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 let minimal = ExprGenConfig::minimal();
982 assert_eq!(minimal.max_depth, 2);
983 assert_eq!(minimal.max_arity, 2);
984
985 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 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 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 for _ in 0..10 {
1036 let expr = gen.gen();
1037
1038 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 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 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 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 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 for _ in 0..10 {
1107 assert!(!rng.gen_bool(0.0));
1108 }
1109
1110 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 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 if graph.tensor_count() > 0 {
1139 let _ = graph.validate();
1141 }
1142 }
1143 }
1144}