Files
gpx.studio/gpx-rs/src/engine/tests.rs
T

1539 lines
48 KiB
Rust
Raw Normal View History

2026-10-08 21:04:29 +02:00
use std::collections::HashSet;
2026-10-10 11:07:07 +02:00
use std::rc::Rc;
2026-10-08 21:04:29 +02:00
use crate::{
2026-10-10 11:07:07 +02:00
ClipboardIds, Delete, DeleteAll, File, Load, Metadata, New, NewTrack, Paste, Style, TrackId,
2026-10-08 21:04:29 +02:00
TrackSegmentId,
};
use super::*;
2026-10-11 10:54:06 +02:00
/// An engine that remembers what the last action it was given did, for the tests to read right
/// after (see `run`).
#[derive(Default)]
struct TestEngine {
engine: Engine,
last: Outcome,
last_error: Option<CommandError>,
}
impl std::ops::Deref for TestEngine {
type Target = Engine;
fn deref(&self) -> &Engine {
&self.engine
}
}
impl std::ops::DerefMut for TestEngine {
fn deref_mut(&mut self) -> &mut Engine {
&mut self.engine
}
}
impl TestEngine {
/// Executes an action and tells whether it changed anything.
fn run(&mut self, action: Action) -> bool {
let result = self.engine.execute(action);
self.last = result.clone().unwrap_or_default();
self.last_error = result.as_ref().err().cloned();
result.is_ok_and(|outcome| outcome.changed)
}
fn restore(&mut self, restored: Restored) -> Outcome {
let outcome = self.engine.restore(restored);
self.last = outcome.clone();
self.last_error = None;
outcome
}
fn last_diff(&self) -> Option<&Diff> {
self.last.diff.as_ref()
}
fn last_error(&self) -> Option<&CommandError> {
self.last_error.as_ref()
}
fn order_changed(&self) -> bool {
self.last.order_changed
}
fn selection_changed(&self) -> bool {
self.last.selection_changed
}
fn clipboard_changed(&self) -> bool {
self.last.clipboard_changed
}
}
2026-10-08 21:04:29 +02:00
fn expand(intervals: &crate::Intervals, len: usize) -> Vec<u8> {
let mut values = Vec::new();
for (i, value) in intervals.values.iter().enumerate() {
let end = intervals.starts.get(i + 1).map_or(len, |end| *end as usize);
values.resize(end, *value);
}
values
}
2026-10-11 10:54:06 +02:00
fn edit(engine: &mut TestEngine, command: Command) -> bool {
2026-10-08 21:04:29 +02:00
engine.run(Action::Edit(command))
}
2026-10-11 10:54:06 +02:00
fn new(engine: &mut TestEngine, name: &str) -> bool {
2026-10-08 21:04:29 +02:00
edit(
engine,
Command::New(New {
name,
trackpoint: None,
}),
)
}
2026-10-11 10:54:06 +02:00
fn load(engine: &mut TestEngine, path: &str) -> bool {
2026-10-08 21:04:29 +02:00
let data = std::fs::read(path).unwrap();
edit(
engine,
Command::Load(Load {
data: &data,
name: "file",
}),
)
}
fn selected(engine: &Engine) -> Vec<FileId> {
match &engine.selection {
Selection::File { file_ids } => file_ids.iter().copied().collect(),
_ => vec![],
}
}
#[test]
fn test_load_file() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(load(&mut engine, "data/simple.gpx"));
assert_eq!(engine.statistics_buffer.total_distance.len(), 80);
}
#[test]
fn test_new_file_has_no_statistics() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(new(&mut engine, "empty"));
2026-10-14 08:17:51 +02:00
assert_eq!(engine.history.current().unwrap().len(), 1);
2026-10-08 21:04:29 +02:00
assert_eq!(engine.order.0.len(), 1);
assert!(engine.statistics().total_distance.is_empty());
}
#[test]
fn test_statistics_follow_selection() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(load(&mut engine, "data/simple.gpx"));
let n = engine.statistics().total_distance.len();
assert!(n > 0);
// a newly created (empty) file becomes the selection
assert!(new(&mut engine, "empty"));
assert!(engine.statistics().total_distance.is_empty());
2026-10-14 08:17:51 +02:00
assert_eq!(engine.history.current().unwrap().len(), 2);
2026-10-08 21:04:29 +02:00
// loading a file selects it again
assert!(load(&mut engine, "data/simple.gpx"));
assert_eq!(engine.statistics().total_distance.len(), n);
}
#[test]
fn test_invalid_load_is_rejected() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(!edit(
&mut engine,
Command::Load(Load {
data: b"<gpx><trk></gpx>",
name: "file",
})
));
2026-10-14 08:17:51 +02:00
assert!(engine.history.current().is_none());
2026-10-16 08:42:45 +02:00
assert!(!engine.history.can_undo(&[]));
2026-10-08 21:04:29 +02:00
assert!(engine.order.0.is_empty());
}
#[test]
fn test_command_with_nothing_to_do_is_rejected() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "file");
assert!(!edit(&mut engine, Command::Reverse(crate::Reverse)));
2026-10-16 08:42:45 +02:00
assert!(!engine.history.can_redo(&[]));
2026-10-14 08:17:51 +02:00
assert_eq!(engine.history.current().unwrap().len(), 1);
2026-10-08 21:04:29 +02:00
}
#[test]
fn test_metadata_renames_selected_file() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "before");
assert!(edit(
&mut engine,
Command::Metadata(Metadata {
name: "after",
desc: "about",
})
));
2026-10-14 08:17:51 +02:00
let file = engine.history.current().unwrap().values().next().unwrap();
2026-10-08 21:04:29 +02:00
assert_eq!(file.info.name, "after");
assert_eq!(file.info.desc.as_deref(), Some("about"));
}
#[test]
fn test_metadata_without_selection_does_not_modify_files() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "file");
engine.selection = Selection::Empty;
edit(
&mut engine,
Command::Metadata(Metadata {
name: "renamed",
desc: "",
}),
);
2026-10-14 08:17:51 +02:00
let file = engine.history.current().unwrap().values().next().unwrap();
2026-10-08 21:04:29 +02:00
assert_eq!(file.info.name, "file");
}
#[test]
fn test_edit_changes_statistics_when_segments_change() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
load(&mut engine, "data/simple.gpx");
let before = engine
.statistics_cache
2026-10-14 08:17:51 +02:00
.get(engine.history.current(), &engine.selection)[0] as *const _;
2026-10-08 21:04:29 +02:00
edit(&mut engine, Command::NewTrack(NewTrack));
edit(
&mut engine,
Command::Style(Style {
color: Some("ff0000"),
opacity: None,
width: None,
}),
);
// styling does not touch the segments: their statistics are reused
let after = engine
.statistics_cache
2026-10-14 08:17:51 +02:00
.get(engine.history.current(), &engine.selection)[0] as *const _;
2026-10-08 21:04:29 +02:00
assert!(std::ptr::eq(before, after));
}
#[test]
fn test_undo_redo_keep_selection_and_order_consistent() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "a");
let a = selected(&engine)[0];
new(&mut engine, "b");
let b = selected(&engine)[0];
assert_eq!(engine.order.0, vec![a, b]);
assert!(engine.run(Action::Undo));
assert_eq!(engine.order.0, vec![a]);
assert!(selected(&engine).is_empty());
assert!(engine.run(Action::Redo));
assert_eq!(engine.order.0, vec![a, b]);
assert!(engine.run(Action::Undo));
assert!(engine.run(Action::Undo));
assert!(engine.order.0.is_empty());
assert!(!engine.run(Action::Undo));
2026-10-14 08:17:51 +02:00
assert!(engine.history.current().is_none());
2026-10-08 21:04:29 +02:00
}
#[test]
fn test_reorder_is_not_undoable_and_returning_files_go_last() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "a");
let a = engine.order.0[0];
new(&mut engine, "b");
let b = engine.order.0[1];
new(&mut engine, "c");
let c = engine.order.0[2];
assert!(engine.run(Action::Reorder {
file_ids: vec![c],
index: 0
}));
assert_eq!(engine.order.0, vec![c, a, b]);
assert!(!engine.run(Action::Reorder {
file_ids: vec![c],
index: 0
}));
// undoing the creation of c does not undo the reorder
assert!(engine.run(Action::Undo));
assert_eq!(engine.order.0, vec![a, b]);
// c comes back at the end
assert!(engine.run(Action::Redo));
assert_eq!(engine.order.0, vec![a, b, c]);
}
#[test]
fn test_structures_and_coordinates_follow_actions() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(engine.order().is_empty());
load(&mut engine, "data/simple.gpx");
let loaded_id = engine.order()[0];
let diff = engine.last_diff().unwrap();
assert_eq!(diff.added, vec![loaded_id]);
assert!(engine.order_changed());
new(&mut engine, "empty");
let empty_id = engine.order()[1];
// only the new file is reported, the other one is not recomputed
let diff = engine.last_diff().unwrap();
assert_eq!(diff.added, vec![empty_id]);
assert!(diff.modified.is_empty() && diff.removed.is_empty());
assert_eq!(engine.file_structure(&empty_id).unwrap().name, "empty");
let structure = engine.file_structure(&loaded_id).unwrap();
let seg = &structure.tracks[0].segments[0];
let (seg_id, len) = (seg.id, seg.len);
assert_eq!(engine.segment_coordinates(&seg_id).len(), len * 2);
assert!(engine.waypoint_coordinates(&loaded_id).is_empty());
// reordering only changes the order
assert!(engine.run(Action::Reorder {
file_ids: vec![loaded_id],
index: 1
}));
assert_eq!(engine.order(), [empty_id, loaded_id]);
assert!(engine.last_diff().is_none());
assert!(engine.order_changed());
// an action that changes nothing reports nothing
assert!(!engine.run(Action::Reorder {
file_ids: vec![loaded_id],
index: 1
}));
assert!(engine.last_diff().is_none());
assert!(!engine.order_changed());
// undo removes the empty file, redoing the load removes the other one too
assert!(engine.run(Action::Undo));
let diff = engine.last_diff().unwrap();
assert_eq!(diff.removed, vec![empty_id]);
assert!(engine.order_changed());
assert!(engine.file_structure(&empty_id).is_none());
assert!(engine.run(Action::Undo));
assert!(engine.order().is_empty());
assert!(engine.file_structure(&loaded_id).is_none());
assert!(engine.segment_coordinates(&seg_id).is_empty());
}
#[test]
fn test_selection_changed_flag() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "a");
let a = engine.order()[0];
// a new file is selected by the edit
assert!(engine.selection_changed());
assert!(!select_files(&mut engine, &[a]));
assert!(!engine.selection_changed());
assert!(select_files(&mut engine, &[]));
assert!(engine.selection_changed());
// undoing drops the selected file from the selection
select_files(&mut engine, &[a]);
assert!(engine.run(Action::Undo));
assert!(engine.selection_changed());
assert_eq!(engine.selection(), &Selection::Empty);
}
#[test]
fn test_undo_updates_statistics() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
load(&mut engine, "data/simple.gpx");
assert!(!engine.statistics().total_distance.is_empty());
engine.run(Action::Undo);
assert!(engine.statistics().total_distance.is_empty());
engine.run(Action::Redo);
select_all(&mut engine);
assert_eq!(engine.statistics().total_distance.len(), 80);
}
#[test]
fn test_selection_actions() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "a");
let a = selected(&engine)[0];
new(&mut engine, "b");
let b = selected(&engine)[0];
assert_eq!(selected(&engine), vec![b]);
assert!(select_files(&mut engine, &[a]));
assert_eq!(selected(&engine), vec![a]);
assert!(select_elements(&mut engine, files(&[b]), SelectMode::Add));
assert_eq!(selected(&engine).len(), 2);
select_files(&mut engine, &[]);
assert!(select_all(&mut engine));
assert_eq!(selected(&engine).len(), 2);
}
#[test]
fn test_selection_does_not_create_undo_steps() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "a");
select_all(&mut engine);
engine.run(Action::Undo);
2026-10-14 08:17:51 +02:00
assert!(engine.history.current().is_none());
2026-10-08 21:04:29 +02:00
}
2026-10-11 10:54:06 +02:00
fn select_elements(engine: &mut TestEngine, selection: Selection, mode: SelectMode) -> bool {
2026-10-08 21:04:29 +02:00
engine.run(Action::Select { selection, mode })
}
fn files(ids: &[FileId]) -> Selection {
Selection::File {
file_ids: ids.iter().copied().collect(),
}
}
2026-10-11 10:54:06 +02:00
fn select_files(engine: &mut TestEngine, ids: &[FileId]) -> bool {
2026-10-08 21:04:29 +02:00
select_elements(engine, files(ids), SelectMode::Replace)
}
2026-10-11 10:54:06 +02:00
fn select_all(engine: &mut TestEngine) -> bool {
2026-10-08 21:04:29 +02:00
let ids = engine.order().to_vec();
select_files(engine, &ids)
}
#[test]
fn test_select_elements() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
load(&mut engine, "data/with_tracks_and_segments.gpx");
let file = engine
2026-10-14 08:17:51 +02:00
.history
2026-10-08 21:04:29 +02:00
.current()
.unwrap()
.values()
.next()
.unwrap()
.clone();
assert!(file.trk.len() >= 2 && file.trk[0].trkseg.len() >= 2);
let (t0, t1) = (file.trk[0].id, file.trk[1].id);
let (s0, s1) = (file.trk[0].trkseg[0].id, file.trk[0].trkseg[1].id);
// tracks, then more tracks
let tracks = |ids: &[TrackId]| Selection::Track {
file_id: file.id,
trk_ids: ids.iter().copied().collect(),
};
assert!(select_elements(
&mut engine,
tracks(&[t0]),
SelectMode::Replace
));
assert_eq!(engine.selection(), &tracks(&[t0]));
assert!(select_elements(&mut engine, tracks(&[t1]), SelectMode::Add));
assert_eq!(engine.selection(), &tracks(&[t0, t1]));
// segments of a track, added only within the same track
let segments = |trk_id, ids: &[TrackSegmentId]| Selection::TrackSegment {
file_id: file.id,
trk_id,
trkseg_ids: ids.iter().copied().collect(),
};
assert!(select_elements(
&mut engine,
segments(t0, &[s0]),
SelectMode::Add
));
assert_eq!(engine.selection(), &segments(t0, &[s0]));
assert!(select_elements(
&mut engine,
segments(t0, &[s1]),
SelectMode::Add
));
assert_eq!(engine.selection(), &segments(t0, &[s0, s1]));
// the statistics follow the selected segments
assert_eq!(
engine.statistics().total_distance.len(),
file.trk[0].trkseg[0].len() + file.trk[0].trkseg[1].len()
);
// the waypoints node
let node = Selection::Waypoints { file_id: file.id };
assert!(select_elements(
&mut engine,
node.clone(),
SelectMode::Replace
));
assert!(!select_elements(&mut engine, node.clone(), SelectMode::Add));
assert_eq!(engine.selection(), &node);
assert!(!engine.selection_changed());
// unknown elements and files are ignored when adding
let unknown = tracks(&[TrackId::default()]);
assert!(!select_elements(
&mut engine,
unknown.clone(),
SelectMode::Add
));
let unknown_file = Selection::Waypoints {
file_id: FileId::default(),
};
assert!(!select_elements(
&mut engine,
unknown_file.clone(),
SelectMode::Add
));
assert!(!select_elements(
&mut engine,
Selection::Empty,
SelectMode::Add
));
assert_eq!(engine.selection(), &node);
// and select nothing otherwise
assert!(select_elements(&mut engine, unknown, SelectMode::Replace));
assert_eq!(engine.selection(), &Selection::Empty);
select_elements(&mut engine, node, SelectMode::Replace);
assert!(select_elements(
&mut engine,
unknown_file,
SelectMode::Replace
));
assert_eq!(engine.selection(), &Selection::Empty);
}
#[test]
fn test_toggle_elements() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
load(&mut engine, "data/with_tracks_and_segments.gpx");
let file = engine
2026-10-14 08:17:51 +02:00
.history
2026-10-08 21:04:29 +02:00
.current()
.unwrap()
.values()
.next()
.unwrap()
.clone();
let (t0, t1) = (file.trk[0].id, file.trk[1].id);
let tracks = |ids: &[TrackId]| Selection::Track {
file_id: file.id,
trk_ids: ids.iter().copied().collect(),
};
2026-10-11 10:54:06 +02:00
let toggle = |engine: &mut TestEngine, ids: &[TrackId]| {
2026-10-08 21:04:29 +02:00
select_elements(engine, tracks(ids), SelectMode::Toggle)
};
select_elements(&mut engine, tracks(&[t0]), SelectMode::Replace);
assert!(toggle(&mut engine, &[t1]));
assert_eq!(engine.selection(), &tracks(&[t0, t1]));
assert!(toggle(&mut engine, &[t0]));
assert_eq!(engine.selection(), &tracks(&[t1]));
// toggling the last element deselects everything
assert!(toggle(&mut engine, &[t1]));
assert_eq!(engine.selection(), &Selection::Empty);
// unknown elements change nothing
assert!(!toggle(&mut engine, &[TrackId::default()]));
assert_eq!(engine.selection(), &Selection::Empty);
}
#[test]
fn test_selected_elements_are_dropped_when_they_disappear() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
load(&mut engine, "data/with_tracks_and_segments.gpx");
let file = engine
2026-10-14 08:17:51 +02:00
.history
2026-10-08 21:04:29 +02:00
.current()
.unwrap()
.values()
.next()
.unwrap()
.clone();
let trk_id = file.trk[0].id;
select_elements(
&mut engine,
Selection::Track {
file_id: file.id,
trk_ids: [trk_id].into(),
},
SelectMode::Replace,
);
engine.run(Action::Undo);
assert_eq!(engine.selection(), &Selection::Empty);
}
#[test]
fn test_can_undo_and_redo() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(!engine.can_undo() && !engine.can_redo());
new(&mut engine, "a");
assert!(engine.can_undo() && !engine.can_redo());
assert!(engine.run(Action::Undo));
assert!(!engine.can_undo() && engine.can_redo());
// selecting does not change the history
new(&mut engine, "b");
let a = engine.order()[0];
select_files(&mut engine, &[a]);
assert!(engine.can_undo() && !engine.can_redo());
assert!(engine.run(Action::Undo));
assert!(engine.run(Action::Redo));
assert!(engine.can_undo() && !engine.can_redo());
}
#[test]
fn test_select_all_follows_the_selected_level() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "a");
new(&mut engine, "b");
let order = engine.order().to_vec();
select_files(&mut engine, &[order[0]]);
// files
assert!(engine.run(Action::SelectAll));
assert_eq!(engine.selection(), &files(&order));
// everything is selected already
assert!(!engine.run(Action::SelectAll));
// the tracks of a file
2026-10-11 10:54:06 +02:00
let load = |engine: &mut TestEngine| load(engine, "data/with_tracks_and_segments.gpx");
2026-10-08 21:04:29 +02:00
load(&mut engine);
let file = engine
2026-10-14 08:17:51 +02:00
.history
2026-10-08 21:04:29 +02:00
.current()
.unwrap()
.values()
.find(|f| !f.trk.is_empty())
.unwrap()
.clone();
let tracks: HashSet<_> = file.trk.iter().map(|t| t.id).collect();
assert!(tracks.len() >= 2);
select_elements(
&mut engine,
Selection::Track {
file_id: file.id,
trk_ids: [file.trk[0].id].into(),
},
SelectMode::Replace,
);
assert!(engine.run(Action::SelectAll));
assert_eq!(
engine.selection(),
&Selection::Track {
file_id: file.id,
trk_ids: tracks
}
);
// nothing selected: all the files
select_elements(&mut engine, Selection::Empty, SelectMode::Replace);
assert!(engine.run(Action::SelectAll));
assert!(matches!(engine.selection(), Selection::File { file_ids } if file_ids.len() == 3));
// the waypoints node: nothing more to select
select_elements(
&mut engine,
Selection::Waypoints { file_id: file.id },
SelectMode::Replace,
);
assert!(!engine.run(Action::SelectAll));
assert_eq!(
engine.selection(),
&Selection::Waypoints { file_id: file.id }
);
}
#[test]
fn test_arrow_select() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "file");
for _ in 0..3 {
edit(&mut engine, Command::NewTrack(NewTrack));
}
let file = engine
2026-10-14 08:17:51 +02:00
.history
2026-10-08 21:04:29 +02:00
.current()
.unwrap()
.values()
.next()
.unwrap()
.clone();
let ids: Vec<TrackId> = file.trk.iter().map(|t| t.id).collect();
assert_eq!(ids.len(), 3);
let tracks = |ids: &[TrackId]| Selection::Track {
file_id: file.id,
trk_ids: ids.iter().copied().collect(),
};
2026-10-11 10:54:06 +02:00
let arrow = |engine: &mut TestEngine, down, add| engine.run(Action::ArrowSelect { down, add });
2026-10-08 21:04:29 +02:00
select_elements(&mut engine, tracks(&[ids[0]]), SelectMode::Replace);
// without shift the next one replaces the selection
assert!(arrow(&mut engine, true, false));
assert_eq!(engine.selection(), &tracks(&[ids[1]]));
assert!(engine.selection_changed());
// with shift it is added
assert!(arrow(&mut engine, true, true));
assert_eq!(engine.selection(), &tracks(&[ids[1], ids[2]]));
// the selection grows from its end, and the other way round from its start
assert!(arrow(&mut engine, false, true));
assert_eq!(engine.selection(), &tracks(&[ids[0], ids[1], ids[2]]));
// at the ends nothing happens
assert!(!arrow(&mut engine, false, false));
assert!(!arrow(&mut engine, true, true));
assert_eq!(engine.selection(), &tracks(&[ids[0], ids[1], ids[2]]));
// and there is nothing to move without selection
select_elements(&mut engine, Selection::Empty, SelectMode::Replace);
assert!(!arrow(&mut engine, true, false));
assert_eq!(engine.selection(), &Selection::Empty);
}
#[test]
fn test_arrow_select_does_not_create_undo_steps() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "a");
new(&mut engine, "b");
let a = engine.order()[0];
select_files(&mut engine, &[a]);
let before = engine.can_redo();
assert!(engine.run(Action::ArrowSelect {
down: true,
add: false
}));
assert_eq!(engine.can_redo(), before);
assert_eq!(engine.selection(), &files(&[engine.order()[1]]));
assert!(engine.run(Action::Undo));
assert_eq!(engine.order().len(), 1);
}
/// A file named `name` with `n` tracks, selected.
2026-10-11 10:54:06 +02:00
fn file_with_tracks(engine: &mut TestEngine, name: &str, n: usize) -> FileId {
2026-10-08 21:04:29 +02:00
new(engine, name);
for _ in 0..n {
edit(engine, Command::NewTrack(NewTrack));
}
*engine.order().last().unwrap()
}
fn track_ids(engine: &Engine, id: FileId) -> Vec<TrackId> {
engine
.file_structure(&id)
.unwrap()
.tracks
.iter()
.map(|t| t.id)
.collect()
}
2026-10-11 10:54:06 +02:00
fn paste(engine: &mut TestEngine) -> bool {
2026-10-08 21:04:29 +02:00
edit(engine, Command::Paste(Paste))
}
#[test]
fn test_copy_and_paste() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
let a = file_with_tracks(&mut engine, "a", 2);
let b = file_with_tracks(&mut engine, "b", 0);
assert!(engine.clipboard().is_none() && !engine.can_paste());
// nothing selected, nothing copied
select_elements(&mut engine, Selection::Empty, SelectMode::Replace);
assert!(!engine.run(Action::Copy));
assert!(engine.clipboard().is_none());
let tracks = track_ids(&engine, a);
let selected_tracks = Selection::Track {
file_id: a,
trk_ids: tracks.iter().copied().collect(),
};
select_elements(&mut engine, selected_tracks.clone(), SelectMode::Replace);
assert!(engine.run(Action::Copy));
assert!(engine.clipboard_changed() && !engine.selection_changed());
assert_eq!(
engine.clipboard().map(|c| (c.content.ids(), c.cut)),
Some((ClipboardIds::Tracks(tracks.clone()), false))
);
// tracks cannot be pasted onto themselves... but into a track, a file or the list of files
assert!(engine.can_paste());
select_elements(&mut engine, files(&[b]), SelectMode::Replace);
assert!(engine.can_paste());
select_elements(
&mut engine,
Selection::Waypoints { file_id: b },
SelectMode::Replace,
);
assert!(!engine.can_paste());
select_elements(&mut engine, files(&[b]), SelectMode::Replace);
// pasting is an edit
assert!(paste(&mut engine));
assert_eq!(track_ids(&engine, a), tracks);
assert_eq!(track_ids(&engine, b).len(), 2);
// a copy stays in the clipboard, to be pasted again
assert!(engine.clipboard().is_some() && !engine.clipboard_changed());
assert!(
matches!(engine.selection(), Selection::Track { file_id, trk_ids } if *file_id == b && trk_ids.len() == 2)
);
assert!(engine.can_paste());
assert!(paste(&mut engine));
assert_eq!(track_ids(&engine, b).len(), 4);
assert_eq!(track_ids(&engine, a), tracks);
// and it can be undone
assert!(engine.run(Action::Undo));
assert_eq!(track_ids(&engine, b).len(), 2);
assert!(engine.run(Action::Undo));
assert!(track_ids(&engine, b).is_empty());
}
#[test]
fn test_cut_and_paste_moves_after_the_paste() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
let a = file_with_tracks(&mut engine, "a", 2);
let b = file_with_tracks(&mut engine, "b", 0);
let tracks = track_ids(&engine, a);
select_elements(
&mut engine,
Selection::Track {
file_id: a,
trk_ids: [tracks[0]].into(),
},
SelectMode::Replace,
);
assert!(engine.run(Action::Cut));
assert!(engine.clipboard().is_some_and(|c| c.cut));
// nothing moves before the paste
assert_eq!(track_ids(&engine, a), tracks);
select_elements(&mut engine, files(&[b]), SelectMode::Replace);
assert!(paste(&mut engine));
assert_eq!(track_ids(&engine, a), vec![tracks[1]]);
assert_eq!(track_ids(&engine, b), vec![tracks[0]]);
assert!(engine.run(Action::Undo));
assert_eq!(track_ids(&engine, a), tracks);
assert!(track_ids(&engine, b).is_empty());
}
#[test]
fn test_clipboard_survives_what_happens_to_the_elements() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
let a = file_with_tracks(&mut engine, "a", 2);
let tracks = track_ids(&engine, a);
select_elements(
&mut engine,
Selection::Track {
file_id: a,
trk_ids: tracks.iter().copied().collect(),
},
SelectMode::Replace,
);
engine.run(Action::Copy);
let clipboard = engine.clipboard().cloned();
assert!(clipboard.is_some());
// one of the tracks is deleted, then the whole file: the clipboard is not affected
select_elements(
&mut engine,
Selection::Track {
file_id: a,
trk_ids: [tracks[0]].into(),
},
SelectMode::Replace,
);
assert!(edit(
&mut engine,
Command::Delete(Delete { whole_files: false })
));
select_elements(&mut engine, files(&[a]), SelectMode::Replace);
assert!(edit(&mut engine, Command::DeleteAll(DeleteAll)));
assert_eq!(engine.clipboard(), clipboard.as_ref());
assert!(!engine.clipboard_changed());
// both tracks are pasted in a new file, as they were when they were copied
let b = file_with_tracks(&mut engine, "b", 0);
select_files(&mut engine, &[b]);
assert!(engine.can_paste());
assert!(paste(&mut engine));
let pasted = track_ids(&engine, b);
assert_eq!(pasted.len(), 2);
// copies, with new ids
assert!(pasted.iter().all(|id| !tracks.contains(id)));
// and again
assert!(paste(&mut engine));
assert_eq!(track_ids(&engine, b).len(), 4);
}
#[test]
fn test_copy_replaces_the_clipboard_and_does_not_change_the_history() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
let a = file_with_tracks(&mut engine, "a", 0);
let b = file_with_tracks(&mut engine, "b", 0);
select_files(&mut engine, &[a]);
engine.run(Action::Copy);
select_files(&mut engine, &[b]);
assert!(engine.run(Action::Cut));
assert_eq!(
engine.clipboard().map(|c| (c.content.ids(), c.cut)),
Some((ClipboardIds::Files(vec![b]), true))
);
// the same elements again are a new copy
assert!(engine.run(Action::Cut));
assert!(engine.clipboard_changed());
// only the creations of the files are in the history
assert!(engine.run(Action::Undo));
assert!(engine.run(Action::Undo));
assert!(!engine.can_undo());
}
#[test]
fn test_move_elements_is_an_edit_and_selects_what_moved() {
use crate::{Move, MoveTarget};
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
let a = file_with_tracks(&mut engine, "a", 3);
let tracks = track_ids(&engine, a);
let moved = Selection::Track {
file_id: a,
trk_ids: [tracks[2]].into(),
};
assert!(edit(
&mut engine,
Command::Move(Move {
what: moved.clone(),
to: MoveTarget::Tracks {
file_id: a,
index: 0
},
})
));
assert_eq!(track_ids(&engine, a), vec![tracks[2], tracks[0], tracks[1]]);
assert_eq!(engine.selection(), &moved);
// invalid moves change nothing
assert!(!edit(
&mut engine,
Command::Move(Move {
what: moved,
to: MoveTarget::Waypoints {
file_id: a,
index: 0
},
})
));
assert!(engine.run(Action::Undo));
assert_eq!(track_ids(&engine, a), tracks);
}
#[test]
fn test_added_files_are_reported_in_the_order_of_the_files() {
use crate::LoadFiles;
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
let datas: Vec<Vec<u8>> = (0..12)
.map(|i| {
format!(r#"<gpx version="1.1"><metadata><name>{i}</name></metadata></gpx>"#)
.into_bytes()
})
.collect();
let loads = datas
.iter()
.map(|data| Load { data, name: "x" })
.collect::<Vec<_>>();
assert!(edit(
&mut engine,
Command::LoadFiles(LoadFiles { files: loads })
));
assert_eq!(engine.last_diff().unwrap().added, engine.order());
// files that come back with an undo too
assert!(engine.run(Action::Undo));
assert!(engine.run(Action::Redo));
assert_eq!(engine.last_diff().unwrap().added, engine.order());
}
#[test]
fn test_load_several_files_is_one_edit() {
use crate::LoadFiles;
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "before");
let gpx = |name: &str| {
format!(r#"<gpx version="1.1"><metadata><name>{name}</name></metadata></gpx>"#).into_bytes()
};
let (a, b) = (gpx("a"), gpx("b"));
assert!(edit(
&mut engine,
Command::LoadFiles(LoadFiles {
files: vec![
Load {
data: &a,
name: "x"
},
Load {
data: &b,
name: "x"
},
],
})
));
assert_eq!(engine.order().len(), 3);
let diff = engine.last_diff().unwrap();
assert_eq!(diff.added.len(), 2);
// the first one that was loaded is selected
assert_eq!(engine.selection(), &files(&[engine.order()[1]]));
// a single undo removes both
assert!(engine.run(Action::Undo));
assert_eq!(engine.order().len(), 1);
assert!(engine.run(Action::Redo));
assert_eq!(engine.order().len(), 3);
}
#[test]
fn test_create_edit_and_move_a_waypoint() {
use crate::{EditWaypoint, MoveWaypoint, NewWaypoint};
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "file");
let file = engine.order()[0];
assert!(edit(
&mut engine,
Command::NewWaypoint(NewWaypoint {
lng: 4.0,
lat: 50.0,
ele: 10.0,
name: "created",
desc: "",
icon: "Flag",
link: "",
})
));
let id = engine.file_structure(&file).unwrap().waypoints[0].id;
let waypoint = engine.waypoint(&file, &id).unwrap();
assert_eq!(waypoint.name.as_deref(), Some("created"));
assert_eq!(waypoint.sym.as_deref(), Some("Flag"));
assert_eq!(waypoint.ele, 10.0);
// unknown waypoint or file
assert!(engine.waypoint(&file, &Default::default()).is_none());
assert!(engine.waypoint(&Default::default(), &id).is_none());
// no selection needed
assert!(edit(
&mut engine,
Command::MoveWaypoint(MoveWaypoint {
file_id: file,
waypoint_id: id,
lng: 5.0,
lat: 51.0,
ele: 20.0,
})
));
let moved = engine.waypoint(&file, &id).unwrap();
assert_eq!(
(moved.coordinates.lng, moved.coordinates.lat, moved.ele),
(5.0, 51.0, 20.0)
);
assert_eq!(moved.name.as_deref(), Some("created"));
// the coordinates buffer follows
assert_eq!(engine.waypoint_coordinates(&file), &[5.0, 51.0]);
assert!(edit(
&mut engine,
Command::EditWaypoint(EditWaypoint {
file_id: file,
waypoint_id: id,
lng: 6.0,
lat: 52.0,
ele: 30.0,
name: "edited",
desc: "text",
icon: "",
link: "https://example.com",
})
));
let edited = engine.waypoint(&file, &id).unwrap();
assert_eq!(edited.name.as_deref(), Some("edited"));
assert_eq!(edited.cmt.as_deref(), Some("text"));
assert!(edited.sym.is_none());
assert_eq!(engine.waypoint_coordinates(&file), &[6.0, 52.0]);
// each of them can be undone
assert!(engine.run(Action::Undo));
assert_eq!(
engine.waypoint(&file, &id).unwrap().name.as_deref(),
Some("created")
);
assert!(engine.run(Action::Undo));
assert_eq!(engine.waypoint_coordinates(&file), &[4.0, 50.0]);
// deleting does not need a selection either
assert!(edit(
&mut engine,
Command::DeleteWaypoint(crate::DeleteWaypoint {
file_id: file,
waypoint_id: id,
})
));
assert!(engine.waypoint(&file, &id).is_none());
assert!(engine.waypoint_coordinates(&file).is_empty());
assert!(engine.run(Action::Undo));
assert!(engine.waypoint(&file, &id).is_some());
}
#[test]
fn test_categories_are_shared_and_kept_by_the_history() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(engine.categories().surface.names().is_empty());
load(&mut engine, "data/with_highway.gpx");
assert_eq!(engine.categories().surface.names(), ["asphalt", "gravel"]);
assert_eq!(
engine.categories().highway.names(),
["residential", "track"]
);
// the trackpoints of the selection refer to them
assert_eq!(expand(&engine.statistics().surface, 5), [1, 1, 0, 2, 2]);
assert_eq!(expand(&engine.statistics().highway, 5), [1, 1, 0, 2, 0]);
assert_eq!(
engine.categories().sac_scale.names(),
["mountain_hiking", "hiking"]
);
assert_eq!(expand(&engine.statistics().sac_scale, 5), [0, 0, 0, 1, 2]);
assert_eq!(expand(&engine.statistics().mtb_scale, 5), [0, 0, 0, 1, 1]);
// a second file goes on with the same table
load(&mut engine, "data/with_surface.gpx");
assert_eq!(
engine.categories().surface.names(),
["asphalt", "gravel", "cobblestone"]
);
assert_eq!(engine.statistics().len(), 80);
let surface = expand(&engine.statistics().surface, 80);
assert_eq!(surface[0], 1);
assert_eq!(surface.iter().filter(|c| **c == 3).count(), 1);
// undoing does not forget what was learned: the codes stay valid in every state
assert!(engine.run(Action::Undo));
assert!(engine.run(Action::Undo));
assert_eq!(
engine.categories().surface.names(),
["asphalt", "gravel", "cobblestone"]
);
assert!(engine.run(Action::Redo));
let first = engine.order()[0];
select_files(&mut engine, &[first]);
assert_eq!(expand(&engine.statistics().surface, 5), [1, 1, 0, 2, 2]);
}
#[test]
fn test_trackpoint() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
load(&mut engine, "data/simple.gpx");
let file = engine.order()[0];
let structure = engine.file_structure(&file).unwrap();
let seg = &structure.tracks[0].segments[0];
let (seg_id, len) = (seg.id, seg.len);
let coordinates = engine.segment_coordinates(&seg_id).to_vec();
let last = engine.trackpoint(&file, &seg_id, len - 1).unwrap();
assert_eq!(
[last.coordinates.lng, last.coordinates.lat],
coordinates[(len - 1) * 2..]
);
assert!(engine.trackpoint(&file, &seg_id, len).is_none());
assert!(engine.trackpoint(&Default::default(), &seg_id, 0).is_none());
}
#[test]
fn test_export_writes_the_file_as_it_is() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(
engine
.export(&FileId::default(), ExportOptions::ALL)
.is_none()
);
load(&mut engine, "data/with_time.gpx");
let id = engine.order()[0];
let bytes = engine.export(&id, ExportOptions::ALL).unwrap();
let again = crate::parse(&bytes, &mut engine.categories().clone()).unwrap();
2026-10-14 08:17:51 +02:00
let file = engine.history.current().unwrap()[&id].clone();
2026-10-08 21:04:29 +02:00
assert_eq!(again.trk.len(), file.trk.len());
assert_eq!(again.trk[0].trkseg[0].len(), file.trk[0].trkseg[0].len());
assert!(again.trk[0].trkseg[0][0].time.is_some());
// the options leave data out
let bytes = engine.export(&id, ExportOptions::NONE).unwrap();
let again = crate::parse(&bytes, &mut engine.categories().clone()).unwrap();
assert!(again.trk[0].trkseg[0].iter().all(|p| p.time.is_none()));
// a file that is gone cannot be exported
engine.run(Action::Edit(Command::DeleteAll(DeleteAll)));
assert!(engine.export(&id, ExportOptions::ALL).is_none());
}
#[test]
fn test_exportable_data_is_the_union_over_the_files() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert_eq!(engine.exportable_data(&[]), ExportOptions::NONE);
load(&mut engine, "data/simple.gpx");
load(&mut engine, "data/with_time.gpx");
let (plain, timed) = (engine.order()[0], engine.order()[1]);
assert!(!engine.exportable_data(&[plain]).time);
assert!(engine.exportable_data(&[timed]).time);
assert!(engine.exportable_data(&[plain, timed]).time);
// unknown files have nothing
assert_eq!(
engine.exportable_data(&[FileId::default()]),
ExportOptions::NONE
);
assert!(!engine.exportable_data(&[plain, FileId::default()]).time);
}
#[test]
fn test_file_statistics_are_the_ones_of_the_whole_file() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(engine.file_statistics(&FileId::default()).is_none());
load(&mut engine, "data/simple.gpx");
let id = engine.order()[0];
let stats = engine.file_statistics(&id).unwrap();
assert!(stats.total_distance > 0.0);
let last = engine.statistics().total_distance.last().copied().unwrap();
assert!((stats.total_distance - last).abs() < 1e-9);
assert!(engine.file_statistics(&FileId::default()).is_none());
// statistics of a file do not depend on the selection
engine.run(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
});
assert_eq!(
engine.file_statistics(&id).unwrap().total_distance,
stats.total_distance
);
engine.run(Action::Undo);
assert!(engine.file_statistics(&id).is_none());
}
#[test]
fn test_reduction_distances_follow_the_selection() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(engine.reduction_distances().is_empty());
load(&mut engine, "data/simple.gpx");
let n = engine.statistics().total_distance.len();
let distances = engine.reduction_distances();
assert_eq!(distances.len(), n);
assert!(distances.iter().all(|d| !d.is_nan()));
engine.run(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
});
assert!(engine.reduction_distances().is_empty());
}
#[test]
fn test_routing_describes_the_anchors_of_the_selection() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(engine.routing().anchor_indices.is_empty());
load(&mut engine, "data/simple.gpx");
let n = engine.statistics().total_distance.len() as u32;
let routing = engine.routing();
assert_eq!(routing.segment_starts, vec![0]);
assert_eq!(routing.segment_ids.len(), 1);
// the ends of a segment are always anchors
assert_eq!(routing.anchor_indices.first(), Some(&0));
assert_eq!(routing.anchor_indices.last(), Some(&(n - 1)));
assert_eq!(routing.anchor_indices.len(), routing.anchor_zooms.len());
assert_eq!(routing.anchor_zooms[0], 0);
// the revision follows what the indices refer to
let revision = routing.revision;
engine.run(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
});
assert!(engine.routing().anchor_indices.is_empty());
assert_ne!(engine.routing().revision, revision);
// nothing changed for the routing: same revision
let revision = engine.routing().revision;
engine.run(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
});
assert_eq!(engine.routing().revision, revision);
}
#[test]
fn test_last_error_tells_why_an_edit_did_nothing() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
assert!(engine.last_error().is_none());
// invalid data
assert!(!edit(
&mut engine,
Command::Load(Load {
data: b"<gpx><trk></gpx>",
name: "file",
})
));
assert!(matches!(
engine.last_error(),
Some(CommandError::InvalidData(_))
));
// nothing to change
new(&mut engine, "file");
assert!(engine.last_error().is_none());
assert!(!edit(&mut engine, Command::Reverse(crate::Reverse)));
assert_eq!(engine.last_error(), Some(&CommandError::NothingToDo));
// the next action starts afresh, whatever it is
engine.run(Action::Undo);
assert!(engine.last_error().is_none());
assert!(!edit(&mut engine, Command::Reverse(crate::Reverse)));
assert!(engine.last_error().is_some());
new(&mut engine, "other");
assert!(engine.last_error().is_none());
}
#[test]
fn test_an_edit_that_changes_nothing_is_not_an_undo_step() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
new(&mut engine, "file");
// "file" is selected: naming it the same still builds a new file, but selecting nothing
// leaves nothing to rename
engine.run(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
});
assert!(!edit(
&mut engine,
Command::Metadata(Metadata {
name: "renamed",
desc: "",
})
));
assert_eq!(engine.last_error(), Some(&CommandError::NothingToDo));
assert!(engine.last_diff().is_none());
// one step to undo: the creation
assert!(engine.run(Action::Undo));
assert!(!engine.can_undo());
assert!(engine.order().is_empty());
}
#[test]
fn test_execute_returns_what_the_action_did() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-08 21:04:29 +02:00
let data = std::fs::read("data/simple.gpx").unwrap();
2026-10-11 10:48:43 +02:00
let outcome = engine
.execute(Action::Edit(Command::Load(Load {
data: &data,
name: "file",
})))
.unwrap();
assert!(outcome.changed);
2026-10-08 21:04:29 +02:00
assert_eq!(outcome.diff.as_ref().unwrap().added, engine.order());
assert!(outcome.order_changed && outcome.selection_changed);
assert!(!outcome.clipboard_changed);
// the outcome of an action does not depend on what is done after it
let kept = outcome.clone();
2026-10-11 10:48:43 +02:00
engine.execute(Action::Undo).unwrap();
2026-10-08 21:04:29 +02:00
assert_eq!(outcome, kept);
// a selection only changes the selection
2026-10-11 10:48:43 +02:00
engine.execute(Action::Redo).unwrap();
2026-10-11 10:54:06 +02:00
let selection = files(engine.order());
2026-10-11 10:48:43 +02:00
let outcome = engine
.execute(Action::Select {
2026-10-11 10:54:06 +02:00
selection,
2026-10-11 10:48:43 +02:00
mode: SelectMode::Replace,
})
.unwrap();
2026-10-08 21:04:29 +02:00
assert!(outcome.changed && outcome.selection_changed);
assert!(outcome.diff.is_none() && !outcome.order_changed);
// an edit with nothing to do says why
2026-10-11 10:48:43 +02:00
engine
.execute(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
})
.unwrap();
let result = engine.execute(Action::Edit(Command::Reverse(crate::Reverse)));
assert_eq!(result, Err(CommandError::NothingToDo));
2026-10-08 21:04:29 +02:00
// a failing one too
2026-10-11 10:48:43 +02:00
let result = engine.execute(Action::Edit(Command::Load(Load {
2026-10-08 21:04:29 +02:00
data: b"<gpx><trk></gpx>",
name: "file",
})));
2026-10-11 10:48:43 +02:00
assert!(matches!(result, Err(CommandError::InvalidData(_))));
2026-10-08 21:04:29 +02:00
}
2026-10-10 11:07:07 +02:00
// What the engine does when it is overwritten with what is stored
fn restored_from(engine: &Engine, keep: &[FileId], extra: Vec<Rc<File>>) -> crate::Restored {
let mut files: Vec<Rc<File>> = keep
.iter()
.map(|id| engine.snapshot().files[id].clone())
.collect();
files.extend(extra);
crate::Restored {
order: files.iter().map(|file| file.id).collect(),
files,
categories: engine.categories().clone(),
}
}
#[test]
fn test_overwriting_reports_what_was_added_removed_and_modified() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-10 11:07:07 +02:00
new(&mut engine, "kept");
new(&mut engine, "modified");
new(&mut engine, "removed");
let [kept, modified, removed] = engine.order()[..] else {
panic!()
};
let mut changed = (*engine.snapshot().files[&modified]).clone();
changed.info.name = "other name".into();
let added = File::default();
let added_id = added.id;
let restored = restored_from(&engine, &[kept], vec![Rc::new(changed), Rc::new(added)]);
let outcome = engine.restore(restored);
assert!(outcome.changed);
let diff = outcome.diff.unwrap();
assert_eq!(diff.added, vec![added_id]);
assert_eq!(diff.removed, vec![removed]);
assert_eq!(diff.modified, vec![modified]);
assert!(outcome.order_changed);
assert_eq!(engine.order(), &[kept, modified, added_id]);
}
#[test]
fn test_overwriting_with_the_same_files_changes_nothing() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-10 11:07:07 +02:00
new(&mut engine, "a");
let ids = engine.order().to_vec();
let outcome = engine.restore(restored_from(&engine, &ids, vec![]));
assert!(!outcome.changed);
assert!(outcome.diff.is_none());
}
#[test]
fn test_overwriting_forgets_the_history() {
2026-10-11 10:54:06 +02:00
let mut engine = TestEngine::default();
2026-10-10 11:07:07 +02:00
new(&mut engine, "a");
new(&mut engine, "b");
assert!(engine.can_undo());
let ids = engine.order().to_vec();
engine.restore(restored_from(&engine, &ids, vec![]));
assert!(!engine.can_undo());
}
2026-10-16 08:42:45 +02:00
mod per_file_history {
use super::*;
fn new_file(engine: &mut Engine<PerFileHistory>, name: &str) -> FileId {
engine
.execute(Action::Edit(Command::New(New {
name,
trackpoint: None,
})))
.unwrap();
*engine.order().last().unwrap()
}
fn select(engine: &mut Engine<PerFileHistory>, ids: &[FileId]) {
engine
.execute(Action::Select {
selection: Selection::File {
file_ids: ids.iter().copied().collect(),
},
mode: SelectMode::Replace,
})
.unwrap();
}
fn rename(engine: &mut Engine<PerFileHistory>, name: &str) {
engine
.execute(Action::Edit(Command::Metadata(Metadata { name, desc: "" })))
.unwrap();
}
fn name_of(engine: &Engine<PerFileHistory>, id: FileId) -> String {
engine.file_structure(&id).unwrap().name.clone()
}
#[test]
fn test_creating_a_file_is_not_undoable() {
let mut engine = Engine::<PerFileHistory>::default();
let id = new_file(&mut engine, "file");
assert!(!engine.can_undo());
assert!(!engine.execute(Action::Undo).unwrap().changed);
assert_eq!(engine.order(), [id]);
}
#[test]
fn test_undo_follows_the_selected_files() {
let mut engine = Engine::<PerFileHistory>::default();
let a = new_file(&mut engine, "a");
let b = new_file(&mut engine, "b");
select(&mut engine, &[a]);
rename(&mut engine, "a2");
select(&mut engine, &[b]);
rename(&mut engine, "b2");
// b is selected: only its change is undone
assert!(engine.can_undo());
let outcome = engine.execute(Action::Undo).unwrap();
assert_eq!(outcome.diff.unwrap().modified, vec![b]);
assert_eq!(name_of(&engine, b), "b");
assert_eq!(name_of(&engine, a), "a2");
assert!(!engine.can_undo());
assert!(engine.can_redo());
select(&mut engine, &[a]);
assert!(engine.can_undo());
assert!(!engine.can_redo());
engine.execute(Action::Undo).unwrap();
assert_eq!(name_of(&engine, a), "a");
select(&mut engine, &[b]);
engine.execute(Action::Redo).unwrap();
assert_eq!(name_of(&engine, b), "b2");
}
#[test]
fn test_undo_without_selection_does_nothing() {
let mut engine = Engine::<PerFileHistory>::default();
new_file(&mut engine, "a");
rename(&mut engine, "a2");
engine
.execute(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
})
.unwrap();
assert!(!engine.can_undo());
assert!(!engine.execute(Action::Undo).unwrap().changed);
}
#[test]
fn test_deleting_a_file_drops_its_history() {
let mut engine = Engine::<PerFileHistory>::default();
let id = new_file(&mut engine, "a");
rename(&mut engine, "a2");
engine
.execute(Action::Edit(Command::Delete(Delete { whole_files: false })))
.unwrap();
assert!(engine.order().is_empty());
assert!(!engine.execute(Action::Undo).unwrap().changed);
assert!(engine.file_structure(&id).is_none());
}
}