llm_git/testing/
fixture.rs1use std::{collections::HashMap, fs, path::Path};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 error::{CommitGenError, Result},
9 types::ConventionalAnalysis,
10};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Manifest {
15 #[serde(default)]
17 pub fixtures: HashMap<String, FixtureEntry>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FixtureEntry {
23 pub description: String,
25 #[serde(default)]
27 pub tags: Vec<String>,
28}
29
30impl Manifest {
31 pub fn load(fixtures_dir: &Path) -> Result<Self> {
33 let path = fixtures_dir.join("manifest.toml");
34 if !path.exists() {
35 return Ok(Self { fixtures: HashMap::new() });
36 }
37 let content = fs::read_to_string(&path)?;
38 toml::from_str(&content)
39 .map_err(|e| CommitGenError::Other(format!("Failed to parse manifest.toml: {e}")))
40 }
41
42 pub fn save(&self, fixtures_dir: &Path) -> Result<()> {
44 let path = fixtures_dir.join("manifest.toml");
45 let content = toml::to_string_pretty(self)
46 .map_err(|e| CommitGenError::Other(format!("Failed to serialize manifest: {e}")))?;
47 fs::write(&path, content)?;
48 Ok(())
49 }
50
51 pub fn add(&mut self, name: String, entry: FixtureEntry) {
53 self.fixtures.insert(name, entry);
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct FixtureMeta {
60 pub source_repo: String,
62 pub source_commit: String,
64 pub description: String,
66 pub captured_at: String,
68 #[serde(default)]
70 pub tags: Vec<String>,
71}
72
73#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct FixtureContext {
76 #[serde(default)]
78 pub recent_commits: Option<String>,
79 #[serde(default)]
81 pub common_scopes: Option<String>,
82 #[serde(default)]
84 pub project_context: Option<String>,
85 #[serde(default)]
87 pub user_context: Option<String>,
88}
89
90#[derive(Debug, Clone)]
92pub struct FixtureInput {
93 pub diff: String,
95 pub stat: String,
97 pub scope_candidates: String,
99 pub context: FixtureContext,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct Golden {
106 pub analysis: ConventionalAnalysis,
108 pub final_message: String,
110}
111
112#[derive(Debug, Clone)]
114pub struct Fixture {
115 pub name: String,
117 pub meta: FixtureMeta,
119 pub input: FixtureInput,
121 pub golden: Option<Golden>,
123}
124
125impl Fixture {
126 pub fn load(fixtures_dir: &Path, name: &str) -> Result<Self> {
128 let fixture_dir = fixtures_dir.join(name);
129 if !fixture_dir.exists() {
130 return Err(CommitGenError::Other(format!(
131 "Fixture '{}' not found at {}",
132 name,
133 fixture_dir.display()
134 )));
135 }
136
137 let meta_path = fixture_dir.join("meta.toml");
139 let meta: FixtureMeta = if meta_path.exists() {
140 let content = fs::read_to_string(&meta_path)?;
141 toml::from_str(&content).map_err(|e| {
142 CommitGenError::Other(format!("Failed to parse {}: {e}", meta_path.display()))
143 })?
144 } else {
145 return Err(CommitGenError::Other(format!("Fixture '{name}' missing meta.toml")));
146 };
147
148 let input_dir = fixture_dir.join("input");
150 let diff = fs::read_to_string(input_dir.join("diff.patch"))
151 .map_err(|e| CommitGenError::Other(format!("Failed to read diff.patch: {e}")))?;
152 let stat = fs::read_to_string(input_dir.join("stat.txt"))
153 .map_err(|e| CommitGenError::Other(format!("Failed to read stat.txt: {e}")))?;
154 let scope_candidates =
155 fs::read_to_string(input_dir.join("scope_candidates.txt")).unwrap_or_default();
156
157 let context_path = input_dir.join("context.toml");
159 let context: FixtureContext = if context_path.exists() {
160 let content = fs::read_to_string(&context_path)?;
161 toml::from_str(&content)
162 .map_err(|e| CommitGenError::Other(format!("Failed to parse context.toml: {e}")))?
163 } else {
164 FixtureContext::default()
165 };
166
167 let golden_dir = fixture_dir.join("golden");
169 let golden = if golden_dir.exists() {
170 let analysis_path = golden_dir.join("analysis.json");
171 let final_path = golden_dir.join("final.txt");
172
173 if analysis_path.exists() && final_path.exists() {
174 let analysis_content = fs::read_to_string(&analysis_path)?;
175 let analysis: ConventionalAnalysis = serde_json::from_str(&analysis_content)
176 .map_err(|e| CommitGenError::Other(format!("Failed to parse analysis.json: {e}")))?;
177 let final_message = fs::read_to_string(&final_path)?;
178 Some(Golden { analysis, final_message })
179 } else {
180 None
181 }
182 } else {
183 None
184 };
185
186 Ok(Self {
187 name: name.to_string(),
188 meta,
189 input: FixtureInput { diff, stat, scope_candidates, context },
190 golden,
191 })
192 }
193
194 pub fn save(&self, fixtures_dir: &Path) -> Result<()> {
196 let fixture_dir = fixtures_dir.join(&self.name);
197 let input_dir = fixture_dir.join("input");
198 let golden_dir = fixture_dir.join("golden");
199
200 fs::create_dir_all(&input_dir)?;
202 fs::create_dir_all(&golden_dir)?;
203
204 let meta_content = toml::to_string_pretty(&self.meta)
206 .map_err(|e| CommitGenError::Other(format!("Failed to serialize meta: {e}")))?;
207 fs::write(fixture_dir.join("meta.toml"), meta_content)?;
208
209 fs::write(input_dir.join("diff.patch"), &self.input.diff)?;
211 fs::write(input_dir.join("stat.txt"), &self.input.stat)?;
212 fs::write(input_dir.join("scope_candidates.txt"), &self.input.scope_candidates)?;
213
214 let context_content = toml::to_string_pretty(&self.input.context)
215 .map_err(|e| CommitGenError::Other(format!("Failed to serialize context: {e}")))?;
216 fs::write(input_dir.join("context.toml"), context_content)?;
217
218 if let Some(golden) = &self.golden {
220 let analysis_json = serde_json::to_string_pretty(&golden.analysis)?;
221 fs::write(golden_dir.join("analysis.json"), analysis_json)?;
222 fs::write(golden_dir.join("final.txt"), &golden.final_message)?;
223 }
224
225 Ok(())
226 }
227
228 pub fn update_golden(&mut self, analysis: ConventionalAnalysis, final_message: String) {
230 self.golden = Some(Golden { analysis, final_message });
231 }
232}
233
234pub fn discover_fixtures(fixtures_dir: &Path) -> Result<Vec<String>> {
236 let mut fixtures = Vec::new();
237
238 if !fixtures_dir.exists() {
239 return Ok(fixtures);
240 }
241
242 for entry in fs::read_dir(fixtures_dir)? {
243 let entry = entry?;
244 let path = entry.path();
245
246 if !path.is_dir() {
248 continue;
249 }
250
251 if path.join("meta.toml").exists()
253 && let Some(name) = path.file_name().and_then(|n| n.to_str())
254 {
255 fixtures.push(name.to_string());
256 }
257 }
258
259 fixtures.sort();
260 Ok(fixtures)
261}