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::{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: &Path) -> 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
103///
104/// # Core Team Compliance
105/// - ✅ Proper error handling with CleanroomError
106/// - ✅ No unwrap() or expect() calls
107/// - ✅ Returns Result<String, CleanroomError>
108/// - ✅ Includes timestamp information
109pub fn generate_junit_xml(results: &CliTestResults) -> Result<String> {
110    use junit_report::{Duration, OffsetDateTime, Report, TestCase, TestSuite};
111
112    let mut test_suite = TestSuite::new("cleanroom_tests");
113    test_suite.set_timestamp(OffsetDateTime::now_utc());
114
115    for test in &results.tests {
116        let duration_secs = test.duration_ms as f64 / 1000.0;
117        let test_case = if !test.passed {
118            if let Some(error) = &test.error {
119                TestCase::failure(
120                    &test.name,
121                    Duration::seconds(duration_secs as i64),
122                    "test_failure",
123                    error,
124                )
125            } else {
126                TestCase::failure(
127                    &test.name,
128                    Duration::seconds(duration_secs as i64),
129                    "test_failure",
130                    "Test failed without error message",
131                )
132            }
133        } else {
134            TestCase::success(&test.name, Duration::seconds(duration_secs as i64))
135        };
136
137        test_suite.add_testcase(test_case);
138    }
139
140    let mut report = Report::new();
141    report.add_testsuite(test_suite);
142
143    let mut xml_output = Vec::new();
144    report.write_xml(&mut xml_output).map_err(|e| {
145        CleanroomError::internal_error("JUnit XML generation failed")
146            .with_context("Failed to serialize test results to JUnit XML")
147            .with_source(e.to_string())
148    })?;
149
150    String::from_utf8(xml_output).map_err(|e| {
151        CleanroomError::internal_error("JUnit XML encoding failed")
152            .with_context("Failed to convert JUnit XML to UTF-8 string")
153            .with_source(e.to_string())
154    })
155}