This commit is contained in:
vcoppe committed 2026-10-08 19:04:46 +02:00
1 parent 840e943626
commit b085a1d476
26 files changed
+1010 -196

No files matched your search

+156
View File
@@ -128,3 +128,159 @@ pub fn decode_waypoints(id: Uuid, bytes: &[u8]) -> Option<WaypointChunk> {
wpt: decode::<Vec<Waypoint>>(bytes)?,
})
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::Chunk;
use super::*;
fn trackpoints(n: usize) -> TrackpointChunk {
TrackpointChunk::new(
(0..n)
.map(|i| Trackpoint {
ele: i as f64,
..Default::default()
})
.collect(),
)
}
#[test]
fn test_encode_decode_round_trip() {
let value = vec![(1_u32, "a".to_string()), (2, "b".to_string())];
let bytes = encode(&value);
assert_eq!(bytes[0], VERSION);
assert_eq!(decode::<Vec<(u32, String)>>(&bytes), Some(value));
}
#[test]
fn test_decode_rejects_other_versions_and_garbage() {
let mut bytes = encode(&42_u32);
assert_eq!(decode::<u32>(&bytes), Some(42));
bytes[0] = VERSION + 1;
assert_eq!(decode::<u32>(&bytes), None);
assert_eq!(decode::<u32>(&[]), None);
// the right version, but not what was asked for
assert_eq!(decode::<Vec<String>>(&[VERSION, 0xff, 0xff, 0xff]), None);
assert_eq!(decode::<u32>(&[VERSION]), None);
}
#[test]
fn test_chunks_keep_their_identity_and_items() {
let chunk = trackpoints(3);
let decoded = decode_trackpoints(chunk.id.0, &encode_trackpoints(&chunk)).unwrap();
assert_eq!(decoded.id, chunk.id);
let eles: Vec<f64> = decoded.trkpt.iter().map(|p| p.ele).collect();
assert_eq!(eles, [0.0, 1.0, 2.0]);
assert!(decode_trackpoints(chunk.id.0, &[VERSION, 9, 9]).is_none());
let chunk = WaypointChunk::new(vec![Waypoint {
name: Some("summit".into()),
..Default::default()
}]);
let decoded = decode_waypoints(chunk.id.0, &encode_waypoints(&chunk)).unwrap();
assert_eq!(decoded.id, chunk.id);
assert_eq!(decoded.wpt[0].name.as_deref(), Some("summit"));
assert!(decode_waypoints(chunk.id.0, &[]).is_none());
}
fn file() -> File {
let mut file = File::default();
file.info.name = "file".into();
for name in ["a", "b"] {
let mut track = Track::default();
track.info.name = Some(name.into());
let mut segment = TrackSegment::default();
segment.push(trackpoints(2));
segment.push(trackpoints(1));
track.trkseg.push(segment);
track.trkseg.push(TrackSegment::default());
file.trk.push(track);
}
file.wpt.push(WaypointChunk::new(vec![Waypoint::default()]));
file
}
/// The chunks of a file, by identity.
fn chunks_of(
file: &File,
) -> (
HashMap<TrackpointChunkId, Rc<TrackpointChunk>>,
HashMap<WaypointChunkId, Rc<WaypointChunk>>,
) {
let trackpoints = file
.trk
.iter()
.flat_map(|track| &track.trkseg)
.flat_map(|segment| segment.chunks().iter().map(|c| (c.id, c.clone())))
.collect();
let waypoints = file
.wpt
.chunks()
.iter()
.map(|c| (c.id, c.clone()))
.collect();
(trackpoints, waypoints)
}
#[test]
fn test_file_record_rebuilds_the_file_with_the_same_chunks() {
let file = file();
let (trackpoints, waypoints) = chunks_of(&file);
let record: FileRecord = decode(&encode(&FileRecord::new(&file))).unwrap();
let built = record
.build(file.id, &|id| trackpoints.get(id).cloned(), &|id| {
waypoints.get(id).cloned()
})
.unwrap();
assert_eq!(built.id, file.id);
assert_eq!(built.info, file.info);
assert_eq!(built.trk.len(), 2);
for (built, original) in built.trk.iter().zip(&file.trk) {
assert_eq!(built.id, original.id);
assert_eq!(built.info, original.info);
assert_eq!(built.trkseg.len(), 2);
for (built, original) in built.trkseg.iter().zip(&original.trkseg) {
assert_eq!(built.id, original.id);
assert_eq!(built.len(), original.len());
for (a, b) in built.chunks().iter().zip(original.chunks()) {
// the chunks are the ones that were given, not copies
assert!(Rc::ptr_eq(a, b));
}
}
}
assert_eq!(built.wpt.len(), 1);
assert!(Rc::ptr_eq(&built.wpt.chunks()[0], &file.wpt.chunks()[0]));
}
#[test]
fn test_file_record_of_a_missing_chunk_builds_nothing() {
let file = file();
let (trackpoints, waypoints) = chunks_of(&file);
let record = || FileRecord::new(&file);
assert!(
record()
.build(file.id, &|_| None, &|id| waypoints.get(id).cloned())
.is_none()
);
assert!(
record()
.build(file.id, &|id| trackpoints.get(id).cloned(), &|_| None)
.is_none()
);
}
#[test]
fn test_file_record_without_data_builds_an_empty_file() {
let file = File::default();
let built = FileRecord::new(&file)
.build(file.id, &|_| None, &|_| None)
.unwrap();
assert!(built.trk.is_empty() && built.wpt.is_empty());
}
}
+107
View File
@@ -103,3 +103,110 @@ impl Storage for MemoryStorage {
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::task::{Context, Poll, Waker};
use super::*;
fn block_on<F: Future>(future: F) -> F::Output {
let mut future = std::pin::pin!(future);
let mut context = Context::from_waker(Waker::noop());
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => value,
Poll::Pending => panic!("the memory storage does not wait"),
}
}
#[test]
fn test_empty_storage_has_nothing() {
let storage = MemoryStorage::default();
let data = block_on(storage.load()).unwrap();
assert_eq!(data, StoredData::default());
assert_eq!(storage.counts(), (0, 0, 0));
assert!(block_on(storage.load_settings()).unwrap().is_empty());
}
#[test]
fn test_commit_puts_and_deletes() {
let storage = MemoryStorage::default();
let (a, b) = (FileId::default(), FileId::default());
let (t, w) = (Uuid::new_v4(), Uuid::new_v4());
block_on(storage.commit(&Batch {
put_files: vec![(a, vec![1]), (b, vec![2])],
put_trackpoint_chunks: vec![(t, vec![3])],
put_waypoint_chunks: vec![(w, vec![4])],
order: Some(vec![5]),
categories: Some(vec![6]),
..Default::default()
}))
.unwrap();
assert_eq!(storage.counts(), (2, 1, 1));
let mut data = block_on(storage.load()).unwrap();
data.files.sort_by_key(|(_, bytes)| bytes.clone());
assert_eq!(data.files, vec![(a, vec![1]), (b, vec![2])]);
assert_eq!(data.trackpoint_chunks, vec![(t, vec![3])]);
assert_eq!(data.waypoint_chunks, vec![(w, vec![4])]);
assert_eq!(
(data.order, data.categories),
(Some(vec![5]), Some(vec![6]))
);
// a put replaces, a delete removes, what is not mentioned stays
block_on(storage.commit(&Batch {
put_files: vec![(a, vec![10])],
delete_files: vec![b],
delete_trackpoint_chunks: vec![t],
..Default::default()
}))
.unwrap();
assert_eq!(storage.counts(), (1, 0, 1));
let data = block_on(storage.load()).unwrap();
assert_eq!(data.files, vec![(a, vec![10])]);
assert_eq!(
(data.order, data.categories),
(Some(vec![5]), Some(vec![6]))
);
block_on(storage.commit(&Batch {
delete_waypoint_chunks: vec![w],
order: Some(vec![]),
..Default::default()
}))
.unwrap();
assert_eq!(storage.counts(), (1, 0, 0));
assert_eq!(block_on(storage.load()).unwrap().order, Some(vec![]));
}
#[test]
fn test_settings() {
let storage = MemoryStorage::default();
block_on(storage.put_setting("a", "1")).unwrap();
block_on(storage.put_setting("b", "2")).unwrap();
block_on(storage.put_setting("a", "3")).unwrap();
let mut settings = block_on(storage.load_settings()).unwrap();
settings.sort();
assert_eq!(
settings,
vec![("a".into(), "3".into()), ("b".into(), "2".into())]
);
block_on(storage.delete_setting("a")).unwrap();
// deleting what is not there is fine
block_on(storage.delete_setting("missing")).unwrap();
assert_eq!(
block_on(storage.load_settings()).unwrap(),
vec![("b".into(), "2".into())]
);
}
#[test]
fn test_settings_are_not_files() {
let storage = MemoryStorage::default();
block_on(storage.put_setting("a", "1")).unwrap();
assert_eq!(block_on(storage.load()).unwrap(), StoredData::default());
}
}
+44
View File
@@ -83,3 +83,47 @@ pub trait Storage {
async fn delete_setting(&self, key: &str) -> Result<(), StorageError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_batch_is_empty_until_it_holds_something() {
assert!(Batch::default().is_empty());
let holds = [
Batch {
put_files: vec![(FileId::default(), vec![])],
..Default::default()
},
Batch {
delete_files: vec![FileId::default()],
..Default::default()
},
Batch {
delete_trackpoint_chunks: vec![Uuid::new_v4()],
..Default::default()
},
Batch {
delete_waypoint_chunks: vec![Uuid::new_v4()],
..Default::default()
},
Batch {
order: Some(vec![]),
..Default::default()
},
Batch {
categories: Some(vec![]),
..Default::default()
},
];
assert!(holds.iter().all(|batch| !batch.is_empty()));
}
#[test]
fn test_storage_error_is_readable() {
let error = StorageError("disk full".into());
assert_eq!(error.to_string(), "storage error: disk full");
let _: &dyn std::error::Error = &error;
}
}