use std::io::{self, Write};
use std::path::PathBuf;
use crate::discovery::Language;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComponentStatus {
Available,
NotFound,
Unusable,
NotImplemented,
}
impl ComponentStatus {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Available => "available",
Self::NotFound => "not found",
Self::Unusable => "unusable",
Self::NotImplemented => "not implemented",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Requirement {
Required,
Optional,
}
impl Requirement {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Required => "required",
Self::Optional => "optional",
}
}
}
#[derive(Debug, Clone)]
pub struct ComponentReport {
pub name: &'static str,
pub requirement: Requirement,
pub status: ComponentStatus,
pub detail: String,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HelperFacts {
pub path: PathBuf,
pub state: HelperState,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HelperState {
Answered(Greeting),
Silent(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Greeting {
pub version: String,
pub protocol: u32,
pub toolchains: Vec<String>,
pub capabilities: Vec<String>,
pub executes: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HelperComponent {
pub name: &'static str,
pub binary: &'static str,
pub analyses: &'static [Language],
pub enables: &'static str,
pub advice: &'static str,
}
pub const RUST_HELPER: HelperComponent = HelperComponent {
name: "rust-compiler-helper",
binary: "codehelion-backend-rust",
analyses: &[Language::Rust],
enables: "semantic analysis of Rust",
advice: "install codehelion-backend-rust beside this binary or on PATH",
};
pub const CLANG_HELPER: HelperComponent = HelperComponent {
name: "clang-helper",
binary: "codehelion-backend-clang",
analyses: &[Language::C, Language::Cpp],
enables: "semantic analysis of C and C++",
advice: "install codehelion-backend-clang beside this binary or on PATH",
};
pub const OPTIONAL_HELPERS: [HelperComponent; 2] = [RUST_HELPER, CLANG_HELPER];
fn inspect_self() -> ComponentReport {
ComponentReport {
name: "codehelion",
requirement: Requirement::Required,
status: ComponentStatus::Available,
detail: format!("codehelion {}", env!("CARGO_PKG_VERSION")),
notes: Vec::new(),
}
}
fn inspect_helper(helper: HelperComponent, found: Option<HelperFacts>) -> ComponentReport {
let (status, detail, notes) = match found {
None => (
ComponentStatus::NotFound,
format!(
"not needed for fast or structural analysis; enables {}. To add it, {}.",
helper.enables, helper.advice
),
Vec::new(),
),
Some(facts) => {
let path = facts.path.display().to_string();
match facts.state {
HelperState::Answered(greeting) => {
(ComponentStatus::Available, path, describe(&greeting))
}
HelperState::Silent(reason) => (
ComponentStatus::Unusable,
path,
vec![format!("this build could not talk to it: {reason}")],
),
}
}
};
ComponentReport {
name: helper.name,
requirement: Requirement::Optional,
status,
detail,
notes,
}
}
fn describe(greeting: &Greeting) -> Vec<String> {
let mut notes = vec![format!(
"version {}, protocol {}",
greeting.version, greeting.protocol
)];
if !greeting.toolchains.is_empty() {
notes.push(format!("analyses with: {}", greeting.toolchains.join(", ")));
}
if greeting.capabilities.is_empty() {
notes.push("supplies: nothing this build asked about".to_string());
} else {
notes.push(format!("supplies: {}", greeting.capabilities.join(", ")));
}
if greeting.executes.is_empty() {
notes.push("runs nothing out of a project, whatever is permitted".to_string());
} else {
notes.push(format!(
"runs when permitted: {}",
greeting.executes.join(", ")
));
}
notes
}
#[must_use]
pub fn diagnose_with(find: &dyn Fn(&str) -> Option<HelperFacts>) -> Vec<ComponentReport> {
let mut reports = vec![inspect_self()];
for helper in OPTIONAL_HELPERS {
reports.push(inspect_helper(helper, find(helper.binary)));
}
reports
}
#[must_use]
pub fn diagnose() -> Vec<ComponentReport> {
diagnose_with(&|_| None)
}
pub fn render(reports: &[ComponentReport], out: &mut impl Write) -> io::Result<()> {
writeln!(out, "codehelion environment diagnostics")?;
writeln!(out)?;
let name_width = reports.iter().map(|r| r.name.len()).max().unwrap_or(0);
for report in reports {
writeln!(
out,
" {name:<name_width$} {req:<8} {status:<15} {detail}",
name = report.name,
req = report.requirement.label(),
status = report.status.label(),
detail = report.detail,
)?;
for note in &report.notes {
writeln!(out, " {:<name_width$} {note}", "")?;
}
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn diagnose_reports_codehelion_first_and_available() {
let reports = diagnose();
let first = reports.first().expect("at least one report");
assert_eq!(first.name, "codehelion");
assert_eq!(first.requirement, Requirement::Required);
assert_eq!(first.status, ComponentStatus::Available);
assert!(first.detail.contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn a_machine_without_helpers_is_told_what_it_still_has() {
let reports = diagnose();
let helpers: Vec<_> = reports.iter().filter(|r| r.name != "codehelion").collect();
assert_eq!(helpers.len(), OPTIONAL_HELPERS.len());
for helper in helpers {
assert_eq!(helper.requirement, Requirement::Optional);
assert_eq!(helper.status, ComponentStatus::NotFound);
assert!(
helper.detail.contains("not needed for fast or structural"),
"{}",
helper.detail
);
}
}
#[test]
fn the_advice_names_the_program_that_was_looked_for() {
for helper in OPTIONAL_HELPERS {
let report = inspect_helper(helper, None);
assert!(report.detail.contains(helper.binary), "{}", report.detail);
}
}
fn greeting() -> Greeting {
Greeting {
version: "0.1.0".to_string(),
protocol: 2,
toolchains: vec!["rust-analyzer 0.0.344".to_string()],
capabilities: vec!["types".to_string(), "name_resolution".to_string()],
executes: vec!["build-script".to_string()],
}
}
fn answered(name: &str) -> HelperFacts {
HelperFacts {
path: PathBuf::from("/opt/bin").join(name),
state: HelperState::Answered(greeting()),
}
}
#[test]
fn a_helper_that_is_there_is_reported_with_where_it_is() {
let reports =
diagnose_with(&|name| (name == OPTIONAL_HELPERS[0].binary).then(|| answered(name)));
let found = &reports[1];
assert_eq!(found.name, OPTIONAL_HELPERS[0].name);
assert_eq!(found.status, ComponentStatus::Available);
assert!(found.detail.contains("/opt/bin"), "{}", found.detail);
assert_eq!(reports[2].status, ComponentStatus::NotFound);
}
#[test]
fn a_helper_that_answered_says_what_it_is_and_what_it_supplies() {
let reports =
diagnose_with(&|name| (name == OPTIONAL_HELPERS[0].binary).then(|| answered(name)));
let notes = reports[1].notes.join("\n");
assert!(notes.contains("version 0.1.0"), "{notes}");
assert!(notes.contains("protocol 2"), "{notes}");
assert!(notes.contains("rust-analyzer 0.0.344"), "{notes}");
assert!(notes.contains("types, name_resolution"), "{notes}");
assert!(
notes.contains("runs when permitted: build-script"),
"{notes}"
);
}
#[test]
fn a_helper_that_offers_nothing_says_so_rather_than_saying_less() {
let report = inspect_helper(
OPTIONAL_HELPERS[0],
Some(HelperFacts {
path: PathBuf::from("/opt/bin/helper"),
state: HelperState::Answered(Greeting {
capabilities: Vec::new(),
executes: Vec::new(),
..greeting()
}),
}),
);
assert!(
report
.notes
.iter()
.any(|note| note.starts_with("supplies:")),
"{:?}",
report.notes
);
assert!(
report
.notes
.iter()
.any(|note| note.contains("runs nothing")),
"{:?}",
report.notes
);
}
#[test]
fn a_helper_that_would_not_answer_is_neither_available_nor_missing() {
let report = inspect_helper(
OPTIONAL_HELPERS[0],
Some(HelperFacts {
path: PathBuf::from("/opt/bin/helper"),
state: HelperState::Silent("speaks protocol 3, this build speaks 2".to_string()),
}),
);
assert_eq!(report.status, ComponentStatus::Unusable);
assert!(
report.detail.contains("/opt/bin/helper"),
"{}",
report.detail
);
assert!(
report.notes.iter().any(|note| note.contains("protocol 3")),
"{:?}",
report.notes
);
}
#[test]
fn what_a_helper_said_is_printed_under_it() {
let mut buffer = Vec::new();
let reports =
diagnose_with(&|name| (name == OPTIONAL_HELPERS[0].binary).then(|| answered(name)));
render(&reports, &mut buffer).expect("render should succeed");
let text = String::from_utf8(buffer).expect("output is utf-8");
let lines: Vec<&str> = text.lines().collect();
let at = lines
.iter()
.position(|line| line.contains(OPTIONAL_HELPERS[0].name))
.expect("the helper is listed");
assert!(lines[at + 1].contains("version 0.1.0"), "{text}");
}
#[test]
fn render_lists_every_component_and_the_version() {
let mut buffer = Vec::new();
render(&diagnose(), &mut buffer).expect("render should succeed");
let text = String::from_utf8(buffer).expect("output is utf-8");
assert!(text.contains("codehelion"));
assert!(text.contains(env!("CARGO_PKG_VERSION")));
assert!(text.contains("rust-compiler-helper"));
assert!(text.contains("not found"));
}
}