cli_testing_specialist/reporter/
markdown.rs

1use crate::error::Result;
2use crate::types::{TestReport, TestStatus};
3use std::fs;
4use std::path::Path;
5
6/// Markdown report generator
7pub struct MarkdownReporter;
8
9impl MarkdownReporter {
10    /// Generate Markdown report from test results
11    pub fn generate(report: &TestReport, output_path: &Path) -> Result<()> {
12        let mut content = String::new();
13
14        // Header
15        content.push_str(&format!("# Test Report: {}\n\n", report.binary_name));
16
17        if let Some(version) = &report.binary_version {
18            content.push_str(&format!("**Version:** {}\n\n", version));
19        }
20
21        content.push_str(&format!(
22            "**Generated:** {}\n\n",
23            report.finished_at.format("%Y-%m-%d %H:%M:%S UTC")
24        ));
25
26        // Summary section
27        content.push_str("## Summary\n\n");
28
29        let success_rate = (report.success_rate() * 100.0) as u32;
30        let status_emoji = if report.all_passed() {
31            "✅"
32        } else if success_rate >= 80 {
33            "⚠️"
34        } else {
35            "❌"
36        };
37
38        content.push_str(&format!(
39            "**Overall Status:** {} {}% passed\n\n",
40            status_emoji, success_rate
41        ));
42
43        content.push_str("| Metric | Value |\n");
44        content.push_str("|--------|-------|\n");
45        content.push_str(&format!("| Total Tests | {} |\n", report.total_tests()));
46        content.push_str(&format!("| Passed | ✅ {} |\n", report.total_passed()));
47        content.push_str(&format!("| Failed | ❌ {} |\n", report.total_failed()));
48        content.push_str(&format!("| Skipped | ⏭️ {} |\n", report.total_skipped()));
49        content.push_str(&format!(
50            "| Duration | {:.2}s |\n",
51            report.total_duration.as_secs_f64()
52        ));
53        content.push_str(&format!("| Suites | {} |\n\n", report.suites.len()));
54
55        // Test Suites section
56        content.push_str("## Test Suites\n\n");
57
58        for suite in &report.suites {
59            let suite_success_rate = (suite.success_rate() * 100.0) as u32;
60            let suite_status = if suite.failed_count() == 0 {
61                "✅"
62            } else {
63                "❌"
64            };
65
66            content.push_str(&format!(
67                "### {} {} ({}%)\n\n",
68                suite_status, suite.name, suite_success_rate
69            ));
70
71            content.push_str(&format!("**File:** `{}`\n\n", suite.file_path));
72            content.push_str(&format!(
73                "**Duration:** {:.2}s\n\n",
74                suite.duration.as_secs_f64()
75            ));
76
77            // Suite summary table
78            content.push_str("| Status | Count |\n");
79            content.push_str("|--------|-------|\n");
80            content.push_str(&format!("| Passed | {} |\n", suite.passed_count()));
81            content.push_str(&format!("| Failed | {} |\n", suite.failed_count()));
82            content.push_str(&format!("| Skipped | {} |\n", suite.skipped_count()));
83            content.push_str(&format!("| Total | {} |\n\n", suite.total_count()));
84
85            // Show failed tests if any
86            let failed_tests: Vec<_> = suite
87                .tests
88                .iter()
89                .filter(|t| t.status.is_failure())
90                .collect();
91
92            if !failed_tests.is_empty() {
93                content.push_str("#### Failed Tests\n\n");
94
95                for test in failed_tests {
96                    content.push_str(&format!("- ❌ **{}**\n", test.name));
97                    if let Some(error) = &test.error_message {
98                        content.push_str(&format!("  - Error: `{}`\n", error));
99                    }
100                    if !test.output.is_empty() {
101                        content.push_str(&format!(
102                            "  - Output:\n    ```\n    {}\n    ```\n",
103                            test.output
104                        ));
105                    }
106                }
107                content.push('\n');
108            }
109        }
110
111        // Environment section
112        content.push_str("## Environment\n\n");
113        content.push_str("| Property | Value |\n");
114        content.push_str("|----------|-------|\n");
115        content.push_str(&format!(
116            "| OS | {} {} |\n",
117            report.environment.os, report.environment.os_version
118        ));
119        content.push_str(&format!(
120            "| Shell | {} |\n",
121            report.environment.shell_version
122        ));
123        content.push_str(&format!("| BATS | {} |\n", report.environment.bats_version));
124        content.push_str(&format!("| Hostname | {} |\n", report.environment.hostname));
125        content.push_str(&format!("| User | {} |\n", report.environment.user));
126
127        // Detailed Results section
128        content.push_str("\n## Detailed Results\n\n");
129
130        for suite in &report.suites {
131            content.push_str(&format!("### {}\n\n", suite.name));
132
133            content.push_str("| # | Test Name | Status | Duration |\n");
134            content.push_str("|---|-----------|--------|----------|\n");
135
136            for (idx, test) in suite.tests.iter().enumerate() {
137                let status_str = match test.status {
138                    TestStatus::Passed => "✅ Passed",
139                    TestStatus::Failed => "❌ Failed",
140                    TestStatus::Skipped => "⏭️ Skipped",
141                    TestStatus::Timeout => "⏱️ Timeout",
142                };
143
144                content.push_str(&format!(
145                    "| {} | {} | {} | {:.0}ms |\n",
146                    idx + 1,
147                    test.name,
148                    status_str,
149                    test.duration.as_millis()
150                ));
151            }
152            content.push('\n');
153        }
154
155        // Write to file
156        fs::write(output_path, content)?;
157
158        Ok(())
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::types::{EnvironmentInfo, TestResult, TestSuite};
166    use chrono::Utc;
167    use std::time::Duration;
168    use tempfile::NamedTempFile;
169
170    fn create_test_report() -> TestReport {
171        let suite = TestSuite {
172            name: "test_suite".to_string(),
173            file_path: "/path/to/test.bats".to_string(),
174            tests: vec![
175                TestResult {
176                    name: "successful test".to_string(),
177                    status: TestStatus::Passed,
178                    duration: Duration::from_millis(150),
179                    output: String::new(),
180                    error_message: None,
181                    file_path: "/path/to/test.bats".to_string(),
182                    line_number: Some(5),
183                    tags: vec![],
184                    priority: crate::types::TestPriority::Important,
185                },
186                TestResult {
187                    name: "failed test".to_string(),
188                    status: TestStatus::Failed,
189                    duration: Duration::from_millis(200),
190                    output: "error output".to_string(),
191                    error_message: Some("assertion failed".to_string()),
192                    file_path: "/path/to/test.bats".to_string(),
193                    line_number: Some(10),
194                    tags: vec![],
195                    priority: crate::types::TestPriority::Important,
196                },
197            ],
198            duration: Duration::from_millis(350),
199            started_at: Utc::now(),
200            finished_at: Utc::now(),
201        };
202
203        TestReport {
204            binary_name: "test-cli".to_string(),
205            binary_version: Some("1.0.0".to_string()),
206            suites: vec![suite],
207            total_duration: Duration::from_millis(350),
208            started_at: Utc::now(),
209            finished_at: Utc::now(),
210            environment: EnvironmentInfo::default(),
211            security_findings: vec![],
212        }
213    }
214
215    #[test]
216    fn test_markdown_generation() {
217        let report = create_test_report();
218        let temp_file = NamedTempFile::new().unwrap();
219        let output_path = temp_file.path();
220
221        MarkdownReporter::generate(&report, output_path).unwrap();
222
223        let content = fs::read_to_string(output_path).unwrap();
224
225        // Verify header
226        assert!(content.contains("# Test Report: test-cli"));
227        assert!(content.contains("**Version:** 1.0.0"));
228
229        // Verify summary
230        assert!(content.contains("## Summary"));
231        assert!(content.contains("Total Tests"));
232        assert!(content.contains("| 2 |"));
233
234        // Verify suite information
235        assert!(content.contains("## Test Suites"));
236        assert!(content.contains("test_suite"));
237
238        // Verify detailed results
239        assert!(content.contains("## Detailed Results"));
240        assert!(content.contains("successful test"));
241        assert!(content.contains("failed test"));
242        assert!(content.contains("✅ Passed"));
243        assert!(content.contains("❌ Failed"));
244
245        // Verify environment section
246        assert!(content.contains("## Environment"));
247    }
248
249    #[test]
250    fn test_markdown_all_passed() {
251        let suite = TestSuite {
252            name: "all_pass".to_string(),
253            file_path: "/test.bats".to_string(),
254            tests: vec![TestResult {
255                name: "test".to_string(),
256                status: TestStatus::Passed,
257                duration: Duration::from_millis(100),
258                output: String::new(),
259                error_message: None,
260                file_path: "/test.bats".to_string(),
261                line_number: None,
262                tags: vec![],
263                priority: crate::types::TestPriority::Important,
264            }],
265            duration: Duration::from_millis(100),
266            started_at: Utc::now(),
267            finished_at: Utc::now(),
268        };
269
270        let report = TestReport {
271            binary_name: "cli".to_string(),
272            binary_version: None,
273            suites: vec![suite],
274            total_duration: Duration::from_millis(100),
275            started_at: Utc::now(),
276            finished_at: Utc::now(),
277            environment: EnvironmentInfo::default(),
278            security_findings: vec![],
279        };
280
281        let temp_file = NamedTempFile::new().unwrap();
282        MarkdownReporter::generate(&report, temp_file.path()).unwrap();
283
284        let content = fs::read_to_string(temp_file.path()).unwrap();
285        assert!(content.contains("✅ 100% passed"));
286    }
287}