use std::num::NonZero;
use std::thread;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct HostCPUs(NonZero<u8>);
impl HostCPUs {
const MIN_GUEST_CPUS: u8 = 1;
const MAX_GUEST_CPUS: u8 = 8;
const GUEST_CPUS_DIVISOR: u8 = 2;
pub(super) fn detect() -> Result<Self> {
let detected = thread::available_parallelism().map_err(Error::HostCpu)?;
Self::from_count(detected)
}
pub(super) fn from_count(count: NonZero<usize>) -> Result<Self> {
let count = NonZero::<u8>::try_from(count).map_err(Error::HostCpuCount)?;
Ok(Self(count))
}
pub(super) fn suggested(self) -> u8 {
(self.0.get() / Self::GUEST_CPUS_DIVISOR).clamp(Self::MIN_GUEST_CPUS, Self::MAX_GUEST_CPUS)
}
}
#[cfg(test)]
mod tests {
use super::HostCPUs;
use crate::Error;
use std::num::NonZero;
#[test]
fn preserves_every_supported_reported_count() {
for count in 1..=usize::from(u8::MAX) {
let cpus = HostCPUs::from_count(NonZero::new(count).unwrap()).unwrap();
assert_eq!(usize::from(cpus.0.get()), count);
}
}
#[test]
fn rejects_counts_above_u8_range_without_saturating() {
for count in [256, 257, usize::MAX] {
assert!(matches!(
HostCPUs::from_count(NonZero::new(count).unwrap()),
Err(Error::HostCpuCount(_))
));
}
}
#[test]
fn halves_capacity_rounds_down_and_applies_default_limits() {
for (count, expected) in [
(1, 1),
(2, 1),
(3, 1),
(4, 2),
(7, 3),
(8, 4),
(15, 7),
(16, 8),
(17, 8),
(64, 8),
(255, 8),
] {
let cpus = HostCPUs::from_count(NonZero::new(count).unwrap()).unwrap();
assert_eq!(cpus.suggested(), expected, "host count: {count}");
}
}
#[test]
fn recommendations_stay_bounded_and_monotonic_across_supported_counts() {
let mut previous = 0;
for count in 1..=usize::from(u8::MAX) {
let cpus = HostCPUs::from_count(NonZero::new(count).unwrap()).unwrap();
let suggested = cpus.suggested();
assert!((1..=8).contains(&suggested), "host count: {count}");
assert!(usize::from(suggested) <= count);
assert!(suggested >= previous);
previous = suggested;
}
}
}