aranet_core/
mock.rs

1//! Mock device implementation for testing.
2//!
3//! This module provides a mock device that can be used for unit testing
4//! without requiring actual BLE hardware.
5//!
6//! The [`MockDevice`] implements the [`AranetDevice`] trait, allowing it to be
7//! used interchangeably with real devices in generic code.
8//!
9//! # Features
10//!
11//! - **Failure injection**: Set the device to fail on specific operations
12//! - **Latency simulation**: Add artificial delays to simulate slow BLE responses
13//! - **Custom behavior**: Inject custom reading generators for dynamic test scenarios
14
15use std::sync::atomic::{AtomicBool, AtomicI16, AtomicU32, AtomicU64, Ordering};
16use std::time::Duration;
17
18use async_trait::async_trait;
19use tokio::sync::RwLock;
20
21use aranet_types::{CurrentReading, DeviceInfo, DeviceType, HistoryRecord, Status};
22
23use crate::error::{Error, Result};
24use crate::history::{HistoryInfo, HistoryOptions};
25use crate::settings::{CalibrationData, MeasurementInterval};
26use crate::traits::AranetDevice;
27
28/// A mock Aranet device for testing.
29///
30/// Implements [`AranetDevice`] trait for use in generic code and testing.
31///
32/// # Example
33///
34/// ```
35/// use aranet_core::{MockDevice, AranetDevice};
36/// use aranet_types::DeviceType;
37///
38/// #[tokio::main]
39/// async fn main() {
40///     let device = MockDevice::new("Test", DeviceType::Aranet4);
41///     device.connect().await.unwrap();
42///
43///     // Can use through trait
44///     async fn read_via_trait<D: AranetDevice>(d: &D) {
45///         let _ = d.read_current().await;
46///     }
47///     read_via_trait(&device).await;
48/// }
49/// ```
50pub struct MockDevice {
51    name: String,
52    address: String,
53    device_type: DeviceType,
54    connected: AtomicBool,
55    current_reading: RwLock<CurrentReading>,
56    device_info: RwLock<DeviceInfo>,
57    history: RwLock<Vec<HistoryRecord>>,
58    interval: RwLock<MeasurementInterval>,
59    calibration: RwLock<CalibrationData>,
60    battery: RwLock<u8>,
61    rssi: AtomicI16,
62    read_count: AtomicU32,
63    should_fail: AtomicBool,
64    fail_message: RwLock<String>,
65    /// Simulated read latency in milliseconds (0 = no delay).
66    read_latency_ms: AtomicU64,
67    /// Simulated connect latency in milliseconds (0 = no delay).
68    connect_latency_ms: AtomicU64,
69    /// Number of operations to fail before succeeding (0 = always succeed/fail based on should_fail).
70    fail_count: AtomicU32,
71    /// Current count of failures (decremented on each failure).
72    remaining_failures: AtomicU32,
73}
74
75impl std::fmt::Debug for MockDevice {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("MockDevice")
78            .field("name", &self.name)
79            .field("address", &self.address)
80            .field("device_type", &self.device_type)
81            .field("connected", &self.connected.load(Ordering::Relaxed))
82            .finish()
83    }
84}
85
86impl MockDevice {
87    /// Create a new mock device with default values.
88    pub fn new(name: &str, device_type: DeviceType) -> Self {
89        Self {
90            name: name.to_string(),
91            address: format!("MOCK-{:06X}", rand::random::<u32>() % 0xFFFFFF),
92            device_type,
93            connected: AtomicBool::new(false),
94            current_reading: RwLock::new(Self::default_reading()),
95            device_info: RwLock::new(Self::default_info(name)),
96            history: RwLock::new(Vec::new()),
97            interval: RwLock::new(MeasurementInterval::FiveMinutes),
98            calibration: RwLock::new(CalibrationData::default()),
99            battery: RwLock::new(85),
100            rssi: AtomicI16::new(-50),
101            read_count: AtomicU32::new(0),
102            should_fail: AtomicBool::new(false),
103            fail_message: RwLock::new("Mock failure".to_string()),
104            read_latency_ms: AtomicU64::new(0),
105            connect_latency_ms: AtomicU64::new(0),
106            fail_count: AtomicU32::new(0),
107            remaining_failures: AtomicU32::new(0),
108        }
109    }
110
111    fn default_reading() -> CurrentReading {
112        CurrentReading {
113            co2: 800,
114            temperature: 22.5,
115            pressure: 1013.2,
116            humidity: 50,
117            battery: 85,
118            status: Status::Green,
119            interval: 300,
120            age: 60,
121            captured_at: None,
122            radon: None,
123            radiation_rate: None,
124            radiation_total: None,
125            radon_avg_24h: None,
126            radon_avg_7d: None,
127            radon_avg_30d: None,
128        }
129    }
130
131    fn default_info(name: &str) -> DeviceInfo {
132        DeviceInfo {
133            name: name.to_string(),
134            model: "Aranet4".to_string(),
135            serial: "MOCK-12345".to_string(),
136            firmware: "v1.5.0".to_string(),
137            hardware: "1.0".to_string(),
138            software: "1.5.0".to_string(),
139            manufacturer: "SAF Tehnika".to_string(),
140        }
141    }
142
143    /// Connect to the mock device.
144    pub async fn connect(&self) -> Result<()> {
145        use crate::error::DeviceNotFoundReason;
146
147        // Simulate connect latency
148        let latency = self.connect_latency_ms.load(Ordering::Relaxed);
149        if latency > 0 {
150            tokio::time::sleep(Duration::from_millis(latency)).await;
151        }
152
153        // Check for transient failures first
154        if self.remaining_failures.load(Ordering::Relaxed) > 0 {
155            self.remaining_failures.fetch_sub(1, Ordering::Relaxed);
156            return Err(Error::DeviceNotFound(DeviceNotFoundReason::NotFound {
157                identifier: self.name.clone(),
158            }));
159        }
160
161        if self.should_fail.load(Ordering::Relaxed) {
162            return Err(Error::DeviceNotFound(DeviceNotFoundReason::NotFound {
163                identifier: self.name.clone(),
164            }));
165        }
166        self.connected.store(true, Ordering::Relaxed);
167        Ok(())
168    }
169
170    /// Disconnect from the mock device.
171    pub async fn disconnect(&self) -> Result<()> {
172        self.connected.store(false, Ordering::Relaxed);
173        Ok(())
174    }
175
176    /// Check if connected (sync method for internal use).
177    pub fn is_connected_sync(&self) -> bool {
178        self.connected.load(Ordering::Relaxed)
179    }
180
181    /// Get the device name.
182    pub fn name(&self) -> &str {
183        &self.name
184    }
185
186    /// Get the device address.
187    pub fn address(&self) -> &str {
188        &self.address
189    }
190
191    /// Get the device type.
192    pub fn device_type(&self) -> DeviceType {
193        self.device_type
194    }
195
196    /// Read current sensor values.
197    pub async fn read_current(&self) -> Result<CurrentReading> {
198        self.check_connected()?;
199        self.check_should_fail().await?;
200
201        self.read_count.fetch_add(1, Ordering::Relaxed);
202        Ok(*self.current_reading.read().await)
203    }
204
205    /// Read battery level.
206    pub async fn read_battery(&self) -> Result<u8> {
207        self.check_connected()?;
208        self.check_should_fail().await?;
209        Ok(*self.battery.read().await)
210    }
211
212    /// Read RSSI (signal strength).
213    pub async fn read_rssi(&self) -> Result<i16> {
214        self.check_connected()?;
215        self.check_should_fail().await?;
216        Ok(self.rssi.load(Ordering::Relaxed))
217    }
218
219    /// Read device info.
220    pub async fn read_device_info(&self) -> Result<DeviceInfo> {
221        self.check_connected()?;
222        self.check_should_fail().await?;
223        Ok(self.device_info.read().await.clone())
224    }
225
226    /// Get history info.
227    pub async fn get_history_info(&self) -> Result<HistoryInfo> {
228        self.check_connected()?;
229        self.check_should_fail().await?;
230
231        let history = self.history.read().await;
232        let interval = self.interval.read().await;
233
234        Ok(HistoryInfo {
235            total_readings: history.len() as u16,
236            interval_seconds: interval.as_seconds(),
237            seconds_since_update: 60,
238        })
239    }
240
241    /// Download history.
242    pub async fn download_history(&self) -> Result<Vec<HistoryRecord>> {
243        self.check_connected()?;
244        self.check_should_fail().await?;
245        Ok(self.history.read().await.clone())
246    }
247
248    /// Download history with options.
249    pub async fn download_history_with_options(
250        &self,
251        options: HistoryOptions,
252    ) -> Result<Vec<HistoryRecord>> {
253        self.check_connected()?;
254        self.check_should_fail().await?;
255
256        let history = self.history.read().await;
257        let start = options.start_index.unwrap_or(0) as usize;
258        let end = options
259            .end_index
260            .map(|e| e as usize)
261            .unwrap_or(history.len());
262
263        // Report progress if callback provided
264        if let Some(ref _callback) = options.progress_callback {
265            // For mock, we report progress immediately
266            let progress = crate::history::HistoryProgress::new(
267                crate::history::HistoryParam::Co2,
268                1,
269                1,
270                history.len().min(end).saturating_sub(start),
271            );
272            options.report_progress(&progress);
273        }
274
275        Ok(history
276            .iter()
277            .skip(start)
278            .take(end.saturating_sub(start))
279            .cloned()
280            .collect())
281    }
282
283    /// Get the measurement interval.
284    pub async fn get_interval(&self) -> Result<MeasurementInterval> {
285        self.check_connected()?;
286        self.check_should_fail().await?;
287        Ok(*self.interval.read().await)
288    }
289
290    /// Set the measurement interval.
291    pub async fn set_interval(&self, interval: MeasurementInterval) -> Result<()> {
292        self.check_connected()?;
293        self.check_should_fail().await?;
294        *self.interval.write().await = interval;
295        Ok(())
296    }
297
298    /// Get calibration data.
299    pub async fn get_calibration(&self) -> Result<CalibrationData> {
300        self.check_connected()?;
301        self.check_should_fail().await?;
302        Ok(self.calibration.read().await.clone())
303    }
304
305    fn check_connected(&self) -> Result<()> {
306        if !self.connected.load(Ordering::Relaxed) {
307            Err(Error::NotConnected)
308        } else {
309            Ok(())
310        }
311    }
312
313    async fn check_should_fail(&self) -> Result<()> {
314        // Simulate read latency
315        let latency = self.read_latency_ms.load(Ordering::Relaxed);
316        if latency > 0 {
317            tokio::time::sleep(Duration::from_millis(latency)).await;
318        }
319
320        // Check for transient failures first
321        if self.remaining_failures.load(Ordering::Relaxed) > 0 {
322            self.remaining_failures.fetch_sub(1, Ordering::Relaxed);
323            return Err(Error::InvalidData(self.fail_message.read().await.clone()));
324        }
325
326        if self.should_fail.load(Ordering::Relaxed) {
327            Err(Error::InvalidData(self.fail_message.read().await.clone()))
328        } else {
329            Ok(())
330        }
331    }
332
333    // --- Test control methods ---
334
335    /// Set the current reading for testing.
336    pub async fn set_reading(&self, reading: CurrentReading) {
337        *self.current_reading.write().await = reading;
338    }
339
340    /// Set CO2 level directly.
341    pub async fn set_co2(&self, co2: u16) {
342        self.current_reading.write().await.co2 = co2;
343    }
344
345    /// Set temperature directly.
346    pub async fn set_temperature(&self, temp: f32) {
347        self.current_reading.write().await.temperature = temp;
348    }
349
350    /// Set battery level.
351    pub async fn set_battery(&self, level: u8) {
352        *self.battery.write().await = level;
353        self.current_reading.write().await.battery = level;
354    }
355
356    /// Set RSSI (signal strength) for testing.
357    pub fn set_rssi(&self, rssi: i16) {
358        self.rssi.store(rssi, Ordering::Relaxed);
359    }
360
361    /// Add history records.
362    pub async fn add_history(&self, records: Vec<HistoryRecord>) {
363        self.history.write().await.extend(records);
364    }
365
366    /// Make the device fail on next operation.
367    pub async fn set_should_fail(&self, fail: bool, message: Option<&str>) {
368        self.should_fail.store(fail, Ordering::Relaxed);
369        if let Some(msg) = message {
370            *self.fail_message.write().await = msg.to_string();
371        }
372    }
373
374    /// Get the number of read operations performed.
375    pub fn read_count(&self) -> u32 {
376        self.read_count.load(Ordering::Relaxed)
377    }
378
379    /// Reset read count.
380    pub fn reset_read_count(&self) {
381        self.read_count.store(0, Ordering::Relaxed);
382    }
383
384    /// Set simulated read latency.
385    ///
386    /// Each read operation will be delayed by this duration.
387    /// Set to `Duration::ZERO` to disable latency simulation.
388    pub fn set_read_latency(&self, latency: Duration) {
389        self.read_latency_ms
390            .store(latency.as_millis() as u64, Ordering::Relaxed);
391    }
392
393    /// Set simulated connect latency.
394    ///
395    /// Connect operations will be delayed by this duration.
396    /// Set to `Duration::ZERO` to disable latency simulation.
397    pub fn set_connect_latency(&self, latency: Duration) {
398        self.connect_latency_ms
399            .store(latency.as_millis() as u64, Ordering::Relaxed);
400    }
401
402    /// Configure transient failures.
403    ///
404    /// The device will fail the next `count` operations, then succeed.
405    /// This is useful for testing retry logic.
406    ///
407    /// # Example
408    ///
409    /// ```
410    /// use aranet_core::MockDevice;
411    /// use aranet_types::DeviceType;
412    ///
413    /// let device = MockDevice::new("Test", DeviceType::Aranet4);
414    /// // First 3 connect attempts will fail, 4th will succeed
415    /// device.set_transient_failures(3);
416    /// ```
417    pub fn set_transient_failures(&self, count: u32) {
418        self.fail_count.store(count, Ordering::Relaxed);
419        self.remaining_failures.store(count, Ordering::Relaxed);
420    }
421
422    /// Reset transient failure counter.
423    pub fn reset_transient_failures(&self) {
424        self.remaining_failures
425            .store(self.fail_count.load(Ordering::Relaxed), Ordering::Relaxed);
426    }
427
428    /// Get the number of remaining transient failures.
429    pub fn remaining_failures(&self) -> u32 {
430        self.remaining_failures.load(Ordering::Relaxed)
431    }
432}
433
434// Implement the AranetDevice trait for MockDevice
435#[async_trait]
436impl AranetDevice for MockDevice {
437    // --- Connection Management ---
438
439    async fn is_connected(&self) -> bool {
440        self.is_connected_sync()
441    }
442
443    async fn disconnect(&self) -> Result<()> {
444        MockDevice::disconnect(self).await
445    }
446
447    // --- Device Identity ---
448
449    fn name(&self) -> Option<&str> {
450        Some(MockDevice::name(self))
451    }
452
453    fn address(&self) -> &str {
454        MockDevice::address(self)
455    }
456
457    fn device_type(&self) -> Option<DeviceType> {
458        Some(MockDevice::device_type(self))
459    }
460
461    // --- Current Readings ---
462
463    async fn read_current(&self) -> Result<CurrentReading> {
464        MockDevice::read_current(self).await
465    }
466
467    async fn read_device_info(&self) -> Result<DeviceInfo> {
468        MockDevice::read_device_info(self).await
469    }
470
471    async fn read_rssi(&self) -> Result<i16> {
472        MockDevice::read_rssi(self).await
473    }
474
475    // --- Battery ---
476
477    async fn read_battery(&self) -> Result<u8> {
478        MockDevice::read_battery(self).await
479    }
480
481    // --- History ---
482
483    async fn get_history_info(&self) -> Result<crate::history::HistoryInfo> {
484        MockDevice::get_history_info(self).await
485    }
486
487    async fn download_history(&self) -> Result<Vec<HistoryRecord>> {
488        MockDevice::download_history(self).await
489    }
490
491    async fn download_history_with_options(
492        &self,
493        options: HistoryOptions,
494    ) -> Result<Vec<HistoryRecord>> {
495        MockDevice::download_history_with_options(self, options).await
496    }
497
498    // --- Settings ---
499
500    async fn get_interval(&self) -> Result<MeasurementInterval> {
501        MockDevice::get_interval(self).await
502    }
503
504    async fn set_interval(&self, interval: MeasurementInterval) -> Result<()> {
505        MockDevice::set_interval(self, interval).await
506    }
507
508    async fn get_calibration(&self) -> Result<CalibrationData> {
509        MockDevice::get_calibration(self).await
510    }
511}
512
513/// Builder for creating mock devices with custom settings.
514#[derive(Debug)]
515pub struct MockDeviceBuilder {
516    name: String,
517    device_type: DeviceType,
518    co2: u16,
519    temperature: f32,
520    pressure: f32,
521    humidity: u8,
522    battery: u8,
523    status: Status,
524    auto_connect: bool,
525}
526
527impl Default for MockDeviceBuilder {
528    fn default() -> Self {
529        Self {
530            name: "Mock Aranet4".to_string(),
531            device_type: DeviceType::Aranet4,
532            co2: 800,
533            temperature: 22.5,
534            pressure: 1013.2,
535            humidity: 50,
536            battery: 85,
537            status: Status::Green,
538            auto_connect: true,
539        }
540    }
541}
542
543impl MockDeviceBuilder {
544    /// Create a new builder.
545    #[must_use]
546    pub fn new() -> Self {
547        Self::default()
548    }
549
550    /// Set the device name.
551    #[must_use]
552    pub fn name(mut self, name: &str) -> Self {
553        self.name = name.to_string();
554        self
555    }
556
557    /// Set the device type.
558    #[must_use]
559    pub fn device_type(mut self, device_type: DeviceType) -> Self {
560        self.device_type = device_type;
561        self
562    }
563
564    /// Set the CO2 level.
565    #[must_use]
566    pub fn co2(mut self, co2: u16) -> Self {
567        self.co2 = co2;
568        self
569    }
570
571    /// Set the temperature.
572    #[must_use]
573    pub fn temperature(mut self, temp: f32) -> Self {
574        self.temperature = temp;
575        self
576    }
577
578    /// Set the pressure.
579    #[must_use]
580    pub fn pressure(mut self, pressure: f32) -> Self {
581        self.pressure = pressure;
582        self
583    }
584
585    /// Set the humidity.
586    #[must_use]
587    pub fn humidity(mut self, humidity: u8) -> Self {
588        self.humidity = humidity;
589        self
590    }
591
592    /// Set the battery level.
593    #[must_use]
594    pub fn battery(mut self, battery: u8) -> Self {
595        self.battery = battery;
596        self
597    }
598
599    /// Set the status.
600    #[must_use]
601    pub fn status(mut self, status: Status) -> Self {
602        self.status = status;
603        self
604    }
605
606    /// Set whether to auto-connect.
607    #[must_use]
608    pub fn auto_connect(mut self, auto: bool) -> Self {
609        self.auto_connect = auto;
610        self
611    }
612
613    /// Build the mock device.
614    ///
615    /// Note: This is a sync method that sets initial state directly.
616    /// The device is created with the specified reading already set.
617    #[must_use]
618    pub fn build(self) -> MockDevice {
619        let reading = CurrentReading {
620            co2: self.co2,
621            temperature: self.temperature,
622            pressure: self.pressure,
623            humidity: self.humidity,
624            battery: self.battery,
625            status: self.status,
626            interval: 300,
627            age: 60,
628            captured_at: None,
629            radon: None,
630            radiation_rate: None,
631            radiation_total: None,
632            radon_avg_24h: None,
633            radon_avg_7d: None,
634            radon_avg_30d: None,
635        };
636
637        MockDevice {
638            name: self.name.clone(),
639            address: format!("MOCK-{:06X}", rand::random::<u32>() % 0xFFFFFF),
640            device_type: self.device_type,
641            connected: AtomicBool::new(self.auto_connect),
642            current_reading: RwLock::new(reading),
643            device_info: RwLock::new(MockDevice::default_info(&self.name)),
644            history: RwLock::new(Vec::new()),
645            interval: RwLock::new(MeasurementInterval::FiveMinutes),
646            calibration: RwLock::new(CalibrationData::default()),
647            battery: RwLock::new(self.battery),
648            rssi: AtomicI16::new(-50),
649            read_count: AtomicU32::new(0),
650            should_fail: AtomicBool::new(false),
651            fail_message: RwLock::new("Mock failure".to_string()),
652            read_latency_ms: AtomicU64::new(0),
653            connect_latency_ms: AtomicU64::new(0),
654            fail_count: AtomicU32::new(0),
655            remaining_failures: AtomicU32::new(0),
656        }
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use crate::traits::AranetDevice;
664
665    #[tokio::test]
666    async fn test_mock_device_connect() {
667        let device = MockDevice::new("Test", DeviceType::Aranet4);
668        assert!(!device.is_connected_sync());
669
670        device.connect().await.unwrap();
671        assert!(device.is_connected_sync());
672
673        device.disconnect().await.unwrap();
674        assert!(!device.is_connected_sync());
675    }
676
677    #[tokio::test]
678    async fn test_mock_device_read() {
679        let device = MockDeviceBuilder::new().co2(1200).temperature(25.0).build();
680
681        let reading = device.read_current().await.unwrap();
682        assert_eq!(reading.co2, 1200);
683        assert!((reading.temperature - 25.0).abs() < 0.01);
684    }
685
686    #[tokio::test]
687    async fn test_mock_device_fail() {
688        let device = MockDeviceBuilder::new().build();
689        device.set_should_fail(true, Some("Test error")).await;
690
691        let result = device.read_current().await;
692        assert!(result.is_err());
693        assert!(result.unwrap_err().to_string().contains("Test error"));
694    }
695
696    #[tokio::test]
697    async fn test_mock_device_not_connected() {
698        let device = MockDeviceBuilder::new().auto_connect(false).build();
699
700        let result = device.read_current().await;
701        assert!(matches!(result, Err(Error::NotConnected)));
702    }
703
704    #[test]
705    fn test_builder_defaults() {
706        let device = MockDeviceBuilder::new().build();
707        assert!(device.is_connected_sync());
708        assert_eq!(device.device_type(), DeviceType::Aranet4);
709    }
710
711    #[tokio::test]
712    async fn test_aranet_device_trait() {
713        let device = MockDeviceBuilder::new().co2(1000).build();
714
715        // Use via trait
716        async fn check_via_trait<D: AranetDevice>(d: &D) -> u16 {
717            d.read_current().await.unwrap().co2
718        }
719
720        assert_eq!(check_via_trait(&device).await, 1000);
721    }
722}