oxur_repl/
metadata.rs

1//! System metadata captured at startup
2//!
3//! Provides information about the runtime environment including
4//! versions, system info, and process details.
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use std::time::SystemTime;
9use sysinfo::System;
10
11/// System metadata captured at startup
12///
13/// This struct captures various system and runtime information when
14/// the REPL starts. It can be queried via the `(info)` command.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct SystemMetadata {
17    /// Oxur version (from Cargo.toml)
18    pub oxur_version: String,
19    /// Rust compiler version (detected at runtime)
20    pub rust_version: String,
21    /// Cargo version (detected at runtime)
22    pub cargo_version: String,
23    /// Operating system name (e.g., "macOS", "Ubuntu")
24    pub os_name: String,
25    /// OS version (e.g., "14.5")
26    pub os_version: String,
27    /// System architecture (e.g., "aarch64", "x86_64")
28    pub arch: String,
29    /// Machine hostname
30    pub hostname: String,
31    /// Process ID
32    pub pid: u32,
33    /// Current working directory at startup
34    pub cwd: PathBuf,
35    /// Startup timestamp
36    pub started_at: SystemTime,
37}
38
39impl SystemMetadata {
40    /// Capture system metadata at startup
41    ///
42    /// This should be called once when the REPL starts to capture
43    /// a snapshot of the system state.
44    pub fn capture() -> Self {
45        Self {
46            oxur_version: env!("CARGO_PKG_VERSION").to_string(),
47            rust_version: detect_rust_version(),
48            cargo_version: detect_cargo_version(),
49            os_name: System::name().unwrap_or_else(|| "unknown".to_string()),
50            os_version: System::os_version().unwrap_or_else(|| "unknown".to_string()),
51            arch: System::cpu_arch().unwrap_or_else(|| "unknown".to_string()),
52            hostname: System::host_name().unwrap_or_else(|| "unknown".to_string()),
53            pid: std::process::id(),
54            cwd: std::env::current_dir().unwrap_or_default(),
55            started_at: SystemTime::now(),
56        }
57    }
58
59    /// Get uptime since startup in seconds
60    pub fn uptime_seconds(&self) -> f64 {
61        self.started_at.elapsed().map(|d| d.as_secs_f64()).unwrap_or(0.0)
62    }
63}
64
65/// Detect the installed Rust compiler version
66fn detect_rust_version() -> String {
67    std::process::Command::new("rustc")
68        .arg("--version")
69        .output()
70        .ok()
71        .and_then(|o| String::from_utf8(o.stdout).ok())
72        .map(|s| s.trim().to_string())
73        .unwrap_or_else(|| "unknown".to_string())
74}
75
76/// Detect the installed Cargo version
77fn detect_cargo_version() -> String {
78    std::process::Command::new("cargo")
79        .arg("--version")
80        .output()
81        .ok()
82        .and_then(|o| String::from_utf8(o.stdout).ok())
83        .map(|s| s.trim().to_string())
84        .unwrap_or_else(|| "unknown".to_string())
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_capture_metadata() {
93        let metadata = SystemMetadata::capture();
94
95        // Oxur version should be set
96        assert!(!metadata.oxur_version.is_empty());
97
98        // Rust version should be detected (or "unknown")
99        assert!(!metadata.rust_version.is_empty());
100
101        // Cargo version should be detected (or "unknown")
102        assert!(!metadata.cargo_version.is_empty());
103
104        // OS info should be present
105        assert!(!metadata.os_name.is_empty());
106        assert!(!metadata.arch.is_empty());
107
108        // PID should be non-zero
109        assert!(metadata.pid > 0);
110
111        // CWD should be set (even if empty path on error)
112        // Just check it exists
113        let _ = metadata.cwd;
114    }
115
116    #[test]
117    fn test_uptime_increases() {
118        let metadata = SystemMetadata::capture();
119
120        // Initial uptime should be very small
121        let uptime1 = metadata.uptime_seconds();
122        assert!(uptime1 < 1.0);
123
124        // Wait a tiny bit
125        std::thread::sleep(std::time::Duration::from_millis(10));
126
127        // Uptime should have increased
128        let uptime2 = metadata.uptime_seconds();
129        assert!(uptime2 > uptime1);
130    }
131
132    #[test]
133    fn test_rust_version_detection() {
134        let version = detect_rust_version();
135        // Should either start with "rustc" or be "unknown"
136        assert!(version.starts_with("rustc") || version == "unknown");
137    }
138
139    #[test]
140    fn test_cargo_version_detection() {
141        let version = detect_cargo_version();
142        // Should either start with "cargo" or be "unknown"
143        assert!(version.starts_with("cargo") || version == "unknown");
144    }
145
146    #[test]
147    fn test_metadata_serialization() {
148        let metadata = SystemMetadata::capture();
149
150        // Should serialize to JSON without errors
151        let json = serde_json::to_string(&metadata).expect("Failed to serialize");
152        assert!(!json.is_empty());
153
154        // Should deserialize back
155        let deserialized: SystemMetadata =
156            serde_json::from_str(&json).expect("Failed to deserialize");
157        assert_eq!(metadata.oxur_version, deserialized.oxur_version);
158        assert_eq!(metadata.pid, deserialized.pid);
159    }
160}