use proptest::prelude::*;
use fit::Decoder;
proptest! {
#[test]
fn random_bytes_dont_panic(bytes in proptest::collection::vec(any::<u8>(), 0..4096)) {
let _ = Decoder::new(&bytes).read_all();
}
#[test]
fn random_bytes_dont_panic_large(bytes in proptest::collection::vec(any::<u8>(), 0..65536)) {
let _ = Decoder::new(&bytes).read_all();
}
#[test]
fn truncated_fit_doesnt_panic(truncate in 0..14usize) {
let records = build_one_record_fit();
let truncated = &records[..truncate.min(records.len())];
let _ = Decoder::new(truncated).read_all();
}
}
fn build_one_record_fit() -> Vec<u8> {
let mut records = Vec::new();
records.push(0x40);
records.extend_from_slice(&[
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x06, ]);
records.push(0x00);
records.extend_from_slice(&0x3B59EFF8u32.to_le_bytes());
let data_size = records.len() as u32;
let mut bytes = vec![14u8, 0x20, 0xD0, 0x52];
bytes.extend_from_slice(&data_size.to_le_bytes());
bytes.extend_from_slice(b".FIT");
bytes.extend_from_slice(&[0, 0]); bytes.extend_from_slice(&records);
bytes.extend_from_slice(&[0, 0]); bytes
}
proptest! {
#[test]
fn crc_is_deterministic(len in 0usize..1024) {
use rand::Rng;
let mut rng = rand::rng();
let bytes: Vec<u8> = (0..len).map(|_| rng.random()).collect();
let crc1 = fit::crc16(&bytes);
let crc2 = fit::crc16(&bytes);
prop_assert_eq!(crc1, crc2);
}
#[test]
fn crc_of_empty_is_zero(_dummy in any::<u8>()) {
prop_assert_eq!(fit::crc16(&[]), 0);
}
}
proptest! {
#[test]
fn accumulator_no_panic(
mesg_num in 0u16..50,
field_def_num in 0u8..30,
values in proptest::collection::vec(0u32..0xFFFFFF, 1..20),
bits in 1u32..64,
) {
let mut acc = fit::transforms::Accumulator::new();
for raw in &values {
let _ = acc.accumulate(mesg_num, field_def_num, *raw as u64, bits);
}
}
}
#[cfg(feature = "chrono")]
proptest! {
#[test]
fn datetime_roundtrip(seconds in 0u32..0xFFFF_FFFF) {
use fit::datetime;
if let Some(dt) = datetime::fit_to_datetime(seconds) {
let back = datetime::datetime_to_fit(dt);
prop_assert_eq!(Some(seconds), back);
}
}
#[test]
fn datetime_roundtrip_via_chrono(epoch_secs in 631065600i64..2147483647i64) {
use chrono::{TimeZone, Utc};
use fit::datetime;
let dt = Utc.timestamp_opt(epoch_secs, 0).unwrap();
if let Some(fit_secs) = datetime::datetime_to_fit(dt) {
let back = datetime::fit_to_datetime(fit_secs);
prop_assert_eq!(Some(dt), back);
}
}
}
proptest! {
#[test]
fn compressed_timestamps_are_post_epoch(
base_ts in 631152000u32..0x7FFFFFFF, offsets in proptest::collection::vec(0u8..32, 1..10),
) {
const FIT_EPOCH: u32 = 631065600;
let mut last_ts = base_ts;
for offset in &offsets {
let last_5bits = last_ts & 0x1F;
let new_ts = if *offset as u32 >= last_5bits {
(last_ts & !0x1F) | (*offset as u32)
} else {
(last_ts & !0x1F).wrapping_add(0x20) | (*offset as u32)
};
last_ts = new_ts;
prop_assert!(new_ts >= FIT_EPOCH, "compressed timestamp {new_ts} is before FIT epoch");
}
}
}
proptest! {
#[test]
fn random_definition_and_data(
field_count in 1u8..8,
) {
use rand::Rng;
let mut rng = rand::rng();
let mut records = Vec::new();
records.push(0x40); records.push(0x00); records.push(0x00); records.extend_from_slice(&0u16.to_le_bytes()); records.push(field_count);
let mut data_size = 0u8;
for _ in 0..field_count {
let fdn: u8 = rng.random_range(0..20);
let size: u8 = rng.random_range(1..8);
records.push(fdn);
records.push(size);
records.push(0x02);
data_size += size;
}
records.push(0x00);
for _ in 0..data_size {
records.push(rng.random());
}
let ds = records.len() as u32;
let mut bytes = vec![14u8, 0x20, 0xD0, 0x52];
bytes.extend_from_slice(&ds.to_le_bytes());
bytes.extend_from_slice(b".FIT");
bytes.extend_from_slice(&[0, 0]);
bytes.extend_from_slice(&records);
bytes.extend_from_slice(&[0, 0]);
let _ = Decoder::new(&bytes).read_all();
}
}