1pub mod error;
22pub mod types;
23pub mod uuid;
24
25pub use error::{ParseError, ParseResult};
26pub use types::{
27 CurrentReading, CurrentReadingBuilder, DeviceInfo, DeviceInfoBuilder, DeviceType,
28 HistoryRecord, HistoryRecordBuilder, MIN_CURRENT_READING_BYTES, Status,
29};
30
31pub use uuid as ble;
34#[doc(hidden)]
35pub use uuid as uuids;
36
37#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
94 fn test_parse_current_reading_from_valid_bytes() {
95 let bytes: [u8; 13] = [
105 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, ];
114
115 let reading = CurrentReading::from_bytes(&bytes).unwrap();
116
117 assert_eq!(reading.co2, 800);
118 assert!((reading.temperature - 22.5).abs() < 0.01);
119 assert!((reading.pressure - 1013.2).abs() < 0.1);
120 assert_eq!(reading.humidity, 45);
121 assert_eq!(reading.battery, 85);
122 assert_eq!(reading.status, Status::Green);
123 assert_eq!(reading.interval, 300);
124 assert_eq!(reading.age, 120);
125 }
126
127 #[test]
128 fn test_parse_current_reading_from_insufficient_bytes() {
129 let bytes: [u8; 10] = [0; 10]; let result = CurrentReading::from_bytes(&bytes);
132
133 assert!(result.is_err());
134 let err = result.unwrap_err();
135 assert_eq!(
136 err,
137 ParseError::InsufficientBytes {
138 expected: 13,
139 actual: 10
140 }
141 );
142 assert!(err.to_string().contains("expected 13"));
143 assert!(err.to_string().contains("got 10"));
144 }
145
146 #[test]
147 fn test_parse_current_reading_zero_bytes() {
148 let bytes: [u8; 0] = [];
149
150 let result = CurrentReading::from_bytes(&bytes);
151 assert!(result.is_err());
152 }
153
154 #[test]
155 fn test_parse_current_reading_all_zeros() {
156 let bytes: [u8; 13] = [0; 13];
157
158 let reading = CurrentReading::from_bytes(&bytes).unwrap();
159 assert_eq!(reading.co2, 0);
160 assert!((reading.temperature - 0.0).abs() < 0.01);
161 assert!((reading.pressure - 0.0).abs() < 0.1);
162 assert_eq!(reading.humidity, 0);
163 assert_eq!(reading.battery, 0);
164 assert_eq!(reading.status, Status::Error);
165 assert_eq!(reading.interval, 0);
166 assert_eq!(reading.age, 0);
167 }
168
169 #[test]
170 fn test_parse_current_reading_max_values() {
171 let bytes: [u8; 13] = [
172 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 3, 0xFF, 0xFF, 0xFF, 0xFF, ];
181
182 let reading = CurrentReading::from_bytes(&bytes).unwrap();
183 assert_eq!(reading.co2, 65535);
184 assert!((reading.temperature - 3276.75).abs() < 0.01); assert!((reading.pressure - 6553.5).abs() < 0.1); assert_eq!(reading.humidity, 255);
187 assert_eq!(reading.battery, 255);
188 assert_eq!(reading.interval, 65535);
189 assert_eq!(reading.age, 65535);
190 }
191
192 #[test]
193 fn test_parse_current_reading_high_co2_red_status() {
194 let bytes: [u8; 13] = [
196 0xD0, 0x07, 0xC2, 0x01, 0x94, 0x27, 50, 80, 3, 0x2C, 0x01, 0x78, 0x00,
201 ];
202
203 let reading = CurrentReading::from_bytes(&bytes).unwrap();
204 assert_eq!(reading.co2, 2000);
205 assert_eq!(reading.status, Status::Red);
206 }
207
208 #[test]
209 fn test_parse_current_reading_moderate_co2_yellow_status() {
210 let bytes: [u8; 13] = [
212 0xB0, 0x04, 0xC2, 0x01, 0x94, 0x27, 50, 80, 2, 0x2C, 0x01, 0x78, 0x00,
215 ];
216
217 let reading = CurrentReading::from_bytes(&bytes).unwrap();
218 assert_eq!(reading.co2, 1200);
219 assert_eq!(reading.status, Status::Yellow);
220 }
221
222 #[test]
223 fn test_parse_current_reading_extra_bytes_ignored() {
224 let bytes: [u8; 16] = [
226 0x20, 0x03, 0xC2, 0x01, 0x94, 0x27, 45, 85, 1, 0x2C, 0x01, 0x78, 0x00, 0xAA, 0xBB, 0xCC,
227 ];
228
229 let reading = CurrentReading::from_bytes(&bytes).unwrap();
230 assert_eq!(reading.co2, 800);
231 }
232
233 #[test]
236 fn test_status_from_u8() {
237 assert_eq!(Status::from(0), Status::Error);
238 assert_eq!(Status::from(1), Status::Green);
239 assert_eq!(Status::from(2), Status::Yellow);
240 assert_eq!(Status::from(3), Status::Red);
241 assert_eq!(Status::from(4), Status::Error);
243 assert_eq!(Status::from(255), Status::Error);
244 }
245
246 #[test]
247 fn test_status_repr_values() {
248 assert_eq!(Status::Error as u8, 0);
249 assert_eq!(Status::Green as u8, 1);
250 assert_eq!(Status::Yellow as u8, 2);
251 assert_eq!(Status::Red as u8, 3);
252 }
253
254 #[test]
255 fn test_status_debug() {
256 assert_eq!(format!("{:?}", Status::Green), "Green");
257 assert_eq!(format!("{:?}", Status::Yellow), "Yellow");
258 assert_eq!(format!("{:?}", Status::Red), "Red");
259 assert_eq!(format!("{:?}", Status::Error), "Error");
260 }
261
262 #[test]
263 fn test_status_clone() {
264 let status = Status::Green;
265 let cloned = status;
267 assert_eq!(status, cloned);
268 }
269
270 #[test]
271 fn test_status_copy() {
272 let status = Status::Red;
273 let copied = status; assert_eq!(status, copied); }
276
277 #[test]
280 fn test_device_type_values() {
281 assert_eq!(DeviceType::Aranet4 as u8, 0xF1);
282 assert_eq!(DeviceType::Aranet2 as u8, 0xF2);
283 assert_eq!(DeviceType::AranetRadon as u8, 0xF3);
284 assert_eq!(DeviceType::AranetRadiation as u8, 0xF4);
285 }
286
287 #[test]
288 fn test_device_type_debug() {
289 assert_eq!(format!("{:?}", DeviceType::Aranet4), "Aranet4");
290 assert_eq!(format!("{:?}", DeviceType::Aranet2), "Aranet2");
291 assert_eq!(format!("{:?}", DeviceType::AranetRadon), "AranetRadon");
292 assert_eq!(
293 format!("{:?}", DeviceType::AranetRadiation),
294 "AranetRadiation"
295 );
296 }
297
298 #[test]
299 fn test_device_type_clone() {
300 let device_type = DeviceType::Aranet4;
301 let cloned = device_type;
303 assert_eq!(device_type, cloned);
304 }
305
306 #[test]
307 fn test_device_type_try_from_u8() {
308 assert_eq!(DeviceType::try_from(0xF1), Ok(DeviceType::Aranet4));
309 assert_eq!(DeviceType::try_from(0xF2), Ok(DeviceType::Aranet2));
310 assert_eq!(DeviceType::try_from(0xF3), Ok(DeviceType::AranetRadon));
311 assert_eq!(DeviceType::try_from(0xF4), Ok(DeviceType::AranetRadiation));
312 }
313
314 #[test]
315 fn test_device_type_try_from_u8_invalid() {
316 let result = DeviceType::try_from(0x00);
317 assert!(result.is_err());
318 assert_eq!(result.unwrap_err(), ParseError::UnknownDeviceType(0x00));
319
320 let result = DeviceType::try_from(0xFF);
321 assert!(result.is_err());
322 assert_eq!(result.unwrap_err(), ParseError::UnknownDeviceType(0xFF));
323 }
324
325 #[test]
326 fn test_device_type_display() {
327 assert_eq!(format!("{}", DeviceType::Aranet4), "Aranet4");
328 assert_eq!(format!("{}", DeviceType::Aranet2), "Aranet2");
329 assert_eq!(format!("{}", DeviceType::AranetRadon), "Aranet Radon");
330 assert_eq!(
331 format!("{}", DeviceType::AranetRadiation),
332 "Aranet Radiation"
333 );
334 }
335
336 #[test]
337 fn test_device_type_hash() {
338 use std::collections::HashSet;
339 let mut set = HashSet::new();
340 set.insert(DeviceType::Aranet4);
341 set.insert(DeviceType::Aranet2);
342 set.insert(DeviceType::Aranet4); assert_eq!(set.len(), 2);
344 assert!(set.contains(&DeviceType::Aranet4));
345 assert!(set.contains(&DeviceType::Aranet2));
346 }
347
348 #[test]
349 fn test_status_display() {
350 assert_eq!(format!("{}", Status::Error), "Error");
351 assert_eq!(format!("{}", Status::Green), "Good");
352 assert_eq!(format!("{}", Status::Yellow), "Moderate");
353 assert_eq!(format!("{}", Status::Red), "High");
354 }
355
356 #[test]
357 fn test_status_hash() {
358 use std::collections::HashSet;
359 let mut set = HashSet::new();
360 set.insert(Status::Green);
361 set.insert(Status::Yellow);
362 set.insert(Status::Green); assert_eq!(set.len(), 2);
364 assert!(set.contains(&Status::Green));
365 assert!(set.contains(&Status::Yellow));
366 }
367
368 #[test]
371 fn test_device_info_creation() {
372 let info = types::DeviceInfo {
373 name: "Aranet4 12345".to_string(),
374 model: "Aranet4".to_string(),
375 serial: "12345".to_string(),
376 firmware: "v1.2.0".to_string(),
377 hardware: "1.0".to_string(),
378 software: "1.2.0".to_string(),
379 manufacturer: "SAF Tehnika".to_string(),
380 };
381
382 assert_eq!(info.name, "Aranet4 12345");
383 assert_eq!(info.serial, "12345");
384 assert_eq!(info.manufacturer, "SAF Tehnika");
385 }
386
387 #[test]
388 fn test_device_info_clone() {
389 let info = types::DeviceInfo {
390 name: "Test".to_string(),
391 model: "Model".to_string(),
392 serial: "123".to_string(),
393 firmware: "1.0".to_string(),
394 hardware: "1.0".to_string(),
395 software: "1.0".to_string(),
396 manufacturer: "Mfg".to_string(),
397 };
398
399 let cloned = info.clone();
400 assert_eq!(cloned.name, info.name);
401 assert_eq!(cloned.serial, info.serial);
402 }
403
404 #[test]
405 fn test_device_info_debug() {
406 let info = types::DeviceInfo {
407 name: "Aranet4".to_string(),
408 model: "".to_string(),
409 serial: "".to_string(),
410 firmware: "".to_string(),
411 hardware: "".to_string(),
412 software: "".to_string(),
413 manufacturer: "".to_string(),
414 };
415
416 let debug_str = format!("{:?}", info);
417 assert!(debug_str.contains("Aranet4"));
418 }
419
420 #[test]
421 fn test_device_info_default() {
422 let info = types::DeviceInfo::default();
423 assert_eq!(info.name, "");
424 assert_eq!(info.model, "");
425 assert_eq!(info.serial, "");
426 assert_eq!(info.firmware, "");
427 assert_eq!(info.hardware, "");
428 assert_eq!(info.software, "");
429 assert_eq!(info.manufacturer, "");
430 }
431
432 #[test]
433 fn test_device_info_equality() {
434 let info1 = types::DeviceInfo {
435 name: "Test".to_string(),
436 model: "Model".to_string(),
437 serial: "123".to_string(),
438 firmware: "1.0".to_string(),
439 hardware: "1.0".to_string(),
440 software: "1.0".to_string(),
441 manufacturer: "Mfg".to_string(),
442 };
443 let info2 = info1.clone();
444 let info3 = types::DeviceInfo {
445 name: "Different".to_string(),
446 ..info1.clone()
447 };
448 assert_eq!(info1, info2);
449 assert_ne!(info1, info3);
450 }
451
452 #[test]
455 fn test_history_record_creation() {
456 use time::OffsetDateTime;
457
458 let record = types::HistoryRecord {
459 timestamp: OffsetDateTime::UNIX_EPOCH,
460 co2: 800,
461 temperature: 22.5,
462 pressure: 1013.2,
463 humidity: 45,
464 radon: None,
465 radiation_rate: None,
466 radiation_total: None,
467 };
468
469 assert_eq!(record.co2, 800);
470 assert!((record.temperature - 22.5).abs() < 0.01);
471 assert!((record.pressure - 1013.2).abs() < 0.1);
472 assert_eq!(record.humidity, 45);
473 assert!(record.radon.is_none());
474 assert!(record.radiation_rate.is_none());
475 assert!(record.radiation_total.is_none());
476 }
477
478 #[test]
479 fn test_history_record_clone() {
480 use time::OffsetDateTime;
481
482 let record = types::HistoryRecord {
483 timestamp: OffsetDateTime::UNIX_EPOCH,
484 co2: 500,
485 temperature: 20.0,
486 pressure: 1000.0,
487 humidity: 50,
488 radon: Some(100),
489 radiation_rate: Some(0.15),
490 radiation_total: Some(1.5),
491 };
492
493 let cloned = record.clone();
494 assert_eq!(cloned.co2, record.co2);
495 assert_eq!(cloned.humidity, record.humidity);
496 assert_eq!(cloned.radon, Some(100));
497 assert_eq!(cloned.radiation_rate, Some(0.15));
498 assert_eq!(cloned.radiation_total, Some(1.5));
499 }
500
501 #[test]
502 fn test_history_record_equality() {
503 use time::OffsetDateTime;
504
505 let record1 = types::HistoryRecord {
506 timestamp: OffsetDateTime::UNIX_EPOCH,
507 co2: 800,
508 temperature: 22.5,
509 pressure: 1013.2,
510 humidity: 45,
511 radon: None,
512 radiation_rate: None,
513 radiation_total: None,
514 };
515 let record2 = record1.clone();
516 assert_eq!(record1, record2);
517 }
518
519 #[test]
520 fn test_current_reading_equality() {
521 let reading1 = CurrentReading {
522 co2: 800,
523 temperature: 22.5,
524 pressure: 1013.2,
525 humidity: 45,
526 battery: 85,
527 status: Status::Green,
528 interval: 300,
529 age: 120,
530 captured_at: None,
531 radon: None,
532 radiation_rate: None,
533 radiation_total: None,
534 radon_avg_24h: None,
535 radon_avg_7d: None,
536 radon_avg_30d: None,
537 };
538 let reading2 = reading1;
540 assert_eq!(reading1, reading2);
541 }
542
543 #[test]
544 fn test_min_current_reading_bytes_const() {
545 assert_eq!(MIN_CURRENT_READING_BYTES, 13);
546 let bytes = [0u8; MIN_CURRENT_READING_BYTES];
548 assert!(CurrentReading::from_bytes(&bytes).is_ok());
549 let short_bytes = [0u8; MIN_CURRENT_READING_BYTES - 1];
551 assert!(CurrentReading::from_bytes(&short_bytes).is_err());
552 }
553
554 #[test]
557 fn test_parse_error_display() {
558 let err = ParseError::invalid_value("test message");
559 assert_eq!(err.to_string(), "Invalid value: test message");
560 }
561
562 #[test]
563 fn test_parse_error_insufficient_bytes() {
564 let err = ParseError::InsufficientBytes {
565 expected: 13,
566 actual: 5,
567 };
568 assert_eq!(err.to_string(), "Insufficient bytes: expected 13, got 5");
569 }
570
571 #[test]
572 fn test_parse_error_unknown_device_type() {
573 let err = ParseError::UnknownDeviceType(0xAB);
574 assert_eq!(err.to_string(), "Unknown device type: 0xAB");
575 }
576
577 #[test]
578 fn test_parse_error_invalid_value() {
579 let err = ParseError::InvalidValue("bad value".to_string());
580 assert_eq!(err.to_string(), "Invalid value: bad value");
581 }
582
583 #[test]
584 fn test_parse_error_debug() {
585 let err = ParseError::invalid_value("debug test");
586 let debug_str = format!("{:?}", err);
587 assert!(debug_str.contains("InvalidValue"));
588 assert!(debug_str.contains("debug test"));
589 }
590
591 #[test]
592 fn test_parse_error_equality() {
593 let err1 = ParseError::InsufficientBytes {
594 expected: 10,
595 actual: 5,
596 };
597 let err2 = ParseError::InsufficientBytes {
598 expected: 10,
599 actual: 5,
600 };
601 let err3 = ParseError::InsufficientBytes {
602 expected: 10,
603 actual: 6,
604 };
605 assert_eq!(err1, err2);
606 assert_ne!(err1, err3);
607 }
608
609 #[test]
612 fn test_current_reading_serialization() {
613 let reading = CurrentReading {
614 co2: 800,
615 temperature: 22.5,
616 pressure: 1013.2,
617 humidity: 45,
618 battery: 85,
619 status: Status::Green,
620 interval: 300,
621 age: 120,
622 captured_at: None,
623 radon: None,
624 radiation_rate: None,
625 radiation_total: None,
626 radon_avg_24h: None,
627 radon_avg_7d: None,
628 radon_avg_30d: None,
629 };
630
631 let json = serde_json::to_string(&reading).unwrap();
632 assert!(json.contains("\"co2\":800"));
633 assert!(json.contains("\"humidity\":45"));
634 }
635
636 #[test]
637 fn test_current_reading_deserialization() {
638 let json = r#"{"co2":800,"temperature":22.5,"pressure":1013.2,"humidity":45,"battery":85,"status":"Green","interval":300,"age":120,"radon":null,"radiation_rate":null,"radiation_total":null}"#;
639
640 let reading: CurrentReading = serde_json::from_str(json).unwrap();
641 assert_eq!(reading.co2, 800);
642 assert_eq!(reading.status, Status::Green);
643 }
644
645 #[test]
646 fn test_status_serialization() {
647 assert_eq!(serde_json::to_string(&Status::Green).unwrap(), "\"Green\"");
648 assert_eq!(
649 serde_json::to_string(&Status::Yellow).unwrap(),
650 "\"Yellow\""
651 );
652 assert_eq!(serde_json::to_string(&Status::Red).unwrap(), "\"Red\"");
653 assert_eq!(serde_json::to_string(&Status::Error).unwrap(), "\"Error\"");
654 }
655
656 #[test]
657 fn test_device_type_serialization() {
658 assert_eq!(
659 serde_json::to_string(&DeviceType::Aranet4).unwrap(),
660 "\"Aranet4\""
661 );
662 assert_eq!(
663 serde_json::to_string(&DeviceType::AranetRadon).unwrap(),
664 "\"AranetRadon\""
665 );
666 }
667
668 #[test]
669 fn test_device_info_serialization_roundtrip() {
670 let info = types::DeviceInfo {
671 name: "Test Device".to_string(),
672 model: "Model X".to_string(),
673 serial: "SN12345".to_string(),
674 firmware: "1.2.3".to_string(),
675 hardware: "2.0".to_string(),
676 software: "3.0".to_string(),
677 manufacturer: "Acme Corp".to_string(),
678 };
679
680 let json = serde_json::to_string(&info).unwrap();
681 let deserialized: types::DeviceInfo = serde_json::from_str(&json).unwrap();
682
683 assert_eq!(deserialized.name, info.name);
684 assert_eq!(deserialized.serial, info.serial);
685 assert_eq!(deserialized.manufacturer, info.manufacturer);
686 }
687
688 #[test]
691 fn test_status_ordering() {
692 assert!(Status::Error < Status::Green);
694 assert!(Status::Green < Status::Yellow);
695 assert!(Status::Yellow < Status::Red);
696
697 assert!(Status::Red > Status::Yellow);
699 assert!(Status::Yellow >= Status::Yellow);
700 assert!(Status::Green <= Status::Yellow);
701 }
702
703 #[test]
704 fn test_device_type_readings_characteristic() {
705 use crate::ble;
706
707 assert_eq!(
709 DeviceType::Aranet4.readings_characteristic(),
710 ble::CURRENT_READINGS_DETAIL
711 );
712
713 assert_eq!(
715 DeviceType::Aranet2.readings_characteristic(),
716 ble::CURRENT_READINGS_DETAIL_ALT
717 );
718 assert_eq!(
719 DeviceType::AranetRadon.readings_characteristic(),
720 ble::CURRENT_READINGS_DETAIL_ALT
721 );
722 assert_eq!(
723 DeviceType::AranetRadiation.readings_characteristic(),
724 ble::CURRENT_READINGS_DETAIL_ALT
725 );
726 }
727
728 #[test]
729 fn test_device_type_from_name_word_boundary() {
730 assert_eq!(
732 DeviceType::from_name("Aranet4 12345"),
733 Some(DeviceType::Aranet4)
734 );
735 assert_eq!(
736 DeviceType::from_name("My Aranet4"),
737 Some(DeviceType::Aranet4)
738 );
739
740 assert_eq!(DeviceType::from_name("ARANET4"), Some(DeviceType::Aranet4));
742 assert_eq!(DeviceType::from_name("aranet2"), Some(DeviceType::Aranet2));
743
744 assert_eq!(
746 DeviceType::from_name("AranetRn+ 306B8"),
747 Some(DeviceType::AranetRadon)
748 );
749 assert_eq!(
750 DeviceType::from_name("aranetrn+ 12345"),
751 Some(DeviceType::AranetRadon)
752 );
753 }
754
755 #[test]
756 fn test_byte_size_constants() {
757 assert_eq!(MIN_CURRENT_READING_BYTES, 13);
758 assert_eq!(types::MIN_ARANET2_READING_BYTES, 7);
759 assert_eq!(types::MIN_RADON_READING_BYTES, 15);
760 assert_eq!(types::MIN_RADON_GATT_READING_BYTES, 18);
761 assert_eq!(types::MIN_RADIATION_READING_BYTES, 28);
762 }
763
764 #[test]
765 fn test_from_bytes_aranet2() {
766 let data = [
768 0x90, 0x01, 0x32, 0x55, 0x01, 0x2C, 0x01, ];
774
775 let reading = CurrentReading::from_bytes_aranet2(&data).unwrap();
776 assert_eq!(reading.co2, 0); assert!((reading.temperature - 20.0).abs() < 0.1);
778 assert_eq!(reading.humidity, 50);
779 assert_eq!(reading.battery, 85);
780 assert_eq!(reading.status, Status::Green);
781 assert_eq!(reading.interval, 300);
782 assert_eq!(reading.pressure, 0.0); }
784
785 #[test]
786 fn test_from_bytes_aranet2_insufficient() {
787 let data = [0u8; 6]; let result = CurrentReading::from_bytes_aranet2(&data);
789 assert!(result.is_err());
790 }
791
792 #[test]
793 fn test_from_bytes_for_device() {
794 let aranet4_data = [0u8; 13];
796 let result = CurrentReading::from_bytes_for_device(&aranet4_data, DeviceType::Aranet4);
797 assert!(result.is_ok());
798
799 let aranet2_data = [0u8; 7];
800 let result = CurrentReading::from_bytes_for_device(&aranet2_data, DeviceType::Aranet2);
801 assert!(result.is_ok());
802 }
803
804 #[test]
805 fn test_builder_with_captured_at() {
806 use time::OffsetDateTime;
807
808 let now = OffsetDateTime::now_utc();
809 let reading = CurrentReading::builder()
810 .co2(800)
811 .temperature(22.5)
812 .captured_at(now)
813 .build();
814
815 assert_eq!(reading.co2, 800);
816 assert_eq!(reading.captured_at, Some(now));
817 }
818
819 #[test]
820 fn test_builder_try_build_valid() {
821 let result = CurrentReading::builder()
822 .co2(800)
823 .temperature(22.5)
824 .pressure(1013.0)
825 .humidity(50)
826 .battery(85)
827 .try_build();
828
829 assert!(result.is_ok());
830 }
831
832 #[test]
833 fn test_builder_try_build_invalid_humidity() {
834 let result = CurrentReading::builder()
835 .humidity(150) .try_build();
837
838 assert!(result.is_err());
839 let err = result.unwrap_err();
840 assert!(err.to_string().contains("humidity"));
841 }
842
843 #[test]
844 fn test_builder_try_build_invalid_battery() {
845 let result = CurrentReading::builder()
846 .battery(120) .try_build();
848
849 assert!(result.is_err());
850 let err = result.unwrap_err();
851 assert!(err.to_string().contains("battery"));
852 }
853
854 #[test]
855 fn test_builder_try_build_invalid_temperature() {
856 let result = CurrentReading::builder()
857 .temperature(-50.0) .try_build();
859
860 assert!(result.is_err());
861 let err = result.unwrap_err();
862 assert!(err.to_string().contains("temperature"));
863 }
864
865 #[test]
866 fn test_builder_try_build_invalid_pressure() {
867 let result = CurrentReading::builder()
868 .temperature(22.0) .pressure(500.0) .try_build();
871
872 assert!(result.is_err());
873 let err = result.unwrap_err();
874 assert!(err.to_string().contains("pressure"));
875 }
876
877 #[test]
878 fn test_with_captured_at() {
879 use time::OffsetDateTime;
880
881 let reading = CurrentReading::builder().age(60).build();
882
883 let now = OffsetDateTime::now_utc();
884 let reading_with_time = reading.with_captured_at(now);
885
886 assert!(reading_with_time.captured_at.is_some());
887 let captured = reading_with_time.captured_at.unwrap();
889 let expected = now - time::Duration::seconds(60);
890 assert!((captured - expected).whole_seconds().abs() < 2);
891 }
892
893 #[test]
894 fn test_parse_error_invalid_value_helper() {
895 let err = ParseError::invalid_value("test error");
896 assert_eq!(err.to_string(), "Invalid value: test error");
897 }
898}
899
900#[cfg(test)]
933mod proptests {
934 use super::*;
935 use proptest::prelude::*;
936
937 proptest! {
938 #[test]
941 fn parse_current_reading_never_panics(data: Vec<u8>) {
942 let _ = CurrentReading::from_bytes(&data);
943 }
944
945 #[test]
947 fn parse_aranet2_never_panics(data: Vec<u8>) {
948 let _ = CurrentReading::from_bytes_aranet2(&data);
949 }
950
951 #[test]
953 fn status_from_u8_never_panics(value: u8) {
954 let status = Status::from(value);
955 let _ = format!("{:?}", status);
957 }
958
959 #[test]
961 fn device_type_try_from_never_panics(value: u8) {
962 let _ = DeviceType::try_from(value);
963 }
964
965 #[test]
967 fn parse_valid_aranet4_bytes(
968 co2 in 0u16..10000u16,
969 temp_raw in 0u16..2000u16,
970 pressure_raw in 8000u16..12000u16,
971 humidity in 0u8..100u8,
972 battery in 0u8..100u8,
973 status_byte in 0u8..4u8,
974 interval in 60u16..3600u16,
975 age in 0u16..3600u16,
976 ) {
977 let mut data = [0u8; 13];
978 data[0..2].copy_from_slice(&co2.to_le_bytes());
979 data[2..4].copy_from_slice(&temp_raw.to_le_bytes());
980 data[4..6].copy_from_slice(&pressure_raw.to_le_bytes());
981 data[6] = humidity;
982 data[7] = battery;
983 data[8] = status_byte;
984 data[9..11].copy_from_slice(&interval.to_le_bytes());
985 data[11..13].copy_from_slice(&age.to_le_bytes());
986
987 let result = CurrentReading::from_bytes(&data);
988 prop_assert!(result.is_ok());
989
990 let reading = result.unwrap();
991 prop_assert_eq!(reading.co2, co2);
992 prop_assert_eq!(reading.humidity, humidity);
993 prop_assert_eq!(reading.battery, battery);
994 prop_assert_eq!(reading.interval, interval);
995 prop_assert_eq!(reading.age, age);
996 }
997
998 #[test]
1000 fn parse_valid_aranet2_bytes(
1001 temp_raw in 0u16..2000u16,
1002 humidity in 0u8..100u8,
1003 battery in 0u8..100u8,
1004 status_byte in 0u8..4u8,
1005 interval in 60u16..3600u16,
1006 ) {
1007 let mut data = [0u8; 7];
1008 data[0..2].copy_from_slice(&temp_raw.to_le_bytes());
1009 data[2] = humidity;
1010 data[3] = battery;
1011 data[4] = status_byte;
1012 data[5..7].copy_from_slice(&interval.to_le_bytes());
1013
1014 let result = CurrentReading::from_bytes_aranet2(&data);
1015 prop_assert!(result.is_ok());
1016
1017 let reading = result.unwrap();
1018 prop_assert_eq!(reading.humidity, humidity);
1019 prop_assert_eq!(reading.battery, battery);
1020 prop_assert_eq!(reading.interval, interval);
1021 }
1022
1023 #[test]
1025 fn current_reading_json_roundtrip(
1026 co2 in 0u16..10000u16,
1027 temperature in -20.0f32..60.0f32,
1028 pressure in 800.0f32..1200.0f32,
1029 humidity in 0u8..100u8,
1030 battery in 0u8..100u8,
1031 interval in 60u16..3600u16,
1032 age in 0u16..3600u16,
1033 ) {
1034 let reading = CurrentReading {
1035 co2,
1036 temperature,
1037 pressure,
1038 humidity,
1039 battery,
1040 status: Status::Green,
1041 interval,
1042 age,
1043 captured_at: None,
1044 radon: None,
1045 radiation_rate: None,
1046 radiation_total: None,
1047 radon_avg_24h: None,
1048 radon_avg_7d: None,
1049 radon_avg_30d: None,
1050 };
1051
1052 let json = serde_json::to_string(&reading).unwrap();
1053 let parsed: CurrentReading = serde_json::from_str(&json).unwrap();
1054
1055 prop_assert_eq!(parsed.co2, reading.co2);
1056 prop_assert_eq!(parsed.humidity, reading.humidity);
1057 prop_assert_eq!(parsed.battery, reading.battery);
1058 prop_assert_eq!(parsed.interval, reading.interval);
1059 prop_assert_eq!(parsed.age, reading.age);
1060 }
1061 }
1062}