use crate::model::MetricState;
use crate::units::{Percent, Rate};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum FilesystemKind {
Physical,
Removable,
Network,
Virtual,
#[default]
Unknown,
}
impl FilesystemKind {
#[must_use]
pub const fn hidden_by_default(self) -> bool {
matches!(self, Self::Virtual)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct InodeUsage {
total: u64,
free: u64,
}
impl InodeUsage {
#[must_use]
pub const fn from_counts(total: u64, free: u64) -> MetricState<Self> {
if total == 0 {
return MetricState::Unsupported;
}
MetricState::Available(Self {
total,
free: if free > total { total } else { free },
})
}
#[must_use]
pub const fn total(self) -> u64 {
self.total
}
#[must_use]
pub const fn free(self) -> u64 {
self.free
}
#[must_use]
pub const fn used(self) -> u64 {
self.total.saturating_sub(self.free)
}
#[must_use]
pub fn usage(self) -> Percent {
Percent::ratio(self.used(), self.total).unwrap_or(Percent::FULL)
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct FilesystemSnapshot {
pub mount_point: Box<str>,
pub device: Option<Box<str>>,
pub fs_type: Option<Box<str>>,
pub total_bytes: u64,
pub available_bytes: MetricState<u64>,
pub used_bytes: MetricState<u64>,
pub usage: MetricState<Percent>,
pub inodes: MetricState<InodeUsage>,
pub kind: FilesystemKind,
pub read_only: bool,
}
impl FilesystemSnapshot {
#[must_use]
pub fn inode_usage(&self) -> MetricState<Percent> {
self.inodes.as_ref().map(|inodes| inodes.usage())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct DiskTotals {
pub read_bytes: u64,
pub write_bytes: u64,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct DiskSnapshot {
pub device: Box<str>,
pub model: Option<Box<str>>,
pub read: MetricState<Rate>,
pub write: MetricState<Rate>,
pub read_ops: MetricState<Rate>,
pub write_ops: MetricState<Rate>,
pub busy: MetricState<Percent>,
pub queue_length: MetricState<f32>,
pub totals: MetricState<DiskTotals>,
pub mount_points: Vec<Box<str>>,
}
impl DiskSnapshot {
#[must_use]
pub fn warming_up(device: Box<str>) -> Self {
Self {
device,
model: None,
read: MetricState::WarmingUp,
write: MetricState::WarmingUp,
read_ops: MetricState::WarmingUp,
write_ops: MetricState::WarmingUp,
busy: MetricState::WarmingUp,
queue_length: MetricState::WarmingUp,
totals: MetricState::WarmingUp,
mount_points: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn virtual_filesystems_are_hidden_by_default_and_real_ones_are_not() {
assert!(FilesystemKind::Virtual.hidden_by_default());
for kind in [
FilesystemKind::Physical,
FilesystemKind::Removable,
FilesystemKind::Network,
FilesystemKind::Unknown,
] {
assert!(!kind.hidden_by_default(), "{kind:?}");
}
}
#[test]
fn a_warming_up_device_reports_no_throughput_and_no_busy_percentage() {
let disk = DiskSnapshot::warming_up("nvme0n1".into());
assert!(disk.read.fresh().is_none());
assert!(disk.busy.fresh().is_none());
assert!(disk.mount_points.is_empty());
}
#[test]
fn capacity_and_throughput_live_in_separate_types() {
fn assert_fields<T>(_: &T) {}
let fs = FilesystemSnapshot {
mount_point: "/".into(),
device: Some("disk3s1s1".into()),
fs_type: Some("apfs".into()),
total_bytes: 494_384_795_648,
available_bytes: MetricState::Available(120_000_000_000),
used_bytes: MetricState::Available(374_384_795_648),
usage: Percent::ratio(374_384_795_648, 494_384_795_648)
.map_or(MetricState::Unsupported, MetricState::Available),
inodes: InodeUsage::from_counts(4_882_812_499, 4_395_698_642),
kind: FilesystemKind::Physical,
read_only: false,
};
assert_fields(&fs);
assert!(fs.usage.fresh().is_some());
let disk = DiskSnapshot::warming_up("disk0".into());
assert_fields(&disk);
}
#[test]
fn a_filesystem_with_no_inode_table_is_unsupported_and_never_zero_of_zero() {
assert_eq!(InodeUsage::from_counts(0, 0), MetricState::Unsupported);
assert_eq!(InodeUsage::from_counts(0, 12), MetricState::Unsupported);
}
#[test]
fn inode_usage_is_a_share_of_the_table_and_cannot_underflow() {
let inodes = InodeUsage::from_counts(1_000, 250)
.fresh()
.copied()
.expect("a thousand inodes is a table");
assert_eq!(inodes.used(), 750);
assert_eq!(inodes.free(), 250);
assert_eq!(inodes.usage(), Percent::new(75.0).expect("finite"));
let nonsense = InodeUsage::from_counts(10, 99)
.fresh()
.copied()
.expect("the table size is still known");
assert_eq!(nonsense.used(), 0);
assert_eq!(nonsense.free(), 10);
}
#[test]
fn the_inode_percentage_carries_the_counts_availability() {
let mut fs = FilesystemSnapshot {
mount_point: "/".into(),
device: None,
fs_type: None,
total_bytes: 1,
available_bytes: MetricState::Unsupported,
used_bytes: MetricState::Unsupported,
usage: MetricState::Unsupported,
inodes: MetricState::PermissionDenied,
kind: FilesystemKind::Physical,
read_only: false,
};
assert_eq!(fs.inode_usage(), MetricState::PermissionDenied);
fs.inodes = InodeUsage::from_counts(4, 1);
assert_eq!(
fs.inode_usage().fresh().map(|percent| percent.value()),
Some(75.0)
);
fs.inodes = fs.inodes.into_stale(core::time::Duration::from_secs(9));
assert!(fs.inode_usage().is_stale());
}
}