use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EnvironmentFingerprint {
pub arch: String,
pub os: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub available_parallelism: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_memory_bytes: Option<u64>,
}
impl EnvironmentFingerprint {
#[must_use]
pub fn detect() -> Self {
Self {
arch: std::env::consts::ARCH.to_owned(),
os: std::env::consts::OS.to_owned(),
available_parallelism: std::thread::available_parallelism()
.ok()
.and_then(|count| u32::try_from(count.get()).ok()),
cpu_model: cpu_model(),
total_memory_bytes: total_memory_bytes(),
}
}
#[must_use]
pub fn comparable_to(&self, other: &Self) -> bool {
self == other
}
#[must_use]
pub fn labels(&self) -> BTreeMap<String, String> {
let mut labels = BTreeMap::new();
let _replaced = labels.insert("arch".to_owned(), self.arch.clone());
let _replaced = labels.insert("os".to_owned(), self.os.clone());
if let Some(cores) = self.available_parallelism {
let _replaced = labels.insert("available_parallelism".to_owned(), cores.to_string());
}
if let Some(model) = &self.cpu_model {
let _replaced = labels.insert("cpu_model".to_owned(), model.clone());
}
if let Some(bytes) = self.total_memory_bytes {
let _replaced = labels.insert("total_memory_bytes".to_owned(), bytes.to_string());
}
labels
}
}
fn cpu_model() -> Option<String> {
cpu_model_in(&std::fs::read_to_string("/proc/cpuinfo").ok()?)
}
fn cpu_model_in(text: &str) -> Option<String> {
text.lines()
.find_map(|line| {
line.split_once(':')
.filter(|(key, _)| key.trim() == "model name")
})
.map(|(_, value)| value.trim().to_owned())
.filter(|model| !model.is_empty())
}
fn total_memory_bytes() -> Option<u64> {
total_memory_bytes_in(&std::fs::read_to_string("/proc/meminfo").ok()?)
}
fn total_memory_bytes_in(text: &str) -> Option<u64> {
let line = text.lines().find(|line| line.starts_with("MemTotal:"))?;
let kibibytes: u64 = line
.split_whitespace()
.nth(1)
.and_then(|value| value.parse().ok())?;
kibibytes.checked_mul(1024)
}
#[cfg(test)]
#[expect(
clippy::panic_in_result_fn,
reason = "a Result-returning test in the Book ch11 shape that also asserts; \
clippy offers no allow-in-tests knob for this lint"
)]
mod tests {
use super::*;
#[test]
fn the_target_triple_fields_are_always_present() {
let fingerprint = EnvironmentFingerprint::detect();
assert!(!fingerprint.arch.is_empty());
assert!(!fingerprint.os.is_empty());
}
#[test]
fn detection_is_stable() {
assert_eq!(
EnvironmentFingerprint::detect(),
EnvironmentFingerprint::detect()
);
}
#[test]
fn a_differing_host_is_not_comparable() -> Result<(), serde_json::Error> {
let here = EnvironmentFingerprint::detect();
let mut elsewhere = here.clone();
elsewhere.arch = format!("{}-elsewhere", here.arch);
assert!(here.comparable_to(&here));
assert!(!here.comparable_to(&elsewhere));
let text = serde_json::to_string(&here)?;
let back: EnvironmentFingerprint = serde_json::from_str(&text)?;
assert_eq!(here, back);
Ok(())
}
#[test]
fn the_label_map_names_the_mandatory_fields() {
let labels = EnvironmentFingerprint::detect().labels();
assert!(labels.contains_key("arch"));
assert!(labels.contains_key("os"));
}
#[test]
fn the_label_map_carries_exactly_what_the_host_disclosed() {
let disclosed = EnvironmentFingerprint {
arch: String::from("aarch64"),
os: String::from("linux"),
available_parallelism: Some(8),
cpu_model: Some(String::from("Neoverse-N1")),
total_memory_bytes: Some(16_777_216),
};
let labels = disclosed.labels();
assert_eq!(
labels.get("available_parallelism").map(String::as_str),
Some("8")
);
assert_eq!(
labels.get("cpu_model").map(String::as_str),
Some("Neoverse-N1")
);
assert_eq!(
labels.get("total_memory_bytes").map(String::as_str),
Some("16777216")
);
let silent = EnvironmentFingerprint {
available_parallelism: None,
cpu_model: None,
total_memory_bytes: None,
..disclosed
};
let labels = silent.labels();
assert_eq!(labels.len(), 2, "{labels:?}");
assert!(!labels.contains_key("cpu_model"));
assert!(!labels.contains_key("total_memory_bytes"));
assert!(!labels.contains_key("available_parallelism"));
}
#[test]
fn the_cpu_model_is_read_from_a_procfs_body() {
const CPUINFO: &str = "processor\t: 0\nvendor_id\t: GenuineIntel\nmodel name\t: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz\ncache size\t: 49152 KB\n\nprocessor\t: 1\nmodel name\t: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz\n";
assert_eq!(
cpu_model_in(CPUINFO).as_deref(),
Some("Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz")
);
assert_eq!(
cpu_model_in("processor\t: 0\nBogoMIPS\t: 50.00\nCPU part\t: 0xd0c\n"),
None
);
assert_eq!(cpu_model_in("model name\t: \n"), None);
assert_eq!(cpu_model_in(""), None);
}
#[test]
fn the_total_memory_is_converted_from_kibibytes() {
const MEMINFO: &str = "MemTotal: 16307176 kB\nMemFree: 9312604 kB\nBuffers: 123456 kB\n";
assert_eq!(total_memory_bytes_in(MEMINFO), Some(16_307_176_u64 * 1024));
assert_eq!(total_memory_bytes_in("MemFree: 100 kB\n"), None);
assert_eq!(total_memory_bytes_in("MemTotal: unknown kB\n"), None);
assert_eq!(total_memory_bytes_in("MemTotal:\n"), None);
assert_eq!(
total_memory_bytes_in(&format!("MemTotal: {} kB\n", u64::MAX)),
None
);
}
}