clnrm_core/formatting/
tap.rs

1//! TAP (Test Anything Protocol) Formatter
2//!
3//! Generates TAP version 13 compatible output.
4//! Widely used in Perl and other testing ecosystems.
5
6use crate::error::Result;
7use crate::formatting::formatter::{Formatter, FormatterType};
8use crate::formatting::test_result::{TestStatus, TestSuite};
9
10/// TAP formatter for test results
11#[derive(Debug, Default)]
12pub struct TapFormatter;
13
14impl TapFormatter {
15    /// Create a new TAP formatter
16    pub fn new() -> Self {
17        Self
18    }
19
20    /// Generate TAP version header
21    fn generate_header() -> String {
22        "TAP version 13".to_string()
23    }
24
25    /// Generate TAP plan line
26    fn generate_plan(total: usize) -> String {
27        format!("1..{}", total)
28    }
29
30    /// Generate TAP test line
31    fn generate_test_line(
32        index: usize,
33        result: &crate::formatting::test_result::TestResult,
34    ) -> Vec<String> {
35        let mut output = Vec::new();
36
37        let status = match result.status {
38            TestStatus::Passed => "ok",
39            TestStatus::Failed => "not ok",
40            TestStatus::Skipped => "ok",
41            TestStatus::Unknown => "not ok",
42        };
43
44        let mut line = format!("{} {} - {}", status, index, result.name);
45
46        // Add skip directive for skipped tests
47        if result.status == TestStatus::Skipped {
48            line.push_str(" # SKIP");
49        }
50
51        output.push(line);
52
53        // Add diagnostic lines for failures
54        if result.status == TestStatus::Failed {
55            if let Some(error) = &result.error {
56                output.push("  ---".to_string());
57                output.push(format!("  message: {}", Self::escape_yaml_string(error)));
58                output.push("  ...".to_string());
59            }
60        }
61
62        // Add duration if present
63        if let Some(duration) = result.duration {
64            output.push(format!("  # Duration: {:.3}s", duration.as_secs_f64()));
65        }
66
67        // Add stdout/stderr as diagnostics
68        if let Some(stdout) = &result.stdout {
69            output.push("  # stdout:".to_string());
70            for line in stdout.lines() {
71                output.push(format!("  # {}", line));
72            }
73        }
74
75        if let Some(stderr) = &result.stderr {
76            output.push("  # stderr:".to_string());
77            for line in stderr.lines() {
78                output.push(format!("  # {}", line));
79            }
80        }
81
82        output
83    }
84
85    /// Escape YAML string for TAP diagnostics
86    fn escape_yaml_string(s: &str) -> String {
87        // Simple escaping for YAML values in TAP
88        if s.contains('\n') || s.contains('#') || s.contains(':') {
89            format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
90        } else {
91            s.to_string()
92        }
93    }
94}
95
96impl Formatter for TapFormatter {
97    fn format(&self, suite: &TestSuite) -> Result<String> {
98        let mut output = Vec::new();
99
100        // TAP version header
101        output.push(Self::generate_header());
102
103        // TAP plan
104        output.push(Self::generate_plan(suite.total_count()));
105
106        // Test lines
107        for (index, result) in suite.results.iter().enumerate() {
108            let test_lines = Self::generate_test_line(index + 1, result);
109            output.extend(test_lines);
110        }
111
112        // Summary comment
113        output.push(format!(
114            "# tests {}, passed {}, failed {}, skipped {}",
115            suite.total_count(),
116            suite.passed_count(),
117            suite.failed_count(),
118            suite.skipped_count()
119        ));
120
121        if let Some(duration) = suite.duration {
122            output.push(format!("# duration: {:.3}s", duration.as_secs_f64()));
123        }
124
125        Ok(output.join("\n"))
126    }
127
128    fn name(&self) -> &'static str {
129        "tap"
130    }
131
132    fn formatter_type(&self) -> FormatterType {
133        FormatterType::Tap
134    }
135}