use std::time::Duration;
use crate::connector::{ServiceConnector, ServiceInfo, ServiceStatus};
use super::helpers::binary_on_path;
const VERSION_TIMEOUT: Duration = Duration::from_secs(2);
pub struct ReviewConnector {
binary: Option<String>,
}
impl ReviewConnector {
pub fn new() -> Self {
Self { binary: None }
}
pub fn with_binary(binary: impl Into<String>) -> Self {
Self {
binary: Some(binary.into()),
}
}
fn binary(&self) -> &str {
self.binary.as_deref().unwrap_or("trusty-review")
}
}
impl Default for ReviewConnector {
fn default() -> Self {
Self::new()
}
}
enum VersionProbe {
Ran(Option<String>),
CannotExecute(String),
}
fn binary_version(binary: &str) -> VersionProbe {
let mut child = match std::process::Command::new(binary)
.arg("--version")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(child) => child,
Err(e) => return VersionProbe::CannotExecute(format!("spawn `{binary} --version`: {e}")),
};
let deadline = std::time::Instant::now() + VERSION_TIMEOUT;
loop {
match child.try_wait() {
Ok(Some(status)) if status.success() => break,
Ok(Some(status)) => {
let _ = child.wait();
return VersionProbe::CannotExecute(format!(
"`{binary} --version` exited {status}"
));
}
Ok(None) if std::time::Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(10));
}
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
return VersionProbe::CannotExecute(format!(
"`{binary} --version` did not exit within {VERSION_TIMEOUT:?}"
));
}
Err(e) => {
return VersionProbe::CannotExecute(format!(
"waiting on `{binary} --version`: {e}"
));
}
}
}
match child.wait_with_output() {
Ok(output) => VersionProbe::Ran(
String::from_utf8_lossy(&output.stdout)
.lines()
.next()
.and_then(|line| line.split_whitespace().nth(1))
.map(str::to_owned),
),
Err(e) => VersionProbe::CannotExecute(format!("reading `{binary} --version`: {e}")),
}
}
impl ServiceConnector for ReviewConnector {
fn id(&self) -> &'static str {
"trusty-review"
}
fn display_name(&self) -> &'static str {
"Trusty Review"
}
fn detect(&self) -> ServiceInfo {
let binary = self.binary();
if !binary_on_path(binary) {
return ServiceInfo {
id: self.id().to_string(),
display_name: self.display_name().to_string(),
status: ServiceStatus::Absent,
version: None,
url: None,
hint: None,
};
}
let (status, version, hint) = match binary_version(binary) {
VersionProbe::Ran(version) => (ServiceStatus::Available, version, None),
VersionProbe::CannotExecute(why) => (
ServiceStatus::Degraded,
None,
Some(format!(
"{binary} is on PATH but did not run: {why}. Reinstall it \
with `cargo install {binary}`."
)),
),
};
ServiceInfo {
id: self.id().to_string(),
display_name: self.display_name().to_string(),
status,
version,
url: None,
hint,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn review_connector_reports_available_when_the_binary_is_present() {
if which::which("cargo").is_err() {
eprintln!("skip: no cargo on PATH to probe as a stand-in binary");
return;
}
let started = std::time::Instant::now();
let info = ReviewConnector::with_binary("cargo").detect();
let elapsed = started.elapsed();
assert_eq!(
info.status,
ServiceStatus::Available,
"a present per-invocation binary is Available"
);
assert!(
info.version.is_some(),
"the card renders the version read off `--version`"
);
assert_eq!(info.id, "trusty-review");
assert_eq!(info.display_name, "Trusty Review");
assert!(info.url.is_none(), "a per-invocation tool has no URL");
assert!(
elapsed < Duration::from_secs(3),
"the detect must not dial anything — a socket dial's own budget is \
3 s, so this bound is what a reintroduced dial would trip: {elapsed:?}"
);
}
#[test]
fn review_connector_reports_absent_when_the_binary_is_missing() {
let info = ReviewConnector::with_binary("trusty-review-does-not-exist-9f3a").detect();
assert_eq!(info.status, ServiceStatus::Absent);
assert!(info.version.is_none());
assert!(info.hint.is_none());
}
#[test]
fn review_connector_never_reaches_running() {
for connector in [
ReviewConnector::new(),
ReviewConnector::with_binary("trusty-review-does-not-exist-9f3a"),
] {
let status = connector.detect().status;
assert!(
matches!(status, ServiceStatus::Available | ServiceStatus::Absent),
"a per-invocation member has only two honest verdicts, got {status:?}"
);
}
}
#[cfg(unix)]
#[test]
fn a_binary_that_cannot_execute_is_not_available() {
let probe = binary_version("/usr/bin/false");
match probe {
VersionProbe::CannotExecute(why) => {
assert!(
why.contains("exited"),
"the operator needs the reason: {why}"
);
}
VersionProbe::Ran(v) => {
panic!("a binary exiting 1 must not read as a clean run, got {v:?}")
}
}
}
}