Skip to main content

rustquty_core/collector/
hack.rs

1//! Hack collector — runs `cargo hack check --feature-powerset`.
2
3use super::{Collector, CollectorError, CollectorOutput};
4use crate::context::Context;
5use std::process::Command;
6
7pub struct HackCollector;
8
9impl HackCollector {
10    pub fn new() -> Self {
11        Self
12    }
13
14    fn parse_feature_count(&self, stderr: &str) -> u32 {
15        // Look for "checking N combinations" or "testing X feature combinations"
16        for line in stderr.lines() {
17            if let Some(n) = line.split_whitespace().find_map(|w| w.parse::<u32>().ok()) {
18                // Heuristic: look for lines with "combinations" nearby
19                if stderr.contains("combination") {
20                    return n;
21                }
22            }
23        }
24        0
25    }
26}
27
28impl Collector for HackCollector {
29    fn name(&self) -> &'static str {
30        "hack"
31    }
32
33    fn is_available(&self) -> bool {
34        Command::new("cargo")
35            .args(["hack", "--version"])
36            .output()
37            .map(|o| o.status.success())
38            .unwrap_or(false)
39    }
40
41    fn collect(&self, ctx: &Context) -> Result<CollectorOutput, CollectorError> {
42        let start = std::time::Instant::now();
43        let output = Command::new("cargo")
44            .args(["hack", "check", "--feature-powerset", "--no-dev-deps"])
45            .current_dir(&ctx.workspace_root)
46            .output()
47            .map_err(|e| CollectorError::IoError(e.to_string()))?;
48
49        let duration_ms = start.elapsed().as_millis() as u64;
50        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
51
52        let feature_combinations = self.parse_feature_count(&stderr);
53        let status = if output.status.success() {
54            crate::schema::CollectorStatus::Pass
55        } else {
56            crate::schema::CollectorStatus::Fail
57        };
58
59        let details = serde_json::json!({
60            "featureCombinationsTested": feature_combinations,
61        });
62
63        Ok(CollectorOutput {
64            status,
65            duration_ms,
66            stdout: serde_json::to_string(&details).unwrap_or_default(),
67            stderr,
68        })
69    }
70}
71
72impl Default for HackCollector {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_parse_feature_count() {
84        let collector = HackCollector::new();
85        let stderr = "Checking feature combinations...\nChecked 16 combinations";
86        assert_eq!(collector.parse_feature_count(stderr), 16);
87    }
88}