1use std::collections::HashMap;
18use std::path::PathBuf;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum ScoringMode {
23 Quick,
25 #[default]
27 Fast,
28 Full,
30}
31
32#[derive(Debug, Clone, Default)]
34pub struct SingleShotScore {
35 pub total: u8,
37 pub compilation: u8,
39 pub type_inference: u8,
41 pub test_coverage: u8,
43 pub code_quality: u8,
45 pub semantic_equivalence: u8,
47 pub gateway_passed: bool,
49 pub mode: ScoringMode,
51}
52
53#[derive(Debug, Clone, Default)]
55pub struct CategoryBreakdown {
56 pub a1_parse: u8,
59 pub a2_type_check: u8,
61 pub a3_cargo_build: u8,
63
64 pub b1_no_e0308: u8,
67 pub b2_no_e0599: u8,
69 pub b3_no_e0425: u8,
71
72 pub c1_doctest: u8,
75 pub c2_unit_test: u8,
77 pub c3_property_test: u8,
79
80 pub d1_clippy: u8,
83 pub d2_tdg: u8,
85 pub d3_complexity: u8,
87
88 pub e1_trace_match: u8,
91 pub e2_output_equiv: u8,
93}
94
95#[derive(Debug, Clone)]
97pub struct CompilationError {
98 pub code: String,
100 pub message: String,
102 pub location: Option<String>,
104 pub line: Option<u32>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Hash)]
110pub enum TranspilerDecision {
111 TypeInference {
113 variable: String,
114 inferred_type: String,
115 },
116 MethodTranslation {
118 python_method: String,
119 rust_method: String,
120 },
121 ImportMapping {
123 python_import: String,
124 rust_import: String,
125 },
126 ValueFallback { context: String },
128 Other(String),
130}
131
132#[derive(Debug, Clone)]
134pub struct SingleShotResult {
135 pub file_path: PathBuf,
137 pub score: SingleShotScore,
139 pub category_breakdown: CategoryBreakdown,
141 pub error_details: Vec<CompilationError>,
143 pub transpiler_decisions: Vec<TranspilerDecision>,
145}
146
147#[derive(Debug, Clone)]
149pub struct ScoringConfig {
150 pub gateway_threshold: f32,
152 pub weights: CategoryWeights,
154 pub enable_semantic_check: bool,
156 pub oracle_feedback: bool,
158}
159
160impl Default for ScoringConfig {
161 fn default() -> Self {
162 Self {
163 gateway_threshold: 0.6,
164 weights: CategoryWeights::default(),
165 enable_semantic_check: true,
166 oracle_feedback: true,
167 }
168 }
169}
170
171#[derive(Debug, Clone)]
173pub struct CategoryWeights {
174 pub compilation: f32,
176 pub type_inference: f32,
178 pub test_coverage: f32,
180 pub code_quality: f32,
182 pub semantic_equiv: f32,
184}
185
186impl Default for CategoryWeights {
187 fn default() -> Self {
188 Self {
189 compilation: 0.40,
190 type_inference: 0.25,
191 test_coverage: 0.15,
192 code_quality: 0.10,
193 semantic_equiv: 0.10,
194 }
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
200pub enum OutputFormat {
201 #[default]
203 Human,
204 Json,
206 Parquet,
208 Markdown,
210}
211
212#[derive(Debug, Clone)]
214pub struct CorpusScoreReport {
215 pub results: Vec<SingleShotResult>,
217 pub aggregate_score: f32,
219 pub grade: Grade,
221 pub category_averages: CategoryBreakdown,
223 pub top_blockers: Vec<Blocker>,
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub enum Grade {
230 APlus, A, AMinus, BPlus, B, C, D, F, }
239
240impl Grade {
241 pub fn from_score(score: f32) -> Self {
243 match score as u8 {
244 95..=100 => Grade::APlus,
245 90..=94 => Grade::A,
246 85..=89 => Grade::AMinus,
247 80..=84 => Grade::BPlus,
248 70..=79 => Grade::B,
249 60..=69 => Grade::C,
250 50..=59 => Grade::D,
251 _ => Grade::F,
252 }
253 }
254
255 pub fn as_str(&self) -> &'static str {
257 match self {
258 Grade::APlus => "A+",
259 Grade::A => "A",
260 Grade::AMinus => "A-",
261 Grade::BPlus => "B+",
262 Grade::B => "B",
263 Grade::C => "C",
264 Grade::D => "D",
265 Grade::F => "F",
266 }
267 }
268}
269
270#[derive(Debug, Clone)]
272pub struct Blocker {
273 pub pattern: String,
275 pub affected_files: usize,
277 pub avg_points_lost: f32,
279}
280
281#[derive(Debug, Clone, Default)]
283pub struct BreakdownInput<'a> {
284 pub parse_ok: bool,
286 pub type_check_ok: bool,
288 pub build_ok: bool,
290 pub errors: &'a [CompilationError],
292 pub doctest_pass: bool,
294 pub unit_test_pass: bool,
296 pub property_test_pass: bool,
298 pub clippy_clean: bool,
300 pub tdg_grade_b_or_better: bool,
302 pub complexity_ok: bool,
304 pub trace_match: bool,
306 pub output_equiv: bool,
308}
309
310pub struct ScoreCalculator {
312 config: ScoringConfig,
313}
314
315impl ScoreCalculator {
316 pub fn new() -> Self {
318 Self {
319 config: ScoringConfig::default(),
320 }
321 }
322
323 pub fn with_config(config: ScoringConfig) -> Self {
325 Self { config }
326 }
327
328 pub fn calculate(&self, breakdown: &CategoryBreakdown, mode: ScoringMode) -> SingleShotScore {
330 let compilation = breakdown.a1_parse + breakdown.a2_type_check + breakdown.a3_cargo_build;
332 let type_inference = breakdown.b1_no_e0308 + breakdown.b2_no_e0599 + breakdown.b3_no_e0425;
333 let test_coverage =
334 breakdown.c1_doctest + breakdown.c2_unit_test + breakdown.c3_property_test;
335 let code_quality = breakdown.d1_clippy + breakdown.d2_tdg + breakdown.d3_complexity;
336 let semantic_equivalence = breakdown.e1_trace_match + breakdown.e2_output_equiv;
337
338 let gateway_threshold = (40.0 * self.config.gateway_threshold) as u8; let gateway_passed = compilation >= gateway_threshold;
341
342 let total = if gateway_passed {
344 compilation + type_inference + test_coverage + code_quality + semantic_equivalence
345 } else {
346 0
347 };
348
349 SingleShotScore {
350 total,
351 compilation,
352 type_inference,
353 test_coverage,
354 code_quality,
355 semantic_equivalence,
356 gateway_passed,
357 mode,
358 }
359 }
360
361 pub fn breakdown_from_errors(&self, input: &BreakdownInput<'_>) -> CategoryBreakdown {
363 let e0308_count = input.errors.iter().filter(|e| e.code == "E0308").count();
365 let e0599_count = input.errors.iter().filter(|e| e.code == "E0599").count();
366 let e0425_count = input.errors.iter().filter(|e| e.code == "E0425").count();
367 let total_errors = input.errors.len().max(1); let e0308_ratio = e0308_count as f32 / total_errors as f32;
371 let e0599_ratio = e0599_count as f32 / total_errors as f32;
372 let e0425_ratio = e0425_count as f32 / total_errors as f32;
373
374 CategoryBreakdown {
375 a1_parse: if input.parse_ok { 10 } else { 0 },
377 a2_type_check: if input.type_check_ok { 15 } else { 0 },
378 a3_cargo_build: if input.build_ok { 15 } else { 0 },
379
380 b1_no_e0308: ((1.0 - e0308_ratio) * 10.0) as u8,
382 b2_no_e0599: ((1.0 - e0599_ratio) * 8.0) as u8,
383 b3_no_e0425: ((1.0 - e0425_ratio) * 7.0) as u8,
384
385 c1_doctest: if input.doctest_pass { 5 } else { 0 },
387 c2_unit_test: if input.unit_test_pass { 5 } else { 0 },
388 c3_property_test: if input.property_test_pass { 5 } else { 0 },
389
390 d1_clippy: if input.clippy_clean { 5 } else { 0 },
392 d2_tdg: if input.tdg_grade_b_or_better { 3 } else { 0 },
393 d3_complexity: if input.complexity_ok { 2 } else { 0 },
394
395 e1_trace_match: if input.trace_match { 5 } else { 0 },
397 e2_output_equiv: if input.output_equiv { 5 } else { 0 },
398 }
399 }
400
401 pub fn aggregate(&self, results: &[SingleShotResult]) -> CorpusScoreReport {
403 if results.is_empty() {
404 return CorpusScoreReport {
405 results: vec![],
406 aggregate_score: 0.0,
407 grade: Grade::F,
408 category_averages: CategoryBreakdown::default(),
409 top_blockers: vec![],
410 };
411 }
412
413 let n = results.len() as f32;
414
415 let aggregate_score: f32 = results.iter().map(|r| r.score.total as f32).sum::<f32>() / n;
417
418 let category_averages = CategoryBreakdown {
419 a1_parse: (results
420 .iter()
421 .map(|r| r.category_breakdown.a1_parse as f32)
422 .sum::<f32>()
423 / n) as u8,
424 a2_type_check: (results
425 .iter()
426 .map(|r| r.category_breakdown.a2_type_check as f32)
427 .sum::<f32>()
428 / n) as u8,
429 a3_cargo_build: (results
430 .iter()
431 .map(|r| r.category_breakdown.a3_cargo_build as f32)
432 .sum::<f32>()
433 / n) as u8,
434 b1_no_e0308: (results
435 .iter()
436 .map(|r| r.category_breakdown.b1_no_e0308 as f32)
437 .sum::<f32>()
438 / n) as u8,
439 b2_no_e0599: (results
440 .iter()
441 .map(|r| r.category_breakdown.b2_no_e0599 as f32)
442 .sum::<f32>()
443 / n) as u8,
444 b3_no_e0425: (results
445 .iter()
446 .map(|r| r.category_breakdown.b3_no_e0425 as f32)
447 .sum::<f32>()
448 / n) as u8,
449 c1_doctest: (results
450 .iter()
451 .map(|r| r.category_breakdown.c1_doctest as f32)
452 .sum::<f32>()
453 / n) as u8,
454 c2_unit_test: (results
455 .iter()
456 .map(|r| r.category_breakdown.c2_unit_test as f32)
457 .sum::<f32>()
458 / n) as u8,
459 c3_property_test: (results
460 .iter()
461 .map(|r| r.category_breakdown.c3_property_test as f32)
462 .sum::<f32>()
463 / n) as u8,
464 d1_clippy: (results
465 .iter()
466 .map(|r| r.category_breakdown.d1_clippy as f32)
467 .sum::<f32>()
468 / n) as u8,
469 d2_tdg: (results
470 .iter()
471 .map(|r| r.category_breakdown.d2_tdg as f32)
472 .sum::<f32>()
473 / n) as u8,
474 d3_complexity: (results
475 .iter()
476 .map(|r| r.category_breakdown.d3_complexity as f32)
477 .sum::<f32>()
478 / n) as u8,
479 e1_trace_match: (results
480 .iter()
481 .map(|r| r.category_breakdown.e1_trace_match as f32)
482 .sum::<f32>()
483 / n) as u8,
484 e2_output_equiv: (results
485 .iter()
486 .map(|r| r.category_breakdown.e2_output_equiv as f32)
487 .sum::<f32>()
488 / n) as u8,
489 };
490
491 let mut error_counts: HashMap<String, (usize, f32)> = HashMap::new();
493 for result in results {
494 for error in &result.error_details {
495 let entry = error_counts.entry(error.code.clone()).or_insert((0, 0.0));
496 entry.0 += 1;
497 entry.1 += 100.0 - result.score.total as f32;
498 }
499 }
500
501 let mut top_blockers: Vec<Blocker> = error_counts
502 .into_iter()
503 .map(|(code, (count, total_lost))| Blocker {
504 pattern: code,
505 affected_files: count,
506 avg_points_lost: total_lost / count as f32,
507 })
508 .collect();
509
510 top_blockers.sort_by(|a, b| b.affected_files.cmp(&a.affected_files));
511 top_blockers.truncate(5);
512
513 CorpusScoreReport {
514 results: results.to_vec(),
515 aggregate_score,
516 grade: Grade::from_score(aggregate_score),
517 category_averages,
518 top_blockers,
519 }
520 }
521}
522
523impl Default for ScoreCalculator {
524 fn default() -> Self {
525 Self::new()
526 }
527}
528
529#[derive(Debug, Clone)]
531pub struct TarantulaScore {
532 pub suspiciousness: f32,
534 pub failed_count: usize,
536 pub passed_count: usize,
538}
539
540#[derive(Debug, Clone, Default)]
542pub struct DecisionStats {
543 pub failed_count: usize,
544 pub passed_count: usize,
545}
546
547impl DecisionStats {
548 pub fn tarantula_score(&self, total_failed: usize, total_passed: usize) -> TarantulaScore {
550 let failed_ratio = if total_failed > 0 {
551 self.failed_count as f32 / total_failed as f32
552 } else {
553 0.0
554 };
555
556 let passed_ratio = if total_passed > 0 {
557 self.passed_count as f32 / total_passed as f32
558 } else {
559 0.0
560 };
561
562 let suspiciousness = if failed_ratio + passed_ratio > 0.0 {
563 failed_ratio / (failed_ratio + passed_ratio)
564 } else {
565 0.0
566 };
567
568 TarantulaScore {
569 suspiciousness,
570 failed_count: self.failed_count,
571 passed_count: self.passed_count,
572 }
573 }
574}
575
576pub fn analyze_score_failures(
578 results: &[SingleShotResult],
579) -> HashMap<TranspilerDecision, TarantulaScore> {
580 let mut stats: HashMap<TranspilerDecision, DecisionStats> = HashMap::new();
581 let mut total_failed = 0;
582 let mut total_passed = 0;
583
584 for result in results {
585 let failed = result.score.total < 80;
586 if failed {
587 total_failed += 1;
588 } else {
589 total_passed += 1;
590 }
591
592 for decision in &result.transpiler_decisions {
593 let entry = stats.entry(decision.clone()).or_default();
594 if failed {
595 entry.failed_count += 1;
596 } else {
597 entry.passed_count += 1;
598 }
599 }
600 }
601
602 stats
603 .into_iter()
604 .map(|(d, s)| (d, s.tarantula_score(total_failed, total_passed)))
605 .collect()
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611
612 #[test]
615 fn test_scoring_mode_default() {
616 let mode = ScoringMode::default();
617 assert_eq!(mode, ScoringMode::Fast);
618 }
619
620 #[test]
621 fn test_scoring_mode_quick() {
622 let mode = ScoringMode::Quick;
623 assert_eq!(mode, ScoringMode::Quick);
624 }
625
626 #[test]
627 fn test_scoring_mode_full() {
628 let mode = ScoringMode::Full;
629 assert_eq!(mode, ScoringMode::Full);
630 }
631
632 #[test]
633 fn test_scoring_mode_clone() {
634 let mode = ScoringMode::Fast;
635 let cloned = mode;
636 assert_eq!(cloned, ScoringMode::Fast);
637 }
638
639 #[test]
640 fn test_scoring_mode_debug() {
641 let debug = format!("{:?}", ScoringMode::Quick);
642 assert!(debug.contains("Quick"));
643 }
644
645 #[test]
648 fn test_single_shot_score_default() {
649 let score = SingleShotScore::default();
650 assert_eq!(score.total, 0);
651 assert_eq!(score.compilation, 0);
652 assert!(!score.gateway_passed);
653 }
654
655 #[test]
656 fn test_single_shot_score_clone() {
657 let score = SingleShotScore {
658 total: 75,
659 compilation: 35,
660 type_inference: 20,
661 test_coverage: 10,
662 code_quality: 5,
663 semantic_equivalence: 5,
664 gateway_passed: true,
665 mode: ScoringMode::Fast,
666 };
667 let cloned = score.clone();
668 assert_eq!(cloned.total, 75);
669 assert_eq!(cloned.compilation, 35);
670 }
671
672 #[test]
673 fn test_single_shot_score_debug() {
674 let score = SingleShotScore::default();
675 let debug = format!("{:?}", score);
676 assert!(debug.contains("SingleShotScore"));
677 }
678
679 #[test]
682 fn test_category_breakdown_default() {
683 let breakdown = CategoryBreakdown::default();
684 assert_eq!(breakdown.a1_parse, 0);
685 assert_eq!(breakdown.a2_type_check, 0);
686 assert_eq!(breakdown.b1_no_e0308, 0);
687 }
688
689 #[test]
690 fn test_category_breakdown_clone() {
691 let breakdown = CategoryBreakdown {
692 a1_parse: 10,
693 a2_type_check: 15,
694 ..Default::default()
695 };
696 let cloned = breakdown.clone();
697 assert_eq!(cloned.a1_parse, 10);
698 assert_eq!(cloned.a2_type_check, 15);
699 }
700
701 #[test]
702 fn test_category_breakdown_debug() {
703 let breakdown = CategoryBreakdown::default();
704 let debug = format!("{:?}", breakdown);
705 assert!(debug.contains("CategoryBreakdown"));
706 }
707
708 #[test]
711 fn test_compilation_error_fields() {
712 let error = CompilationError {
713 code: "E0308".to_string(),
714 message: "mismatched types".to_string(),
715 location: Some("src/lib.rs".to_string()),
716 line: Some(42),
717 };
718 assert_eq!(error.code, "E0308");
719 assert_eq!(error.message, "mismatched types");
720 assert_eq!(error.location, Some("src/lib.rs".to_string()));
721 assert_eq!(error.line, Some(42));
722 }
723
724 #[test]
725 fn test_compilation_error_none_fields() {
726 let error = CompilationError {
727 code: "E0001".to_string(),
728 message: "error".to_string(),
729 location: None,
730 line: None,
731 };
732 assert!(error.location.is_none());
733 assert!(error.line.is_none());
734 }
735
736 #[test]
737 fn test_compilation_error_clone() {
738 let error = CompilationError {
739 code: "E0308".to_string(),
740 message: "test".to_string(),
741 location: None,
742 line: None,
743 };
744 let cloned = error.clone();
745 assert_eq!(cloned.code, error.code);
746 }
747
748 #[test]
749 fn test_compilation_error_debug() {
750 let error = CompilationError {
751 code: "E0308".to_string(),
752 message: "test".to_string(),
753 location: None,
754 line: None,
755 };
756 let debug = format!("{:?}", error);
757 assert!(debug.contains("CompilationError"));
758 }
759
760 #[test]
763 fn test_transpiler_decision_type_inference() {
764 let decision = TranspilerDecision::TypeInference {
765 variable: "x".to_string(),
766 inferred_type: "i32".to_string(),
767 };
768 assert!(matches!(decision, TranspilerDecision::TypeInference { .. }));
769 }
770
771 #[test]
772 fn test_transpiler_decision_method_translation() {
773 let decision = TranspilerDecision::MethodTranslation {
774 python_method: "append".to_string(),
775 rust_method: "push".to_string(),
776 };
777 assert!(matches!(
778 decision,
779 TranspilerDecision::MethodTranslation { .. }
780 ));
781 }
782
783 #[test]
784 fn test_transpiler_decision_import_mapping() {
785 let decision = TranspilerDecision::ImportMapping {
786 python_import: "json".to_string(),
787 rust_import: "serde_json".to_string(),
788 };
789 assert!(matches!(decision, TranspilerDecision::ImportMapping { .. }));
790 }
791
792 #[test]
793 fn test_transpiler_decision_value_fallback() {
794 let decision = TranspilerDecision::ValueFallback {
795 context: "unknown type".to_string(),
796 };
797 assert!(matches!(decision, TranspilerDecision::ValueFallback { .. }));
798 }
799
800 #[test]
801 fn test_transpiler_decision_other() {
802 let decision = TranspilerDecision::Other("custom decision".to_string());
803 assert!(matches!(decision, TranspilerDecision::Other(_)));
804 }
805
806 #[test]
807 fn test_transpiler_decision_clone() {
808 let decision = TranspilerDecision::TypeInference {
809 variable: "x".to_string(),
810 inferred_type: "i32".to_string(),
811 };
812 let cloned = decision.clone();
813 assert_eq!(cloned, decision);
814 }
815
816 #[test]
817 fn test_transpiler_decision_hash() {
818 use std::collections::HashSet;
819 let mut set = HashSet::new();
820 let d1 = TranspilerDecision::Other("a".to_string());
821 let d2 = TranspilerDecision::Other("b".to_string());
822 set.insert(d1.clone());
823 set.insert(d2.clone());
824 assert_eq!(set.len(), 2);
825 assert!(set.contains(&d1));
826 }
827
828 #[test]
831 fn test_scoring_config_default() {
832 let config = ScoringConfig::default();
833 assert!((config.gateway_threshold - 0.6).abs() < f32::EPSILON);
834 assert!(config.enable_semantic_check);
835 assert!(config.oracle_feedback);
836 }
837
838 #[test]
839 fn test_scoring_config_clone() {
840 let config = ScoringConfig::default();
841 let cloned = config.clone();
842 assert_eq!(cloned.gateway_threshold, config.gateway_threshold);
843 }
844
845 #[test]
846 fn test_scoring_config_debug() {
847 let config = ScoringConfig::default();
848 let debug = format!("{:?}", config);
849 assert!(debug.contains("ScoringConfig"));
850 }
851
852 #[test]
855 fn test_category_weights_default() {
856 let weights = CategoryWeights::default();
857 assert!((weights.compilation - 0.40).abs() < f32::EPSILON);
858 assert!((weights.type_inference - 0.25).abs() < f32::EPSILON);
859 assert!((weights.test_coverage - 0.15).abs() < f32::EPSILON);
860 assert!((weights.code_quality - 0.10).abs() < f32::EPSILON);
861 assert!((weights.semantic_equiv - 0.10).abs() < f32::EPSILON);
862 }
863
864 #[test]
865 fn test_category_weights_sum_to_one() {
866 let weights = CategoryWeights::default();
867 let sum = weights.compilation
868 + weights.type_inference
869 + weights.test_coverage
870 + weights.code_quality
871 + weights.semantic_equiv;
872 assert!((sum - 1.0).abs() < f32::EPSILON);
873 }
874
875 #[test]
876 fn test_category_weights_clone() {
877 let weights = CategoryWeights::default();
878 let cloned = weights.clone();
879 assert_eq!(cloned.compilation, weights.compilation);
880 }
881
882 #[test]
885 fn test_output_format_default() {
886 let format = OutputFormat::default();
887 assert_eq!(format, OutputFormat::Human);
888 }
889
890 #[test]
891 fn test_output_format_json() {
892 let format = OutputFormat::Json;
893 assert_eq!(format, OutputFormat::Json);
894 }
895
896 #[test]
897 fn test_output_format_parquet() {
898 let format = OutputFormat::Parquet;
899 assert_eq!(format, OutputFormat::Parquet);
900 }
901
902 #[test]
903 fn test_output_format_markdown() {
904 let format = OutputFormat::Markdown;
905 assert_eq!(format, OutputFormat::Markdown);
906 }
907
908 #[test]
909 fn test_output_format_debug() {
910 let debug = format!("{:?}", OutputFormat::Json);
911 assert!(debug.contains("Json"));
912 }
913
914 #[test]
917 fn test_grade_as_str() {
918 assert_eq!(Grade::APlus.as_str(), "A+");
919 assert_eq!(Grade::A.as_str(), "A");
920 assert_eq!(Grade::AMinus.as_str(), "A-");
921 assert_eq!(Grade::BPlus.as_str(), "B+");
922 assert_eq!(Grade::B.as_str(), "B");
923 assert_eq!(Grade::C.as_str(), "C");
924 assert_eq!(Grade::D.as_str(), "D");
925 assert_eq!(Grade::F.as_str(), "F");
926 }
927
928 #[test]
929 fn test_grade_clone() {
930 let grade = Grade::APlus;
931 let cloned = grade;
932 assert_eq!(cloned, Grade::APlus);
933 }
934
935 #[test]
936 fn test_grade_debug() {
937 let debug = format!("{:?}", Grade::APlus);
938 assert!(debug.contains("APlus"));
939 }
940
941 #[test]
942 fn test_grade_eq() {
943 assert_eq!(Grade::A, Grade::A);
944 assert_ne!(Grade::A, Grade::B);
945 }
946
947 #[test]
950 fn test_blocker_fields() {
951 let blocker = Blocker {
952 pattern: "E0308".to_string(),
953 affected_files: 5,
954 avg_points_lost: 15.5,
955 };
956 assert_eq!(blocker.pattern, "E0308");
957 assert_eq!(blocker.affected_files, 5);
958 assert!((blocker.avg_points_lost - 15.5).abs() < f32::EPSILON);
959 }
960
961 #[test]
962 fn test_blocker_clone() {
963 let blocker = Blocker {
964 pattern: "test".to_string(),
965 affected_files: 3,
966 avg_points_lost: 10.0,
967 };
968 let cloned = blocker.clone();
969 assert_eq!(cloned.pattern, blocker.pattern);
970 }
971
972 #[test]
973 fn test_blocker_debug() {
974 let blocker = Blocker {
975 pattern: "test".to_string(),
976 affected_files: 1,
977 avg_points_lost: 5.0,
978 };
979 let debug = format!("{:?}", blocker);
980 assert!(debug.contains("Blocker"));
981 }
982
983 #[test]
986 fn test_breakdown_input_default() {
987 let input = BreakdownInput::default();
988 assert!(!input.parse_ok);
989 assert!(!input.type_check_ok);
990 assert!(!input.build_ok);
991 }
992
993 #[test]
994 fn test_breakdown_input_clone() {
995 let empty: Vec<CompilationError> = vec![];
996 let input = BreakdownInput {
997 parse_ok: true,
998 type_check_ok: true,
999 build_ok: false,
1000 errors: &empty,
1001 ..Default::default()
1002 };
1003 let cloned = input.clone();
1004 assert_eq!(cloned.parse_ok, input.parse_ok);
1005 }
1006
1007 #[test]
1008 fn test_breakdown_input_debug() {
1009 let input = BreakdownInput::default();
1010 let debug = format!("{:?}", input);
1011 assert!(debug.contains("BreakdownInput"));
1012 }
1013
1014 #[test]
1017 fn test_score_calculator_new() {
1018 let calc = ScoreCalculator::new();
1019 assert!((calc.config.gateway_threshold - 0.6).abs() < f32::EPSILON);
1020 }
1021
1022 #[test]
1023 fn test_score_calculator_default() {
1024 let calc = ScoreCalculator::default();
1025 assert!((calc.config.gateway_threshold - 0.6).abs() < f32::EPSILON);
1026 }
1027
1028 #[test]
1029 fn test_score_calculator_with_config() {
1030 let config = ScoringConfig {
1031 gateway_threshold: 0.8,
1032 ..Default::default()
1033 };
1034 let calc = ScoreCalculator::with_config(config);
1035 assert!((calc.config.gateway_threshold - 0.8).abs() < f32::EPSILON);
1036 }
1037
1038 #[test]
1039 fn test_score_calculation_perfect() {
1040 let calculator = ScoreCalculator::new();
1041 let breakdown = CategoryBreakdown {
1042 a1_parse: 10,
1043 a2_type_check: 15,
1044 a3_cargo_build: 15,
1045 b1_no_e0308: 10,
1046 b2_no_e0599: 8,
1047 b3_no_e0425: 7,
1048 c1_doctest: 5,
1049 c2_unit_test: 5,
1050 c3_property_test: 5,
1051 d1_clippy: 5,
1052 d2_tdg: 3,
1053 d3_complexity: 2,
1054 e1_trace_match: 5,
1055 e2_output_equiv: 5,
1056 };
1057
1058 let score = calculator.calculate(&breakdown, ScoringMode::Full);
1059
1060 assert_eq!(score.total, 100);
1061 assert_eq!(score.compilation, 40);
1062 assert_eq!(score.type_inference, 25);
1063 assert_eq!(score.test_coverage, 15);
1064 assert_eq!(score.code_quality, 10);
1065 assert_eq!(score.semantic_equivalence, 10);
1066 assert!(score.gateway_passed);
1067 }
1068
1069 #[test]
1070 fn test_gateway_blocks_when_compilation_fails() {
1071 let calculator = ScoreCalculator::new();
1072 let breakdown = CategoryBreakdown {
1073 a1_parse: 10,
1074 a2_type_check: 5, a3_cargo_build: 0, b1_no_e0308: 10,
1077 b2_no_e0599: 8,
1078 b3_no_e0425: 7,
1079 c1_doctest: 5,
1080 c2_unit_test: 5,
1081 c3_property_test: 5,
1082 d1_clippy: 5,
1083 d2_tdg: 3,
1084 d3_complexity: 2,
1085 e1_trace_match: 5,
1086 e2_output_equiv: 5,
1087 };
1088
1089 let score = calculator.calculate(&breakdown, ScoringMode::Full);
1090
1091 assert_eq!(score.compilation, 15);
1094 assert!(!score.gateway_passed);
1095 assert_eq!(score.total, 0); }
1097
1098 #[test]
1099 fn test_gateway_passes_at_threshold() {
1100 let calculator = ScoreCalculator::new();
1101 let breakdown = CategoryBreakdown {
1102 a1_parse: 10,
1103 a2_type_check: 14, a3_cargo_build: 0, ..Default::default()
1106 };
1107
1108 let score = calculator.calculate(&breakdown, ScoringMode::Quick);
1109
1110 assert_eq!(score.compilation, 24);
1112 assert!(score.gateway_passed);
1113 }
1114
1115 #[test]
1116 fn test_grade_mapping() {
1117 assert_eq!(Grade::from_score(100.0), Grade::APlus);
1118 assert_eq!(Grade::from_score(95.0), Grade::APlus);
1119 assert_eq!(Grade::from_score(94.0), Grade::A);
1120 assert_eq!(Grade::from_score(90.0), Grade::A);
1121 assert_eq!(Grade::from_score(89.0), Grade::AMinus);
1122 assert_eq!(Grade::from_score(85.0), Grade::AMinus);
1123 assert_eq!(Grade::from_score(84.0), Grade::BPlus);
1124 assert_eq!(Grade::from_score(80.0), Grade::BPlus);
1125 assert_eq!(Grade::from_score(79.0), Grade::B);
1126 assert_eq!(Grade::from_score(70.0), Grade::B);
1127 assert_eq!(Grade::from_score(69.0), Grade::C);
1128 assert_eq!(Grade::from_score(60.0), Grade::C);
1129 assert_eq!(Grade::from_score(59.0), Grade::D);
1130 assert_eq!(Grade::from_score(50.0), Grade::D);
1131 assert_eq!(Grade::from_score(49.0), Grade::F);
1132 assert_eq!(Grade::from_score(0.0), Grade::F);
1133 }
1134
1135 #[test]
1136 fn test_breakdown_from_errors() {
1137 let calculator = ScoreCalculator::new();
1138 let errors = vec![
1139 CompilationError {
1140 code: "E0308".to_string(),
1141 message: "type mismatch".to_string(),
1142 location: None,
1143 line: None,
1144 },
1145 CompilationError {
1146 code: "E0308".to_string(),
1147 message: "type mismatch 2".to_string(),
1148 location: None,
1149 line: None,
1150 },
1151 CompilationError {
1152 code: "E0599".to_string(),
1153 message: "method not found".to_string(),
1154 location: None,
1155 line: None,
1156 },
1157 ];
1158
1159 let breakdown = calculator.breakdown_from_errors(&BreakdownInput {
1160 parse_ok: true,
1161 type_check_ok: false,
1162 build_ok: false,
1163 errors: &errors,
1164 doctest_pass: false,
1165 unit_test_pass: false,
1166 property_test_pass: false,
1167 clippy_clean: false,
1168 tdg_grade_b_or_better: false,
1169 complexity_ok: true,
1170 trace_match: false,
1171 output_equiv: false,
1172 });
1173
1174 assert_eq!(breakdown.a1_parse, 10);
1175 assert_eq!(breakdown.a2_type_check, 0);
1176 assert_eq!(breakdown.a3_cargo_build, 0);
1177
1178 assert!(breakdown.b1_no_e0308 <= 4);
1180
1181 assert!(breakdown.b2_no_e0599 >= 5);
1183
1184 assert_eq!(breakdown.b3_no_e0425, 7);
1186
1187 assert_eq!(breakdown.d3_complexity, 2);
1188 }
1189
1190 #[test]
1191 fn test_breakdown_from_errors_all_pass() {
1192 let calculator = ScoreCalculator::new();
1193 let empty: Vec<CompilationError> = vec![];
1194
1195 let breakdown = calculator.breakdown_from_errors(&BreakdownInput {
1196 parse_ok: true,
1197 type_check_ok: true,
1198 build_ok: true,
1199 errors: &empty,
1200 doctest_pass: true,
1201 unit_test_pass: true,
1202 property_test_pass: true,
1203 clippy_clean: true,
1204 tdg_grade_b_or_better: true,
1205 complexity_ok: true,
1206 trace_match: true,
1207 output_equiv: true,
1208 });
1209
1210 assert_eq!(breakdown.a1_parse, 10);
1211 assert_eq!(breakdown.a2_type_check, 15);
1212 assert_eq!(breakdown.a3_cargo_build, 15);
1213 assert_eq!(breakdown.c1_doctest, 5);
1214 assert_eq!(breakdown.c2_unit_test, 5);
1215 assert_eq!(breakdown.c3_property_test, 5);
1216 assert_eq!(breakdown.d1_clippy, 5);
1217 assert_eq!(breakdown.d2_tdg, 3);
1218 assert_eq!(breakdown.d3_complexity, 2);
1219 assert_eq!(breakdown.e1_trace_match, 5);
1220 assert_eq!(breakdown.e2_output_equiv, 5);
1221 }
1222
1223 #[test]
1226 fn test_tarantula_score() {
1227 let stats = DecisionStats {
1228 failed_count: 8,
1229 passed_count: 2,
1230 };
1231
1232 let tarantula = stats.tarantula_score(10, 10);
1233
1234 assert!((tarantula.suspiciousness - 0.8).abs() < 0.01);
1238 }
1239
1240 #[test]
1241 fn test_tarantula_score_zero_failed() {
1242 let stats = DecisionStats {
1243 failed_count: 0,
1244 passed_count: 5,
1245 };
1246
1247 let tarantula = stats.tarantula_score(0, 10);
1248 assert_eq!(tarantula.suspiciousness, 0.0);
1249 }
1250
1251 #[test]
1252 fn test_tarantula_score_zero_passed() {
1253 let stats = DecisionStats {
1254 failed_count: 5,
1255 passed_count: 0,
1256 };
1257
1258 let tarantula = stats.tarantula_score(10, 0);
1259 assert!((tarantula.suspiciousness - 1.0).abs() < 0.01);
1263 }
1264
1265 #[test]
1266 fn test_tarantula_score_both_zero() {
1267 let stats = DecisionStats {
1268 failed_count: 0,
1269 passed_count: 0,
1270 };
1271
1272 let tarantula = stats.tarantula_score(0, 0);
1273 assert_eq!(tarantula.suspiciousness, 0.0);
1274 }
1275
1276 #[test]
1277 fn test_tarantula_score_clone() {
1278 let score = TarantulaScore {
1279 suspiciousness: 0.5,
1280 failed_count: 3,
1281 passed_count: 7,
1282 };
1283 let cloned = score.clone();
1284 assert_eq!(cloned.suspiciousness, score.suspiciousness);
1285 }
1286
1287 #[test]
1288 fn test_tarantula_score_debug() {
1289 let score = TarantulaScore {
1290 suspiciousness: 0.5,
1291 failed_count: 3,
1292 passed_count: 7,
1293 };
1294 let debug = format!("{:?}", score);
1295 assert!(debug.contains("TarantulaScore"));
1296 }
1297
1298 #[test]
1301 fn test_decision_stats_default() {
1302 let stats = DecisionStats::default();
1303 assert_eq!(stats.failed_count, 0);
1304 assert_eq!(stats.passed_count, 0);
1305 }
1306
1307 #[test]
1308 fn test_decision_stats_clone() {
1309 let stats = DecisionStats {
1310 failed_count: 5,
1311 passed_count: 10,
1312 };
1313 let cloned = stats.clone();
1314 assert_eq!(cloned.failed_count, stats.failed_count);
1315 }
1316
1317 #[test]
1318 fn test_decision_stats_debug() {
1319 let stats = DecisionStats::default();
1320 let debug = format!("{:?}", stats);
1321 assert!(debug.contains("DecisionStats"));
1322 }
1323
1324 #[test]
1327 fn test_corpus_aggregation() {
1328 let calculator = ScoreCalculator::new();
1329
1330 let results = vec![
1331 SingleShotResult {
1332 file_path: PathBuf::from("a.py"),
1333 score: SingleShotScore {
1334 total: 80,
1335 compilation: 40,
1336 type_inference: 20,
1337 test_coverage: 10,
1338 code_quality: 5,
1339 semantic_equivalence: 5,
1340 gateway_passed: true,
1341 mode: ScoringMode::Fast,
1342 },
1343 category_breakdown: CategoryBreakdown::default(),
1344 error_details: vec![],
1345 transpiler_decisions: vec![],
1346 },
1347 SingleShotResult {
1348 file_path: PathBuf::from("b.py"),
1349 score: SingleShotScore {
1350 total: 60,
1351 compilation: 30,
1352 type_inference: 15,
1353 test_coverage: 10,
1354 code_quality: 3,
1355 semantic_equivalence: 2,
1356 gateway_passed: true,
1357 mode: ScoringMode::Fast,
1358 },
1359 category_breakdown: CategoryBreakdown::default(),
1360 error_details: vec![],
1361 transpiler_decisions: vec![],
1362 },
1363 ];
1364
1365 let report = calculator.aggregate(&results);
1366
1367 assert!((report.aggregate_score - 70.0).abs() < 0.01);
1368 assert_eq!(report.grade, Grade::B);
1369 }
1370
1371 #[test]
1372 fn test_corpus_aggregation_empty() {
1373 let calculator = ScoreCalculator::new();
1374 let results: Vec<SingleShotResult> = vec![];
1375
1376 let report = calculator.aggregate(&results);
1377
1378 assert_eq!(report.aggregate_score, 0.0);
1379 assert_eq!(report.grade, Grade::F);
1380 assert!(report.results.is_empty());
1381 assert!(report.top_blockers.is_empty());
1382 }
1383
1384 #[test]
1385 fn test_corpus_aggregation_with_errors() {
1386 let calculator = ScoreCalculator::new();
1387
1388 let results = vec![
1389 SingleShotResult {
1390 file_path: PathBuf::from("a.py"),
1391 score: SingleShotScore {
1392 total: 50,
1393 ..Default::default()
1394 },
1395 category_breakdown: CategoryBreakdown::default(),
1396 error_details: vec![CompilationError {
1397 code: "E0308".to_string(),
1398 message: "type mismatch".to_string(),
1399 location: None,
1400 line: None,
1401 }],
1402 transpiler_decisions: vec![],
1403 },
1404 SingleShotResult {
1405 file_path: PathBuf::from("b.py"),
1406 score: SingleShotScore {
1407 total: 50,
1408 ..Default::default()
1409 },
1410 category_breakdown: CategoryBreakdown::default(),
1411 error_details: vec![CompilationError {
1412 code: "E0308".to_string(),
1413 message: "type mismatch".to_string(),
1414 location: None,
1415 line: None,
1416 }],
1417 transpiler_decisions: vec![],
1418 },
1419 ];
1420
1421 let report = calculator.aggregate(&results);
1422
1423 assert!(!report.top_blockers.is_empty());
1425 assert_eq!(report.top_blockers[0].pattern, "E0308");
1426 assert_eq!(report.top_blockers[0].affected_files, 2);
1427 }
1428
1429 #[test]
1430 fn test_corpus_score_report_clone() {
1431 let report = CorpusScoreReport {
1432 results: vec![],
1433 aggregate_score: 75.0,
1434 grade: Grade::B,
1435 category_averages: CategoryBreakdown::default(),
1436 top_blockers: vec![],
1437 };
1438 let cloned = report.clone();
1439 assert_eq!(cloned.aggregate_score, report.aggregate_score);
1440 }
1441
1442 #[test]
1443 fn test_corpus_score_report_debug() {
1444 let report = CorpusScoreReport {
1445 results: vec![],
1446 aggregate_score: 0.0,
1447 grade: Grade::F,
1448 category_averages: CategoryBreakdown::default(),
1449 top_blockers: vec![],
1450 };
1451 let debug = format!("{:?}", report);
1452 assert!(debug.contains("CorpusScoreReport"));
1453 }
1454
1455 #[test]
1458 fn test_analyze_score_failures_empty() {
1459 let results: Vec<SingleShotResult> = vec![];
1460 let analysis = analyze_score_failures(&results);
1461 assert!(analysis.is_empty());
1462 }
1463
1464 #[test]
1465 fn test_analyze_score_failures_with_decisions() {
1466 let results = vec![
1467 SingleShotResult {
1468 file_path: PathBuf::from("a.py"),
1469 score: SingleShotScore {
1470 total: 50, ..Default::default()
1472 },
1473 category_breakdown: CategoryBreakdown::default(),
1474 error_details: vec![],
1475 transpiler_decisions: vec![TranspilerDecision::TypeInference {
1476 variable: "x".to_string(),
1477 inferred_type: "Value".to_string(),
1478 }],
1479 },
1480 SingleShotResult {
1481 file_path: PathBuf::from("b.py"),
1482 score: SingleShotScore {
1483 total: 90, ..Default::default()
1485 },
1486 category_breakdown: CategoryBreakdown::default(),
1487 error_details: vec![],
1488 transpiler_decisions: vec![TranspilerDecision::TypeInference {
1489 variable: "x".to_string(),
1490 inferred_type: "Value".to_string(),
1491 }],
1492 },
1493 ];
1494
1495 let analysis = analyze_score_failures(&results);
1496
1497 assert_eq!(analysis.len(), 1);
1498 let decision = TranspilerDecision::TypeInference {
1499 variable: "x".to_string(),
1500 inferred_type: "Value".to_string(),
1501 };
1502 let score = analysis.get(&decision).unwrap();
1503 assert!((score.suspiciousness - 0.5).abs() < 0.01);
1505 }
1506
1507 #[test]
1510 fn test_single_shot_result_clone() {
1511 let result = SingleShotResult {
1512 file_path: PathBuf::from("test.py"),
1513 score: SingleShotScore::default(),
1514 category_breakdown: CategoryBreakdown::default(),
1515 error_details: vec![],
1516 transpiler_decisions: vec![],
1517 };
1518 let cloned = result.clone();
1519 assert_eq!(cloned.file_path, result.file_path);
1520 }
1521
1522 #[test]
1523 fn test_single_shot_result_debug() {
1524 let result = SingleShotResult {
1525 file_path: PathBuf::from("test.py"),
1526 score: SingleShotScore::default(),
1527 category_breakdown: CategoryBreakdown::default(),
1528 error_details: vec![],
1529 transpiler_decisions: vec![],
1530 };
1531 let debug = format!("{:?}", result);
1532 assert!(debug.contains("SingleShotResult"));
1533 }
1534}