llm_git/testing/
compare.rs1use crate::types::ConventionalAnalysis;
4
5#[derive(Debug, Clone)]
7pub struct CompareResult {
8 pub type_match: bool,
10 pub scope_match: bool,
12 pub scope_diff: Option<String>,
14 pub golden_detail_count: usize,
16 pub actual_detail_count: usize,
18 pub passed: bool,
20 pub summary: String,
22}
23
24pub fn compare_analysis(
26 golden: &ConventionalAnalysis,
27 actual: &ConventionalAnalysis,
28) -> CompareResult {
29 let type_match = golden.commit_type == actual.commit_type;
30
31 let scope_match = golden.scope == actual.scope;
32 let scope_diff = if scope_match {
33 None
34 } else {
35 Some(format!(
36 "{} → {}",
37 golden.scope.as_ref().map_or("null", |s| s.as_str()),
38 actual.scope.as_ref().map_or("null", |s| s.as_str())
39 ))
40 };
41
42 let golden_detail_count = golden.details.len();
43 let actual_detail_count = actual.details.len();
44
45 let passed = type_match;
48
49 let summary = if passed && scope_match {
50 format!(
51 "✓ {} | {} | {} details",
52 actual.commit_type.as_str(),
53 actual.scope.as_ref().map_or("(no scope)", |s| s.as_str()),
54 actual_detail_count
55 )
56 } else if passed {
57 format!(
58 "≈ {} | scope: {} | {} details",
59 actual.commit_type.as_str(),
60 scope_diff.as_ref().unwrap(),
61 actual_detail_count
62 )
63 } else {
64 format!(
65 "✗ type: {} → {} | {} details",
66 golden.commit_type.as_str(),
67 actual.commit_type.as_str(),
68 actual_detail_count
69 )
70 };
71
72 CompareResult {
73 type_match,
74 scope_match,
75 scope_diff,
76 golden_detail_count,
77 actual_detail_count,
78 passed,
79 summary,
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use std::collections::HashSet;
86
87 use super::*;
88 use crate::types::{CommitType, Scope};
89
90 fn jaccard_similarity(a: &str, b: &str) -> f64 {
92 let words_a: HashSet<&str> = a.split_whitespace().collect();
93 let words_b: HashSet<&str> = b.split_whitespace().collect();
94
95 if words_a.is_empty() && words_b.is_empty() {
96 return 1.0;
97 }
98
99 let intersection = words_a.intersection(&words_b).count();
100 let union = words_a.union(&words_b).count();
101
102 if union == 0 {
103 return 0.0;
104 }
105
106 intersection as f64 / union as f64
107 }
108
109 #[test]
110 fn test_compare_exact_match() {
111 let golden = ConventionalAnalysis {
112 commit_type: CommitType::new("feat").unwrap(),
113 scope: Some(Scope::new("api").unwrap()),
114 details: vec![],
115 issue_refs: vec![],
116 };
117 let actual = golden.clone();
118
119 let result = compare_analysis(&golden, &actual);
120 assert!(result.passed);
121 assert!(result.type_match);
122 assert!(result.scope_match);
123 }
124
125 #[test]
126 fn test_compare_type_mismatch() {
127 let golden = ConventionalAnalysis {
128 commit_type: CommitType::new("feat").unwrap(),
129 scope: None,
130 details: vec![],
131 issue_refs: vec![],
132 };
133 let actual = ConventionalAnalysis {
134 commit_type: CommitType::new("fix").unwrap(),
135 scope: None,
136 details: vec![],
137 issue_refs: vec![],
138 };
139
140 let result = compare_analysis(&golden, &actual);
141 assert!(!result.passed);
142 assert!(!result.type_match);
143 }
144
145 #[test]
146 fn test_compare_scope_mismatch() {
147 let golden = ConventionalAnalysis {
148 commit_type: CommitType::new("feat").unwrap(),
149 scope: Some(Scope::new("api").unwrap()),
150 details: vec![],
151 issue_refs: vec![],
152 };
153 let actual = ConventionalAnalysis {
154 commit_type: CommitType::new("feat").unwrap(),
155 scope: Some(Scope::new("api/client").unwrap()),
156 details: vec![],
157 issue_refs: vec![],
158 };
159
160 let result = compare_analysis(&golden, &actual);
161 assert!(result.passed); assert!(!result.scope_match);
163 assert!(result.scope_diff.is_some());
164 }
165
166 #[test]
167 fn test_jaccard_similarity() {
168 assert!((jaccard_similarity("hello world", "hello world") - 1.0).abs() < 0.001);
169 assert!((jaccard_similarity("hello world", "hello there") - 0.333).abs() < 0.1);
170 assert!((jaccard_similarity("", "") - 1.0).abs() < 0.001);
171 }
172}