use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum VersionOutputMode {
Concise,
Verbose,
}
#[derive(Debug, Clone)]
pub(crate) struct VersionInfo {
cabin_version: String,
os: Option<String>,
}
impl VersionInfo {
pub(crate) fn current() -> Self {
Self {
cabin_version: env!("CARGO_PKG_VERSION").to_owned(),
os: detect_os_string(),
}
}
#[cfg(test)]
fn for_tests(cabin_version: &str, os: Option<&str>) -> Self {
Self {
cabin_version: cabin_version.to_owned(),
os: os.map(str::to_owned),
}
}
pub(crate) fn format(&self, mode: VersionOutputMode) -> String {
match mode {
VersionOutputMode::Concise => format!("cabin {}\n", self.cabin_version),
VersionOutputMode::Verbose => self.format_verbose(),
}
}
fn format_verbose(&self) -> String {
let mut out = String::with_capacity(256);
let _ = writeln!(out, "cabin {}", self.cabin_version);
let _ = writeln!(out, "release: {}", self.cabin_version);
if let Some(os) = self.os.as_deref() {
let _ = writeln!(out, "os: {os}");
}
out
}
}
fn detect_os_string() -> Option<String> {
let info = os_info::get();
let os_type = info.os_type();
let version = info.version();
let bitness = info.bitness();
let mut buf = String::new();
let _ = write!(buf, "{os_type}");
if !matches!(version, os_info::Version::Unknown) {
let _ = write!(buf, " {version}");
}
if !matches!(bitness, os_info::Bitness::Unknown) {
let _ = write!(buf, " [{bitness}]");
}
let buf = buf.trim().to_owned();
if buf.is_empty() { None } else { Some(buf) }
}
#[cfg(test)]
mod tests {
use super::*;
fn full() -> VersionInfo {
VersionInfo::for_tests("x.y.z", Some("Ubuntu 24.04 [64-bit]"))
}
fn minimal() -> VersionInfo {
VersionInfo::for_tests("x.y.z", None)
}
#[test]
fn concise_format_is_single_line_with_release_name() {
let info = full();
assert_eq!(info.format(VersionOutputMode::Concise), "cabin x.y.z\n");
}
#[test]
fn concise_format_works_with_minimal_metadata() {
let info = minimal();
assert_eq!(info.format(VersionOutputMode::Concise), "cabin x.y.z\n");
}
#[test]
fn verbose_format_header_contains_release_name() {
let info = full();
let out = info.format(VersionOutputMode::Verbose);
let header = out.lines().next().expect("at least one line");
assert_eq!(header, "cabin x.y.z");
}
#[test]
fn verbose_format_emits_release_and_os() {
let info = full();
let out = info.format(VersionOutputMode::Verbose);
let expected = "\
cabin x.y.z
release: x.y.z
os: Ubuntu 24.04 [64-bit]
";
assert_eq!(out, expected);
}
#[test]
fn verbose_format_omits_missing_optional_rows() {
let info = minimal();
let out = info.format(VersionOutputMode::Verbose);
let expected = "\
cabin x.y.z
release: x.y.z
";
assert_eq!(out, expected);
}
#[test]
fn verbose_format_uses_short_labels_no_uppercase() {
let info = full();
for line in info.format(VersionOutputMode::Verbose).lines() {
if !line.contains(':') {
continue;
}
let label = line.split(':').next().expect("label before colon");
assert!(
label.chars().all(|c| c.is_ascii_lowercase() || c == '-'),
"labels must be lowercase / dashed: got `{label}`"
);
}
}
#[test]
fn current_uses_package_version_as_cabin_version() {
let info = VersionInfo::current();
assert_eq!(info.cabin_version, env!("CARGO_PKG_VERSION"));
}
#[test]
fn verbose_format_never_leaks_local_paths() {
let info = full();
let out = info.format(VersionOutputMode::Verbose);
for needle in ["/Users/", "/home/", "/private/", "/opt/", "/tmp/"] {
assert!(
!out.contains(needle),
"verbose output must not leak `{needle}`: {out}"
);
}
}
}