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!(
    ISSUE_RE,
    r"^(.+?)\((\d+),(\d+)\): (error|warning) (CS\d+): (.+)$"
);
define_regex!(BUILD_RE, r"^Build (succeeded|FAILED)\.");
define_regex!(
    TEST_RE,
    r"(?:Failed|Passed)!\s+-\s+Failed:\s+(\d+),\s+Passed:\s+(\d+),\s+Skipped:\s+(\d+)"
);

pub fn parse(output: &str) -> ParsedResult {
    let mut failures = Vec::new();
    let mut warnings = Vec::new();
    let mut build_result: Option<&str> = None;
    let mut passed = 0;
    let mut failed = 0;
    let mut skipped = 0;

    for line in output.lines() {
        if let Some(caps) = ISSUE_RE.captures(line) {
            let severity = &caps[4];
            let diag = Diagnostic {
                name: format!("dotnet/{}", &caps[5]),
                location: Some(format!("{}:{}:{}", &caps[1], &caps[2], &caps[3])),
                message: caps[6].to_string(),
            };
            if severity == "error" {
                failures.push(diag);
            } else {
                warnings.push(diag);
            }
            continue;
        }

        if let Some(caps) = BUILD_RE.captures(line) {
            build_result = Some(if &caps[1] == "succeeded" {
                "succeeded"
            } else {
                "FAILED"
            });
        }

        if let Some(caps) = TEST_RE.captures(line) {
            failed = caps[1].parse().unwrap_or(0);
            passed = caps[2].parse().unwrap_or(0);
            skipped = caps[3].parse().unwrap_or(0);
        }
    }

    let summary = match build_result {
        Some("succeeded") => format!("build succeeded, {} warning(s)", warnings.len()),
        Some("FAILED") => format!("build FAILED, {} error(s)", failures.len()),
        _ if passed + failed + skipped > 0 => {
            format!("{passed} passed, {failed} failed, {skipped} skipped")
        }
        _ => format!("{} error(s), {} warning(s)", failures.len(), warnings.len()),
    };

    ParsedResult {
        summary,
        passed,
        failed: if failed > 0 { failed } else { failures.len() },
        skipped,
        failures,
        warnings,
        tail: None,
    }
}

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

    #[test]
    fn parse_dotnet_build_failure() {
        let output = "\
Program.cs(42,10): error CS1002: ; expected
Program.cs(15,5): warning CS0168: The variable 'x' is declared but never used
Build FAILED.
    1 Error(s)
    1 Warning(s)
";
        let result = parse(output);
        assert_eq!(result.failures.len(), 1);
        assert_eq!(result.warnings.len(), 1);
        assert_eq!(
            result.failures[0].location.as_deref(),
            Some("Program.cs:42:10")
        );
        assert!(result.summary.contains("FAILED"));
    }

    #[test]
    fn parse_dotnet_build_success() {
        let output = "\
Build succeeded.
    0 Error(s)
    0 Warning(s)
";
        let result = parse(output);
        assert_eq!(result.failures.len(), 0);
        assert!(result.summary.contains("succeeded"));
    }

    #[test]
    fn parse_dotnet_test_summary() {
        let output = "Passed!  - Failed:     0, Passed:    10, Skipped:     0, Total:    10\n";
        let result = parse(output);
        assert_eq!(result.passed, 10);
        assert_eq!(result.failed, 0);
    }
}