leankg 0.11.1

Lightweight Knowledge Graph for AI-Assisted Development
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestResult {
    pub name: String,
    pub status: TestStatus,
    pub duration_ms: Option<u64>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TestStatus {
    Passed,
    Failed,
    Ignored,
}

pub struct CargoTestCompressor {
    max_failures: usize,
}

impl Default for CargoTestCompressor {
    fn default() -> Self {
        Self::new()
    }
}

impl CargoTestCompressor {
    pub fn new() -> Self {
        Self { max_failures: 10 }
    }

    pub fn with_max_failures(max: usize) -> Self {
        Self { max_failures: max }
    }

    pub fn compress(&self, output: &str) -> String {
        let lines: Vec<&str> = output.lines().collect();
        let mut result = Vec::new();
        let mut passed_count = 0;
        let mut _failed_count = 0;
        let mut failed_tests = Vec::new();
        let mut ignored_count = 0;
        let mut in_failures = false;

        for line in lines {
            let line = line.trim();

            if line.starts_with("test result:") {
                if let Some(stats) = self.parse_test_result(line) {
                    passed_count = stats.0;
                    _failed_count = stats.1;
                    ignored_count = stats.2;
                }
                continue;
            }

            if line.starts_with("failures:") {
                in_failures = true;
                continue;
            }

            if in_failures {
                if line.is_empty() {
                    in_failures = false;
                    continue;
                }
                if failed_tests.len() < self.max_failures {
                    if let Some(test_name) = self.extract_failed_test(line) {
                        failed_tests.push(test_name);
                    }
                }
                continue;
            }

            if self.is_noise_line(line) {
                continue;
            }

            if line.starts_with("running ") || line.starts_with("test ") {
                if line.contains(" ... ") {
                    if let Some(test) = self.parse_test_line(line) {
                        match test.status {
                            TestStatus::Passed => passed_count += 1,
                            TestStatus::Failed => {
                                if failed_tests.len() < self.max_failures {
                                    failed_tests.push(test.name);
                                }
                            }
                            TestStatus::Ignored => ignored_count += 1,
                        }
                    }
                }
                continue;
            }

            if self.is_summary_line(line) {
                if let Some(stats) = self.parse_test_result(line) {
                    passed_count = stats.0;
                    _failed_count = stats.1;
                    ignored_count = stats.2;
                }
                continue;
            }
        }

        result.push("[TEST SUMMARY]".to_string());

        if failed_tests.is_empty() {
            result.push(format!(
                "ok. {} passed; {} ignored",
                passed_count, ignored_count
            ));
        } else {
            result.push(format!(
                "FAIL: {}/{} failed",
                failed_tests.len(),
                passed_count + failed_tests.len()
            ));
            result.push("Failures:".to_string());
            for (i, test) in failed_tests.iter().enumerate() {
                if i >= self.max_failures {
                    result.push(format!(
                        "  ... and {} more",
                        failed_tests.len() - self.max_failures
                    ));
                    break;
                }
                result.push(format!("  - {}", test));
            }
        }

        result.join("\n")
    }

    fn parse_test_result(&self, line: &str) -> Option<(usize, usize, usize)> {
        let parts: Vec<&str> = line.split(';').collect();
        let mut passed = 0;
        let mut failed_count = 0;
        let mut ignored = 0;

        for part in parts {
            let part = part.trim();
            if part.contains("passed") {
                if let Some(n) = self.extract_number(part) {
                    passed = n;
                }
            } else if part.contains("failed") {
                if let Some(n) = self.extract_number(part) {
                    failed_count = n;
                }
            } else if part.contains("ignored") {
                if let Some(n) = self.extract_number(part) {
                    ignored = n;
                }
            }
        }

        if passed > 0 || failed_count > 0 || ignored > 0 {
            Some((passed, failed_count, ignored))
        } else {
            None
        }
    }

    fn parse_test_line(&self, line: &str) -> Option<TestResult> {
        let parts: Vec<&str> = line.split(" ... ").collect();
        if parts.len() != 2 {
            return None;
        }

        let name = parts[0]
            .trim()
            .strip_prefix("test ")
            .map(|s| s.to_string())?;
        let status_str = parts[1].trim();

        let status = if status_str.starts_with("ok") {
            TestStatus::Passed
        } else if status_str.starts_with("FAILED") {
            TestStatus::Failed
        } else if status_str.starts_with("ignored") {
            TestStatus::Ignored
        } else {
            return None;
        };

        Some(TestResult {
            name,
            status,
            duration_ms: None,
        })
    }

    fn extract_failed_test(&self, line: &str) -> Option<String> {
        let line = line.trim();
        if line.starts_with("test ") {
            Some(line.strip_prefix("test ")?.to_string())
        } else {
            None
        }
    }

    fn is_noise_line(&self, line: &str) -> bool {
        line.starts_with("Compiling")
            || line.starts_with("Finished")
            || line.starts_with("Running")
            || line.contains("warning: unused")
            || line.contains("warning: field ")
            || line.contains("warning: method ")
            || line.starts_with("   Compiling")
            || line.starts_with("   Finished")
            || line.contains("note: ")
            || line.starts_with("     Running")
            || line.is_empty()
    }

    fn is_summary_line(&self, line: &str) -> bool {
        line.starts_with("test result:")
    }

    fn extract_number(&self, part: &str) -> Option<usize> {
        let digits: String = part.chars().filter(|c| c.is_ascii_digit()).collect();
        digits.parse().ok()
    }

    pub fn estimate_savings(&self, original: &str, compressed: &str) -> f64 {
        let original_tokens = original.len() / 4;
        let compressed_tokens = compressed.len() / 4;
        if original_tokens == 0 {
            return 0.0;
        }
        ((original_tokens - compressed_tokens) as f64 / original_tokens as f64) * 100.0
    }
}

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

    #[test]
    fn test_compress_empty() {
        let compressor = CargoTestCompressor::new();
        let result = compressor.compress("");
        assert!(result.contains("[TEST SUMMARY]"));
    }

    #[test]
    fn test_compress_passed_tests() {
        let compressor = CargoTestCompressor::new();
        let output = r#"running 2 tests
test test_one ... ok
test test_two ... ok
test result: ok. 2 passed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s"#;
        let result = compressor.compress(output);
        assert!(result.contains("ok. 2 passed"));
    }

    #[test]
    fn test_compress_failed_tests() {
        let compressor = CargoTestCompressor::new();
        let output = r#"running 3 tests
test test_one ... FAILED
test test_two ... ok
test test_three ... ok
test result: FAILED. 2 passed; 1 failed; 0 ignored"#;
        let result = compressor.compress(output);
        assert!(result.contains("FAIL:"));
        assert!(result.contains("test_one"));
    }

    #[test]
    fn test_strips_noise() {
        let compressor = CargoTestCompressor::new();
        let output = r#"Compiling leankg v0.8.3
   Compiling leankg v0.8.3
warning: unused variable: `foo`
  --> src/lib.rs:10:5
Finished dev [unoptimized] target(s)
running 1 test
test test_one ... ok
test result: ok. 1 passed; 0 ignored"#;
        let result = compressor.compress(output);
        assert!(!result.contains("Compiling"));
        assert!(!result.contains("Finished"));
        assert!(!result.contains("warning:"));
    }

    #[test]
    fn test_estimate_savings() {
        let compressor = CargoTestCompressor::new();
        let original = "x".repeat(1000);
        let compressed = "x".repeat(100);
        let savings = compressor.estimate_savings(&original, &compressed);
        assert!((savings - 90.0).abs() < 0.1);
    }
}