1use super::{
4 compare::{CompareResult, compare_analysis},
5 fixture::{Fixture, discover_fixtures},
6};
7use crate::{
8 api::{AnalysisContext, generate_analysis_with_map_reduce},
9 config::CommitConfig,
10 error::Result,
11 normalization::format_commit_message,
12 tokens::create_token_counter,
13 types::{CommitType, ConventionalAnalysis, ConventionalCommit},
14};
15
16#[derive(Debug)]
18pub struct RunResult {
19 pub name: String,
21 pub comparison: Option<CompareResult>,
23 pub analysis: crate::types::ConventionalAnalysis,
25 pub final_message: String,
27 pub error: Option<String>,
29}
30
31pub struct TestRunner {
33 pub fixtures_dir: std::path::PathBuf,
35 pub config: CommitConfig,
37 pub filter: Option<String>,
39}
40
41impl TestRunner {
42 pub fn new(fixtures_dir: impl Into<std::path::PathBuf>, config: CommitConfig) -> Self {
44 Self { fixtures_dir: fixtures_dir.into(), config, filter: None }
45 }
46
47 pub fn with_filter(mut self, filter: Option<String>) -> Self {
49 self.filter = filter;
50 self
51 }
52
53 pub async fn run_all(&self) -> Result<Vec<RunResult>> {
55 let fixture_names = discover_fixtures(&self.fixtures_dir)?;
56 let mut results = Vec::new();
57
58 for name in fixture_names {
59 if let Some(pattern) = &self.filter
61 && !name.contains(pattern)
62 {
63 continue;
64 }
65
66 let result = self.run_fixture(&name).await;
67 results.push(result);
68 }
69
70 Ok(results)
71 }
72
73 pub async fn run_fixture(&self, name: &str) -> RunResult {
75 match self.run_fixture_inner(name).await {
76 Ok(result) => result,
77 Err(e) => RunResult {
78 name: name.to_string(),
79 comparison: None,
80 analysis: ConventionalAnalysis {
81 commit_type: CommitType::new("chore").expect("valid type"),
82 scope: None,
83 summary: None,
84 details: vec![],
85 issue_refs: vec![],
86 },
87 final_message: String::new(),
88 error: Some(e.to_string()),
89 },
90 }
91 }
92
93 async fn run_fixture_inner(&self, name: &str) -> Result<RunResult> {
94 let fixture = Fixture::load(&self.fixtures_dir, name)?;
95 let token_counter = create_token_counter(&self.config);
96
97 let ctx = AnalysisContext {
99 user_context: fixture.input.context.user_context.as_deref(),
100 recent_commits: fixture.input.context.recent_commits.as_deref(),
101 common_scopes: fixture.input.context.common_scopes.as_deref(),
102 project_context: fixture.input.context.project_context.as_deref(),
103 debug_output: None,
104 debug_prefix: None,
105 };
106
107 let analysis = generate_analysis_with_map_reduce(
109 &fixture.input.stat,
110 &fixture.input.diff,
111 &self.config.analysis_model,
112 &fixture.input.scope_candidates,
113 &ctx,
114 &self.config,
115 &token_counter,
116 )
117 .await?;
118
119 let detail_points = analysis.body_texts();
122 let summary = match crate::api::summary_from_holistic_analysis(&analysis, &self.config) {
123 Ok(Some(summary)) => summary,
124 Ok(None) => crate::api::generate_summary_from_analysis(
125 &fixture.input.stat,
126 analysis.commit_type.as_str(),
127 analysis.scope.as_ref().map(|s| s.as_str()),
128 &detail_points,
129 fixture.input.context.user_context.as_deref(),
130 &self.config,
131 None,
132 None,
133 )
134 .await
135 .unwrap_or_else(|_| {
136 crate::api::fallback_summary(
137 &fixture.input.stat,
138 &detail_points,
139 analysis.commit_type.as_str(),
140 &self.config,
141 )
142 }),
143 Err(_) => crate::api::fallback_summary(
144 &fixture.input.stat,
145 &detail_points,
146 analysis.commit_type.as_str(),
147 &self.config,
148 ),
149 };
150
151 let final_commit = ConventionalCommit {
152 commit_type: analysis.commit_type.clone(),
153 scope: analysis.scope.clone(),
154 summary,
155 body: detail_points,
156 footers: vec![],
157 };
158 let final_message = format_commit_message(&final_commit);
159
160 let comparison = fixture
162 .golden
163 .as_ref()
164 .map(|g| compare_analysis(&g.analysis, &analysis));
165
166 Ok(RunResult { name: name.to_string(), comparison, analysis, final_message, error: None })
167 }
168
169 pub async fn update_all(&self) -> Result<Vec<String>> {
171 let fixture_names = discover_fixtures(&self.fixtures_dir)?;
172 let mut updated = Vec::new();
173
174 for name in fixture_names {
175 if let Some(pattern) = &self.filter
176 && !name.contains(pattern)
177 {
178 continue;
179 }
180
181 self.update_fixture(&name).await?;
182 updated.push(name);
183 }
184
185 Ok(updated)
186 }
187
188 pub async fn update_fixture(&self, name: &str) -> Result<()> {
190 let result = self.run_fixture(name).await;
191
192 if let Some(err) = result.error {
193 return Err(crate::error::CommitGenError::Other(format!(
194 "Failed to run fixture '{name}': {err}"
195 )));
196 }
197
198 let mut fixture = Fixture::load(&self.fixtures_dir, name)?;
199 fixture.update_golden(result.analysis, result.final_message);
200 fixture.save(&self.fixtures_dir)?;
201
202 Ok(())
203 }
204}
205
206#[derive(Debug, Default)]
208pub struct TestSummary {
209 pub total: usize,
210 pub passed: usize,
211 pub failed: usize,
212 pub no_golden: usize,
213 pub errors: usize,
214}
215
216impl TestSummary {
217 pub fn from_results(results: &[RunResult]) -> Self {
219 let mut summary = Self { total: results.len(), ..Default::default() };
220
221 for result in results {
222 if result.error.is_some() {
223 summary.errors += 1;
224 } else if let Some(cmp) = &result.comparison {
225 if cmp.passed {
226 summary.passed += 1;
227 } else {
228 summary.failed += 1;
229 }
230 } else {
231 summary.no_golden += 1;
232 }
233 }
234
235 summary
236 }
237
238 pub const fn all_passed(&self) -> bool {
240 self.failed == 0 && self.errors == 0
241 }
242}