use std::collections::HashMap;
use std::path::PathBuf;
use fit::profile::MesgNum;
use fit::{Decoder, RawValue};
fn fixture(name: &str) -> PathBuf {
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.push("tests/fixtures/test_data");
p.push(name);
p
}
fn read_fixture(name: &str) -> Vec<u8> {
std::fs::read(fixture(name)).expect("fixture must be readable")
}
fn read_csv() -> String {
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.push("tests/fixtures/example_files/Activity.csv");
std::fs::read_to_string(p).expect("Activity.csv must be readable")
}
fn csv_data_counts(csv: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for line in csv.lines() {
if let Some(rest) = line.strip_prefix("Data,") {
let mut parts = rest.splitn(3, ',');
let _local = parts.next();
if let Some(name) = parts.next() {
*counts.entry(name.to_string()).or_insert(0) += 1;
}
}
}
counts
}
#[test]
fn activity_full_decode_total_count() {
let bytes = read_fixture("Activity.fit");
let (msgs, errs) = Decoder::new(&bytes).read_all();
assert!(errs.is_empty(), "got errors: {errs:?}");
assert_eq!(
msgs.len(),
3611,
"total message count must match CSV Data rows"
);
}
#[test]
fn activity_per_message_counts_match_csv() {
let bytes = read_fixture("Activity.fit");
let (msgs, errs) = Decoder::new(&bytes).read_all();
assert!(errs.is_empty());
let mut ours: HashMap<String, usize> = HashMap::new();
for m in &msgs {
let name = MesgNum::from_value(m.global_mesg_num)
.map(|n| n.as_str().to_string())
.unwrap_or_else(|| format!("mesg_num_{}", m.global_mesg_num));
*ours.entry(name).or_insert(0) += 1;
}
let csv = read_csv();
let theirs = csv_data_counts(&csv);
for (name, expected) in &theirs {
let actual = ours.get(name).copied().unwrap_or(0);
assert_eq!(
actual, *expected,
"mismatch for `{name}`: decoder={actual}, CSV={expected}",
);
}
for (name, decoded) in &ours {
let expected = theirs.get(name).copied().unwrap_or(0);
assert_eq!(
*decoded, expected,
"decoder produced extra messages for `{name}` (our={decoded}, CSV={expected})",
);
}
}
#[test]
fn activity_first_record_timestamp_is_csv_value() {
let bytes = read_fixture("Activity.fit");
let (msgs, errs) = Decoder::new(&bytes).read_all();
assert!(errs.is_empty());
let first_record = msgs
.iter()
.find(|m| m.global_mesg_num == 20 )
.expect("Activity.fit must have at least one record message");
let ts = first_record
.field(253)
.expect("record must have a timestamp field");
assert_eq!(
ts.value.as_u32(),
Some(995749880),
"timestamp of first record must match the CSV value",
);
}
#[test]
fn activity_first_file_id_decodes_with_correct_types() {
let bytes = read_fixture("Activity.fit");
let (msgs, errs) = Decoder::new(&bytes).read_all();
assert!(errs.is_empty());
let first = &msgs[0];
assert_eq!(first.global_mesg_num, 0);
assert_eq!(first.field(0).unwrap().value.as_u8(), Some(4), "type");
assert_eq!(
first.field(1).unwrap().value.as_u16(),
Some(255),
"manufacturer"
);
assert_eq!(first.field(2).unwrap().value.as_u16(), Some(0), "product");
assert_eq!(
first.field(4).unwrap().value.as_u32(),
Some(995749880),
"time_created"
);
let serial = first.field(3).expect("file_id.serial_number missing");
assert!(
matches!(&serial.value, RawValue::U32zScalar(_)),
"serial_number must decode as a length-1 UInt32z; got {:?}",
serial.value,
);
}
#[test]
fn hrm_plugin_decodes_without_errors() {
let bytes = read_fixture("HrmPluginTestActivity.fit");
let (msgs, errs) = Decoder::new(&bytes).read_all();
assert!(errs.is_empty(), "got errors: {errs:?}");
assert!(!msgs.is_empty());
}
#[test]
fn gear_change_decodes_without_errors() {
let bytes = read_fixture("WithGearChangeData.fit");
let (msgs, errs) = Decoder::new(&bytes).read_all();
assert!(errs.is_empty(), "got errors: {errs:?}");
assert!(!msgs.is_empty());
}
#[test]
fn multi_fit_chain_doubles_message_count() {
let single = read_fixture("Activity.fit");
let chained: Vec<u8> = single.iter().chain(single.iter()).copied().collect();
let (msgs, errs) = Decoder::new(&chained).read_all();
assert!(
errs.is_empty(),
"chained file should decode cleanly: {errs:?}"
);
assert_eq!(
msgs.len(),
3611 * 2,
"two concatenated Activity.fit files must yield 2× messages (decoder must reset \
LocalDefinitions at the chain boundary)"
);
}
#[test]
fn iterator_terminates_after_fatal_error() {
let bytes = read_fixture("Activity.fit");
let truncated = &bytes[..bytes.len() / 2];
let mut decoder = Decoder::new(truncated);
let mut saw_error = false;
for item in decoder.by_ref() {
if item.is_err() {
saw_error = true;
break;
}
}
assert!(saw_error, "truncated file must surface an error");
assert!(decoder.next().is_none());
assert!(decoder.next().is_none());
}