xacli_testing/testing/
execute.rs

1use std::collections::VecDeque;
2
3use indexmap::IndexMap;
4
5use super::{ctx::MockContext, test::TestCase};
6use crate::{
7    report::xacli::{
8        TestCaseConfig, TestCaseResult, TestCaseStatus, TestError, TestFailure, TestSuiteResult,
9        TestSuitesResult,
10    },
11    spec, TestingApp,
12};
13
14pub struct ExecuteResult {
15    pub start_time: std::time::Instant,
16    pub duration: std::time::Duration,
17    pub code: i64,
18    pub stdout: String,
19    pub stderr: String,
20}
21
22impl TestingApp {
23    /// Execute a test case and return the test result
24    ///
25    /// This method parses the arguments, runs the application with a mock context,
26    /// and validates all assertions.
27    pub fn execute(&self, test_case: TestCase) -> TestCaseResult {
28        let start_time = std::time::Instant::now();
29        let timestamp = std::time::SystemTime::now();
30
31        // Extract test config from test case
32        let config = TestCaseConfig {
33            commands: test_case.commands.clone(),
34            args: test_case.args.clone(),
35        };
36
37        // Build full args: [app_name, ...commands, ...args]
38        let mut full_args = vec![self.app.info.name.clone()];
39        full_args.extend(test_case.commands.clone());
40        full_args.extend(test_case.args.clone());
41
42        let ctx_info = match self.app.parse(full_args) {
43            Ok(info) => info,
44            Err(e) => {
45                return TestCaseResult {
46                    config,
47                    timestamp,
48                    duration: std::time::Duration::new(0, 0),
49                    stdout: String::new(),
50                    stderr: e.to_string(),
51                    status: TestCaseStatus::Error {
52                        error: TestError {
53                            message: "Failed to parse arguments".to_string(),
54                        },
55                    },
56                };
57            }
58        };
59
60        let mut ctx = MockContext::new(
61            ctx_info,
62            VecDeque::from(test_case.input_events.clone()),
63            self.config.tty,
64        );
65
66        let result = self.app.execute_with_ctx(&mut ctx);
67        let code = match &result {
68            Ok(_) => 0,
69            Err(_) => 1,
70        };
71
72        let end_time = std::time::Instant::now();
73        let duration = end_time.duration_since(start_time);
74
75        let stdout = ctx.stdout_plain();
76        let stderr = ctx.stderr_plain();
77
78        // Build ExecuteResult for assertions
79        let execute_result = ExecuteResult {
80            start_time,
81            duration,
82            code,
83            stdout: stdout.clone(),
84            stderr: stderr.clone(),
85        };
86
87        // If execution failed, return Error status immediately
88        if let Err(e) = result {
89            return TestCaseResult {
90                config,
91                timestamp,
92                duration,
93                stdout,
94                stderr: format!("{}\n{}", stderr, e),
95                status: TestCaseStatus::Error {
96                    error: TestError {
97                        message: e.to_string(),
98                    },
99                },
100            };
101        }
102
103        // Run all assertions
104        let mut all_success = true;
105        let mut error_messages = Vec::new();
106
107        for asserter in &test_case.assertions {
108            if let Ok(result) = asserter.validate(&execute_result) {
109                if !result.success {
110                    all_success = false;
111                    error_messages.extend(result.messages);
112                }
113            }
114        }
115
116        // Determine status based on assertions
117        let status = if all_success {
118            TestCaseStatus::Passed
119        } else {
120            TestCaseStatus::Failed {
121                failure: TestFailure {
122                    message: error_messages.join("\n"),
123                },
124            }
125        };
126
127        TestCaseResult {
128            config,
129            timestamp,
130            duration,
131            stdout,
132            stderr,
133            status,
134        }
135    }
136
137    /// Execute all tests in a spec file and return results
138    ///
139    /// This method iterates through all suites and tests in the spec file,
140    /// converts each test case, executes it, and collects results.
141    pub fn execute_spec(&self, spec: &spec::SpecFile) -> TestSuitesResult {
142        let mut suites_result = IndexMap::new();
143
144        for (suite_name, suite) in &spec.suite {
145            // Skip suite if marked as skip
146            if suite.features.skip {
147                continue;
148            }
149
150            let mut suite_result = IndexMap::new();
151
152            for (test_name, test_case) in &suite.test {
153                // Skip test if marked as skip
154                if test_case.features.skip {
155                    continue;
156                }
157
158                // Convert and execute test case
159                let test_result = match TestCase::try_from((test_name.as_str(), test_case)) {
160                    Ok(testing_case) => {
161                        let mut result = self.execute(testing_case);
162                        // Update config with commands from spec
163                        result.config.commands = test_case.commands.clone();
164                        result
165                    }
166                    Err(e) => TestCaseResult {
167                        config: TestCaseConfig {
168                            commands: test_case.commands.clone(),
169                            args: test_case.args.clone(),
170                        },
171                        timestamp: std::time::SystemTime::now(),
172                        duration: std::time::Duration::default(),
173                        stdout: String::new(),
174                        stderr: e.to_string(),
175                        status: TestCaseStatus::Error {
176                            error: TestError {
177                                message: format!("Failed to convert test case: {}", e),
178                            },
179                        },
180                    },
181                };
182
183                suite_result.insert(test_name.clone(), test_result);
184            }
185
186            if !suite_result.is_empty() {
187                suites_result.insert(
188                    suite_name.clone(),
189                    TestSuiteResult {
190                        tests: suite_result,
191                    },
192                );
193            }
194        }
195
196        TestSuitesResult {
197            suites: suites_result,
198        }
199    }
200}