diff --git a/src/assertions/file.rs b/src/assertions/file.rs index 41dc94d..9be518c 100644 --- a/src/assertions/file.rs +++ b/src/assertions/file.rs @@ -43,14 +43,39 @@ pub fn check_file_content( } /// Check if workspace file exists -pub fn check_workspace_file(work_dir: &Path, file_path: &str) -> Result<(), String> { +pub fn check_workspace_file( + work_dir: &Path, + file_path: &str, + copy_to_output: bool, + log_output_dir: Option<&Path>, +) -> Result<(), String> { let full_path = work_dir.join(file_path); - if full_path.exists() && full_path.is_file() { - Ok(()) - } else { - Err(format!("Workspace file '{}' does not exist", file_path)) + if !(full_path.exists() && full_path.is_file()) { + return Err(format!("Workspace file '{}' does not exist", file_path)); + } + + // Copy file to output directory if requested + if copy_to_output { + if let Some(output_dir) = log_output_dir { + if let Err(e) = copy_file_to_dir(&full_path, file_path, output_dir) { + return Err(format!("Failed to copy '{}' to output: {}", file_path, e)); + } + } } + + Ok(()) +} + +/// Copy a file to the output directory, preserving its relative path structure +fn copy_file_to_dir(source: &Path, relative_path: &str, output_dir: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(output_dir)?; + let dest = output_dir.join(relative_path); + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::copy(source, &dest)?; + Ok(()) } /// Check if workspace directory exists diff --git a/src/assertions/mod.rs b/src/assertions/mod.rs index 6539ebb..ca6b225 100644 --- a/src/assertions/mod.rs +++ b/src/assertions/mod.rs @@ -23,15 +23,17 @@ pub struct AssertionChecker { #[cfg(not(test))] log_data: Vec, work_dir: std::path::PathBuf, + log_output_dir: Option, } impl AssertionChecker { /// Create a new assertion checker - pub fn new(log_file: &Path, work_dir: &Path) -> Self { + pub fn new(log_file: &Path, work_dir: &Path, log_output_dir: Option<&Path>) -> Self { let log_data = Self::load_log_file(log_file); Self { log_data, work_dir: work_dir.to_path_buf(), + log_output_dir: log_output_dir.map(|p| p.to_path_buf()), } } @@ -71,7 +73,12 @@ impl AssertionChecker { let result = match cmd.as_str() { "workspace-file" => { let path = check.command.path.as_ref().ok_or("Missing path")?; - file::check_workspace_file(&self.work_dir, path) + file::check_workspace_file( + &self.work_dir, + path, + check.command.copy_to_output.unwrap_or(false), + self.log_output_dir.as_deref(), + ) } "workspace-dir" => { let path = check.command.path.as_ref().ok_or("Missing path")?; diff --git a/src/assertions/tests.rs b/src/assertions/tests.rs index e1fc615..7623c46 100644 --- a/src/assertions/tests.rs +++ b/src/assertions/tests.rs @@ -8,7 +8,7 @@ mod tests { fn create_checker(log_file: &str) -> AssertionChecker { let log_path = Path::new(log_file); let work_dir = tempfile::tempdir().unwrap(); - AssertionChecker::new(log_path, work_dir.path()) + AssertionChecker::new(log_path, work_dir.path(), None) } #[test] @@ -132,11 +132,20 @@ mod tests { let test_file = work_dir.path().join("output.txt"); std::fs::write(&test_file, "test content").unwrap(); - let result = crate::assertions::file::check_workspace_file(work_dir.path(), "output.txt"); + let result = crate::assertions::file::check_workspace_file( + work_dir.path(), + "output.txt", + false, + None, + ); assert!(result.is_ok(), "workspace file should exist"); - let result = - crate::assertions::file::check_workspace_file(work_dir.path(), "nonexistent.txt"); + let result = crate::assertions::file::check_workspace_file( + work_dir.path(), + "nonexistent.txt", + false, + None, + ); assert!(result.is_err(), "nonexistent file should not exist"); } @@ -188,7 +197,7 @@ mod tests { let empty_log = work_dir.path().join("empty.log"); std::fs::write(&empty_log, "").unwrap(); - let checker = AssertionChecker::new(&empty_log, work_dir.path()); + let checker = AssertionChecker::new(&empty_log, work_dir.path(), None); let init = checker.init_message(); assert!(init.is_none(), "empty log should not have init message"); } @@ -244,7 +253,7 @@ mod tests { let nonexistent_log = work_dir.path().join("nonexistent.log"); // Should not panic, just return empty checker - let checker = AssertionChecker::new(&nonexistent_log, work_dir.path()); + let checker = AssertionChecker::new(&nonexistent_log, work_dir.path(), None); assert_eq!( checker.log_data.len(), 0, diff --git a/src/models/check.rs b/src/models/check.rs index 9dad9c5..4f4dc1b 100644 --- a/src/models/check.rs +++ b/src/models/check.rs @@ -44,4 +44,6 @@ pub struct CheckData { pub query: Option, #[serde(default)] pub deny: Option, + #[serde(default)] + pub copy_to_output: Option, } diff --git a/src/runtime/executor.rs b/src/runtime/executor.rs index aeb0265..8ab9e49 100644 --- a/src/runtime/executor.rs +++ b/src/runtime/executor.rs @@ -155,7 +155,8 @@ impl TestExecutor { } // Run assertions - let checker = AssertionChecker::new(&log_path, workspace.path()); + let checker = + AssertionChecker::new(&log_path, workspace.path(), self.log_output_dir.as_deref()); let check_results: Vec = desc .test .checks