Skip to main content

perfgate_fake/
host_probe.rs

1//! Fake host probe for deterministic testing.
2
3use perfgate_adapters::{HostProbe, HostProbeOptions};
4use perfgate_types::HostInfo;
5use std::sync::{Arc, Mutex};
6
7/// A host probe that returns pre-configured host information.
8///
9/// This is useful for testing code that depends on [`HostProbe`] without
10/// actually querying system information.
11///
12/// # Thread Safety
13///
14/// All configuration methods are `&self` (not `&mut self`), making it safe
15/// to share a single instance across multiple threads in tests.
16///
17/// # Example
18///
19/// ```
20/// use perfgate_fake::FakeHostProbe;
21/// use perfgate_adapters::{HostProbe, HostProbeOptions};
22/// use perfgate_types::HostInfo;
23///
24/// let probe = FakeHostProbe::new()
25///     .with_os("linux")
26///     .with_arch("x86_64")
27///     .with_cpu_count(8)
28///     .with_memory_bytes(16 * 1024 * 1024 * 1024);
29///
30/// let options = HostProbeOptions { include_hostname_hash: false };
31/// let info = probe.probe(&options);
32///
33/// assert_eq!(info.os, "linux");
34/// assert_eq!(info.arch, "x86_64");
35/// assert_eq!(info.cpu_count, Some(8));
36/// ```
37#[derive(Debug, Clone)]
38pub struct FakeHostProbe {
39    inner: Arc<Mutex<FakeHostProbeInner>>,
40}
41
42#[derive(Debug, Clone)]
43struct FakeHostProbeInner {
44    os: String,
45    arch: String,
46    cpu_count: Option<u32>,
47    memory_bytes: Option<u64>,
48    hostname_hash: Option<String>,
49}
50
51impl Default for FakeHostProbe {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl FakeHostProbe {
58    /// Create a new `FakeHostProbe` with default values.
59    ///
60    /// Defaults:
61    /// - `os`: "unknown"
62    /// - `arch`: "unknown"
63    /// - `cpu_count`: `None`
64    /// - `memory_bytes`: `None`
65    /// - `hostname_hash`: `None` (always, unless explicitly set)
66    pub fn new() -> Self {
67        Self {
68            inner: Arc::new(Mutex::new(FakeHostProbeInner {
69                os: "unknown".to_string(),
70                arch: "unknown".to_string(),
71                cpu_count: None,
72                memory_bytes: None,
73                hostname_hash: None,
74            })),
75        }
76    }
77
78    /// Create a `FakeHostProbe` that mimics a specific platform.
79    ///
80    /// # Example
81    ///
82    /// ```
83    /// use perfgate_fake::FakeHostProbe;
84    ///
85    /// // Create a probe that looks like a Linux system
86    /// let probe = FakeHostProbe::platform("linux", "x86_64", 8, 16 * 1024 * 1024 * 1024);
87    /// ```
88    pub fn platform(os: &str, arch: &str, cpu_count: u32, memory_bytes: u64) -> Self {
89        Self::new()
90            .with_os(os)
91            .with_arch(arch)
92            .with_cpu_count(cpu_count)
93            .with_memory_bytes(memory_bytes)
94    }
95
96    /// Set the OS string.
97    pub fn with_os(self, os: &str) -> Self {
98        self.inner.lock().expect("lock").os = os.to_string();
99        self
100    }
101
102    /// Set the architecture string.
103    pub fn with_arch(self, arch: &str) -> Self {
104        self.inner.lock().expect("lock").arch = arch.to_string();
105        self
106    }
107
108    /// Set the CPU count.
109    pub fn with_cpu_count(self, count: u32) -> Self {
110        self.inner.lock().expect("lock").cpu_count = Some(count);
111        self
112    }
113
114    /// Set the memory in bytes.
115    pub fn with_memory_bytes(self, bytes: u64) -> Self {
116        self.inner.lock().expect("lock").memory_bytes = Some(bytes);
117        self
118    }
119
120    /// Set the hostname hash directly.
121    ///
122    /// Note: This overrides the `include_hostname_hash` option behavior.
123    /// If set, this value is always returned.
124    pub fn with_hostname_hash(self, hash: &str) -> Self {
125        self.inner.lock().expect("lock").hostname_hash = Some(hash.to_string());
126        self
127    }
128
129    /// Set CPU count to `None`.
130    pub fn without_cpu_count(self) -> Self {
131        self.inner.lock().expect("lock").cpu_count = None;
132        self
133    }
134
135    /// Set memory to `None`.
136    pub fn without_memory(self) -> Self {
137        self.inner.lock().expect("lock").memory_bytes = None;
138        self
139    }
140}
141
142impl HostProbe for FakeHostProbe {
143    fn probe(&self, options: &HostProbeOptions) -> HostInfo {
144        let inner = self.inner.lock().expect("lock");
145
146        HostInfo {
147            os: inner.os.clone(),
148            arch: inner.arch.clone(),
149            cpu_count: inner.cpu_count,
150            memory_bytes: inner.memory_bytes,
151            hostname_hash: if options.include_hostname_hash {
152                inner.hostname_hash.clone()
153            } else {
154                None
155            },
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn new_probe_has_defaults() {
166        let probe = FakeHostProbe::new();
167        let info = probe.probe(&HostProbeOptions::default());
168
169        assert_eq!(info.os, "unknown");
170        assert_eq!(info.arch, "unknown");
171        assert!(info.cpu_count.is_none());
172        assert!(info.memory_bytes.is_none());
173        assert!(info.hostname_hash.is_none());
174    }
175
176    #[test]
177    fn with_methods_configure_values() {
178        let probe = FakeHostProbe::new()
179            .with_os("linux")
180            .with_arch("x86_64")
181            .with_cpu_count(8)
182            .with_memory_bytes(16 * 1024 * 1024 * 1024);
183
184        let info = probe.probe(&HostProbeOptions::default());
185
186        assert_eq!(info.os, "linux");
187        assert_eq!(info.arch, "x86_64");
188        assert_eq!(info.cpu_count, Some(8));
189        assert_eq!(info.memory_bytes, Some(16 * 1024 * 1024 * 1024));
190    }
191
192    #[test]
193    fn hostname_hash_respects_option() {
194        let probe = FakeHostProbe::new().with_hostname_hash("abc123");
195
196        let info_without = probe.probe(&HostProbeOptions {
197            include_hostname_hash: false,
198        });
199        assert!(info_without.hostname_hash.is_none());
200
201        let info_with = probe.probe(&HostProbeOptions {
202            include_hostname_hash: true,
203        });
204        assert_eq!(info_with.hostname_hash, Some("abc123".to_string()));
205    }
206
207    #[test]
208    fn platform_convenience_constructor() {
209        let probe = FakeHostProbe::platform("macos", "aarch64", 10, 32 * 1024 * 1024 * 1024);
210        let info = probe.probe(&HostProbeOptions::default());
211
212        assert_eq!(info.os, "macos");
213        assert_eq!(info.arch, "aarch64");
214        assert_eq!(info.cpu_count, Some(10));
215        assert_eq!(info.memory_bytes, Some(32 * 1024 * 1024 * 1024));
216    }
217
218    #[test]
219    fn without_methods_clear_values() {
220        let probe = FakeHostProbe::new()
221            .with_cpu_count(8)
222            .with_memory_bytes(1024)
223            .without_cpu_count()
224            .without_memory();
225
226        let info = probe.probe(&HostProbeOptions::default());
227
228        assert!(info.cpu_count.is_none());
229        assert!(info.memory_bytes.is_none());
230    }
231
232    #[test]
233    fn thread_safe_sharing() {
234        use std::sync::Arc;
235        use std::thread;
236
237        let probe = Arc::new(FakeHostProbe::new().with_os("linux"));
238
239        let handles: Vec<_> = (0..4)
240            .map(|_| {
241                let p = probe.clone();
242                thread::spawn(move || {
243                    let info = p.probe(&HostProbeOptions::default());
244                    assert_eq!(info.os, "linux");
245                })
246            })
247            .collect();
248
249        for h in handles {
250            h.join().unwrap();
251        }
252    }
253}