ggen_e2e/
fixture.rs

1//! Test fixture management
2//!
3//! Handles loading ggen project fixtures (ontologies, templates, manifests)
4//! and discovering fixtures in the test suite.
5
6use crate::error::{FixtureError, Result};
7use crate::golden::GoldenFile;
8use std::fs;
9use std::path::{Path, PathBuf};
10use tempfile::TempDir;
11
12/// A test fixture representing a complete ggen project
13#[derive(Debug, Clone)]
14pub struct TestFixture {
15    /// Unique fixture identifier (e.g., "thesis-gen", "minimal")
16    pub name: String,
17    /// Path to fixture directory
18    pub path: PathBuf,
19    /// TTL/RDF ontology files
20    pub ontology_files: Vec<PathBuf>,
21    /// Tera template files
22    pub template_files: Vec<PathBuf>,
23    /// Path to ggen.toml manifest
24    pub ggen_toml: PathBuf,
25    /// Directory containing expected output (golden files)
26    pub golden_dir: PathBuf,
27}
28
29impl TestFixture {
30    /// Load a fixture from a directory
31    pub fn load(path: impl AsRef<Path>, name: &str) -> Result<Self> {
32        let path = path.as_ref().to_path_buf();
33
34        if !path.exists() {
35            return Err(FixtureError::NotFound(path).into());
36        }
37
38        let ggen_toml = path.join("ggen.toml");
39        if !ggen_toml.exists() {
40            return Err(FixtureError::MissingFile(ggen_toml).into());
41        }
42
43        let ontology_dir = path.join("ontology");
44        let template_dir = path.join("templates");
45
46        let ontology_files = if ontology_dir.exists() {
47            discover_files(&ontology_dir, &["ttl", "rdf"])?
48        } else {
49            Vec::new()
50        };
51
52        let template_files = if template_dir.exists() {
53            discover_files(&template_dir, &["tera", "jinja2"])?
54        } else {
55            Vec::new()
56        };
57
58        // Golden directory is typically in tests/e2e/golden/{fixture_name}
59        let golden_dir = PathBuf::from("tests/e2e/golden").join(name);
60
61        Ok(TestFixture {
62            name: name.to_string(),
63            path,
64            ontology_files,
65            template_files,
66            ggen_toml,
67            golden_dir,
68        })
69    }
70
71    /// Copy fixture to a temporary directory
72    pub fn copy_to_temp(&self) -> Result<TempDir> {
73        let temp_dir = TempDir::new().map_err(|e| FixtureError::CopyFailed(e.to_string()))?;
74
75        copy_dir_recursive(&self.path, temp_dir.path())
76            .map_err(|e| FixtureError::CopyFailed(e.to_string()))?;
77
78        Ok(temp_dir)
79    }
80
81    /// Get all golden files for this fixture
82    pub fn golden_files(&self) -> Result<Vec<GoldenFile>> {
83        let mut files = Vec::new();
84
85        if !self.golden_dir.exists() {
86            return Ok(files);
87        }
88
89        discover_golden_files(&self.golden_dir, &self.golden_dir, &mut files)?;
90        Ok(files)
91    }
92
93    /// Check if fixture has required files
94    pub fn validate(&self) -> Result<()> {
95        if !self.ggen_toml.exists() {
96            return Err(FixtureError::MissingFile(self.ggen_toml.clone()).into());
97        }
98
99        if self.ontology_files.is_empty() {
100            return Err(FixtureError::Configuration(format!(
101                "Fixture '{}' has no ontology files",
102                self.name
103            ))
104            .into());
105        }
106
107        if self.template_files.is_empty() {
108            return Err(FixtureError::Configuration(format!(
109                "Fixture '{}' has no template files",
110                self.name
111            ))
112            .into());
113        }
114
115        Ok(())
116    }
117
118    /// Get the fixture's ggen.toml content
119    pub fn manifest_content(&self) -> Result<String> {
120        fs::read_to_string(&self.ggen_toml).map_err(|e| FixtureError::Io(e).into())
121    }
122}
123
124/// Discover fixtures in the standard location
125pub fn discover_fixtures(fixtures_dir: &Path) -> Result<Vec<TestFixture>> {
126    let mut fixtures = Vec::new();
127
128    if !fixtures_dir.exists() {
129        return Ok(fixtures);
130    }
131
132    for entry in fs::read_dir(fixtures_dir).map_err(|e| FixtureError::Io(e))? {
133        let entry = entry.map_err(|e| FixtureError::Io(e))?;
134        let path = entry.path();
135
136        if path.is_dir() {
137            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
138                if let Ok(fixture) = TestFixture::load(&path, name) {
139                    fixtures.push(fixture);
140                }
141            }
142        }
143    }
144
145    Ok(fixtures)
146}
147
148/// Discover files with specific extensions in a directory
149fn discover_files(dir: &Path, extensions: &[&str]) -> Result<Vec<PathBuf>> {
150    let mut files = Vec::new();
151
152    for entry in fs::read_dir(dir).map_err(|e| FixtureError::Io(e))? {
153        let entry = entry.map_err(|e| FixtureError::Io(e))?;
154        let path = entry.path();
155
156        if path.is_file() {
157            if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
158                if extensions.contains(&ext) {
159                    files.push(path);
160                }
161            }
162        }
163    }
164
165    Ok(files)
166}
167
168/// Recursively discover golden files
169fn discover_golden_files(dir: &Path, base_dir: &Path, files: &mut Vec<GoldenFile>) -> Result<()> {
170    for entry in fs::read_dir(dir).map_err(|e| FixtureError::Io(e))? {
171        let entry = entry.map_err(|e| FixtureError::Io(e))?;
172        let path = entry.path();
173
174        if path.is_file() {
175            if let Ok(relative_path) = path.strip_prefix(base_dir) {
176                if let Ok(golden) = GoldenFile::load(base_dir, relative_path) {
177                    files.push(golden);
178                }
179            }
180        } else if path.is_dir() {
181            discover_golden_files(&path, base_dir, files)?;
182        }
183    }
184
185    Ok(())
186}
187
188/// Recursively copy a directory
189fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
190    fs::create_dir_all(dst)?;
191
192    for entry in fs::read_dir(src)? {
193        let entry = entry?;
194        let path = entry.path();
195        let file_name = entry.file_name();
196        let dest_path = dst.join(&file_name);
197
198        if path.is_dir() {
199            copy_dir_recursive(&path, &dest_path)?;
200        } else {
201            fs::copy(&path, &dest_path)?;
202        }
203    }
204
205    Ok(())
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_discover_files() {
214        let temp_dir = tempfile::TempDir::new().unwrap();
215        let test_dir = temp_dir.path().join("test");
216        fs::create_dir(&test_dir).unwrap();
217
218        fs::write(test_dir.join("file1.ttl"), "").unwrap();
219        fs::write(test_dir.join("file2.ttl"), "").unwrap();
220        fs::write(test_dir.join("file3.rdf"), "").unwrap();
221        fs::write(test_dir.join("file4.txt"), "").unwrap();
222
223        let ttl_files = discover_files(&test_dir, &["ttl"]).unwrap();
224        assert_eq!(ttl_files.len(), 2);
225
226        let rdf_files = discover_files(&test_dir, &["rdf"]).unwrap();
227        assert_eq!(rdf_files.len(), 1);
228
229        let all_files = discover_files(&test_dir, &["ttl", "rdf"]).unwrap();
230        assert_eq!(all_files.len(), 3);
231    }
232
233    #[test]
234    fn test_copy_dir_recursive() {
235        let temp_dir = tempfile::TempDir::new().unwrap();
236        let src = temp_dir.path().join("src");
237        let dst = temp_dir.path().join("dst");
238
239        fs::create_dir(&src).unwrap();
240        fs::create_dir(src.join("subdir")).unwrap();
241        fs::write(src.join("file1.txt"), "content1").unwrap();
242        fs::write(src.join("subdir/file2.txt"), "content2").unwrap();
243
244        copy_dir_recursive(&src, &dst).unwrap();
245
246        assert!(dst.join("file1.txt").exists());
247        assert!(dst.join("subdir/file2.txt").exists());
248        assert_eq!(
249            fs::read_to_string(dst.join("file1.txt")).unwrap(),
250            "content1"
251        );
252    }
253
254    #[test]
255    fn test_fixture_copy_to_temp() {
256        let temp_dir = tempfile::TempDir::new().unwrap();
257        let fixture_dir = temp_dir.path().join("fixture");
258
259        fs::create_dir(&fixture_dir).unwrap();
260        fs::create_dir(fixture_dir.join("ontology")).unwrap();
261        fs::create_dir(fixture_dir.join("templates")).unwrap();
262        fs::write(fixture_dir.join("ggen.toml"), "[package]").unwrap();
263        fs::write(fixture_dir.join("ontology/schema.ttl"), "").unwrap();
264        fs::write(fixture_dir.join("templates/main.tera"), "").unwrap();
265
266        let fixture = TestFixture::load(&fixture_dir, "test").unwrap();
267        let copy = fixture.copy_to_temp().unwrap();
268
269        assert!(copy.path().join("ggen.toml").exists());
270        assert!(copy.path().join("ontology/schema.ttl").exists());
271    }
272}