prx 0.5.9

Praxis — agent-native Unix tools. Single binary replacing grep, cat, find, sed, diff for AI coding agents.
use super::{Diagnostic, ParsedResult, define_regex};

define_regex!(COVERAGE_TABLE_RE, r"^All files\s+\|\s+([\d.]+)\s+\|");
define_regex!(
    FILE_COV_RE,
    r"^\s+(\S+\.(?:ts|tsx|js|jsx))\s+\|\s+([\d.]+)\s+\|"
);
define_regex!(TEST_SUMMARY_RE, r"Tests:\s+(\d+) passed, (\d+) total");

pub fn parse(output: &str) -> ParsedResult {
    let mut overall_coverage = String::new();
    let mut warnings = Vec::new();
    let mut passed = 0;
    let mut failed = 0;

    for line in output.lines() {
        if let Some(caps) = COVERAGE_TABLE_RE.captures(line) {
            overall_coverage = caps[1].to_string();
        }

        if let Some(caps) = FILE_COV_RE.captures(line) {
            let file = &caps[1];
            let pct_str = &caps[2];
            let pct: f32 = pct_str.parse().unwrap_or(100.0);
            if pct < 80.0 {
                warnings.push(Diagnostic {
                    name: file.to_string(),
                    location: None,
                    message: format!("{}% statement coverage", pct_str),
                });
            }
        }

        if let Some(caps) = TEST_SUMMARY_RE.captures(line) {
            passed = caps[1].parse().unwrap_or(0);
            let total: usize = caps[2].parse().unwrap_or(0);
            failed = total.saturating_sub(passed);
        }
    }

    let summary = if overall_coverage.is_empty() {
        format!("{passed} passed, {failed} failed")
    } else {
        format!("{}% statement coverage", overall_coverage)
    };

    ParsedResult {
        summary,
        passed,
        failed,
        skipped: 0,
        failures: vec![],
        warnings,
        tail: None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_jest_coverage_table() {
        let output = "\
----------|---------|----------|---------|---------|
File      | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files |   85.71 |      100 |   83.33 |   85.71 |
 auth.ts  |   71.42 |      100 |   66.66 |   71.42 |
 index.ts |  100    |      100 |  100    |  100    |
----------|---------|----------|---------|---------|

Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total
";
        let result = parse(output);
        assert!(result.summary.contains("85.71%"));
        assert_eq!(result.warnings.len(), 1);
        assert_eq!(result.warnings[0].name, "auth.ts");
        assert!(result.warnings[0].message.contains("71.42%"));
        assert_eq!(result.passed, 5);
    }

    #[test]
    fn parse_jest_all_high_coverage() {
        let output = "\
----------|---------|----------|---------|---------|
File      | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files |   95.00 |      100 |   95.00 |   95.00 |
 auth.ts  |   95.00 |      100 |   95.00 |   95.00 |
 index.ts |  100    |      100 |  100    |  100    |
----------|---------|----------|---------|---------|

Tests:       10 passed, 10 total
";
        let result = parse(output);
        assert!(result.summary.contains("95.00%"));
        assert!(result.warnings.is_empty());
        assert_eq!(result.passed, 10);
    }

    #[test]
    fn parse_jest_no_coverage_table() {
        let output = "Tests:       3 passed, 3 total\n";
        let result = parse(output);
        assert_eq!(result.passed, 3);
        assert!(!result.summary.contains("%"));
    }
}