use crate::pipeline::LoadedTool;
use std::fmt::Write as _;
pub fn print_report(loaded: &LoadedTool) {
print!("{}{}", build_report(loaded), report_hint(&loaded.tool));
}
pub fn build_report(loaded: &LoadedTool) -> String {
let mut out = String::new();
writeln!(out, "mandible --doctor {}", loaded.tool).unwrap();
writeln!(out).unwrap();
let resolved = mandible_extract::resolve_tool(&loaded.tool);
let framework = mandible_extract::framework::identify(&resolved);
writeln!(out, "framework: {}", framework.describe()).unwrap();
writeln!(out).unwrap();
writeln!(out, "tiers:").unwrap();
for status in &loaded.tier_statuses {
let state = if !status.detected {
"not detected".to_string()
} else if let Some(err) = &status.error {
format!("FAILED: {err}")
} else {
"ok".to_string()
};
writeln!(out, " {:<28} {}", status.tier, state).unwrap();
}
writeln!(out).unwrap();
match &loaded.root {
Some(_) => {
let nodes = loaded.node_count();
let flags = loaded.flag_count();
let describable = loaded.describable_flag_count();
let pct = if describable == 0 {
"—".to_string()
} else {
format!("{:.1}%", loaded.flag_description_ratio() * 100.0)
};
writeln!(out, "nodes: {nodes}").unwrap();
writeln!(out, "flags: {flags} ({pct} flags with text)").unwrap();
}
None => {
writeln!(
out,
"result: no tier produced a root node for {:?}",
loaded.tool
)
.unwrap();
}
}
writeln!(out).unwrap();
writeln!(out, "accuracy: unmeasured").unwrap();
writeln!(
out,
"elapsed: {:.2}ms",
loaded.elapsed.as_secs_f64() * 1000.0
)
.unwrap();
out
}
pub fn report_hint(tool: &str) -> String {
format!("\nFound a bad parse? Run `mandible --report {tool}` for a paste-ready bug report.\n")
}
#[cfg(test)]
mod tests {
use super::*;
use mandible_core::{CommandNode, Flag, Provenance, Source};
use mandible_extract::{ExtractionResult, TierStatus};
#[test]
fn reports_an_em_dash_not_zero_percent_when_nothing_is_describable() {
let mut root = CommandNode::new("git", Provenance::single(Source::HelpText));
for name in ["paginate", "git-dir", "no-pager"] {
root.flags.push(Flag::long(
name,
Provenance::single(Source::HelpTextSynopsis),
));
}
let loaded = ExtractionResult {
tool: "git".to_string(),
root: Some(root),
tier_statuses: vec![TierStatus {
tier: "help_text",
detected: true,
error: None,
}],
elapsed: std::time::Duration::default(),
};
let report = build_report(&loaded);
assert!(
report.contains("flags: 3 (— flags with text)"),
"expected the em-dash line, got:\n{report}"
);
assert!(
!report.contains("0.0%"),
"must never fall back to the old described/total arithmetic:\n{report}"
);
}
#[test]
fn reports_a_percentage_when_some_flags_are_describable() {
let mut root = CommandNode::new("sometool", Provenance::single(Source::HelpText));
let mut f = Flag::long("verbose", Provenance::single(Source::HelpText));
f.description = Some(mandible_core::Text::sanitize("be more talkative"));
root.flags.push(f);
let loaded = ExtractionResult {
tool: "sometool".to_string(),
root: Some(root),
tier_statuses: Vec::new(),
elapsed: std::time::Duration::default(),
};
let report = build_report(&loaded);
assert!(
report.contains("flags: 1 (100.0% flags with text)"),
"expected a real percentage, got:\n{report}"
);
}
}