rustquty_core/collector/
hack.rs1use 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 for line in stderr.lines() {
17 if let Some(n) = line.split_whitespace().find_map(|w| w.parse::<u32>().ok()) {
18 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}