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!(ERROR_RE, r"^\[ERROR\]\s+(.+)$");
define_regex!(LOCATION_RE, r"^(.+?):\[(\d+),(\d+)\]\s+(.+)$");
define_regex!(
    TESTS_RE,
    r"Tests run: (\d+), Failures: (\d+), Errors: (\d+), Skipped: (\d+)"
);
define_regex!(BUILD_RE, r"^\[INFO\] BUILD (SUCCESS|FAILURE)");

fn is_noise(line: &str) -> bool {
    line.starts_with("[INFO] Downloading from")
        || line.starts_with("[INFO] Downloaded from")
        || line.starts_with("[INFO] Progress")
}

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

    for line in output.lines() {
        if is_noise(line) {
            continue;
        }

        if let Some(caps) = ERROR_RE.captures(line) {
            let msg = caps[1].to_string();
            let (location, message) = if let Some(loc_caps) = LOCATION_RE.captures(&msg) {
                (
                    Some(format!(
                        "{}:{}:{}",
                        &loc_caps[1], &loc_caps[2], &loc_caps[3]
                    )),
                    loc_caps[4].to_string(),
                )
            } else {
                (None, msg)
            };
            failures.push(Diagnostic {
                name: "mvn/error".to_string(),
                location,
                message,
            });
            continue;
        }

        if let Some(caps) = TESTS_RE.captures(line) {
            let total: usize = caps[1].parse().unwrap_or(0);
            let f: usize = caps[2].parse().unwrap_or(0);
            let e: usize = caps[3].parse().unwrap_or(0);
            let s: usize = caps[4].parse().unwrap_or(0);
            failed = f + e;
            skipped = s;
            passed = total.saturating_sub(failed + skipped);
            continue;
        }

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

    let summary = match build_result {
        Some("SUCCESS") => {
            if passed + failed + skipped > 0 {
                format!("BUILD SUCCESS, {passed} passed, {failed} failed, {skipped} skipped")
            } else {
                "BUILD SUCCESS".to_string()
            }
        }
        Some("FAILURE") => format!("BUILD FAILURE, {} error(s)", failures.len()),
        _ if failures.is_empty() => "complete".to_string(),
        _ => format!("{} error(s)", failures.len()),
    };

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

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

    #[test]
    fn parse_mvn_compile_error() {
        let output = "\
[INFO] Scanning for projects...
[INFO] Downloading from central: https://repo.maven.apache.org/foo
[INFO] Downloaded from central: https://repo.maven.apache.org/foo
[ERROR] /src/main/java/App.java:[42,10] error: ';' expected
[INFO] BUILD FAILURE
[INFO] Total time: 5.123 s
";
        let result = parse(output);
        assert_eq!(result.failures.len(), 1);
        assert_eq!(
            result.failures[0].location.as_deref(),
            Some("/src/main/java/App.java:42:10")
        );
        assert!(result.summary.contains("FAILURE"));
    }

    #[test]
    fn parse_mvn_test_success() {
        let output = "\
[INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
";
        let result = parse(output);
        assert_eq!(result.passed, 10);
        assert_eq!(result.failed, 0);
        assert!(result.summary.contains("SUCCESS"));
    }

    #[test]
    fn parse_mvn_test_failure() {
        let output = "\
[INFO] Tests run: 10, Failures: 1, Errors: 0, Skipped: 2
[INFO] BUILD FAILURE
";
        let result = parse(output);
        assert_eq!(result.passed, 7);
        assert_eq!(result.failed, 1);
        assert_eq!(result.skipped, 2);
    }
}