use std::path::PathBuf;
use fit::{Decoder, Encoder, Field, FieldKind, Message, Value};
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")
}
#[test]
fn roundtrip_activity_message_count() {
let bytes = read_fixture("Activity.fit");
let (messages, errors) = Decoder::builder(&bytes).build().read_all();
assert!(errors.is_empty(), "decode errors: {errors:?}");
let enc = Encoder::new();
let encoded = enc.encode(&messages).expect("encode must succeed");
fit::check_integrity(&encoded).expect("encoded file must pass CRC check");
let (messages2, errors2) = Decoder::builder(&encoded).build().read_all();
assert!(errors2.is_empty(), "re-decode errors: {errors2:?}");
assert_eq!(
messages.len(),
messages2.len(),
"message count must survive round-trip"
);
}
#[test]
fn roundtrip_activity_message_names() {
let bytes = read_fixture("Activity.fit");
let (messages, _errors) = Decoder::builder(&bytes).build().read_all();
let enc = Encoder::new();
let encoded = enc.encode(&messages).unwrap();
let (messages2, _errors2) = Decoder::builder(&encoded).build().read_all();
for (a, b) in messages.iter().zip(messages2.iter()) {
assert_eq!(a.global_mesg_num, b.global_mesg_num, "mesg_num mismatch");
assert_eq!(
a.name, b.name,
"name mismatch for mesg_num {}",
a.global_mesg_num
);
}
}
#[test]
fn roundtrip_activity_timestamps() {
let bytes = read_fixture("Activity.fit");
let (messages, _errors) = Decoder::builder(&bytes).build().read_all();
let enc = Encoder::new();
let encoded = enc.encode(&messages).unwrap();
let (messages2, _errors2) = Decoder::builder(&encoded).build().read_all();
for (a, b) in messages.iter().zip(messages2.iter()) {
for (fa, fb) in a.fields.iter().zip(b.fields.iter()) {
if let (Value::DateTime(da), Value::DateTime(db)) = (&fa.value, &fb.value) {
assert_eq!(da, db, "DateTime mismatch in {}", a.name);
}
}
}
}
#[test]
fn roundtrip_activity_is_valid_fit() {
let bytes = read_fixture("Activity.fit");
let (messages, _errors) = Decoder::builder(&bytes).build().read_all();
let enc = Encoder::new();
let encoded = enc.encode(&messages).unwrap();
assert!(
fit::is_fit(&encoded),
"encoded bytes must be a valid FIT file"
);
fit::check_integrity(&encoded).expect("CRC must be valid");
}
#[test]
fn roundtrip_specific_field_values() {
let bytes = read_fixture("Activity.fit");
let (messages, _errors) = Decoder::builder(&bytes).build().read_all();
let enc = Encoder::new();
let encoded = enc.encode(&messages).unwrap();
let (messages2, _errors2) = Decoder::builder(&encoded).build().read_all();
let file_id_1 = messages.iter().find(|m| m.global_mesg_num == 0);
let file_id_2 = messages2.iter().find(|m| m.global_mesg_num == 0);
if let (Some(a), Some(b)) = (file_id_1, file_id_2) {
assert_eq!(
a.field("type").map(|f| &f.value),
b.field("type").map(|f| &f.value),
"file_id.type must survive round-trip"
);
}
let session_1 = messages.iter().find(|m| m.name == "session");
let session_2 = messages2.iter().find(|m| m.name == "session");
if let (Some(a), Some(b)) = (session_1, session_2) {
assert_eq!(
a.field("sport").map(|f| &f.value),
b.field("sport").map(|f| &f.value),
"session.sport must survive round-trip"
);
}
}
#[cfg(feature = "chrono")]
#[test]
fn encode_single_record_message() {
use chrono::{TimeZone, Utc};
let messages = vec![Message {
global_mesg_num: 0, name: "file_id",
fields: vec![
Field {
name: "type".to_string(),
kind: FieldKind::Standard { field_def_num: 0 },
value: Value::Enum("activity".into()),
units: None,
},
Field {
name: "time_created".to_string(),
kind: FieldKind::Standard { field_def_num: 3 },
value: Value::DateTime(Utc.with_ymd_and_hms(2021, 7, 19, 21, 11, 20).unwrap()),
units: None,
},
],
}];
let enc = Encoder::new();
let encoded = enc.encode(&messages).unwrap();
assert!(fit::is_fit(&encoded));
fit::check_integrity(&encoded).expect("CRC must be valid");
let (decoded, errors) = Decoder::builder(&encoded).build().read_all();
assert!(errors.is_empty(), "errors: {errors:?}");
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].name, "file_id");
let type_field = decoded[0].field("type").unwrap();
assert_eq!(type_field.value, Value::Enum("activity".into()));
}
#[test]
fn encode_with_uint_fields() {
let messages = vec![Message {
global_mesg_num: 0, name: "file_id",
fields: vec![
Field {
name: "type".to_string(),
kind: FieldKind::Standard { field_def_num: 0 },
value: Value::Enum("activity".into()),
units: None,
},
Field {
name: "manufacturer".to_string(),
kind: FieldKind::Standard { field_def_num: 1 },
value: Value::UInt(1), units: None,
},
Field {
name: "product".to_string(),
kind: FieldKind::Standard { field_def_num: 2 },
value: Value::UInt(3415),
units: None,
},
],
}];
let enc = Encoder::new();
let encoded = enc.encode(&messages).unwrap();
fit::check_integrity(&encoded).expect("CRC must be valid");
let (decoded, errors) = Decoder::builder(&encoded).build().read_all();
assert!(errors.is_empty(), "errors: {errors:?}");
assert_eq!(decoded.len(), 1);
let msg = &decoded[0];
assert_eq!(
msg.field("manufacturer").unwrap().value,
Value::Enum("garmin".into())
);
assert_eq!(
msg.field("garmin_product").unwrap().value,
Value::UInt(3415)
);
}
#[test]
fn encode_multiple_messages() {
let messages = vec![
Message {
global_mesg_num: 0, name: "file_id",
fields: vec![Field {
name: "type".to_string(),
kind: FieldKind::Standard { field_def_num: 0 },
value: Value::Enum("activity".into()),
units: None,
}],
},
Message {
global_mesg_num: 49, name: "file_creator",
fields: vec![Field {
name: "software_version".to_string(),
kind: FieldKind::Standard { field_def_num: 0 },
value: Value::UInt(1),
units: None,
}],
},
];
let enc = Encoder::new();
let encoded = enc.encode(&messages).unwrap();
fit::check_integrity(&encoded).expect("CRC must be valid");
let (decoded, errors) = Decoder::builder(&encoded).build().read_all();
assert!(errors.is_empty(), "errors: {errors:?}");
assert_eq!(decoded.len(), 2);
assert_eq!(decoded[0].name, "file_id");
assert_eq!(decoded[1].name, "file_creator");
}
#[test]
fn raw_roundtrip_activity() {
let bytes = read_fixture("Activity.fit");
let (raw_msgs, errors) = Decoder::new(&bytes).read_all();
assert!(errors.is_empty(), "raw decode errors: {errors:?}");
let (typed_msgs, t_errors) = Decoder::builder(&bytes).build().read_all();
assert!(t_errors.is_empty());
let enc = Encoder::new();
let encoded = enc.encode(&typed_msgs).unwrap();
let (raw_msgs2, errors2) = Decoder::new(&encoded).read_all();
assert!(errors2.is_empty(), "raw re-decode errors: {errors2:?}");
assert_eq!(
raw_msgs.len(),
raw_msgs2.len(),
"raw message count must survive round-trip"
);
}
fn values_roughly_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Float(x), Value::Float(y)) => (x - y).abs() < 1e-6,
(Value::Array(xs), Value::Array(ys)) if xs.len() == ys.len() => xs
.iter()
.zip(ys.iter())
.all(|(x, y)| values_roughly_equal(x, y)),
_ => a == b,
}
}
#[test]
fn roundtrip_activity_field_values_full() {
let bytes = read_fixture("Activity.fit");
let (messages, _) = Decoder::builder(&bytes).build().read_all();
let enc = Encoder::new();
let encoded = enc.encode(&messages).unwrap();
let (messages2, errors2) = Decoder::builder(&encoded).build().read_all();
assert!(errors2.is_empty(), "re-decode errors: {errors2:?}");
assert_eq!(messages.len(), messages2.len(), "msg count");
let mut compared = 0usize;
for (a, b) in messages.iter().zip(messages2.iter()) {
assert_eq!(a.global_mesg_num, b.global_mesg_num);
for fa in &a.fields {
if !matches!(fa.kind, FieldKind::Standard { .. }) {
continue;
}
let Some(fb) = b.field(&fa.name) else {
continue;
};
assert!(
values_roughly_equal(&fa.value, &fb.value),
"{}.{}: {:?} vs {:?}",
a.name,
fa.name,
fa.value,
fb.value,
);
compared += 1;
}
}
assert!(compared > 1000, "only compared {compared} fields");
}
#[test]
fn roundtrip_preserves_record_speed_with_scale() {
let bytes = read_fixture("Activity.fit");
let (messages, _) = Decoder::builder(&bytes).build().read_all();
let speed_before: Vec<f64> = messages
.iter()
.filter(|m| m.name == "record")
.filter_map(|m| m.field("speed"))
.filter_map(|f| match &f.value {
Value::Float(v) => Some(*v),
_ => None,
})
.collect();
assert!(
!speed_before.is_empty(),
"fixture must have at least one record.speed Float"
);
let enc = Encoder::new();
let encoded = enc.encode(&messages).unwrap();
let (messages2, _) = Decoder::builder(&encoded).build().read_all();
let speed_after: Vec<f64> = messages2
.iter()
.filter(|m| m.name == "record")
.filter_map(|m| m.field("speed"))
.filter_map(|f| match &f.value {
Value::Float(v) => Some(*v),
_ => None,
})
.collect();
assert_eq!(speed_before.len(), speed_after.len(), "speed count");
for (a, b) in speed_before.iter().zip(speed_after.iter()) {
assert!((a - b).abs() < 1e-3, "speed drift: {a} vs {b}");
}
}