From 5f42a62a865371c16ecc3043f2ad7c9e9b431a59 Mon Sep 17 00:00:00 2001 From: hmziqrs Date: Mon, 1 Jun 2026 02:49:11 +0500 Subject: [PATCH] Refactor: extract status module into toride-status crate Move status module from crates/toride/src/status/ to crates/toride-status/ as a standalone crate, matching the toride-ssh pattern. - Create crates/toride-status/ with its own Cargo.toml and feature flags - Re-export as toride::status via pub use toride_status in toride lib.rs - Update all internal imports from crate::status:: to crate:: - Update doctests and examples to use toride_status - 519 unit tests + 72 doctests pass --- crates/toride-status/Cargo.toml | 59 + .../src}/capabilities.rs | 10 +- .../status => toride-status/src}/collector.rs | 32 +- .../status => toride-status/src}/daemon.rs | 6 +- .../status => toride-status/src}/doctor.rs | 78 +- .../src/status => toride-status/src}/error.rs | 6 +- crates/toride-status/src/lib.rs | 1175 +++++++++++++++++ .../src/status => toride-status/src}/mod.rs | 18 +- .../status => toride-status/src}/presets.rs | 34 +- .../status => toride-status/src}/privacy.rs | 22 +- .../status => toride-status/src}/provider.rs | 20 +- ..._daemon__tests__daemon_status_display.snap | 0 ..._doctor__tests__doctor_report_display.snap | 0 ...tatus__ssh__tests__ssh_status_display.snap | 0 ..._system__tests__system_status_display.snap | 0 ..._status__tests__toride_status_display.snap | 0 ..._daemon__tests__daemon_status_display.snap | 11 + ..._doctor__tests__doctor_report_display.snap | 40 + ...tatus__ssh__tests__ssh_status_display.snap | 10 + ..._system__tests__system_status_display.snap | 28 + ..._status__tests__toride_status_display.snap | 115 ++ .../src/status => toride-status/src}/ssh.rs | 6 +- .../status => toride-status/src}/system.rs | 80 +- .../src/status => toride-status/src}/units.rs | 0 crates/toride/Cargo.toml | 36 +- crates/toride/examples/disk_rates.rs | 4 +- crates/toride/examples/doctor.rs | 2 +- crates/toride/examples/gpu_status.rs | 4 +- crates/toride/examples/hardware_inventory.rs | 4 +- crates/toride/examples/network_rates.rs | 4 +- crates/toride/examples/privacy_safe_report.rs | 2 +- crates/toride/examples/simple_snapshot.rs | 4 +- crates/toride/examples/task_manager_loop.rs | 4 +- crates/toride/examples/top_processes.rs | 4 +- crates/toride/src/lib.rs | 2 +- 35 files changed, 1612 insertions(+), 208 deletions(-) create mode 100644 crates/toride-status/Cargo.toml rename crates/{toride/src/status => toride-status/src}/capabilities.rs (99%) rename crates/{toride/src/status => toride-status/src}/collector.rs (98%) rename crates/{toride/src/status => toride-status/src}/daemon.rs (99%) rename crates/{toride/src/status => toride-status/src}/doctor.rs (96%) rename crates/{toride/src/status => toride-status/src}/error.rs (98%) create mode 100644 crates/toride-status/src/lib.rs rename crates/{toride/src/status => toride-status/src}/mod.rs (99%) rename crates/{toride/src/status => toride-status/src}/presets.rs (97%) rename crates/{toride/src/status => toride-status/src}/privacy.rs (96%) rename crates/{toride/src/status => toride-status/src}/provider.rs (94%) rename crates/{toride/src/status => toride-status/src}/snapshots/toride__status__daemon__tests__daemon_status_display.snap (100%) rename crates/{toride/src/status => toride-status/src}/snapshots/toride__status__doctor__tests__doctor_report_display.snap (100%) rename crates/{toride/src/status => toride-status/src}/snapshots/toride__status__ssh__tests__ssh_status_display.snap (100%) rename crates/{toride/src/status => toride-status/src}/snapshots/toride__status__system__tests__system_status_display.snap (100%) rename crates/{toride/src/status => toride-status/src}/snapshots/toride__status__tests__toride_status_display.snap (100%) create mode 100644 crates/toride-status/src/snapshots/toride_status__daemon__tests__daemon_status_display.snap create mode 100644 crates/toride-status/src/snapshots/toride_status__doctor__tests__doctor_report_display.snap create mode 100644 crates/toride-status/src/snapshots/toride_status__ssh__tests__ssh_status_display.snap create mode 100644 crates/toride-status/src/snapshots/toride_status__system__tests__system_status_display.snap create mode 100644 crates/toride-status/src/snapshots/toride_status__tests__toride_status_display.snap rename crates/{toride/src/status => toride-status/src}/ssh.rs (99%) rename crates/{toride/src/status => toride-status/src}/system.rs (98%) rename crates/{toride/src/status => toride-status/src}/units.rs (100%) diff --git a/crates/toride-status/Cargo.toml b/crates/toride-status/Cargo.toml new file mode 100644 index 0000000..52569ff --- /dev/null +++ b/crates/toride-status/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "toride-status" +version = "0.1.0" +edition = "2024" + +[lib] +name = "toride_status" +path = "src/lib.rs" + +[features] +default = ["sysinfo-provider"] +sysinfo-provider = [] +linux-procfs = ["procfs"] +linux-sensors = ["lm-sensors"] +linux-udev = ["udev"] +linux-rtnetlink = ["rtnetlink"] +linux-cgroups = ["cgroups-rs"] +os-info = ["os_info"] +cpu-cpuid = ["raw-cpuid"] +hardware-dmi = ["dmidecode"] +hardware-pci = ["pci-info", "pci-ids"] +hardware-topology = ["hwlocality"] +gpu-nvidia = ["nvml-wrapper"] +battery = ["starship-battery"] +commands = ["dep:duct"] + +[dependencies] +sysinfo = "0.35" +sysconf = "0.3" +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +which = { workspace = true } +nix = { version = "0.31", features = ["user", "signal", "fs"] } +dirs = { workspace = true } + +# Optional dependencies +procfs = { version = "0.18", optional = true } +os_info = { version = "3", optional = true } +nvml-wrapper = { version = "0.10", optional = true } +starship-battery = { version = "0.8", optional = true } +dmidecode = { version = "1.0", optional = true } +raw-cpuid = { version = "11", optional = true } +hwlocality = { version = "1.0.0-alpha.12", optional = true } +pci-info = { version = "0.2", optional = true } +pci-ids = { version = "0.2", optional = true } +cgroups-rs = { version = "0.5", optional = true } +duct = { version = "0.13", optional = true } +lm-sensors = { version = "0.1", optional = true } +udev = { version = "0.8", optional = true } +rtnetlink = { version = "0.14", optional = true } + +[dev-dependencies] +insta = "1" +tempfile = "3" + +[lints] +workspace = true diff --git a/crates/toride/src/status/capabilities.rs b/crates/toride-status/src/capabilities.rs similarity index 99% rename from crates/toride/src/status/capabilities.rs rename to crates/toride-status/src/capabilities.rs index 60662da..1b5a690 100644 --- a/crates/toride/src/status/capabilities.rs +++ b/crates/toride-status/src/capabilities.rs @@ -32,7 +32,7 @@ //! # Examples //! //! ```no_run -//! use toride::status::capabilities::Capabilities; +//! use toride_status::capabilities::Capabilities; //! //! let caps = Capabilities::detect(); //! if caps.system.load_average { @@ -49,8 +49,8 @@ //! Check capabilities before collecting: //! //! ```no_run -//! use toride::status::capabilities::Capabilities; -//! use toride::status::system::SystemStatus; +//! use toride_status::capabilities::Capabilities; +//! use toride_status::system::SystemStatus; //! //! let caps = Capabilities::detect(); //! let status = SystemStatus::collect(); @@ -73,7 +73,7 @@ use serde::Serialize; /// # Examples /// /// ```no_run -/// use toride::status::capabilities::Capabilities; +/// use toride_status::capabilities::Capabilities; /// /// let caps = Capabilities::detect(); /// println!("{}", caps); @@ -304,7 +304,7 @@ impl Capabilities { /// # Examples /// /// ``` - /// use toride::status::capabilities::Capabilities; + /// use toride_status::capabilities::Capabilities; /// /// let caps = Capabilities::detect(); /// assert!(caps.system.cpu_usage); // Always available. diff --git a/crates/toride/src/status/collector.rs b/crates/toride-status/src/collector.rs similarity index 98% rename from crates/toride/src/status/collector.rs rename to crates/toride-status/src/collector.rs index 1879e09..60b43b2 100644 --- a/crates/toride/src/status/collector.rs +++ b/crates/toride-status/src/collector.rs @@ -21,8 +21,8 @@ //! //! ```no_run //! use std::time::Duration; -//! use toride::status::collector::Collector; -//! use toride::status::presets::Preset; +//! use toride_status::collector::Collector; +//! use toride_status::presets::Preset; //! //! let mut collector = Collector::new(Duration::from_secs(1), Preset::Diagnostics); //! @@ -42,7 +42,7 @@ //! //! ```no_run //! use std::time::Duration; -//! use toride::status::collector::Collector; +//! use toride_status::collector::Collector; //! //! let mut collector = Collector::default_collector(); //! loop { @@ -56,9 +56,9 @@ use std::time::{Duration, Instant}; use serde::Serialize; -use crate::status::presets::Preset; -use crate::status::system::SystemStatus; -use crate::status::TorideStatus; +use crate::presets::Preset; +use crate::system::SystemStatus; +use crate::TorideStatus; /// Collector for periodic status snapshots. pub struct Collector { @@ -77,7 +77,7 @@ pub struct Collector { /// /// ```no_run /// use std::time::Duration; -/// use toride::status::collector::Collector; +/// use toride_status::collector::Collector; /// /// let mut collector = Collector::default_collector(); /// collector.collect(); // First call, no delta. @@ -185,8 +185,8 @@ impl Collector { /// /// ``` /// use std::time::Duration; - /// use toride::status::collector::Collector; - /// use toride::status::presets::Preset; + /// use toride_status::collector::Collector; + /// use toride_status::presets::Preset; /// /// let collector = Collector::new(Duration::from_secs(5), Preset::Minimal); /// assert_eq!(collector.interval(), Duration::from_secs(5)); @@ -211,7 +211,7 @@ impl Collector { /// # Examples /// /// ``` - /// use toride::status::collector::Collector; + /// use toride_status::collector::Collector; /// /// let collector = Collector::default_collector(); /// ``` @@ -245,7 +245,7 @@ impl Collector { /// # Examples /// /// ```no_run - /// use toride::status::collector::Collector; + /// use toride_status::collector::Collector; /// /// let mut collector = Collector::default_collector(); /// let (status, delta) = collector.collect(); @@ -274,7 +274,7 @@ impl Collector { /// /// ```no_run /// use std::time::Duration; - /// use toride::status::collector::Collector; + /// use toride_status::collector::Collector; /// /// let mut collector = Collector::new(Duration::from_millis(100), Default::default()); /// let (status, delta) = collector.collect_after_interval(); @@ -298,7 +298,7 @@ impl Collector { /// # Examples /// /// ``` - /// use toride::status::collector::Collector; + /// use toride_status::collector::Collector; /// /// let mut collector = Collector::default_collector(); /// collector.collect(); @@ -321,7 +321,7 @@ impl Collector { /// /// ``` /// use std::time::Duration; -/// use toride::status::collector::Collector; +/// use toride_status::collector::Collector; /// /// let collector = Collector::builder() /// .interval(Duration::from_secs(2)) @@ -587,7 +587,7 @@ impl SystemStatus { /// /// ```no_run /// use std::time::Duration; - /// use toride::status::system::SystemStatus; + /// use toride_status::system::SystemStatus; /// /// let prev = SystemStatus::collect(); /// std::thread::sleep(Duration::from_secs(1)); @@ -647,7 +647,7 @@ impl std::fmt::Display for SystemDelta { #[cfg(test)] mod tests { use super::*; - use crate::status::system::{ + use crate::system::{ DiskIoSnapshot, DiskStatus, HardwareInventory, MemoryStatus, NetworkStatus, OsInfo, ProcessSnapshot, SensorSnapshot, StaticInfo, SystemStatus, VirtualizationSnapshot, }; diff --git a/crates/toride/src/status/daemon.rs b/crates/toride-status/src/daemon.rs similarity index 99% rename from crates/toride/src/status/daemon.rs rename to crates/toride-status/src/daemon.rs index b75aea3..16b9be6 100644 --- a/crates/toride/src/status/daemon.rs +++ b/crates/toride-status/src/daemon.rs @@ -33,7 +33,7 @@ //! # Examples //! //! ```no_run -//! use toride::status::daemon::DaemonStatus; +//! use toride_status::daemon::DaemonStatus; //! //! let status = DaemonStatus::collect(); //! if status.alive { @@ -78,7 +78,7 @@ impl DaemonStatus { /// # Examples /// /// ```no_run - /// use toride::status::daemon::DaemonStatus; + /// use toride_status::daemon::DaemonStatus; /// /// let status = DaemonStatus::collect(); /// if status.alive { @@ -111,7 +111,7 @@ impl DaemonStatus { /// /// ``` /// use std::path::Path; - /// use toride::status::daemon::DaemonStatus; + /// use toride_status::daemon::DaemonStatus; /// /// let status = DaemonStatus::collect_with_paths( /// Path::new("/var/run/myapp.pid"), diff --git a/crates/toride/src/status/doctor.rs b/crates/toride-status/src/doctor.rs similarity index 96% rename from crates/toride/src/status/doctor.rs rename to crates/toride-status/src/doctor.rs index 3b29773..e0fb5f7 100644 --- a/crates/toride/src/status/doctor.rs +++ b/crates/toride-status/src/doctor.rs @@ -38,7 +38,7 @@ //! Run all checks and print the report: //! //! ```no_run -//! use toride::status::doctor::DoctorReport; +//! use toride_status::doctor::DoctorReport; //! //! let report = DoctorReport::check(); //! println!("{report}"); @@ -52,10 +52,10 @@ //! Run checks against pre-collected snapshots: //! //! ```no_run -//! use toride::status::daemon::DaemonStatus; -//! use toride::status::doctor::DoctorReport; -//! use toride::status::ssh::SshStatus; -//! use toride::status::system::SystemStatus; +//! use toride_status::daemon::DaemonStatus; +//! use toride_status::doctor::DoctorReport; +//! use toride_status::ssh::SshStatus; +//! use toride_status::system::SystemStatus; //! //! let system = SystemStatus::collect(); //! let daemon = DaemonStatus::collect(); @@ -68,10 +68,10 @@ use std::fmt; use serde::Serialize; use which::which; -use crate::status::daemon::DaemonStatus; -use crate::status::privacy::PrivacyMode; -use crate::status::ssh::SshStatus; -use crate::status::system::SystemStatus; +use crate::daemon::DaemonStatus; +use crate::privacy::PrivacyMode; +use crate::ssh::SshStatus; +use crate::system::SystemStatus; /// A collection of health checks. #[derive(Debug, Clone, Serialize)] @@ -112,7 +112,7 @@ impl DoctorReport { /// # Examples /// /// ```no_run - /// use toride::status::doctor::DoctorReport; + /// use toride_status::doctor::DoctorReport; /// /// let report = DoctorReport::check(); /// for check in &report.checks { @@ -136,10 +136,10 @@ impl DoctorReport { /// # Examples /// /// ``` - /// use toride::status::daemon::DaemonStatus; - /// use toride::status::doctor::DoctorReport; - /// use toride::status::ssh::SshStatus; - /// use toride::status::system::SystemStatus; + /// use toride_status::daemon::DaemonStatus; + /// use toride_status::doctor::DoctorReport; + /// use toride_status::ssh::SshStatus; + /// use toride_status::system::SystemStatus; /// /// let system = SystemStatus::collect(); /// let daemon = DaemonStatus::collect(); @@ -184,7 +184,7 @@ impl DoctorReport { /// # Examples /// /// ``` - /// use toride::status::doctor::DoctorReport; + /// use toride_status::doctor::DoctorReport; /// /// let report = DoctorReport::check(); /// if report.all_passed() { @@ -204,7 +204,7 @@ impl DoctorReport { /// # Examples /// /// ``` - /// use toride::status::doctor::DoctorReport; + /// use toride_status::doctor::DoctorReport; /// /// let report = DoctorReport::check(); /// let (pass, warn, fail) = report.summary(); @@ -787,7 +787,7 @@ mod tests { #[test] fn check_with_custom_statuses() { - use crate::status::system::{DiskIoSnapshot, DiskStatus, HardwareInventory, MemoryStatus, NetworkStatus, OsInfo, SensorSnapshot, StaticInfo, VirtualizationSnapshot}; + use crate::system::{DiskIoSnapshot, DiskStatus, HardwareInventory, MemoryStatus, NetworkStatus, OsInfo, SensorSnapshot, StaticInfo, VirtualizationSnapshot}; let system = SystemStatus { cpu_usage: Some(50.0), memory: MemoryStatus { @@ -849,7 +849,7 @@ mod tests { network_interfaces: vec![], sensors: vec![], boot_time: None, - processes: crate::status::system::ProcessSnapshot { + processes: crate::system::ProcessSnapshot { processes: vec![], total_count: 0, }, @@ -900,7 +900,7 @@ mod tests { #[test] fn check_system_hostname_empty_fails() { - use crate::status::system::{DiskIoSnapshot, DiskStatus, HardwareInventory, MemoryStatus, NetworkStatus, OsInfo, SensorSnapshot, StaticInfo, VirtualizationSnapshot}; + use crate::system::{DiskIoSnapshot, DiskStatus, HardwareInventory, MemoryStatus, NetworkStatus, OsInfo, SensorSnapshot, StaticInfo, VirtualizationSnapshot}; let system = SystemStatus { cpu_usage: Some(50.0), memory: MemoryStatus { used_bytes: 100, total_bytes: 200, percentage: 50.0, free_bytes: 0, available_bytes: 0, cached_bytes: 0, buffers_bytes: 0 }, @@ -910,7 +910,7 @@ mod tests { os_info: OsInfo { name: Some("TestOS".to_string()), version: Some("1.0".to_string()), kernel_version: None, arch: "x86_64".to_string(), os_type: None, edition: None, codename: None, bitness: None, timezone: None, locale: None, current_user: None, is_root: false, container_detected: false, vm_detected: false, wsl_detected: false, systemd_detected: false, target_triple: None }, cpu_cores: vec![], physical_cores: Some(4), swap: None, disks: vec![], network_interfaces: vec![], sensors: vec![], boot_time: None, - processes: crate::status::system::ProcessSnapshot { processes: vec![], total_count: 0 }, + processes: crate::system::ProcessSnapshot { processes: vec![], total_count: 0 }, gpu: vec![], battery: None, disk_io: DiskIoSnapshot::default(), virtualization: VirtualizationSnapshot::default(), @@ -941,13 +941,13 @@ mod tests { let ssh = SshStatus { mux_master_alive: false, control_path_valid: false, config_valid: false, agent_running: false, key_count: 0 }; let report = DoctorReport::check_with(&system, &daemon, &ssh); let hostname_check = report.checks.iter().find(|c| c.name == "system.hostname").unwrap(); - assert_eq!(hostname_check.status, crate::status::doctor::CheckStatus::Fail); + assert_eq!(hostname_check.status, crate::doctor::CheckStatus::Fail); } // --- Helpers --- fn happy_system() -> SystemStatus { - use crate::status::system::{DiskIoSnapshot, DiskStatus, HardwareInventory, MemoryStatus, NetworkStatus, OsInfo, SensorSnapshot, StaticInfo, VirtualizationSnapshot}; + use crate::system::{DiskIoSnapshot, DiskStatus, HardwareInventory, MemoryStatus, NetworkStatus, OsInfo, SensorSnapshot, StaticInfo, VirtualizationSnapshot}; SystemStatus { cpu_usage: Some(50.0), memory: MemoryStatus { @@ -1025,7 +1025,7 @@ mod tests { network_interfaces: vec![], sensors: vec![], boot_time: None, - processes: crate::status::system::ProcessSnapshot { + processes: crate::system::ProcessSnapshot { processes: vec![], total_count: 0, }, @@ -1099,7 +1099,7 @@ mod tests { #[test] fn check_with_zero_memory_produces_fail() { let mut system = happy_system(); - system.memory = crate::status::system::MemoryStatus { + system.memory = crate::system::MemoryStatus { used_bytes: 0, total_bytes: 0, percentage: 0.0, @@ -1296,7 +1296,7 @@ mod tests { #[test] fn snapshot_doctor_report_display() { - use crate::status::system::{DiskIoSnapshot, DiskStatus, HardwareInventory, MemoryStatus, NetworkStatus, OsInfo, SensorSnapshot, StaticInfo, VirtualizationSnapshot}; + use crate::system::{DiskIoSnapshot, DiskStatus, HardwareInventory, MemoryStatus, NetworkStatus, OsInfo, SensorSnapshot, StaticInfo, VirtualizationSnapshot}; let system = SystemStatus { cpu_usage: Some(42.5), memory: MemoryStatus { @@ -1329,7 +1329,7 @@ mod tests { bytes_received: 100_000_000_000, bytes_transmitted: 50_000_000_000, }, - load_average: Some(crate::status::system::LoadAverage { + load_average: Some(crate::system::LoadAverage { one: 2.50, five: 3.00, fifteen: 3.50, @@ -1362,7 +1362,7 @@ mod tests { network_interfaces: vec![], sensors: vec![], boot_time: Some(1_700_000_000), - processes: crate::status::system::ProcessSnapshot { + processes: crate::system::ProcessSnapshot { processes: vec![], total_count: 0, }, @@ -1474,7 +1474,7 @@ mod tests { #[test] fn network_provider_passes_with_interfaces() { - use crate::status::system::NetworkInterface; + use crate::system::NetworkInterface; let mut system = happy_system(); system.network_interfaces = vec![NetworkInterface { name: "en0".to_string(), @@ -1505,7 +1505,7 @@ mod tests { #[test] fn network_counters_monotonic_warns_on_zero_interface() { - use crate::status::system::NetworkInterface; + use crate::system::NetworkInterface; let mut system = happy_system(); system.network_interfaces = vec![NetworkInterface { name: "lo".to_string(), @@ -1536,7 +1536,7 @@ mod tests { #[test] fn network_counters_monotonic_passes_with_nonzero_interface() { - use crate::status::system::NetworkInterface; + use crate::system::NetworkInterface; let mut system = happy_system(); system.network_interfaces = vec![NetworkInterface { name: "eth0".to_string(), @@ -1582,7 +1582,7 @@ mod tests { #[test] fn privacy_serial_redacted_warns_when_serial_present() { - use crate::status::system::HardwareInventory; + use crate::system::HardwareInventory; let mut system = happy_system(); system.static_info.hardware = HardwareInventory { system_serial: Some("SN-123456".to_string()), @@ -1625,7 +1625,7 @@ mod tests { #[test] fn privacy_mac_redacted_warns_when_mac_present() { - use crate::status::system::NetworkInterface; + use crate::system::NetworkInterface; let mut system = happy_system(); system.network_interfaces = vec![NetworkInterface { name: "en0".to_string(), @@ -1687,8 +1687,8 @@ mod tests { #[test] fn privacy_command_args_warns_when_cmdline_present() { let mut system = happy_system(); - system.processes = crate::status::system::ProcessSnapshot { - processes: vec![crate::status::system::ProcessStatus { + system.processes = crate::system::ProcessSnapshot { + processes: vec![crate::system::ProcessStatus { pid: 1, parent_pid: None, name: "test".to_string(), @@ -1763,7 +1763,7 @@ mod tests { #[test] fn quality_monotonic_counters_passes_with_nonzero() { let mut system = happy_system(); - system.network = crate::status::system::NetworkStatus { + system.network = crate::system::NetworkStatus { bytes_received: 1000, bytes_transmitted: 500, }; @@ -1775,7 +1775,7 @@ mod tests { #[test] fn quality_monotonic_counters_warns_for_zero() { let mut system = happy_system(); - system.network = crate::status::system::NetworkStatus { + system.network = crate::system::NetworkStatus { bytes_received: 0, bytes_transmitted: 0, }; @@ -1830,7 +1830,7 @@ mod tests { #[test] fn quality_virtual_fs_filtered_warns_for_tmpfs() { let mut system = happy_system(); - system.disks.push(crate::status::system::DiskStatus { + system.disks.push(crate::system::DiskStatus { name: "tmpfs".to_string(), mount_point: "/tmp".to_string(), filesystem: "tmpfs".to_string(), @@ -1854,7 +1854,7 @@ mod tests { #[test] fn quality_gpu_identity_no_metrics_warns() { - use crate::status::system::GpuInfo; + use crate::system::GpuInfo; let mut system = happy_system(); system.gpu = vec![GpuInfo { name: "Test GPU".to_string(), @@ -1884,7 +1884,7 @@ mod tests { #[test] fn quality_gpu_identity_no_metrics_passes_with_utilization() { - use crate::status::system::GpuInfo; + use crate::system::GpuInfo; let mut system = happy_system(); system.gpu = vec![GpuInfo { name: "Test GPU".to_string(), diff --git a/crates/toride/src/status/error.rs b/crates/toride-status/src/error.rs similarity index 98% rename from crates/toride/src/status/error.rs rename to crates/toride-status/src/error.rs index b72fc9b..48e58ed 100644 --- a/crates/toride/src/status/error.rs +++ b/crates/toride-status/src/error.rs @@ -12,7 +12,7 @@ //! Using the `?` operator with I/O errors: //! //! ``` -//! use toride::status::error::{StatusError, StatusResult}; +//! use toride_status::error::{StatusError, StatusResult}; //! //! fn read_pid_file(path: &str) -> StatusResult { //! let content = std::fs::read_to_string(path)?; @@ -25,7 +25,7 @@ //! Matching on specific error variants: //! //! ``` -//! use toride::status::error::StatusError; +//! use toride_status::error::StatusError; //! //! fn handle_error(err: StatusError) { //! match err { @@ -72,7 +72,7 @@ //! Collecting results into a vector: //! //! ``` -//! use toride::status::error::{StatusError, StatusResult}; +//! use toride_status::error::{StatusError, StatusResult}; //! //! fn collect_metrics() -> Vec> { //! vec![ diff --git a/crates/toride-status/src/lib.rs b/crates/toride-status/src/lib.rs new file mode 100644 index 0000000..4a9f246 --- /dev/null +++ b/crates/toride-status/src/lib.rs @@ -0,0 +1,1175 @@ +//! Toride status subsystem. +//! +//! Provides [`TorideStatus`] — a point-in-time snapshot of every monitored +//! subsystem (OS metrics, daemon liveness, SSH health). +//! +//! ```no_run +//! use toride_status::TorideStatus; +//! +//! let status = TorideStatus::collect(); +//! println!("{status}"); +//! ``` + +pub mod capabilities; +pub mod collector; +pub mod daemon; +pub mod doctor; +pub mod error; +pub mod presets; +pub mod privacy; +pub mod provider; +pub mod ssh; +pub mod system; +pub mod units; + +use std::fmt; + +use serde::Serialize; + +pub use capabilities::Capabilities; +pub use collector::{Collector, DiskIoDelta, GpuDelta, ProcessDelta, SystemDelta}; +pub use daemon::DaemonStatus; +pub use doctor::{CheckStatus, DoctorCheck, DoctorReport}; +pub use error::{StatusError, StatusResult}; +pub use presets::Preset; +pub use privacy::{PrivacyMode, Redactor}; +pub use ssh::SshStatus; +pub use system::{ + BatteryInfo, CpuCore, CpuSample, CpuStatic, DiskIoSnapshot, DiskStatus, GpuInfo, + HardwareInventory, LoadAverage, MemoryStatus, NetworkInterface, NetworkStatus, OsInfo, + ProcessSnapshot, ProcessStatus, SensorSnapshot, SensorStatus, StaticInfo, SwapStatus, + SystemStatus, VirtualizationSnapshot, +}; +pub use units::{Bytes, Celsius, Hertz, Rpm, Volts, Watts}; + +/// Top-level aggregated status snapshot. +/// +/// Collects data from all subsystems in a single [`collect`](Self::collect) +/// call. Each sub-status is independent — a failure in one subsystem does not +/// prevent the others from being collected. +/// +/// # Examples +/// +/// ```no_run +/// use toride_status::TorideStatus; +/// +/// let status = TorideStatus::collect(); +/// assert!(!status.system.hostname.is_empty()); +/// ``` +#[derive(Debug, Clone, Serialize)] +pub struct TorideStatus { + /// OS-level metrics (CPU, memory, disk, network, uptime). + pub system: SystemStatus, + /// Daemon liveness and health (PID, restart count, socket state). + pub daemon: DaemonStatus, + /// SSH subsystem health (mux master, control path, agent, keys). + pub ssh: SshStatus, + /// Platform capabilities. + pub capabilities: Capabilities, + /// Non-fatal warnings collected during status gathering. + #[serde(skip)] + pub warnings: Vec, + /// Wall-clock time when this snapshot was collected. + #[serde(skip)] + pub collected_at: std::time::SystemTime, +} + +impl TorideStatus { + /// Collect a point-in-time snapshot of all subsystems. + /// + /// Delegates to [`collect_with_preset`](Self::collect_with_preset) using + /// the [`Preset::default`] preset (`Diagnostics`), which includes every + /// available metric. + /// + /// Each subsystem is collected independently — if one fails, its fields + /// will contain `None` values rather than propagating the error. + /// + /// # Examples + /// + /// ```no_run + /// use toride_status::TorideStatus; + /// + /// let status = TorideStatus::collect(); + /// println!("{}", status.system.hostname); + /// ``` + #[must_use] + pub fn collect() -> Self { + Self::collect_with_preset(Preset::default()) + } + + /// Collect a snapshot filtered by the given [`Preset`]. + /// + /// Always-collected fields (`cpu_usage`, memory, disk, network, `os_info`, + /// hostname, uptime) are populated regardless of preset. Preset-gated + /// fields are zeroed / set to `None` when the preset excludes them. + /// + /// # Examples + /// + /// ```no_run + /// use toride_status::{TorideStatus, Preset}; + /// + /// let status = TorideStatus::collect_with_preset(Preset::Minimal); + /// assert!(status.system.cpu_cores.is_empty()); + /// ``` + #[must_use] + pub fn collect_with_preset(preset: Preset) -> Self { + let mut status = Self::collect_all(); + status.apply_preset(preset); + status + } + + /// Collect a snapshot with privacy-aware redaction applied. + /// + /// Uses the default preset (`Diagnostics`). The [`PrivacyMode`] + /// controls which fields are redacted before they are stored. + /// + /// # Examples + /// + /// ```no_run + /// use toride_status::{TorideStatus, PrivacyMode}; + /// + /// let status = TorideStatus::collect_with_privacy(PrivacyMode::Safe); + /// assert_eq!(status.system.hostname, "[redacted]"); + /// ``` + #[must_use] + pub fn collect_with_privacy(mode: PrivacyMode) -> Self { + Self::collect_with_options(Preset::default(), mode) + } + + /// Collect a snapshot with both preset filtering and privacy redaction. + /// + /// Combines [`collect_with_preset`](Self::collect_with_preset) and + /// privacy redaction in a single call. The preset is applied first, + /// then privacy redaction is applied to the remaining fields. + /// + /// # Examples + /// + /// ```no_run + /// use toride_status::{TorideStatus, Preset, PrivacyMode}; + /// + /// let status = TorideStatus::collect_with_options( + /// Preset::Minimal, + /// PrivacyMode::Safe, + /// ); + /// assert_eq!(status.system.hostname, "[redacted]"); + /// assert!(status.system.cpu_cores.is_empty()); + /// ``` + #[must_use] + pub fn collect_with_options(preset: Preset, privacy: PrivacyMode) -> Self { + let mut status = Self::collect_with_preset(preset); + status.apply_privacy(privacy); + status + } + + // ── Internal helpers ──────────────────────────────────────────── + + /// Collect every subsystem without any filtering. + fn collect_all() -> Self { + let system = SystemStatus::collect(); + let daemon = DaemonStatus::collect(); + let ssh = SshStatus::collect(); + let capabilities = Capabilities::detect(); + let mut warnings = Vec::new(); + if system.hostname.is_empty() { + warnings.push(StatusError::DataUnavailable("hostname unavailable".to_string())); + } + if system.memory.total_bytes == 0 { + warnings.push(StatusError::DataUnavailable("memory info unavailable".to_string())); + } + Self { system, daemon, ssh, capabilities, warnings, collected_at: std::time::SystemTime::now() } + } + + /// Zero out fields excluded by the given preset. + /// + /// Always-collected fields (`cpu_usage`, memory, disk, network, `os_info`, + /// hostname, uptime, `load_average`, `boot_time`) are never touched. + fn apply_preset(&mut self, preset: Preset) { + if !preset.includes_per_core_cpu() { + self.system.cpu_cores.clear(); + self.system.physical_cores = None; + } + if !preset.includes_swap() { + self.system.swap = None; + } + if !preset.includes_sensors() { + self.system.sensors.clear(); + } + if !preset.includes_processes() { + self.system.processes = system::ProcessSnapshot { + processes: Vec::new(), + total_count: 0, + }; + } + if !preset.includes_network_interfaces() { + self.system.network_interfaces.clear(); + } + if !preset.includes_all_disks() { + self.system.disks.clear(); + } + if !preset.includes_gpu() { + self.system.gpu.clear(); + } + if !preset.includes_battery() { + self.system.battery = None; + } + } + + /// Apply privacy redaction to sensitive fields. + /// + /// Uses the [`Redactor`] from the privacy module to redact hostnames, + /// MAC addresses, serial numbers, command lines, and other identifying + /// information according to the given mode. + fn apply_privacy(&mut self, mode: PrivacyMode) { + let redactor = Redactor::new(mode); + + // Hostname + self.system.hostname = redactor.redact_hostname(&self.system.hostname); + + // Static info hostname + self.system.static_info.hostname = + redactor.redact_hostname(&self.system.static_info.hostname); + + // Network interface MAC addresses + for iface in &mut self.system.network_interfaces { + if let Some(ref mac) = iface.mac_address { + iface.mac_address = Some(redactor.redact_mac(mac)); + } + } + + // Process command lines + for proc in &mut self.system.processes.processes { + if let Some(ref cmd) = proc.command_line { + proc.command_line = Some(redactor.redact_command_line(cmd)); + } + } + + // Process usernames (hide unless Full mode) + if !redactor.should_show_username() { + for proc in &mut self.system.processes.processes { + proc.user = None; + } + } + + // Hardware serial numbers, UUIDs, asset tags + if let Some(ref serial) = self.system.static_info.hardware.system_serial { + self.system.static_info.hardware.system_serial = + Some(redactor.redact_serial(serial)); + } + if let Some(ref uuid) = self.system.static_info.hardware.system_uuid { + self.system.static_info.hardware.system_uuid = Some(redactor.redact_uuid(uuid)); + } + if let Some(ref tag) = self.system.static_info.hardware.asset_tag { + self.system.static_info.hardware.asset_tag = Some(redactor.redact_asset_tag(tag)); + } + + // Disk serial numbers + for disk in &mut self.system.disks { + if let Some(ref serial) = disk.serial { + disk.serial = Some(redactor.redact_serial(serial)); + } + } + } +} + +impl fmt::Display for TorideStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "=== Toride Status ===")?; + write!(f, "{}", self.system)?; + write!(f, "{}", self.daemon)?; + write!(f, "{}", self.ssh)?; + write!(f, "{}", self.capabilities)?; + if !self.warnings.is_empty() { + writeln!(f, "Warnings:")?; + for w in &self.warnings { + writeln!(f, " \u{26a0} {w}")?; + } + } + Ok(()) + } +} + +/// Simple API entry point for collecting system status. +/// +/// `SysProbe` provides the primary user-facing API as specified in the +/// system-status spec. For advanced use cases (streaming, delta tracking), +/// use [`Collector`] instead. +/// +/// # Examples +/// +/// ```no_run +/// use toride_status::SysProbe; +/// +/// let probe = SysProbe::new(); +/// let snapshot = probe.snapshot(); +/// println!("{}", snapshot.system.cpu_usage.unwrap_or(0.0)); +/// ``` +pub struct SysProbe { + preset: Preset, + privacy: PrivacyMode, +} + +impl SysProbe { + /// Create a new `SysProbe` with default settings (Diagnostics preset, no privacy). + #[must_use] + pub fn new() -> Self { + Self { + preset: Preset::Diagnostics, + privacy: PrivacyMode::Diagnostics, + } + } + + /// Create a builder for customizing the probe. + pub fn builder() -> SysProbeBuilder { + SysProbeBuilder::default() + } + + /// Collect a system status snapshot. + #[must_use] + pub fn snapshot(&self) -> TorideStatus { + TorideStatus::collect_with_options(self.preset, self.privacy) + } + + /// Detect platform capabilities. + #[must_use] + pub fn capabilities(&self) -> Capabilities { + Capabilities::detect() + } +} + +impl Default for SysProbe { + fn default() -> Self { + Self::new() + } +} + +/// Builder for [`SysProbe`]. +#[derive(Debug, Clone)] +pub struct SysProbeBuilder { + preset: Preset, + privacy: PrivacyMode, +} + +impl SysProbeBuilder { + pub fn new() -> Self { + Self { + preset: Preset::Diagnostics, + privacy: PrivacyMode::Diagnostics, + } + } + + pub fn preset(mut self, preset: Preset) -> Self { + self.preset = preset; + self + } + + pub fn privacy(mut self, mode: PrivacyMode) -> Self { + self.privacy = mode; + self + } + + pub fn build(self) -> SysProbe { + SysProbe { + preset: self.preset, + privacy: self.privacy, + } + } +} + +impl Default for SysProbeBuilder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn collect_returns_all_subsystems() { + let status = TorideStatus::collect(); + // SystemStatus should always have a hostname on any platform + assert!( + !status.system.hostname.is_empty(), + "hostname should not be empty" + ); + } + + #[test] + fn display_contains_section_headers() { + let status = TorideStatus::collect(); + let output = format!("{status}"); + assert!(output.contains("=== Toride Status ===")); + assert!(output.contains("System:")); + assert!(output.contains("Daemon:")); + assert!(output.contains("SSH:")); + if !status.warnings.is_empty() { + assert!(output.contains("Warnings:")); + } + } + + #[test] + fn serialize_to_json_succeeds() { + let status = TorideStatus::collect(); + let json = serde_json::to_string(&status); + assert!(json.is_ok(), "serialization should succeed: {:?}", json.err()); + } + + #[test] + fn snapshot_toride_status_display() { + use crate::system::{ + CpuCore, DiskStatus, LoadAverage, MemoryStatus, NetworkInterface, NetworkStatus, + OsInfo, SensorStatus, SwapStatus, + }; + let status = TorideStatus { + system: SystemStatus { + cpu_usage: Some(42.5), + memory: MemoryStatus { + used_bytes: 8 * 1024 * 1024 * 1024, + total_bytes: 16 * 1024 * 1024 * 1024, + percentage: 50.0, + cached_bytes: 0, + available_bytes: 0, + free_bytes: 0, + buffers_bytes: 0, + }, + disk: DiskStatus { + name: "Macintosh HD".to_string(), + mount_point: "/".to_string(), + filesystem: "apfs".to_string(), + used_bytes: 500 * 1024 * 1024 * 1024, + total_bytes: 1024 * 1024 * 1024 * 1024, + percentage: 50.0, + is_removable: false, + disk_type: "Unknown".to_string(), + available_bytes: 0, + free_bytes: 0, + physical_device_path: None, + model: None, + serial: None, + temperature: None, + wear_percent: None, + }, + network: NetworkStatus { + bytes_received: 100 * 1024 * 1024 * 1024, + bytes_transmitted: 50 * 1024 * 1024 * 1024, + }, + load_average: Some(LoadAverage { + one: 2.50, + five: 3.00, + fifteen: 3.50, + }), + uptime_secs: Some(90061), + hostname: "test-host".to_string(), + os_info: OsInfo { + name: Some("macOS".to_string()), + version: Some("15.0".to_string()), + kernel_version: Some("24.0.0".to_string()), + arch: "aarch64".to_string(), + os_type: None, + edition: None, + codename: None, + bitness: None, + timezone: None, + locale: None, + current_user: None, + is_root: false, + container_detected: false, + vm_detected: false, + wsl_detected: false, + systemd_detected: false, + target_triple: None, + }, + cpu_cores: vec![ + CpuCore { + name: "cpu0".to_string(), + usage: 45.0, + frequency: 3200, + }, + CpuCore { + name: "cpu1".to_string(), + usage: 40.0, + frequency: 3200, + }, + ], + physical_cores: Some(8), + swap: Some(SwapStatus { + used_bytes: 512 * 1024 * 1024, + total_bytes: 2 * 1024 * 1024 * 1024, + percentage: 25.0, + free_bytes: 2 * 1024 * 1024 * 1024 - 512 * 1024 * 1024, + }), + disks: vec![], + network_interfaces: vec![ + NetworkInterface { + name: "en0".to_string(), + bytes_received: 60 * 1024 * 1024 * 1024, + bytes_transmitted: 30 * 1024 * 1024 * 1024, + packets_received: 1_000_000, + packets_transmitted: 500_000, + errors_received: 0, + errors_transmitted: 0, + drops_transmitted: 0, + drops_received: 0, + mtu: None, + mac_address: None, + display_name: None, + description: None, + ipv4_addresses: Vec::new(), + ipv6_addresses: Vec::new(), + gateway: None, + dns: None, + link_status: None, + speed_bps: None, + duplex: None, + }, + ], + sensors: vec![ + SensorStatus { + label: "CPU".to_string(), + temperature: Some(55.5), + fan_rpm: None, + voltage: None, + thermal_throttling: None, + }, + ], + boot_time: Some(1_700_000_000), + processes: crate::system::ProcessSnapshot { + processes: vec![], + total_count: 0, + }, + gpu: vec![], + battery: None, + disk_io: DiskIoSnapshot::default(), + virtualization: VirtualizationSnapshot::default(), + sensor_snapshot: SensorSnapshot { readings: Vec::new(), cpu_temperature: None, gpu_temperature: None }, + static_info: StaticInfo { + os: OsInfo { name: None, version: None, kernel_version: None, arch: String::new(), os_type: None, edition: None, codename: None, bitness: None, timezone: None, locale: None, current_user: None, is_root: false, container_detected: false, vm_detected: false, wsl_detected: false, systemd_detected: false, target_triple: None }, + kernel_version: None, + hostname: String::new(), + cpu_brand: String::new(), + cpu_vendor: String::new(), + cpu_frequency: 0, + physical_cores: None, + logical_cores: 0, + memory_total_bytes: 0, + hardware: HardwareInventory::default(), + sockets: None, + cores_per_socket: None, + threads_per_core: None, + base_frequency: None, + max_frequency: None, + cache_l1d: None, + cache_l1i: None, + cache_l2: None, + cache_l3: None, + }, + }, + daemon: DaemonStatus { + alive: true, + pid: Some(54321), + uptime_secs: Some(86400), + restart_count: 2, + stale_socket: false, + }, + ssh: SshStatus { + mux_master_alive: true, + control_path_valid: true, + config_valid: true, + agent_running: true, + key_count: 3, + }, + capabilities: Capabilities::detect(), + warnings: vec![], + collected_at: std::time::SystemTime::now(), + }; + insta::assert_snapshot!("toride_status_display", format!("{}", status)); + } + + #[test] + fn collect_includes_capabilities() { + let status = TorideStatus::collect(); + assert!(status.capabilities.system.cpu_usage); + } + + #[test] + fn collect_includes_processes() { + let status = TorideStatus::collect(); + assert!(status.system.processes.total_count > 0); + } + + #[test] + fn collector_produces_status_and_delta() { + let mut collector = Collector::default_collector(); + let (status, delta) = collector.collect(); + assert!(!status.system.hostname.is_empty()); + assert!(delta.is_none()); // first collect + std::thread::sleep(std::time::Duration::from_millis(50)); + let (_, delta2) = collector.collect(); + assert!(delta2.is_some()); + } + + #[test] + fn redactor_safe_mode() { + let r = Redactor::new(PrivacyMode::Safe); + assert_eq!(r.redact_hostname("myhost"), "[redacted]"); + } + + #[test] + fn preset_diagnostics_includes_all() { + let p = Preset::Diagnostics; + assert!(p.includes_per_core_cpu()); + assert!(p.includes_processes()); + } + + // ── Integration: Full pipeline ───────────────────────────────── + + #[test] + fn integration_full_pipeline_collect_serialize_display() { + // Collect a full TorideStatus snapshot. + let status = TorideStatus::collect(); + + // Verify all subsystems are populated with non-trivial data. + assert!(!status.system.hostname.is_empty(), "system hostname must be populated"); + assert!(status.system.memory.total_bytes > 0, "memory total must be nonzero"); + assert!(status.system.disk.total_bytes > 0 || !status.system.disk.mount_point.is_empty(), + "disk must be populated"); + assert!(!status.system.os_info.arch.is_empty(), "OS arch must be populated"); + assert!(status.system.processes.total_count > 0, "process count must be nonzero"); + // daemon and ssh fields are always set (even if not alive/running). + // capabilities always populated via detect(). + assert!(status.capabilities.system.cpu_usage, "capabilities must report cpu_usage"); + + // Serialize to JSON and verify it parses as valid JSON. + let json = serde_json::to_string(&status).expect("serialization must succeed"); + let parsed: serde_json::Value = + serde_json::from_str(&json).expect("JSON must be valid and parseable"); + assert!(parsed.is_object(), "JSON must be an object"); + assert!(parsed.get("system").is_some(), "JSON must contain 'system' key"); + assert!(parsed.get("daemon").is_some(), "JSON must contain 'daemon' key"); + assert!(parsed.get("ssh").is_some(), "JSON must contain 'ssh' key"); + assert!(parsed.get("capabilities").is_some(), "JSON must contain 'capabilities' key"); + + // Display and verify all section headers are present. + let display = format!("{status}"); + assert!(display.contains("=== Toride Status ==="), "display must have top header"); + assert!(display.contains("System:"), "display must have System section"); + assert!(display.contains("Daemon:"), "display must have Daemon section"); + assert!(display.contains("SSH:"), "display must have SSH section"); + assert!(display.contains("Capabilities"), "display must have Capabilities section"); + } + + // ── Integration: Collector ───────────────────────────────────── + + #[test] + fn integration_collector_two_collects_with_delta() { + let mut collector = Collector::default_collector(); + + // First collect: status present, delta absent. + let (status1, delta1) = collector.collect(); + assert!(!status1.system.hostname.is_empty(), "first collect must return valid status"); + assert!(delta1.is_none(), "first collect must have no delta"); + + // Sleep briefly so elapsed > 0 for the delta. + std::thread::sleep(std::time::Duration::from_millis(100)); + + // Second collect: delta present with reasonable values. + let (status2, delta2) = collector.collect(); + assert!(!status2.system.hostname.is_empty(), "second collect must return valid status"); + let d = delta2.expect("second collect must produce a delta"); + + // Elapsed must be at least as long as we slept. + assert!(d.elapsed >= std::time::Duration::from_millis(80), + "delta elapsed ({:?}) must be >= 80ms", d.elapsed); + + // Rates must be non-negative and finite. + assert!(d.network.bytes_received_rate.is_finite(), "RX rate must be finite"); + assert!(d.network.bytes_received_rate >= 0.0, "RX rate must be non-negative"); + assert!(d.network.bytes_transmitted_rate.is_finite(), "TX rate must be finite"); + assert!(d.network.bytes_transmitted_rate >= 0.0, "TX rate must be non-negative"); + + // Deltas must be non-negative (saturating_sub). + // (They could be 0 if the system had no traffic, which is fine.) + + // CPU delta: if both snapshots had CPU data, the delta should be Some. + if status1.system.cpu_usage.is_some() && status2.system.cpu_usage.is_some() { + assert!(d.cpu_usage_delta.is_some(), "CPU delta must be Some when both snapshots have CPU data"); + } + } + + // ── Integration: Privacy ─────────────────────────────────────── + + #[test] + fn integration_privacy_redactor_on_toride_hostname() { + let status = TorideStatus::collect(); + let hostname = &status.system.hostname; + assert!(!hostname.is_empty(), "hostname must be non-empty for this test"); + + // Safe mode: hostname is fully redacted. + let safe = Redactor::new(PrivacyMode::Safe); + let redacted = safe.redact_hostname(hostname); + assert_eq!(redacted, "[redacted]", "Safe mode must redact hostname"); + assert_ne!(redacted, *hostname, "redacted value must differ from original"); + + // Diagnostics mode: hostname is shown as-is. + let diag = Redactor::new(PrivacyMode::Diagnostics); + let shown = diag.redact_hostname(hostname); + assert_eq!(shown, *hostname, "Diagnostics mode must preserve hostname"); + + // Full mode: hostname is also shown. + let full = Redactor::new(PrivacyMode::Full); + let full_shown = full.redact_hostname(hostname); + assert_eq!(full_shown, *hostname, "Full mode must preserve hostname"); + } + + // ── Integration: Presets ─────────────────────────────────────── + + #[test] + fn integration_preset_diagnostics_includes_all_features() { + let p = Preset::Diagnostics; + assert!(p.includes_per_core_cpu(), "Diagnostics must include per-core CPU"); + assert!(p.includes_swap(), "Diagnostics must include swap"); + assert!(p.includes_sensors(), "Diagnostics must include sensors"); + assert!(p.includes_processes(), "Diagnostics must include processes"); + assert!(p.includes_network_interfaces(), "Diagnostics must include network interfaces"); + assert!(p.includes_all_disks(), "Diagnostics must include all disks"); + assert!(p.includes_os_info(), "Diagnostics must include OS info"); + } + + #[test] + fn integration_preset_minimal_excludes_features() { + let p = Preset::Minimal; + assert!(!p.includes_per_core_cpu(), "Minimal must exclude per-core CPU"); + assert!(!p.includes_swap(), "Minimal must exclude swap"); + assert!(!p.includes_sensors(), "Minimal must exclude sensors"); + assert!(!p.includes_processes(), "Minimal must exclude processes"); + assert!(!p.includes_network_interfaces(), "Minimal must exclude network interfaces"); + assert!(!p.includes_all_disks(), "Minimal must exclude all disks"); + // Minimal always includes OS info. + assert!(p.includes_os_info(), "Minimal must include OS info"); + } + + // ── Integration: Doctor ──────────────────────────────────────── + + #[test] + fn integration_doctor_report_has_system_daemon_ssh_checks() { + let report = DoctorReport::check(); + assert!(!report.checks.is_empty(), "doctor report must have checks"); + + // Verify checks exist for each subsystem category. + let has_system = report.checks.iter().any(|c| c.name.starts_with("system.")); + let has_daemon = report.checks.iter().any(|c| c.name.starts_with("daemon.")); + let has_ssh = report.checks.iter().any(|c| c.name.starts_with("ssh.")); + assert!(has_system, "report must include system checks"); + assert!(has_daemon, "report must include daemon checks"); + assert!(has_ssh, "report must include ssh checks"); + + // Verify summary counts are consistent. + let (pass, warn, fail) = report.summary(); + assert_eq!( + pass + warn + fail, + report.checks.len(), + "summary counts must equal total check count" + ); + } + + // ── Integration: Error handling ──────────────────────────────── + + #[test] + fn integration_error_variants_work() { + // PermissionDenied + let err = StatusError::PermissionDenied("/secret".into()); + assert!(err.to_string().contains("permission denied")); + assert!(err.to_string().contains("/secret")); + + // CommandNotFound + let err = StatusError::CommandNotFound("foobar".into()); + assert!(err.to_string().contains("command not found")); + assert!(err.to_string().contains("foobar")); + + // CommandFailed + let err = StatusError::CommandFailed { + command: "ls".into(), + code: 1, + stderr: "no such file".into(), + }; + let msg = err.to_string(); + assert!(msg.contains("command failed"), "msg: {msg}"); + assert!(msg.contains("exited 1"), "msg: {msg}"); + assert!(msg.contains("no such file"), "msg: {msg}"); + + // CommandTimeout + let err = StatusError::CommandTimeout("ping".into()); + assert!(err.to_string().contains("timed out")); + + // ParseError + let err = StatusError::ParseError("bad data".into()); + assert!(err.to_string().contains("parse error")); + + // Io + let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"); + let err = StatusError::Io(io_err); + assert!(err.to_string().contains("io error")); + + // Unsupported + let err = StatusError::Unsupported("plan9".into()); + assert!(err.to_string().contains("unsupported platform")); + + // DataUnavailable + let err = StatusError::DataUnavailable("gpu".into()); + assert!(err.to_string().contains("data unavailable")); + } + + #[test] + fn integration_error_clone_works_for_all_variants() { + let variants = vec![ + StatusError::PermissionDenied("path".into()), + StatusError::CommandNotFound("cmd".into()), + StatusError::CommandFailed { + command: "run".into(), + code: 42, + stderr: "err".into(), + }, + StatusError::CommandTimeout("slow".into()), + StatusError::ParseError("bad".into()), + StatusError::Io(std::io::Error::other("io")), + StatusError::Unsupported("os".into()), + StatusError::DataUnavailable("info".into()), + ]; + + for original in &variants { + let cloned = original.clone(); + // Cloned error must have the same Display output. + assert_eq!( + original.to_string(), + cloned.to_string(), + "Clone must preserve Display output for variant" + ); + } + } + + #[test] + fn integration_error_display_produces_nonempty_strings() { + let variants = vec![ + StatusError::PermissionDenied("p".into()), + StatusError::CommandNotFound("c".into()), + StatusError::CommandFailed { command: "r".into(), code: 1, stderr: "e".into() }, + StatusError::CommandTimeout("t".into()), + StatusError::ParseError("p".into()), + StatusError::Io(std::io::Error::other("o")), + StatusError::Unsupported("u".into()), + StatusError::DataUnavailable("d".into()), + ]; + + for variant in &variants { + let display = variant.to_string(); + assert!(!display.is_empty(), "Display must produce non-empty string for {variant:?}"); + // Every Display string must be at least as long as the prefix. + assert!(display.len() >= 5, "Display string suspiciously short: {display}"); + } + } + + // ── collect_with_preset ──────────────────────────────────────── + + #[test] + fn collect_with_preset_minimal_excludes_gated_fields() { + let status = TorideStatus::collect_with_preset(Preset::Minimal); + + // Always-collected fields must still be populated. + assert!(status.system.cpu_usage.is_some(), "cpu_usage must be collected"); + assert!(status.system.memory.total_bytes > 0, "memory must be collected"); + assert!(!status.system.hostname.is_empty(), "hostname must be collected"); + assert!(!status.system.os_info.arch.is_empty(), "os_info must be collected"); + + // Preset-gated fields must be cleared. + assert!(status.system.cpu_cores.is_empty(), "Minimal must exclude cpu_cores"); + assert!(status.system.physical_cores.is_none(), "Minimal must exclude physical_cores"); + assert!(status.system.swap.is_none(), "Minimal must exclude swap"); + assert!(status.system.sensors.is_empty(), "Minimal must exclude sensors"); + assert_eq!(status.system.processes.total_count, 0, "Minimal must exclude processes"); + assert!(status.system.processes.processes.is_empty(), "Minimal must exclude process list"); + assert!(status.system.network_interfaces.is_empty(), "Minimal must exclude network interfaces"); + assert!(status.system.disks.is_empty(), "Minimal must exclude all_disks"); + assert!(status.system.gpu.is_empty(), "Minimal must exclude gpu"); + assert!(status.system.battery.is_none(), "Minimal must exclude battery"); + } + + #[test] + fn collect_with_preset_diagnostics_includes_all_fields() { + let status = TorideStatus::collect_with_preset(Preset::Diagnostics); + + // Always-collected fields. + assert!(status.system.cpu_usage.is_some(), "cpu_usage must be collected"); + assert!(status.system.memory.total_bytes > 0, "memory must be collected"); + assert!(!status.system.hostname.is_empty(), "hostname must be collected"); + assert!(!status.system.os_info.arch.is_empty(), "os_info must be collected"); + + // Diagnostics includes everything — nothing should be cleared. + // cpu_cores and processes are populated on real hardware. + assert!(!status.system.cpu_cores.is_empty(), "Diagnostics must include cpu_cores"); + assert!(status.system.processes.total_count > 0, "Diagnostics must include processes"); + } + + #[test] + fn collect_with_preset_task_manager_includes_expected_fields() { + let status = TorideStatus::collect_with_preset(Preset::TaskManager); + + // TaskManager includes: per_core_cpu, sensors, processes, all_disks. + assert!(!status.system.cpu_cores.is_empty(), "TaskManager must include cpu_cores"); + assert!(status.system.processes.total_count > 0, "TaskManager must include processes"); + + // TaskManager excludes: swap, network_interfaces, gpu, battery. + assert!(status.system.swap.is_none(), "TaskManager must exclude swap"); + assert!(status.system.network_interfaces.is_empty(), "TaskManager must exclude network interfaces"); + assert!(status.system.gpu.is_empty(), "TaskManager must exclude gpu"); + assert!(status.system.battery.is_none(), "TaskManager must exclude battery"); + } + + #[test] + fn collect_with_preset_server_monitoring_expected_fields() { + let status = TorideStatus::collect_with_preset(Preset::ServerMonitoring); + + // ServerMonitoring includes: swap, network_interfaces. + // (swap may be None if not configured, so we only check the field isn't forcibly cleared + // by verifying that the preset *would* include it) + + // ServerMonitoring excludes: per_core_cpu, sensors, processes, all_disks, gpu, battery. + assert!(status.system.cpu_cores.is_empty(), "ServerMonitoring must exclude cpu_cores"); + assert!(status.system.sensors.is_empty(), "ServerMonitoring must exclude sensors"); + assert_eq!(status.system.processes.total_count, 0, "ServerMonitoring must exclude processes"); + assert!(status.system.disks.is_empty(), "ServerMonitoring must exclude all_disks"); + assert!(status.system.gpu.is_empty(), "ServerMonitoring must exclude gpu"); + assert!(status.system.battery.is_none(), "ServerMonitoring must exclude battery"); + } + + #[test] + fn collect_with_preset_privacy_safe_excludes_gated_fields() { + let status = TorideStatus::collect_with_preset(Preset::PrivacySafeBugReport); + + // PrivacySafeBugReport excludes most gated fields. + assert!(status.system.cpu_cores.is_empty(), "PrivacySafe must exclude cpu_cores"); + assert!(status.system.swap.is_none(), "PrivacySafe must exclude swap"); + assert!(status.system.sensors.is_empty(), "PrivacySafe must exclude sensors"); + assert_eq!(status.system.processes.total_count, 0, "PrivacySafe must exclude processes"); + assert!(status.system.network_interfaces.is_empty(), "PrivacySafe must exclude network interfaces"); + assert!(status.system.disks.is_empty(), "PrivacySafe must exclude all_disks"); + assert!(status.system.battery.is_none(), "PrivacySafe must exclude battery"); + + // PrivacySafeBugReport includes gpu. + // (gpu may be empty on some hardware; we just verify it wasn't forcibly cleared + // by checking the preset logic — gpu is included for PrivacySafeBugReport) + } + + #[test] + fn collect_with_preset_always_collects_core_fields() { + // Verify that always-collected fields survive every preset. + let presets = [ + Preset::Minimal, + Preset::TaskManager, + Preset::Diagnostics, + Preset::ServerMonitoring, + Preset::PrivacySafeBugReport, + ]; + + for preset in presets { + let status = TorideStatus::collect_with_preset(preset); + assert!(status.system.cpu_usage.is_some(), "{preset}: cpu_usage must be collected"); + assert!(status.system.memory.total_bytes > 0, "{preset}: memory must be collected"); + assert!(!status.system.hostname.is_empty(), "{preset}: hostname must be collected"); + assert!(!status.system.os_info.arch.is_empty(), "{preset}: os_info must be collected"); + // disk (root) is always collected. + assert!(!status.system.disk.mount_point.is_empty(), "{preset}: root disk must be collected"); + // network aggregate is always collected. + // (bytes may be 0 on idle systems, but the struct is populated) + let _ = status.system.network.bytes_received; + let _ = status.system.network.bytes_transmitted; + } + } + + // ── collect_with_privacy ─────────────────────────────────────── + + #[test] + fn collect_with_privacy_safe_redacts_hostname() { + let status = TorideStatus::collect_with_privacy(PrivacyMode::Safe); + assert_eq!(status.system.hostname, "[redacted]", "Safe mode must redact hostname"); + } + + #[test] + fn collect_with_privacy_diagnostics_preserves_hostname() { + let status = TorideStatus::collect_with_privacy(PrivacyMode::Diagnostics); + assert_ne!(status.system.hostname, "[redacted]", "Diagnostics mode must not redact hostname"); + assert!(!status.system.hostname.is_empty(), "Diagnostics mode hostname must not be empty"); + } + + #[test] + fn collect_with_privacy_full_preserves_hostname() { + let status = TorideStatus::collect_with_privacy(PrivacyMode::Full); + assert_ne!(status.system.hostname, "[redacted]", "Full mode must not redact hostname"); + assert!(!status.system.hostname.is_empty(), "Full mode hostname must not be empty"); + } + + #[test] + fn collect_with_privacy_safe_does_not_affect_other_fields() { + let safe = TorideStatus::collect_with_privacy(PrivacyMode::Safe); + let full = TorideStatus::collect_with_privacy(PrivacyMode::Full); + + // Non-hostname fields should be identical in structure. + assert_eq!( + safe.system.memory.total_bytes, full.system.memory.total_bytes, + "privacy must not affect memory" + ); + assert_eq!( + safe.system.os_info.arch, full.system.os_info.arch, + "privacy must not affect os_info" + ); + } + + // ── collect_with_options ─────────────────────────────────────── + + #[test] + fn collect_with_options_combines_preset_and_privacy() { + let status = TorideStatus::collect_with_options(Preset::Minimal, PrivacyMode::Safe); + + // Privacy: hostname redacted. + assert_eq!(status.system.hostname, "[redacted]", "Safe must redact hostname"); + + // Preset: gated fields cleared. + assert!(status.system.cpu_cores.is_empty(), "Minimal must exclude cpu_cores"); + assert!(status.system.swap.is_none(), "Minimal must exclude swap"); + assert!(status.system.sensors.is_empty(), "Minimal must exclude sensors"); + assert_eq!(status.system.processes.total_count, 0, "Minimal must exclude processes"); + assert!(status.system.network_interfaces.is_empty(), "Minimal must exclude network interfaces"); + assert!(status.system.disks.is_empty(), "Minimal must exclude all_disks"); + + // Always-collected fields still present. + assert!(status.system.cpu_usage.is_some(), "cpu_usage must be collected"); + assert!(status.system.memory.total_bytes > 0, "memory must be collected"); + } + + #[test] + fn collect_with_options_diagnostics_full_shows_everything() { + let status = TorideStatus::collect_with_options(Preset::Diagnostics, PrivacyMode::Full); + + // Full privacy: hostname shown. + assert_ne!(status.system.hostname, "[redacted]", "Full must not redact hostname"); + + // Diagnostics preset: everything included. + assert!(!status.system.cpu_cores.is_empty(), "Diagnostics must include cpu_cores"); + assert!(status.system.processes.total_count > 0, "Diagnostics must include processes"); + } + + #[test] + fn collect_with_options_all_preset_privacy_combinations() { + // Smoke test: every preset + privacy mode combination must not panic. + let presets = [ + Preset::Minimal, + Preset::TaskManager, + Preset::Diagnostics, + Preset::ServerMonitoring, + Preset::PrivacySafeBugReport, + ]; + let modes = [PrivacyMode::Safe, PrivacyMode::Diagnostics, PrivacyMode::Full]; + + for preset in presets { + for mode in modes { + let status = TorideStatus::collect_with_options(preset, mode); + // Must always have a hostname (possibly redacted). + assert!( + !status.system.hostname.is_empty(), + "{preset} + {mode:?}: hostname must not be empty" + ); + // Must always have memory. + assert!( + status.system.memory.total_bytes > 0, + "{preset} + {mode:?}: memory must be nonzero" + ); + } + } + } + + // ── collect() backward compatibility ─────────────────────────── + + #[test] + fn collect_uses_diagnostics_preset() { + let status = TorideStatus::collect(); + + // Diagnostics includes everything, so gated fields should be populated. + assert!(!status.system.cpu_cores.is_empty(), "collect() must include cpu_cores (Diagnostics default)"); + assert!(status.system.processes.total_count > 0, "collect() must include processes (Diagnostics default)"); + // Hostname must not be redacted (no privacy applied). + assert_ne!(status.system.hostname, "[redacted]", "collect() must not redact hostname"); + } + + #[test] + fn collect_matches_collect_with_preset_diagnostics() { + let a = TorideStatus::collect(); + let b = TorideStatus::collect_with_preset(Preset::Diagnostics); + + // Both use the same preset (Diagnostics) and no privacy, so + // structural properties must be identical. Exact counts may + // differ because system state changes between the two calls. + assert_eq!(a.system.hostname, b.system.hostname, "hostname must match"); + assert!(!a.system.cpu_cores.is_empty(), "collect() cpu_cores must be non-empty"); + assert!(!b.system.cpu_cores.is_empty(), "collect_with_preset cpu_cores must be non-empty"); + assert!(a.system.processes.total_count > 0, "collect() must have processes"); + assert!(b.system.processes.total_count > 0, "collect_with_preset must have processes"); + } + + // ── Display with preset/privacy ──────────────────────────────── + + #[test] + fn display_with_preset_minimal_omits_cleared_sections() { + let status = TorideStatus::collect_with_preset(Preset::Minimal); + let output = format!("{status}"); + + // Always-visible sections. + assert!(output.contains("=== Toride Status ==="), "must have top header"); + assert!(output.contains("System:"), "must have System section"); + assert!(output.contains("Hostname:"), "must have Hostname"); + assert!(output.contains("Memory:"), "must have Memory"); + + // Cleared sections should not appear. + // Use precise prefixes matching the Display format (" Swap: "). + assert!(status.system.cpu_cores.is_empty(), "cpu_cores must be empty"); + assert!(status.system.swap.is_none(), "swap must be None"); + assert!(status.system.sensors.is_empty(), "sensors must be empty"); + assert!(status.system.processes.total_count == 0, "processes must be empty"); + assert!(status.system.network_interfaces.is_empty(), "network_interfaces must be empty"); + assert!(status.system.disks.is_empty(), "disks must be empty"); + } + + #[test] + fn display_with_privacy_safe_shows_redacted_hostname() { + let status = TorideStatus::collect_with_privacy(PrivacyMode::Safe); + let output = format!("{status}"); + + assert!(output.contains("[redacted]"), "display must contain [redacted]"); + assert!(output.contains("Hostname: [redacted]"), "hostname line must show [redacted]"); + } + + // ── Serialization with preset/privacy ────────────────────────── + + #[test] + fn serialize_with_preset_minimal() { + let status = TorideStatus::collect_with_preset(Preset::Minimal); + let json = serde_json::to_string(&status); + assert!(json.is_ok(), "serialization must succeed: {:?}", json.err()); + + let parsed: serde_json::Value = serde_json::from_str(&json.unwrap()).unwrap(); + let system = parsed.get("system").unwrap(); + // cpu_cores should be empty array. + let cores = system.get("cpu_cores").unwrap().as_array().unwrap(); + assert!(cores.is_empty(), "Minimal cpu_cores must be empty in JSON"); + } + + #[test] + fn serialize_with_privacy_safe() { + let status = TorideStatus::collect_with_privacy(PrivacyMode::Safe); + let json = serde_json::to_string(&status).expect("serialization must succeed"); + let parsed: serde_json::Value = serde_json::from_str(&json).expect("JSON must parse"); + + let system = parsed.get("system").unwrap(); + let hostname = system.get("hostname").unwrap().as_str().unwrap(); + assert_eq!(hostname, "[redacted]", "JSON hostname must be [redacted]"); + } +} diff --git a/crates/toride/src/status/mod.rs b/crates/toride-status/src/mod.rs similarity index 99% rename from crates/toride/src/status/mod.rs rename to crates/toride-status/src/mod.rs index f7a87bf..4a9f246 100644 --- a/crates/toride/src/status/mod.rs +++ b/crates/toride-status/src/mod.rs @@ -4,7 +4,7 @@ //! subsystem (OS metrics, daemon liveness, SSH health). //! //! ```no_run -//! use toride::status::TorideStatus; +//! use toride_status::TorideStatus; //! //! let status = TorideStatus::collect(); //! println!("{status}"); @@ -51,7 +51,7 @@ pub use units::{Bytes, Celsius, Hertz, Rpm, Volts, Watts}; /// # Examples /// /// ```no_run -/// use toride::status::TorideStatus; +/// use toride_status::TorideStatus; /// /// let status = TorideStatus::collect(); /// assert!(!status.system.hostname.is_empty()); @@ -87,7 +87,7 @@ impl TorideStatus { /// # Examples /// /// ```no_run - /// use toride::status::TorideStatus; + /// use toride_status::TorideStatus; /// /// let status = TorideStatus::collect(); /// println!("{}", status.system.hostname); @@ -106,7 +106,7 @@ impl TorideStatus { /// # Examples /// /// ```no_run - /// use toride::status::{TorideStatus, Preset}; + /// use toride_status::{TorideStatus, Preset}; /// /// let status = TorideStatus::collect_with_preset(Preset::Minimal); /// assert!(status.system.cpu_cores.is_empty()); @@ -126,7 +126,7 @@ impl TorideStatus { /// # Examples /// /// ```no_run - /// use toride::status::{TorideStatus, PrivacyMode}; + /// use toride_status::{TorideStatus, PrivacyMode}; /// /// let status = TorideStatus::collect_with_privacy(PrivacyMode::Safe); /// assert_eq!(status.system.hostname, "[redacted]"); @@ -145,7 +145,7 @@ impl TorideStatus { /// # Examples /// /// ```no_run - /// use toride::status::{TorideStatus, Preset, PrivacyMode}; + /// use toride_status::{TorideStatus, Preset, PrivacyMode}; /// /// let status = TorideStatus::collect_with_options( /// Preset::Minimal, @@ -297,7 +297,7 @@ impl fmt::Display for TorideStatus { /// # Examples /// /// ```no_run -/// use toride::status::SysProbe; +/// use toride_status::SysProbe; /// /// let probe = SysProbe::new(); /// let snapshot = probe.snapshot(); @@ -417,7 +417,7 @@ mod tests { #[test] fn snapshot_toride_status_display() { - use crate::status::system::{ + use crate::system::{ CpuCore, DiskStatus, LoadAverage, MemoryStatus, NetworkInterface, NetworkStatus, OsInfo, SensorStatus, SwapStatus, }; @@ -534,7 +534,7 @@ mod tests { }, ], boot_time: Some(1_700_000_000), - processes: crate::status::system::ProcessSnapshot { + processes: crate::system::ProcessSnapshot { processes: vec![], total_count: 0, }, diff --git a/crates/toride/src/status/presets.rs b/crates/toride-status/src/presets.rs similarity index 97% rename from crates/toride/src/status/presets.rs rename to crates/toride-status/src/presets.rs index 3bc9684..ed974ec 100644 --- a/crates/toride/src/status/presets.rs +++ b/crates/toride-status/src/presets.rs @@ -40,7 +40,7 @@ //! # Examples //! //! ```no_run -//! use toride::status::presets::Preset; +//! use toride_status::presets::Preset; //! //! let preset = Preset::TaskManager; //! println!("Preset: {preset}"); @@ -80,7 +80,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::TaskManager.includes_per_core_cpu()); /// assert!(!Preset::Minimal.includes_per_core_cpu()); @@ -97,7 +97,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_swap()); /// assert!(!Preset::Minimal.includes_swap()); @@ -114,7 +114,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_sensors()); /// assert!(!Preset::ServerMonitoring.includes_sensors()); @@ -131,7 +131,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::TaskManager.includes_processes()); /// assert!(!Preset::Minimal.includes_processes()); @@ -148,7 +148,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::ServerMonitoring.includes_network_interfaces()); /// assert!(!Preset::Minimal.includes_network_interfaces()); @@ -166,7 +166,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_all_disks()); /// assert!(!Preset::ServerMonitoring.includes_all_disks()); @@ -183,7 +183,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_gpu()); /// assert!(!Preset::Minimal.includes_gpu()); @@ -200,7 +200,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_battery()); /// assert!(!Preset::Minimal.includes_battery()); @@ -217,7 +217,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Minimal.includes_os_info()); /// assert!(Preset::Diagnostics.includes_os_info()); @@ -234,7 +234,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_hardware_inventory()); /// assert!(!Preset::Minimal.includes_hardware_inventory()); @@ -251,7 +251,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_virtualization()); /// assert!(!Preset::Minimal.includes_virtualization()); @@ -268,7 +268,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_disk_io()); /// assert!(!Preset::Minimal.includes_disk_io()); @@ -288,7 +288,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_static_info()); /// assert!(!Preset::Minimal.includes_static_info()); @@ -308,7 +308,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_network_addresses()); /// assert!(!Preset::Minimal.includes_network_addresses()); @@ -325,7 +325,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert!(Preset::Diagnostics.includes_process_disk_io()); /// assert!(!Preset::Minimal.includes_process_disk_io()); @@ -340,7 +340,7 @@ impl Preset { /// # Examples /// /// ``` - /// use toride::status::presets::Preset; + /// use toride_status::presets::Preset; /// /// assert_eq!(Preset::Minimal.name(), "Minimal"); /// assert_eq!(Preset::Diagnostics.name(), "Diagnostics"); diff --git a/crates/toride/src/status/privacy.rs b/crates/toride-status/src/privacy.rs similarity index 96% rename from crates/toride/src/status/privacy.rs rename to crates/toride-status/src/privacy.rs index a851019..412220c 100644 --- a/crates/toride/src/status/privacy.rs +++ b/crates/toride-status/src/privacy.rs @@ -22,7 +22,7 @@ //! Redacting sensitive data for logging: //! //! ``` -//! use toride::status::privacy::{PrivacyMode, Redactor}; +//! use toride_status::privacy::{PrivacyMode, Redactor}; //! //! let redactor = Redactor::new(PrivacyMode::Safe); //! assert_eq!(redactor.redact_hostname("my-host.local"), "[redacted]"); @@ -32,7 +32,7 @@ //! Using diagnostics mode for troubleshooting: //! //! ``` -//! use toride::status::privacy::{PrivacyMode, Redactor}; +//! use toride_status::privacy::{PrivacyMode, Redactor}; //! //! let redactor = Redactor::new(PrivacyMode::Diagnostics); //! assert_eq!(redactor.redact_hostname("my-host.local"), "my-host.local"); @@ -42,7 +42,7 @@ //! Full mode for local debugging: //! //! ``` -//! use toride::status::privacy::{PrivacyMode, Redactor}; +//! use toride_status::privacy::{PrivacyMode, Redactor}; //! //! let redactor = Redactor::new(PrivacyMode::Full); //! assert_eq!(redactor.redact_serial("C02X12345678"), "C02X12345678"); @@ -81,7 +81,7 @@ impl Redactor { /// # Examples /// /// ``` - /// use toride::status::privacy::{PrivacyMode, Redactor}; + /// use toride_status::privacy::{PrivacyMode, Redactor}; /// /// let redactor = Redactor::new(PrivacyMode::Safe); /// ``` @@ -98,7 +98,7 @@ impl Redactor { /// # Examples /// /// ``` - /// use toride::status::privacy::{PrivacyMode, Redactor}; + /// use toride_status::privacy::{PrivacyMode, Redactor}; /// /// let safe = Redactor::new(PrivacyMode::Safe); /// assert_eq!(safe.redact_hostname("my-host.local"), "[redacted]"); @@ -122,7 +122,7 @@ impl Redactor { /// # Examples /// /// ``` - /// use toride::status::privacy::{PrivacyMode, Redactor}; + /// use toride_status::privacy::{PrivacyMode, Redactor}; /// /// let redactor = Redactor::new(PrivacyMode::Diagnostics); /// assert_eq!(redactor.redact_mac("AA:BB:CC:DD:EE:FF"), "[redacted]"); @@ -146,7 +146,7 @@ impl Redactor { /// # Examples /// /// ``` - /// use toride::status::privacy::{PrivacyMode, Redactor}; + /// use toride_status::privacy::{PrivacyMode, Redactor}; /// /// let redactor = Redactor::new(PrivacyMode::Safe); /// assert_eq!(redactor.redact_serial("C02X12345678"), "[redacted]"); @@ -168,7 +168,7 @@ impl Redactor { /// # Examples /// /// ``` - /// use toride::status::privacy::{PrivacyMode, Redactor}; + /// use toride_status::privacy::{PrivacyMode, Redactor}; /// /// let safe = Redactor::new(PrivacyMode::Safe); /// assert_eq!(safe.redact_command_line("/usr/bin/sshd -D -R -p 22"), "[redacted]"); @@ -198,7 +198,7 @@ impl Redactor { /// # Examples /// /// ``` - /// use toride::status::privacy::{PrivacyMode, Redactor}; + /// use toride_status::privacy::{PrivacyMode, Redactor}; /// /// let safe = Redactor::new(PrivacyMode::Safe); /// assert!(!safe.should_show_username()); @@ -219,7 +219,7 @@ impl Redactor { /// # Examples /// /// ``` - /// use toride::status::privacy::{PrivacyMode, Redactor}; + /// use toride_status::privacy::{PrivacyMode, Redactor}; /// /// let safe = Redactor::new(PrivacyMode::Safe); /// assert_eq!(safe.redact_uuid("550e8400-e29b-41d4-a716-446655440000"), "[redacted]"); @@ -243,7 +243,7 @@ impl Redactor { /// # Examples /// /// ``` - /// use toride::status::privacy::{PrivacyMode, Redactor}; + /// use toride_status::privacy::{PrivacyMode, Redactor}; /// /// let safe = Redactor::new(PrivacyMode::Safe); /// assert_eq!(safe.redact_asset_tag("ASSET-001234"), "[redacted]"); diff --git a/crates/toride/src/status/provider.rs b/crates/toride-status/src/provider.rs similarity index 94% rename from crates/toride/src/status/provider.rs rename to crates/toride-status/src/provider.rs index 7295bb5..4272db7 100644 --- a/crates/toride/src/status/provider.rs +++ b/crates/toride-status/src/provider.rs @@ -32,9 +32,9 @@ //! ## Implementing a custom provider //! //! ```ignore -//! use toride::status::provider::*; -//! use toride::status::system::*; -//! use toride::status::error::StatusResult; +//! use toride_status::provider::*; +//! use toride_status::system::*; +//! use toride_status::error::StatusResult; //! //! struct MyProvider; //! @@ -60,22 +60,22 @@ //! //! ## Error handling //! -//! All provider methods return [`StatusResult`](crate::status::error::StatusResult). -//! Implementations should return appropriate [`StatusError`](crate::status::error::StatusError) +//! All provider methods return [`StatusResult`](crate::error::StatusResult). +//! Implementations should return appropriate [`StatusError`](crate::error::StatusError) //! variants when data cannot be read: //! -//! - [`StatusError::PermissionDenied`](crate::status::error::StatusError::PermissionDenied) +//! - [`StatusError::PermissionDenied`](crate::error::StatusError::PermissionDenied) //! when access is denied -//! - [`StatusError::DataUnavailable`](crate::status::error::StatusError::DataUnavailable) +//! - [`StatusError::DataUnavailable`](crate::error::StatusError::DataUnavailable) //! when the metric is not available on this platform -//! - [`StatusError::Io`](crate::status::error::StatusError::Io) for filesystem errors +//! - [`StatusError::Io`](crate::error::StatusError::Io) for filesystem errors #![allow(clippy::missing_errors_doc)] // Internal trait methods; errors are documented via StatusResult use std::collections::HashMap; -use crate::status::error::StatusResult; -use crate::status::system::{ +use crate::error::StatusResult; +use crate::system::{ BatteryInfo, CpuCore, CpuSample, CpuStatic, DiskIoSnapshot, DiskStatus, GpuInfo, LoadAverage, MemoryStatus, NetworkInterface, NetworkStatus, OsInfo, ProcessSnapshot, SensorStatus, StaticInfo, SwapStatus, VirtualizationSnapshot, diff --git a/crates/toride/src/status/snapshots/toride__status__daemon__tests__daemon_status_display.snap b/crates/toride-status/src/snapshots/toride__status__daemon__tests__daemon_status_display.snap similarity index 100% rename from crates/toride/src/status/snapshots/toride__status__daemon__tests__daemon_status_display.snap rename to crates/toride-status/src/snapshots/toride__status__daemon__tests__daemon_status_display.snap diff --git a/crates/toride/src/status/snapshots/toride__status__doctor__tests__doctor_report_display.snap b/crates/toride-status/src/snapshots/toride__status__doctor__tests__doctor_report_display.snap similarity index 100% rename from crates/toride/src/status/snapshots/toride__status__doctor__tests__doctor_report_display.snap rename to crates/toride-status/src/snapshots/toride__status__doctor__tests__doctor_report_display.snap diff --git a/crates/toride/src/status/snapshots/toride__status__ssh__tests__ssh_status_display.snap b/crates/toride-status/src/snapshots/toride__status__ssh__tests__ssh_status_display.snap similarity index 100% rename from crates/toride/src/status/snapshots/toride__status__ssh__tests__ssh_status_display.snap rename to crates/toride-status/src/snapshots/toride__status__ssh__tests__ssh_status_display.snap diff --git a/crates/toride/src/status/snapshots/toride__status__system__tests__system_status_display.snap b/crates/toride-status/src/snapshots/toride__status__system__tests__system_status_display.snap similarity index 100% rename from crates/toride/src/status/snapshots/toride__status__system__tests__system_status_display.snap rename to crates/toride-status/src/snapshots/toride__status__system__tests__system_status_display.snap diff --git a/crates/toride/src/status/snapshots/toride__status__tests__toride_status_display.snap b/crates/toride-status/src/snapshots/toride__status__tests__toride_status_display.snap similarity index 100% rename from crates/toride/src/status/snapshots/toride__status__tests__toride_status_display.snap rename to crates/toride-status/src/snapshots/toride__status__tests__toride_status_display.snap diff --git a/crates/toride-status/src/snapshots/toride_status__daemon__tests__daemon_status_display.snap b/crates/toride-status/src/snapshots/toride_status__daemon__tests__daemon_status_display.snap new file mode 100644 index 0000000..58002e0 --- /dev/null +++ b/crates/toride-status/src/snapshots/toride_status__daemon__tests__daemon_status_display.snap @@ -0,0 +1,11 @@ +--- +source: crates/toride-status/src/daemon.rs +assertion_line: 638 +expression: "format!(\"{}\", status)" +--- +Daemon: + Alive: yes + PID: 54321 + Uptime: 86400s + Restarts: 2 + Socket: ok diff --git a/crates/toride-status/src/snapshots/toride_status__doctor__tests__doctor_report_display.snap b/crates/toride-status/src/snapshots/toride_status__doctor__tests__doctor_report_display.snap new file mode 100644 index 0000000..cca2652 --- /dev/null +++ b/crates/toride-status/src/snapshots/toride_status__doctor__tests__doctor_report_display.snap @@ -0,0 +1,40 @@ +--- +source: crates/toride-status/src/doctor.rs +expression: "format!(\"{}\", report)" +--- +=== Doctor Report === + ✓ system.hostname: hostname: test-host + ✓ system.cpu: CPU usage: 42.5% + ✓ system.memory: memory: 8000000000 used / 16000000000 total + ⚠ system.disks: 0 disk(s) found + ✓ system.os_info: OS: macOS 15.0 + ⚠ system.gpu_provider: 0 GPU(s) detected + ⚠ system.battery_provider: no battery detected + ⚠ system.sensor_provider: 0 sensor(s) found + ✓ system.cpu_sample_quality: CPU usage: 42.5% + ✓ system.memory_sanity: memory: 8000000000 / 16000000000 (used <= total: true) + ✓ system.disk_duplicates: 0 disk(s), no duplicates + ✓ system.virtualization: bare metal or unknown + ⚠ system.disk_io: read: 0 bytes, written: 0 bytes + ✓ quality.cpu_interval: CPU usage: 42.5% + ✓ quality.monotonic_counters: rx: 100000000000 bytes, tx: 50000000000 bytes + ✓ quality.clock_sanity: boot time 1700000000 is reasonable + ✓ quality.virtual_fs_filtered: no virtual filesystems in disk list + ✓ daemon.alive: daemon alive (PID 54321) + ✓ daemon.socket: socket ok + ✓ ssh.binary: ssh found + ✓ ssh.agent_binary: ssh-add found + ✓ ssh.config: config valid + ✓ ssh.agent: agent running (3 key(s)) + ✓ privacy.hostname: hostname present + ⚠ privacy.process_details: no process details available + ⚠ permissions.process_executable: cannot read /proc/self/exe (permission denied or unsupported) + ⚠ permissions.process_cmdline: cannot read /proc/self/cmdline (permission denied or unsupported) + ✓ permissions.network_stats: network counters are readable + ✓ permissions.is_admin: not running as root + ⚠ network.provider: no network interfaces detected + ✓ network.counters_monotonic: no interfaces to check + ✓ privacy.serial_redacted: no serial numbers in collected data + ✓ privacy.mac_redacted: no MAC addresses in collected data + ✓ privacy.command_args: no command line arguments in collected data +--- 25 passed, 9 warnings, 0 failures diff --git a/crates/toride-status/src/snapshots/toride_status__ssh__tests__ssh_status_display.snap b/crates/toride-status/src/snapshots/toride_status__ssh__tests__ssh_status_display.snap new file mode 100644 index 0000000..195ab29 --- /dev/null +++ b/crates/toride-status/src/snapshots/toride_status__ssh__tests__ssh_status_display.snap @@ -0,0 +1,10 @@ +--- +source: crates/toride-status/src/ssh.rs +expression: "format!(\"{}\", status)" +--- +SSH: + Mux master: alive + Control path: valid + Config: ok + Agent: running + Keys: 3 diff --git a/crates/toride-status/src/snapshots/toride_status__system__tests__system_status_display.snap b/crates/toride-status/src/snapshots/toride_status__system__tests__system_status_display.snap new file mode 100644 index 0000000..e8cd45b --- /dev/null +++ b/crates/toride-status/src/snapshots/toride_status__system__tests__system_status_display.snap @@ -0,0 +1,28 @@ +--- +source: crates/toride-status/src/system.rs +expression: "format!(\"{}\", status)" +--- +System: + Hostname: test-host + OS: macOS 15.0 (kernel 24.0.0) aarch64 + CPU: 42.5% + Physical cores: 8 + CPU cores: + cpu0: 45.0% (3200 MHz) + cpu1: 40.0% (3200 MHz) + Memory: 8.0 GiB / 16.0 GiB (50.0%) + Swap: 512.0 MiB / 2.0 GiB (25.0%) + Disk: 500.0 GiB / 1.0 TiB (50.0%) + Disks: + / (Macintosh HD) [apfs]: 500.0 GiB / 1.0 TiB (50.0%) + /Volumes/External (External) [exfat]: 100.0 GiB / 500.0 GiB (20.0%) + Network: 50.0 GiB sent, 100.0 GiB received + Network interfaces: + en0: 30.0 GiB sent, 60.0 GiB received + lo0: 20.0 GiB sent, 40.0 GiB received + Load: 2.50 / 3.00 / 3.50 + Sensors: + CPU: 55.5°C + GPU: 48.0°C + Uptime: 1d 1h 1m 1s + Boot time: 1700000000 diff --git a/crates/toride-status/src/snapshots/toride_status__tests__toride_status_display.snap b/crates/toride-status/src/snapshots/toride_status__tests__toride_status_display.snap new file mode 100644 index 0000000..1a83d8e --- /dev/null +++ b/crates/toride-status/src/snapshots/toride_status__tests__toride_status_display.snap @@ -0,0 +1,115 @@ +--- +source: crates/toride-status/src/lib.rs +expression: "format!(\"{}\", status)" +--- +=== Toride Status === +System: + Hostname: test-host + OS: macOS 15.0 (kernel 24.0.0) aarch64 + CPU: 42.5% + Physical cores: 8 + CPU cores: + cpu0: 45.0% (3200 MHz) + cpu1: 40.0% (3200 MHz) + Memory: 8.0 GiB / 16.0 GiB (50.0%) + Swap: 512.0 MiB / 2.0 GiB (25.0%) + Disk: 500.0 GiB / 1.0 TiB (50.0%) + Network: 50.0 GiB sent, 100.0 GiB received + Network interfaces: + en0: 30.0 GiB sent, 60.0 GiB received + Load: 2.50 / 3.00 / 3.50 + Sensors: + CPU: 55.5°C + Uptime: 1d 1h 1m 1s + Boot time: 1700000000 +Daemon: + Alive: yes + PID: 54321 + Uptime: 86400s + Restarts: 2 + Socket: ok +SSH: + Mux master: alive + Control path: valid + Config: ok + Agent: running + Keys: 3 +=== Capabilities === +System: + CPU usage: yes + Per-core CPU: yes + Memory: yes + Swap: yes + Disk: yes + Network: yes + Load average: yes + Uptime: yes + Hostname: yes + OS info: yes + Sensors: yes + +OS: + OS info: yes + Hostname: yes + Uptime: yes + Boot time: yes + Load average: yes + Virtualization: no + +GPU: + Identity: yes + NVIDIA NVML: no + Utilization: no + Temperature: no + Memory: no + Per-process: no + +Battery: + Available: yes + Charge percent: yes + Time remaining: yes + Cycle count: yes + Health: yes + +Process: + List: yes + CPU usage: yes + Memory usage: yes + Command line: yes + Thread count: yes + User: yes + Disk I/O: no + Tree: yes + +Storage: + Disk usage: yes + Disk I/O: yes + Disk type: yes + Model: yes + SMART: no + Temperature: no + +Network: + Interfaces: yes + Counters: yes + Addresses: yes + Gateway: yes + DNS: yes + Link status: yes + +Sensors: + CPU temperature: yes + GPU temperature: no + Fan speed: yes + Voltage: no + +Daemon: + PID check: yes + Uptime for PID: yes + Stale socket: yes + +SSH: + Mux check: yes + Config validation: yes + Agent check: yes + Key counting: yes diff --git a/crates/toride/src/status/ssh.rs b/crates/toride-status/src/ssh.rs similarity index 99% rename from crates/toride/src/status/ssh.rs rename to crates/toride-status/src/ssh.rs index 201e143..ef357d7 100644 --- a/crates/toride/src/status/ssh.rs +++ b/crates/toride-status/src/ssh.rs @@ -64,7 +64,7 @@ //! # Examples //! //! ```no_run -//! use toride::status::ssh::SshStatus; +//! use toride_status::ssh::SshStatus; //! //! let status = SshStatus::collect(); //! println!("Mux master: {}", if status.mux_master_alive { "alive" } else { "dead" }); @@ -131,7 +131,7 @@ impl SshStatus { /// # Examples /// /// ```no_run - /// use toride::status::ssh::SshStatus; + /// use toride_status::ssh::SshStatus; /// /// let status = SshStatus::collect(); /// println!("Mux alive: {}", status.mux_master_alive); @@ -162,7 +162,7 @@ impl SshStatus { /// /// ```no_run /// use std::path::Path; - /// use toride::status::ssh::SshStatus; + /// use toride_status::ssh::SshStatus; /// /// let status = SshStatus::collect_with_paths( /// Path::new("~/.ssh/controlmasters/%r@%h-%p"), diff --git a/crates/toride/src/status/system.rs b/crates/toride-status/src/system.rs similarity index 98% rename from crates/toride/src/status/system.rs rename to crates/toride-status/src/system.rs index 37e663b..5722383 100644 --- a/crates/toride/src/status/system.rs +++ b/crates/toride-status/src/system.rs @@ -28,7 +28,7 @@ //! Collect a full system snapshot: //! //! ```no_run -//! use toride::status::system::SystemStatus; +//! use toride_status::system::SystemStatus; //! //! let status = SystemStatus::collect(); //! println!("CPU: {:.1}%", status.cpu_usage.unwrap_or(0.0)); @@ -39,7 +39,7 @@ //! Get top CPU-consuming processes: //! //! ```no_run -//! use toride::status::system::SystemStatus; +//! use toride_status::system::SystemStatus; //! //! let status = SystemStatus::collect(); //! for proc in status.processes.top_by_cpu(5) { @@ -50,7 +50,7 @@ //! Display formatted output: //! //! ```no_run -//! use toride::status::system::SystemStatus; +//! use toride_status::system::SystemStatus; //! //! let status = SystemStatus::collect(); //! println!("{status}"); @@ -65,8 +65,8 @@ use sysinfo::{ ProcessesToUpdate, RefreshKind, System, }; -use crate::status::error::StatusResult; -use crate::status::provider::{ +use crate::error::StatusResult; +use crate::provider::{ BatteryProvider, CpuProvider, DiskIoProvider, DiskProvider, GpuProvider, MemoryProvider, NetworkProvider, OsProvider, ProcessProvider, SensorProvider, StaticInfoProvider, VirtualizationProvider, @@ -1825,7 +1825,7 @@ impl ProcessSnapshot { /// # Examples /// /// ``` - /// use toride::status::system::{ProcessSnapshot, ProcessStatus}; + /// use toride_status::system::{ProcessSnapshot, ProcessStatus}; /// /// let snapshot = ProcessSnapshot { /// processes: vec![ @@ -1856,7 +1856,7 @@ impl ProcessSnapshot { /// # Examples /// /// ``` - /// use toride::status::system::{ProcessSnapshot, ProcessStatus}; + /// use toride_status::system::{ProcessSnapshot, ProcessStatus}; /// /// let snapshot = ProcessSnapshot { /// processes: vec![ @@ -1908,7 +1908,7 @@ impl SystemStatus { /// # Examples /// /// ```no_run - /// use toride::status::system::SystemStatus; + /// use toride_status::system::SystemStatus; /// /// let status = SystemStatus::collect(); /// println!("CPU: {:?}", status.cpu_usage); @@ -2657,8 +2657,8 @@ impl fmt::Display for SystemStatus { /// # Examples /// /// ```no_run -/// use toride::status::system::SysinfoProvider; -/// use toride::status::provider::*; +/// use toride_status::system::SysinfoProvider; +/// use toride_status::provider::*; /// /// let mut provider = SysinfoProvider::new(); /// let cpu = provider.cpu_usage().unwrap(); @@ -3246,7 +3246,7 @@ impl SystemStatus { /// # Examples /// /// ```no_run - /// use toride::status::system::SystemStatus; + /// use toride_status::system::SystemStatus; /// /// let status = SystemStatus::collect_via_provider(); /// assert!(!status.hostname.is_empty()); @@ -5153,7 +5153,7 @@ mod tests { #[test] fn provider_implements_status_provider() { - use crate::status::provider::StatusProvider; + use crate::provider::StatusProvider; fn assert_provider() {} assert_provider::(); } @@ -5613,125 +5613,125 @@ mod tests { } impl CpuProvider for FakeProvider { - fn cpu_usage(&mut self) -> crate::status::error::StatusResult> { + fn cpu_usage(&mut self) -> crate::error::StatusResult> { Ok(self.cpu_usage_val) } - fn cpu_cores(&mut self) -> crate::status::error::StatusResult> { + fn cpu_cores(&mut self) -> crate::error::StatusResult> { Ok(self.cpu_cores_val.clone()) } - fn physical_cores(&self) -> crate::status::error::StatusResult> { + fn physical_cores(&self) -> crate::error::StatusResult> { Ok(Some(self.cpu_cores_val.len())) } - fn cpu_static(&self) -> crate::status::error::StatusResult { + fn cpu_static(&self) -> crate::error::StatusResult { Ok(CpuStatic { vendor: String::new(), brand: String::new(), arch: String::new(), sockets: None, cores_per_socket: None, threads_per_core: None, base_freq: None, max_freq: None, cache_l1d: None, cache_l1i: None, cache_l2: None, cache_l3: None, }) } - fn cpu_sample(&mut self) -> crate::status::error::StatusResult { + fn cpu_sample(&mut self) -> crate::error::StatusResult { Ok(CpuSample { total_usage: self.cpu_usage_val, per_core: vec![] }) } } impl MemoryProvider for FakeProvider { - fn memory(&mut self) -> crate::status::error::StatusResult { + fn memory(&mut self) -> crate::error::StatusResult { Ok(self.memory_val) } - fn swap(&mut self) -> crate::status::error::StatusResult> { + fn swap(&mut self) -> crate::error::StatusResult> { Ok(self.swap_val) } - fn memory_pressure(&self) -> crate::status::error::StatusResult> { + fn memory_pressure(&self) -> crate::error::StatusResult> { Ok(None) } } impl DiskProvider for FakeProvider { - fn root_disk(&mut self) -> crate::status::error::StatusResult { + fn root_disk(&mut self) -> crate::error::StatusResult { Ok(self.disk_val.clone()) } - fn all_disks(&mut self) -> crate::status::error::StatusResult> { + fn all_disks(&mut self) -> crate::error::StatusResult> { Ok(self.disks_val.clone()) } } impl NetworkProvider for FakeProvider { - fn aggregate(&mut self) -> crate::status::error::StatusResult { + fn aggregate(&mut self) -> crate::error::StatusResult { Ok(self.network_val) } - fn interfaces(&mut self) -> crate::status::error::StatusResult> { + fn interfaces(&mut self) -> crate::error::StatusResult> { Ok(self.interfaces_val.clone()) } - fn gateway(&self) -> crate::status::error::StatusResult> { + fn gateway(&self) -> crate::error::StatusResult> { Ok(None) } - fn dns_servers(&self) -> crate::status::error::StatusResult> { + fn dns_servers(&self) -> crate::error::StatusResult> { Ok(vec![]) } } impl OsProvider for FakeProvider { - fn os_info(&self) -> crate::status::error::StatusResult { + fn os_info(&self) -> crate::error::StatusResult { Ok(self.os_info_val.clone()) } - fn hostname(&self) -> crate::status::error::StatusResult { + fn hostname(&self) -> crate::error::StatusResult { Ok(self.hostname_val.clone()) } - fn uptime(&self) -> crate::status::error::StatusResult> { + fn uptime(&self) -> crate::error::StatusResult> { Ok(self.uptime_val) } - fn boot_time(&self) -> crate::status::error::StatusResult> { + fn boot_time(&self) -> crate::error::StatusResult> { Ok(self.boot_time_val) } - fn load_average(&self) -> crate::status::error::StatusResult> { + fn load_average(&self) -> crate::error::StatusResult> { Ok(self.load_avg_val) } - fn os_detailed(&self) -> crate::status::error::StatusResult { + fn os_detailed(&self) -> crate::error::StatusResult { Ok(self.os_info_val.clone()) } } impl ProcessProvider for FakeProvider { - fn processes(&mut self) -> crate::status::error::StatusResult { + fn processes(&mut self) -> crate::error::StatusResult { Ok(self.processes_val.clone()) } - fn process_tree(&mut self) -> crate::status::error::StatusResult>> { + fn process_tree(&mut self) -> crate::error::StatusResult>> { Ok(std::collections::HashMap::new()) } } impl GpuProvider for FakeProvider { - fn gpus(&self) -> crate::status::error::StatusResult> { + fn gpus(&self) -> crate::error::StatusResult> { Ok(self.gpus_val.clone()) } } impl BatteryProvider for FakeProvider { - fn battery(&self) -> crate::status::error::StatusResult> { + fn battery(&self) -> crate::error::StatusResult> { Ok(self.battery_val.clone()) } } impl SensorProvider for FakeProvider { - fn sensors(&self) -> crate::status::error::StatusResult> { + fn sensors(&self) -> crate::error::StatusResult> { Ok(self.sensors_val.clone()) } } impl VirtualizationProvider for FakeProvider { - fn virtualization(&self) -> crate::status::error::StatusResult { + fn virtualization(&self) -> crate::error::StatusResult { Ok(VirtualizationSnapshot::default()) } } impl DiskIoProvider for FakeProvider { - fn disk_io(&self) -> crate::status::error::StatusResult { + fn disk_io(&self) -> crate::error::StatusResult { Ok(DiskIoSnapshot::default()) } } impl StaticInfoProvider for FakeProvider { - fn static_info(&self) -> crate::status::error::StatusResult { + fn static_info(&self) -> crate::error::StatusResult { Ok(StaticInfo { os: self.os_info_val.clone(), kernel_version: None, hostname: String::new(), cpu_brand: String::new(), cpu_vendor: String::new(), cpu_frequency: 0, diff --git a/crates/toride/src/status/units.rs b/crates/toride-status/src/units.rs similarity index 100% rename from crates/toride/src/status/units.rs rename to crates/toride-status/src/units.rs diff --git a/crates/toride/Cargo.toml b/crates/toride/Cargo.toml index 0318e08..bd5db1c 100644 --- a/crates/toride/Cargo.toml +++ b/crates/toride/Cargo.toml @@ -41,41 +41,7 @@ rattles = "0.3.1" ratatui-wgpu = "0.5.0" toride-ssh = { path = "../toride-ssh" } - -# Status subsystem optional dependencies -sysinfo = "0.35" -sysconf = "0.3" -procfs = { version = "0.18", optional = true } -os_info = { version = "3", optional = true } -nvml-wrapper = { version = "0.10", optional = true } -starship-battery = { version = "0.8", optional = true } -dmidecode = { version = "1.0", optional = true } -raw-cpuid = { version = "11", optional = true } -hwlocality = { version = "1.0.0-alpha.12", optional = true } -pci-info = { version = "0.2", optional = true } -pci-ids = { version = "0.2", optional = true } -cgroups-rs = { version = "0.5", optional = true } -duct = { version = "0.13", optional = true } -lm-sensors = { version = "0.1", optional = true } -udev = { version = "0.8", optional = true } -rtnetlink = { version = "0.14", optional = true } - -[features] -default = ["sysinfo-provider"] -sysinfo-provider = [] -linux-procfs = ["procfs"] -linux-sensors = ["lm-sensors"] -linux-udev = ["udev"] -linux-rtnetlink = ["rtnetlink"] -linux-cgroups = ["cgroups-rs"] -os-info = ["os_info"] -cpu-cpuid = ["raw-cpuid"] -hardware-dmi = ["dmidecode"] -hardware-pci = ["pci-info", "pci-ids"] -hardware-topology = ["hwlocality"] -gpu-nvidia = ["nvml-wrapper"] -battery = ["starship-battery"] -commands = ["dep:duct"] +toride-status = { path = "../toride-status" } [dev-dependencies] insta = "1" diff --git a/crates/toride/examples/disk_rates.rs b/crates/toride/examples/disk_rates.rs index 5e90640..3aff0a6 100644 --- a/crates/toride/examples/disk_rates.rs +++ b/crates/toride/examples/disk_rates.rs @@ -7,8 +7,8 @@ use std::time::Duration; -use toride::status::units::format_bytes; -use toride::status::{Collector, Preset}; +use toride_status::units::format_bytes; +use toride_status::{Collector, Preset}; fn main() { let mut collector = Collector::new(Duration::from_secs(1), Preset::TaskManager); diff --git a/crates/toride/examples/doctor.rs b/crates/toride/examples/doctor.rs index bffcf56..f0dd179 100644 --- a/crates/toride/examples/doctor.rs +++ b/crates/toride/examples/doctor.rs @@ -5,7 +5,7 @@ //! //! Run with: `cargo run --example doctor` -use toride::status::{CheckStatus, DoctorReport}; +use toride_status::{CheckStatus, DoctorReport}; fn main() { println!("Running health checks..."); diff --git a/crates/toride/examples/gpu_status.rs b/crates/toride/examples/gpu_status.rs index b813c8a..d394657 100644 --- a/crates/toride/examples/gpu_status.rs +++ b/crates/toride/examples/gpu_status.rs @@ -5,8 +5,8 @@ //! //! Run with: `cargo run --example gpu_status` -use toride::status::units::{Bytes, Celsius}; -use toride::status::TorideStatus; +use toride_status::units::{Bytes, Celsius}; +use toride_status::TorideStatus; fn main() { let status = TorideStatus::collect(); diff --git a/crates/toride/examples/hardware_inventory.rs b/crates/toride/examples/hardware_inventory.rs index 7506bc2..036d967 100644 --- a/crates/toride/examples/hardware_inventory.rs +++ b/crates/toride/examples/hardware_inventory.rs @@ -5,8 +5,8 @@ //! //! Run with: `cargo run --example hardware_inventory` -use toride::status::units::{format_bytes, Bytes, Celsius, Hertz}; -use toride::status::{Preset, TorideStatus}; +use toride_status::units::{format_bytes, Bytes, Celsius, Hertz}; +use toride_status::{Preset, TorideStatus}; fn main() { let status = TorideStatus::collect_with_preset(Preset::HardwareInventory); diff --git a/crates/toride/examples/network_rates.rs b/crates/toride/examples/network_rates.rs index 80fb6e5..c6538f2 100644 --- a/crates/toride/examples/network_rates.rs +++ b/crates/toride/examples/network_rates.rs @@ -7,8 +7,8 @@ use std::time::Duration; -use toride::status::units::format_bytes; -use toride::status::{Collector, Preset}; +use toride_status::units::format_bytes; +use toride_status::{Collector, Preset}; fn main() { let mut collector = Collector::new(Duration::from_secs(1), Preset::ServerMonitoring); diff --git a/crates/toride/examples/privacy_safe_report.rs b/crates/toride/examples/privacy_safe_report.rs index 57f8c00..1b538c5 100644 --- a/crates/toride/examples/privacy_safe_report.rs +++ b/crates/toride/examples/privacy_safe_report.rs @@ -5,7 +5,7 @@ //! //! Run with: `cargo run --example privacy_safe_report` -use toride::status::{Preset, PrivacyMode, TorideStatus}; +use toride_status::{Preset, PrivacyMode, TorideStatus}; fn main() { println!("=== Privacy Mode Comparison ==="); diff --git a/crates/toride/examples/simple_snapshot.rs b/crates/toride/examples/simple_snapshot.rs index ce14ba4..ad5a5e9 100644 --- a/crates/toride/examples/simple_snapshot.rs +++ b/crates/toride/examples/simple_snapshot.rs @@ -2,8 +2,8 @@ //! //! Run with: `cargo run --example simple_snapshot` -use toride::status::units::{format_bytes, format_duration}; -use toride::status::SysProbe; +use toride_status::units::{format_bytes, format_duration}; +use toride_status::SysProbe; fn main() { let probe = SysProbe::new(); diff --git a/crates/toride/examples/task_manager_loop.rs b/crates/toride/examples/task_manager_loop.rs index 93adbd0..634202f 100644 --- a/crates/toride/examples/task_manager_loop.rs +++ b/crates/toride/examples/task_manager_loop.rs @@ -7,8 +7,8 @@ use std::time::Duration; -use toride::status::units::format_bytes; -use toride::status::{Collector, Preset}; +use toride_status::units::format_bytes; +use toride_status::{Collector, Preset}; fn main() { let mut collector = Collector::new(Duration::from_secs(1), Preset::TaskManager); diff --git a/crates/toride/examples/top_processes.rs b/crates/toride/examples/top_processes.rs index f948d86..6e12ef7 100644 --- a/crates/toride/examples/top_processes.rs +++ b/crates/toride/examples/top_processes.rs @@ -2,8 +2,8 @@ //! //! Run with: `cargo run --example top_processes` -use toride::status::units::format_bytes; -use toride::status::TorideStatus; +use toride_status::units::format_bytes; +use toride_status::TorideStatus; fn main() { let status = TorideStatus::collect(); diff --git a/crates/toride/src/lib.rs b/crates/toride/src/lib.rs index dd00eca..dfadbd0 100644 --- a/crates/toride/src/lib.rs +++ b/crates/toride/src/lib.rs @@ -4,5 +4,5 @@ pub mod action; pub mod app; -pub mod status; +pub use toride_status as status; pub mod ui;