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