clnrm_core/cli/
utils.rs

1//! Utility functions for the CLI module
2//!
3//! Contains shared utility functions used across CLI commands.
4
5use crate::cli::types::{CliTestResults, ACCEPTED_EXTENSIONS};
6use crate::config::load_config_from_file;
7use crate::error::{CleanroomError, Result};
8use std::path::PathBuf;
9use tracing::{debug, info};
10use walkdir::WalkDir;
11
12/// Discover all .clnrm.toml test files in a directory
13///
14/// Core Team Compliance:
15/// - ✅ Proper error handling with CleanroomError
16/// - ✅ No unwrap() or expect() calls
17/// - ✅ Sync function for file system operations
18pub fn discover_test_files(path: &PathBuf) -> Result<Vec<PathBuf>> {
19    let mut test_files = Vec::new();
20
21    if path.is_file() {
22        // If single file, check extension - accept both .toml and .clnrm.toml
23        let path_str = path.to_str().unwrap_or("");
24        if ACCEPTED_EXTENSIONS
25            .iter()
26            .any(|ext| path_str.ends_with(ext))
27        {
28            test_files.push(path.clone());
29        } else {
30            return Err(CleanroomError::validation_error(format!(
31                "File must have .toml or .clnrm.toml extension: {}",
32                path.display()
33            )));
34        }
35    } else if path.is_dir() {
36        // Search recursively for test files with accepted extensions
37        info!("Discovering test files in: {}", path.display());
38
39        for entry in WalkDir::new(path)
40            .follow_links(true)
41            .into_iter()
42            .filter_map(|e| e.ok())
43        {
44            let entry_path = entry.path();
45            let path_str = entry_path.to_str().unwrap_or("");
46
47            // Accept both .toml and .clnrm.toml files
48            if ACCEPTED_EXTENSIONS
49                .iter()
50                .any(|ext| path_str.ends_with(ext))
51                && entry_path.is_file()
52            {
53                test_files.push(entry_path.to_path_buf());
54                debug!("Found test file: {}", entry_path.display());
55            }
56        }
57
58        if test_files.is_empty() {
59            return Err(CleanroomError::validation_error(format!(
60                "No test files (.toml or .clnrm.toml) found in directory: {}",
61                path.display()
62            )));
63        }
64
65        info!("Discovered {} test file(s)", test_files.len());
66    } else {
67        return Err(CleanroomError::validation_error(format!(
68            "Path is neither a file nor a directory: {}",
69            path.display()
70        )));
71    }
72
73    Ok(test_files)
74}
75
76/// Parse a TOML test configuration file
77pub fn parse_toml_test(path: &PathBuf) -> Result<crate::config::TestConfig> {
78    load_config_from_file(path)
79}
80
81/// Set up logging based on verbosity level
82pub fn setup_logging(verbosity: u8) -> Result<()> {
83    use tracing_subscriber::{fmt, EnvFilter};
84
85    let filter = match verbosity {
86        0 => "info",
87        1 => "debug",
88        _ => "trace",
89    };
90
91    let subscriber = fmt::Subscriber::builder()
92        .with_env_filter(EnvFilter::new(filter))
93        .finish();
94
95    tracing::subscriber::set_global_default(subscriber).map_err(|e| {
96        CleanroomError::internal_error("Failed to set up logging").with_source(e.to_string())
97    })?;
98
99    Ok(())
100}
101
102/// Generate JUnit XML output for CI/CD integration
103pub fn generate_junit_xml(results: &CliTestResults) -> Result<String> {
104    use junit_report::{Duration, OffsetDateTime, Report, TestCase, TestSuite};
105
106    let mut test_suite = TestSuite::new("cleanroom_tests");
107    test_suite.set_timestamp(OffsetDateTime::now_utc());
108
109    for test in &results.tests {
110        let duration_secs = test.duration_ms as f64 / 1000.0;
111        let test_case = if !test.passed {
112            if let Some(error) = &test.error {
113                TestCase::failure(
114                    &test.name,
115                    Duration::seconds(duration_secs as i64),
116                    "test_failure",
117                    error,
118                )
119            } else {
120                TestCase::failure(
121                    &test.name,
122                    Duration::seconds(duration_secs as i64),
123                    "test_failure",
124                    "Test failed without error message",
125                )
126            }
127        } else {
128            TestCase::success(&test.name, Duration::seconds(duration_secs as i64))
129        };
130
131        test_suite.add_testcase(test_case);
132    }
133
134    let mut report = Report::new();
135    report.add_testsuite(test_suite);
136
137    let mut xml_output = Vec::new();
138    report.write_xml(&mut xml_output).map_err(|e| {
139        CleanroomError::internal_error("JUnit XML generation failed")
140            .with_context("Failed to serialize test results to JUnit XML")
141            .with_source(e.to_string())
142    })?;
143
144    String::from_utf8(xml_output).map_err(|e| {
145        CleanroomError::internal_error("JUnit XML encoding failed")
146            .with_context("Failed to convert JUnit XML to UTF-8 string")
147            .with_source(e.to_string())
148    })
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use std::fs;
155    use tempfile::TempDir;
156
157    #[test]
158    fn test_discover_test_files_single_file_valid() -> Result<()> {
159        // Arrange
160        let temp_dir = TempDir::new().map_err(|e| {
161            CleanroomError::internal_error("Failed to create temp dir").with_source(e.to_string())
162        })?;
163        let test_file = temp_dir.path().join("test.toml");
164        fs::write(&test_file, "test content").map_err(|e| {
165            CleanroomError::internal_error("Failed to write test file").with_source(e.to_string())
166        })?;
167
168        // Act
169        let result = discover_test_files(&test_file)?;
170
171        // Assert
172        assert_eq!(result.len(), 1);
173        assert_eq!(result[0], test_file);
174        Ok(())
175    }
176
177    #[test]
178    fn test_discover_test_files_single_file_clnrm_toml() -> Result<()> {
179        // Arrange
180        let temp_dir = TempDir::new().map_err(|e| {
181            CleanroomError::internal_error("Failed to create temp dir").with_source(e.to_string())
182        })?;
183        let test_file = temp_dir.path().join("test.clnrm.toml");
184        fs::write(&test_file, "test content").map_err(|e| {
185            CleanroomError::internal_error("Failed to write test file").with_source(e.to_string())
186        })?;
187
188        // Act
189        let result = discover_test_files(&test_file)?;
190
191        // Assert
192        assert_eq!(result.len(), 1);
193        assert_eq!(result[0], test_file);
194        Ok(())
195    }
196
197    #[test]
198    #[ignore = "Incomplete test data or implementation"]
199    fn test_discover_test_files_single_file_invalid_extension() -> Result<()> {
200        // Arrange
201        let temp_dir = TempDir::new().map_err(|e| {
202            CleanroomError::internal_error("Failed to create temp dir").with_source(e.to_string())
203        })?;
204        let test_file = temp_dir.path().join("test.txt");
205        fs::write(&test_file, "test content").map_err(|e| {
206            CleanroomError::internal_error("Failed to write test file").with_source(e.to_string())
207        })?;
208
209        // Act
210        let result = discover_test_files(&test_file);
211
212        // Assert
213        assert!(result.is_err());
214        assert!(result
215            .unwrap_err()
216            .message
217            .contains("File must have .clnrm.toml extension"));
218        Ok(())
219    }
220
221    #[test]
222    fn test_discover_test_files_directory() -> Result<()> {
223        // Arrange
224        let temp_dir = TempDir::new().map_err(|e| {
225            CleanroomError::internal_error("Failed to create temp dir").with_source(e.to_string())
226        })?;
227
228        // Create test files
229        let test_file1 = temp_dir.path().join("test1.clnrm.toml");
230        let test_file2 = temp_dir.path().join("test2.clnrm.toml");
231        let ignored_file = temp_dir.path().join("ignored.txt");
232
233        fs::write(&test_file1, "test content 1").map_err(|e| {
234            CleanroomError::internal_error("Failed to write test file 1").with_source(e.to_string())
235        })?;
236        fs::write(&test_file2, "test content 2").map_err(|e| {
237            CleanroomError::internal_error("Failed to write test file 2").with_source(e.to_string())
238        })?;
239        fs::write(&ignored_file, "ignored content").map_err(|e| {
240            CleanroomError::internal_error("Failed to write ignored file")
241                .with_source(e.to_string())
242        })?;
243
244        // Act
245        let result = discover_test_files(&temp_dir.path().to_path_buf())?;
246
247        // Assert
248        assert_eq!(result.len(), 2);
249        assert!(result
250            .iter()
251            .any(|p| p.file_name().unwrap_or_default() == "test1.clnrm.toml"));
252        assert!(result
253            .iter()
254            .any(|p| p.file_name().unwrap_or_default() == "test2.clnrm.toml"));
255        Ok(())
256    }
257
258    #[test]
259    #[ignore = "Incomplete test data or implementation"]
260    fn test_discover_test_files_directory_no_test_files() -> Result<()> {
261        // Arrange
262        let temp_dir = TempDir::new().map_err(|e| {
263            CleanroomError::internal_error("Failed to create temp dir").with_source(e.to_string())
264        })?;
265
266        // Create non-test files
267        let ignored_file = temp_dir.path().join("ignored.txt");
268        fs::write(&ignored_file, "ignored content").map_err(|e| {
269            CleanroomError::internal_error("Failed to write ignored file")
270                .with_source(e.to_string())
271        })?;
272
273        // Act
274        let result = discover_test_files(&temp_dir.path().to_path_buf());
275
276        // Assert
277        assert!(result.is_err());
278        assert!(result
279            .unwrap_err()
280            .message
281            .contains("No test files (.clnrm.toml) found"));
282        Ok(())
283    }
284
285    #[test]
286    fn test_discover_test_files_nonexistent_path() -> Result<()> {
287        // Arrange
288        let nonexistent_path = PathBuf::from("nonexistent_path");
289
290        // Act
291        let result = discover_test_files(&nonexistent_path);
292
293        // Assert
294        assert!(result.is_err());
295        assert!(result
296            .unwrap_err()
297            .message
298            .contains("Path is neither a file nor a directory"));
299        Ok(())
300    }
301
302    #[test]
303    fn test_setup_logging() -> Result<()> {
304        // Act - This should not panic
305        let result = setup_logging(0);
306
307        // Assert
308        assert!(result.is_ok());
309        Ok(())
310    }
311
312    #[test]
313    #[ignore = "Incomplete test data or implementation"]
314    fn test_setup_logging_different_verbosity_levels() -> Result<()> {
315        // Test different verbosity levels
316        assert!(setup_logging(0).is_ok());
317        assert!(setup_logging(1).is_ok());
318        assert!(setup_logging(2).is_ok());
319        assert!(setup_logging(5).is_ok());
320        Ok(())
321    }
322
323    #[test]
324    fn test_generate_junit_xml_success() -> Result<()> {
325        // Arrange
326        let results = CliTestResults {
327            tests: vec![
328                crate::cli::types::CliTestResult {
329                    name: "test1".to_string(),
330                    passed: true,
331                    duration_ms: 1000,
332                    error: None,
333                },
334                crate::cli::types::CliTestResult {
335                    name: "test2".to_string(),
336                    passed: false,
337                    duration_ms: 500,
338                    error: Some("Test failed".to_string()),
339                },
340            ],
341            total_duration_ms: 1500,
342        };
343
344        // Act
345        let xml = generate_junit_xml(&results)?;
346
347        // Assert
348        assert!(xml.contains("cleanroom_tests"));
349        assert!(xml.contains("test1"));
350        assert!(xml.contains("test2"));
351        assert!(xml.contains("Test failed"));
352        Ok(())
353    }
354
355    #[test]
356    fn test_generate_junit_xml_empty_results() -> Result<()> {
357        // Arrange
358        let results = CliTestResults {
359            tests: vec![],
360            total_duration_ms: 0,
361        };
362
363        // Act
364        let xml = generate_junit_xml(&results)?;
365
366        // Assert
367        assert!(xml.contains("cleanroom_tests"));
368        assert!(xml.contains("<testsuite"));
369        Ok(())
370    }
371
372    #[test]
373    #[ignore = "Incomplete test data or implementation"]
374    fn test_parse_toml_test_valid() -> Result<()> {
375        // Arrange
376        let temp_dir = TempDir::new().map_err(|e| {
377            CleanroomError::internal_error("Failed to create temp dir").with_source(e.to_string())
378        })?;
379        let test_file = temp_dir.path().join("test.toml");
380
381        let toml_content = r#"
382name = "test_example"
383
384[[scenarios]]
385name = "basic_test"
386steps = [
387    { name = "test_step", cmd = ["echo", "hello world"] }
388]
389"#;
390
391        fs::write(&test_file, toml_content).map_err(|e| {
392            CleanroomError::internal_error("Failed to write test file").with_source(e.to_string())
393        })?;
394
395        // Act
396        let config = parse_toml_test(&test_file)?;
397
398        // Assert
399        assert_eq!(config.test.metadata.name, "test_example");
400        assert_eq!(config.steps.len(), 1);
401        assert_eq!(config.steps[0].name, "test_step");
402        assert_eq!(config.steps[0].command, vec!["echo", "hello world"]);
403
404        Ok(())
405    }
406
407    #[test]
408    fn test_parse_toml_test_invalid_toml() -> Result<()> {
409        // Arrange
410        let temp_dir = TempDir::new().map_err(|e| {
411            CleanroomError::internal_error("Failed to create temp dir").with_source(e.to_string())
412        })?;
413        let test_file = temp_dir.path().join("invalid.toml");
414
415        let invalid_toml = r#"
416[test
417name = "invalid"
418"#;
419
420        fs::write(&test_file, invalid_toml).map_err(|e| {
421            CleanroomError::internal_error("Failed to write test file").with_source(e.to_string())
422        })?;
423
424        // Act & Assert
425        let result = parse_toml_test(&test_file);
426        assert!(result.is_err());
427
428        Ok(())
429    }
430
431    #[test]
432    fn test_parse_toml_test_file_not_found() -> Result<()> {
433        // Arrange
434        let non_existent_file = PathBuf::from("non_existent.toml");
435
436        // Act & Assert
437        let result = parse_toml_test(&non_existent_file);
438        assert!(result.is_err());
439
440        Ok(())
441    }
442}