1use crate::Result;
7use serde::de::DeserializeOwned;
8use std::path::Path;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum FixtureLoadErrorMode {
13 FailFast,
15 WarnAndContinue,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum FixtureFileGranularity {
22 Single,
24 Array,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum FixtureFileFormat {
31 Yaml,
33 Json,
35}
36
37#[derive(Debug, Clone)]
39pub struct FixtureLoadOptions {
40 pub formats: Vec<FixtureFileFormat>,
42 pub error_mode: FixtureLoadErrorMode,
44 pub granularity: FixtureFileGranularity,
46}
47
48impl FixtureLoadOptions {
49 pub fn yaml_single() -> Self {
51 Self {
52 formats: vec![FixtureFileFormat::Yaml],
53 error_mode: FixtureLoadErrorMode::WarnAndContinue,
54 granularity: FixtureFileGranularity::Single,
55 }
56 }
57
58 pub fn yaml_json_single() -> Self {
60 Self {
61 formats: vec![FixtureFileFormat::Yaml, FixtureFileFormat::Json],
62 error_mode: FixtureLoadErrorMode::WarnAndContinue,
63 granularity: FixtureFileGranularity::Single,
64 }
65 }
66
67 pub fn yaml_array_strict() -> Self {
69 Self {
70 formats: vec![FixtureFileFormat::Yaml],
71 error_mode: FixtureLoadErrorMode::FailFast,
72 granularity: FixtureFileGranularity::Array,
73 }
74 }
75}
76
77pub fn load_fixtures_from_dir<T: DeserializeOwned>(
97 dir: &Path,
98 options: &FixtureLoadOptions,
99) -> Result<Vec<T>> {
100 if !dir.exists() {
101 tracing::debug!("Fixture directory does not exist: {}", dir.display());
102 return Ok(Vec::new());
103 }
104
105 let entries = std::fs::read_dir(dir).map_err(|e| {
106 crate::Error::io_with_context(
107 format!("reading fixture directory {}", dir.display()),
108 e.to_string(),
109 )
110 })?;
111
112 let mut fixtures = Vec::new();
113
114 for entry in entries {
115 let entry = match entry {
116 Ok(e) => e,
117 Err(e) => {
118 tracing::warn!("Failed to read directory entry: {}", e);
119 continue;
120 }
121 };
122
123 let path = entry.path();
124 if !path.is_file() {
125 continue;
126 }
127
128 let format = match path.extension().and_then(|e| e.to_str()) {
129 Some("yaml" | "yml") if options.formats.contains(&FixtureFileFormat::Yaml) => {
130 FixtureFileFormat::Yaml
131 }
132 Some("json") if options.formats.contains(&FixtureFileFormat::Json) => {
133 FixtureFileFormat::Json
134 }
135 _ => continue,
136 };
137
138 match load_fixture_file::<T>(&path, format, options.granularity) {
139 Ok(loaded) => fixtures.extend(loaded),
140 Err(e) => match options.error_mode {
141 FixtureLoadErrorMode::FailFast => return Err(e),
142 FixtureLoadErrorMode::WarnAndContinue => {
143 tracing::warn!("Failed to load fixture {}: {}", path.display(), e);
144 }
145 },
146 }
147 }
148
149 tracing::debug!("Loaded {} fixtures from {}", fixtures.len(), dir.display());
150 Ok(fixtures)
151}
152
153fn load_fixture_file<T: DeserializeOwned>(
154 path: &Path,
155 format: FixtureFileFormat,
156 granularity: FixtureFileGranularity,
157) -> Result<Vec<T>> {
158 let content = std::fs::read_to_string(path).map_err(|e| {
159 crate::Error::io_with_context(format!("reading fixture {}", path.display()), e.to_string())
160 })?;
161
162 match (format, granularity) {
163 (FixtureFileFormat::Yaml, FixtureFileGranularity::Single) => {
164 let fixture: T = serde_yaml::from_str(&content)?;
165 Ok(vec![fixture])
166 }
167 (FixtureFileFormat::Yaml, FixtureFileGranularity::Array) => {
168 let fixtures: Vec<T> = serde_yaml::from_str(&content)?;
169 Ok(fixtures)
170 }
171 (FixtureFileFormat::Json, FixtureFileGranularity::Single) => {
172 let fixture: T = serde_json::from_str(&content)?;
173 Ok(vec![fixture])
174 }
175 (FixtureFileFormat::Json, FixtureFileGranularity::Array) => {
176 let fixtures: Vec<T> = serde_json::from_str(&content)?;
177 Ok(fixtures)
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use serde::Deserialize;
186 use std::fs;
187
188 #[derive(Debug, Deserialize, PartialEq)]
189 struct TestFixture {
190 name: String,
191 value: i32,
192 }
193
194 #[test]
195 fn test_load_yaml_single() {
196 let dir = tempfile::tempdir().unwrap();
197 fs::write(dir.path().join("test.yaml"), "name: hello\nvalue: 42\n").unwrap();
198
199 let fixtures: Vec<TestFixture> =
200 load_fixtures_from_dir(dir.path(), &FixtureLoadOptions::yaml_single()).unwrap();
201
202 assert_eq!(fixtures.len(), 1);
203 assert_eq!(fixtures[0].name, "hello");
204 assert_eq!(fixtures[0].value, 42);
205 }
206
207 #[test]
208 fn test_load_json_single() {
209 let dir = tempfile::tempdir().unwrap();
210 fs::write(dir.path().join("test.json"), r#"{"name": "world", "value": 99}"#).unwrap();
211
212 let fixtures: Vec<TestFixture> =
213 load_fixtures_from_dir(dir.path(), &FixtureLoadOptions::yaml_json_single()).unwrap();
214
215 assert_eq!(fixtures.len(), 1);
216 assert_eq!(fixtures[0].name, "world");
217 }
218
219 #[test]
220 fn test_load_yaml_array() {
221 let dir = tempfile::tempdir().unwrap();
222 fs::write(dir.path().join("items.yaml"), "- name: a\n value: 1\n- name: b\n value: 2\n")
223 .unwrap();
224
225 let fixtures: Vec<TestFixture> =
226 load_fixtures_from_dir(dir.path(), &FixtureLoadOptions::yaml_array_strict()).unwrap();
227
228 assert_eq!(fixtures.len(), 2);
229 }
230
231 #[test]
232 fn test_nonexistent_dir_returns_empty() {
233 let fixtures: Vec<TestFixture> = load_fixtures_from_dir(
234 Path::new("/nonexistent/path"),
235 &FixtureLoadOptions::yaml_single(),
236 )
237 .unwrap();
238
239 assert!(fixtures.is_empty());
240 }
241
242 #[test]
243 fn test_warn_and_continue_skips_bad_files() {
244 let dir = tempfile::tempdir().unwrap();
245 fs::write(dir.path().join("good.yaml"), "name: ok\nvalue: 1\n").unwrap();
246 fs::write(dir.path().join("bad.yaml"), "not valid yaml: [[[").unwrap();
247
248 let fixtures: Vec<TestFixture> =
249 load_fixtures_from_dir(dir.path(), &FixtureLoadOptions::yaml_single()).unwrap();
250
251 assert_eq!(fixtures.len(), 1);
252 assert_eq!(fixtures[0].name, "ok");
253 }
254
255 #[test]
256 fn test_fail_fast_propagates_error() {
257 let dir = tempfile::tempdir().unwrap();
258 fs::write(dir.path().join("bad.yaml"), "not valid yaml: [[[").unwrap();
259
260 let result: Result<Vec<TestFixture>> =
261 load_fixtures_from_dir(dir.path(), &FixtureLoadOptions::yaml_array_strict());
262
263 assert!(result.is_err());
264 }
265
266 #[test]
267 fn test_ignores_non_matching_extensions() {
268 let dir = tempfile::tempdir().unwrap();
269 fs::write(dir.path().join("readme.txt"), "not a fixture").unwrap();
270 fs::write(dir.path().join("data.yaml"), "name: x\nvalue: 0\n").unwrap();
271
272 let fixtures: Vec<TestFixture> =
273 load_fixtures_from_dir(dir.path(), &FixtureLoadOptions::yaml_single()).unwrap();
274
275 assert_eq!(fixtures.len(), 1);
276 }
277}