mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-16 08:19:59 +00:00
cross-tab sync
This commit is contained in:
1 parent
77929cfa04
commit
a2b4d89150
9 files changed
+537
-71
No files matched your search
@@ -129,32 +129,40 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts files in an engine that has none, typically the ones read from the storage. They are
|
||||
/// the state to start from: they cannot be undone. The files are reported as added, and the
|
||||
/// order, the selection and the clipboard as changed.
|
||||
/// Replaces what the engine holds by the files read from the storage: at the start, or when
|
||||
/// another writer (a tab) committed since, which makes what was held here out of date. What
|
||||
/// was here is gone, edits that were not saved included, and the files are the state to start
|
||||
/// from: they cannot be undone, and nothing from before can be.
|
||||
///
|
||||
/// The selection keeps what still exists, and so does the clipboard. The files that differ
|
||||
/// from the ones that were here are reported as added, removed or modified (all the files
|
||||
/// that were here, as they are not the same objects), and the order as changed if it is.
|
||||
pub fn restore(&mut self, restored: Restored) -> Outcome {
|
||||
let selection_before = self.selection.clone();
|
||||
let order_before = self.order.0.clone();
|
||||
let before = self.stack.current().cloned();
|
||||
let files: StackEntry = restored
|
||||
.files
|
||||
.into_iter()
|
||||
.map(|file| (file.id, file))
|
||||
.collect();
|
||||
let diff = Stack::diff_between(before.as_ref(), Some(&files));
|
||||
let mut diff =
|
||||
(!(diff.added.is_empty() && diff.removed.is_empty() && diff.modified.is_empty()))
|
||||
.then_some(diff);
|
||||
self.categories = restored.categories;
|
||||
self.order = FileOrder(restored.order);
|
||||
self.selection = Selection::Empty;
|
||||
self.clipboard = None;
|
||||
let mut diff = Some(Diff {
|
||||
added: files.keys().copied().collect(),
|
||||
..Default::default()
|
||||
});
|
||||
self.stack = Stack::restored(files);
|
||||
self.refresh(diff.as_ref());
|
||||
sort_diff(&mut diff, &self.order.0);
|
||||
let outcome = Outcome {
|
||||
changed: true,
|
||||
changed: diff.is_some()
|
||||
|| self.order.0 != order_before
|
||||
|| self.selection != selection_before,
|
||||
diff,
|
||||
order_changed: true,
|
||||
selection_changed: true,
|
||||
clipboard_changed: true,
|
||||
order_changed: self.order.0 != order_before,
|
||||
selection_changed: self.selection != selection_before,
|
||||
clipboard_changed: false,
|
||||
error: None,
|
||||
};
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -120,8 +120,14 @@ impl Stack {
|
||||
if prev == cur {
|
||||
return None;
|
||||
}
|
||||
let prev = prev.map_or_default(|i| self.get(i));
|
||||
let cur = cur.map_or_default(|i| self.get(i));
|
||||
Some(Self::diff_between(
|
||||
prev.map_or_default(|i| self.get(i)),
|
||||
cur.map_or_default(|i| self.get(i)),
|
||||
))
|
||||
}
|
||||
|
||||
/// Which files differ between two states.
|
||||
pub fn diff_between(prev: Option<&StackEntry>, cur: Option<&StackEntry>) -> Diff {
|
||||
let prev_ids: HashSet<FileId> = prev.map_or_default(|e| e.keys().copied().collect());
|
||||
let cur_ids: HashSet<FileId> = cur.map_or_default(|e| e.keys().copied().collect());
|
||||
let mut modified = vec![];
|
||||
@@ -132,11 +138,11 @@ impl Stack {
|
||||
modified.push(*id);
|
||||
}
|
||||
}
|
||||
Some(Diff {
|
||||
Diff {
|
||||
added: cur_ids.difference(&prev_ids).copied().collect(),
|
||||
removed: prev_ids.difference(&cur_ids).copied().collect(),
|
||||
modified,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::collections::HashSet;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
ClipboardIds, Delete, DeleteAll, Load, Metadata, New, NewTrack, Paste, Style, TrackId,
|
||||
ClipboardIds, Delete, DeleteAll, File, Load, Metadata, New, NewTrack, Paste, Style, TrackId,
|
||||
TrackSegmentId,
|
||||
};
|
||||
|
||||
@@ -1308,3 +1309,66 @@ fn test_execute_returns_what_the_action_did() {
|
||||
assert!(!outcome.changed);
|
||||
assert!(matches!(outcome.error, Some(CommandError::InvalidData(_))));
|
||||
}
|
||||
|
||||
// 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(),
|
||||
unreadable: 0,
|
||||
read_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overwriting_reports_what_was_added_removed_and_modified() {
|
||||
let mut engine = Engine::default();
|
||||
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() {
|
||||
let mut engine = Engine::default();
|
||||
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() {
|
||||
let mut engine = Engine::default();
|
||||
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());
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Batch, ChunkKey, ChunkKind, FileId, Storage, StorageError, StoredData};
|
||||
use crate::{
|
||||
Batch, ChunkKey, ChunkKind, CommitError, FileId, Revision, Storage, StorageError, StoredData,
|
||||
};
|
||||
|
||||
/// A storage that lives as long as the session. It is what an engine without a host storage uses,
|
||||
/// and what the tests use.
|
||||
@@ -12,6 +14,7 @@ pub struct MemoryStorage {
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct State {
|
||||
head: Revision,
|
||||
files: HashMap<FileId, Vec<u8>>,
|
||||
chunks: HashMap<ChunkKey, Vec<u8>>,
|
||||
order: Option<Vec<u8>>,
|
||||
@@ -46,11 +49,19 @@ impl Storage for MemoryStorage {
|
||||
files: state.files.iter().map(|(k, v)| (*k, v.clone())).collect(),
|
||||
chunks: state.chunks.iter().map(|(k, v)| (*k, v.clone())).collect(),
|
||||
order: state.order.clone(),
|
||||
head: state.head,
|
||||
})
|
||||
}
|
||||
|
||||
async fn commit(&self, batch: &Batch) -> Result<(), StorageError> {
|
||||
async fn head(&self) -> Result<Revision, StorageError> {
|
||||
Ok(self.state.borrow().head)
|
||||
}
|
||||
|
||||
async fn commit(&self, batch: &Batch) -> Result<Revision, CommitError> {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if state.head != batch.expect_head {
|
||||
return Err(CommitError::Stale { head: state.head });
|
||||
}
|
||||
state.files.extend(batch.put_files.iter().cloned());
|
||||
for id in &batch.delete_files {
|
||||
state.files.remove(id);
|
||||
@@ -62,7 +73,8 @@ impl Storage for MemoryStorage {
|
||||
if let Some(order) = &batch.order {
|
||||
state.order = Some(order.clone());
|
||||
}
|
||||
Ok(())
|
||||
state.head = state.head.next();
|
||||
Ok(state.head)
|
||||
}
|
||||
|
||||
async fn load_settings(&self) -> Result<Vec<(String, String)>, StorageError> {
|
||||
@@ -135,6 +147,7 @@ mod tests {
|
||||
(ChunkKey::waypoints(w), vec![4]),
|
||||
],
|
||||
order: Some(vec![5]),
|
||||
expect_head: Revision(0),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
@@ -158,6 +171,7 @@ mod tests {
|
||||
put_files: vec![(a, vec![10])],
|
||||
delete_files: vec![b],
|
||||
delete_chunks: vec![ChunkKey::trackpoints(t)],
|
||||
expect_head: Revision(1),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
@@ -169,6 +183,7 @@ mod tests {
|
||||
block_on(storage.commit(&Batch {
|
||||
delete_chunks: vec![ChunkKey::waypoints(w)],
|
||||
order: Some(vec![]),
|
||||
expect_head: Revision(2),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
@@ -204,4 +219,50 @@ mod tests {
|
||||
block_on(storage.put_setting("a", "1")).unwrap();
|
||||
assert_eq!(block_on(storage.load()).unwrap(), StoredData::default());
|
||||
}
|
||||
|
||||
fn batch(expect_head: u64, entry: u8) -> Batch {
|
||||
Batch {
|
||||
put_files: vec![(FileId::default(), vec![entry])],
|
||||
expect_head: Revision(expect_head),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_every_commit_advances_the_head() {
|
||||
let storage = MemoryStorage::default();
|
||||
assert_eq!(block_on(storage.head()).unwrap(), Revision(0));
|
||||
assert_eq!(block_on(storage.load()).unwrap().head, Revision(0));
|
||||
assert_eq!(block_on(storage.commit(&batch(0, 1))), Ok(Revision(1)));
|
||||
assert_eq!(block_on(storage.commit(&batch(1, 2))), Ok(Revision(2)));
|
||||
assert_eq!(block_on(storage.head()).unwrap(), Revision(2));
|
||||
assert_eq!(block_on(storage.load()).unwrap().head, Revision(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_commit_based_on_another_head_changes_nothing() {
|
||||
let storage = MemoryStorage::default();
|
||||
block_on(storage.commit(&batch(0, 1))).unwrap();
|
||||
block_on(storage.commit(&batch(1, 2))).unwrap();
|
||||
let before = block_on(storage.load()).unwrap();
|
||||
|
||||
// behind, or ahead: the head is not the one it is based on
|
||||
for expect in [0, 1, 3] {
|
||||
let stale = Batch {
|
||||
delete_files: vec![before.files[0].0],
|
||||
order: Some(vec![9]),
|
||||
..batch(expect, 3)
|
||||
};
|
||||
assert_eq!(
|
||||
block_on(storage.commit(&stale)),
|
||||
Err(CommitError::Stale { head: Revision(2) })
|
||||
);
|
||||
}
|
||||
let after = block_on(storage.load()).unwrap();
|
||||
assert_eq!(after.head, Revision(2));
|
||||
assert_eq!(after.files.len(), before.files.len());
|
||||
assert_eq!(after.order, before.order);
|
||||
// and it can be done again from the right head
|
||||
assert_eq!(block_on(storage.commit(&batch(2, 3))), Ok(Revision(3)));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,12 @@
|
||||
//! that lists the chunks it is made of. An edit only writes the new chunks and the records of the
|
||||
//! files that changed; the chunks that nothing refers to anymore are deleted at the same time.
|
||||
//!
|
||||
//! Several engines can write to a storage (tabs, a server). It has a head, a revision that each
|
||||
//! commit advances, and a commit only applies if it is based on the current head: an engine that
|
||||
//! is behind is refused, and has to read the storage again, which replaces what it holds, before
|
||||
//! it writes. So what an engine knows of the storage is exact whenever it writes, which is what
|
||||
//! lets it write only what changed and delete what it does not use anymore.
|
||||
//!
|
||||
//! The surface, highway and scales of the trackpoints are stored as names, in the chunk that has
|
||||
//! them: the codes that the engine gives them (see [`crate::TrackpointCategories`]) depend on the
|
||||
//! order in which it learned the names, so they mean nothing outside of it. A storage that two
|
||||
@@ -35,6 +41,44 @@ impl std::fmt::Display for StorageError {
|
||||
|
||||
impl std::error::Error for StorageError {}
|
||||
|
||||
/// A state of the storage: the head is the revision of the last commit, 0 for a storage that was
|
||||
/// never written to.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Revision(pub u64);
|
||||
|
||||
impl Revision {
|
||||
pub fn next(self) -> Self {
|
||||
Self(self.0 + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a commit did not happen.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CommitError {
|
||||
/// The head is not the one the commit is based on: another engine wrote since.
|
||||
Stale {
|
||||
head: Revision,
|
||||
},
|
||||
Failed(StorageError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CommitError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Stale { head } => write!(f, "the storage changed (now at revision {})", head.0),
|
||||
Self::Failed(error) => error.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CommitError {}
|
||||
|
||||
impl From<StorageError> for CommitError {
|
||||
fn from(error: StorageError) -> Self {
|
||||
Self::Failed(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// What a chunk holds.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ChunkKind {
|
||||
@@ -73,6 +117,8 @@ pub struct StoredData {
|
||||
pub chunks: Vec<(ChunkKey, Vec<u8>)>,
|
||||
/// The order of the files.
|
||||
pub order: Option<Vec<u8>>,
|
||||
/// The revision of the storage that this is the state of.
|
||||
pub head: Revision,
|
||||
}
|
||||
|
||||
/// A change of the stored data, to be applied all at once: a stored state is always one that the
|
||||
@@ -84,6 +130,8 @@ pub struct Batch {
|
||||
pub put_chunks: Vec<(ChunkKey, Vec<u8>)>,
|
||||
pub delete_chunks: Vec<ChunkKey>,
|
||||
pub order: Option<Vec<u8>>,
|
||||
/// The head that the batch is based on: it is only applied if the head still is that one.
|
||||
pub expect_head: Revision,
|
||||
}
|
||||
|
||||
impl Batch {
|
||||
@@ -118,8 +166,12 @@ pub trait Storage {
|
||||
/// Reads the files.
|
||||
async fn load(&self) -> Result<StoredData, StorageError>;
|
||||
|
||||
/// Applies a batch: either all of it is stored, or none.
|
||||
async fn commit(&self, batch: &Batch) -> Result<(), StorageError>;
|
||||
/// The head of the storage.
|
||||
async fn head(&self) -> Result<Revision, StorageError>;
|
||||
|
||||
/// Applies a batch if the head is the one it is based on (`Stale` otherwise): either all of it
|
||||
/// is stored, or none. The head advances. Gives the new head.
|
||||
async fn commit(&self, batch: &Batch) -> Result<Revision, CommitError>;
|
||||
|
||||
/// Reads the settings, as `(key, value)`. The engine does not look into them.
|
||||
async fn load_settings(&self) -> Result<Vec<(String, String)>, StorageError>;
|
||||
|
||||
@@ -9,8 +9,8 @@ use super::codec::{
|
||||
encode_order, encode_trackpoints, encode_waypoints,
|
||||
};
|
||||
use crate::{
|
||||
Batch, ChunkKey, File, FileId, StackEntry, Storage, StorageError, TrackpointCategories,
|
||||
TrackpointChunk, WaypointChunk,
|
||||
Batch, ChunkKey, CommitError, File, FileId, Revision, StackEntry, Storage, StorageError,
|
||||
TrackpointCategories, TrackpointChunk, WaypointChunk,
|
||||
};
|
||||
|
||||
/// What there is to keep of the engine: the files of the current state, their order, and the
|
||||
@@ -50,6 +50,8 @@ pub struct Persister {
|
||||
stored_files: HashSet<FileId>,
|
||||
chunks: HashSet<ChunkKey>,
|
||||
order: Option<Vec<FileId>>,
|
||||
/// The head of the storage that all of this is the state of.
|
||||
head: Revision,
|
||||
/// Set by `restore`, see [`Restored::read_only`].
|
||||
read_only: bool,
|
||||
}
|
||||
@@ -156,26 +158,37 @@ impl Persister {
|
||||
self.chunks = chunk_bytes.into_keys().collect();
|
||||
self.order = stored_order;
|
||||
restored.categories = categories.into_inner();
|
||||
self.head = data.head;
|
||||
self.read_only = newer.0.get();
|
||||
restored.read_only = self.read_only;
|
||||
Ok(restored)
|
||||
}
|
||||
|
||||
/// The revision of the storage that the persister knows.
|
||||
pub fn head(&self) -> Revision {
|
||||
self.head
|
||||
}
|
||||
|
||||
/// Writes what changed since the last save. Nothing is written if nothing changed.
|
||||
///
|
||||
/// The write only happens if nothing else was committed since the persister read or wrote
|
||||
/// last: otherwise it is refused (`Stale`), nothing is written, and the persister has to read
|
||||
/// the storage again ([`Persister::restore`]) before it can write.
|
||||
pub async fn save<S: Storage>(
|
||||
&mut self,
|
||||
storage: &S,
|
||||
snapshot: Snapshot,
|
||||
) -> Result<(), StorageError> {
|
||||
) -> Result<(), CommitError> {
|
||||
if self.read_only {
|
||||
return Ok(());
|
||||
}
|
||||
let plan = self.plan(snapshot);
|
||||
let mut plan = self.plan(snapshot);
|
||||
if plan.batch.is_empty() {
|
||||
self.files = plan.snapshot.files;
|
||||
return Ok(());
|
||||
}
|
||||
storage.commit(&plan.batch).await?;
|
||||
plan.batch.expect_head = self.head;
|
||||
self.head = storage.commit(&plan.batch).await?;
|
||||
|
||||
self.stored_files = plan.snapshot.files.keys().copied().collect();
|
||||
self.files = plan.snapshot.files;
|
||||
@@ -184,6 +197,13 @@ impl Persister {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether something else was committed to the storage since the persister read or wrote it
|
||||
/// last: what it knows of the storage is out of date, and it cannot write until it reads
|
||||
/// again ([`Persister::restore`]).
|
||||
pub async fn is_behind<S: Storage>(&self, storage: &S) -> Result<bool, StorageError> {
|
||||
Ok(storage.head().await? != self.head)
|
||||
}
|
||||
|
||||
fn plan(&self, snapshot: Snapshot) -> Plan {
|
||||
let mut batch = Batch::default();
|
||||
// the chunks that the files use, which the storage has afterwards
|
||||
@@ -268,9 +288,14 @@ mod tests {
|
||||
self.inner.load().await
|
||||
}
|
||||
|
||||
async fn commit(&self, batch: &Batch) -> Result<(), StorageError> {
|
||||
async fn head(&self) -> Result<Revision, StorageError> {
|
||||
self.inner.head().await
|
||||
}
|
||||
|
||||
async fn commit(&self, batch: &Batch) -> Result<Revision, CommitError> {
|
||||
let revision = self.inner.commit(batch).await?;
|
||||
self.batches.borrow_mut().push(batch.clone());
|
||||
self.inner.commit(batch).await
|
||||
Ok(revision)
|
||||
}
|
||||
|
||||
async fn load_settings(&self) -> Result<Vec<(String, String)>, StorageError> {
|
||||
@@ -441,6 +466,7 @@ mod tests {
|
||||
let broken = snapshot.order[0];
|
||||
block_on(storage.inner.commit(&Batch {
|
||||
put_files: vec![(broken, vec![super::super::codec::LATEST, 0xff, 0xff])],
|
||||
expect_head: persister.head(),
|
||||
..Default::default()
|
||||
}))
|
||||
.unwrap();
|
||||
@@ -467,6 +493,7 @@ mod tests {
|
||||
block_on(Persister::default().save(&storage, snapshot.clone())).unwrap();
|
||||
let mut batch = Batch::default();
|
||||
damage(&snapshot, &mut batch);
|
||||
batch.expect_head = block_on(storage.inner.head()).unwrap();
|
||||
block_on(storage.inner.commit(&batch)).unwrap();
|
||||
storage.batches.borrow_mut().clear();
|
||||
(storage, snapshot)
|
||||
@@ -527,33 +554,130 @@ mod tests {
|
||||
assert_eq!(clean.inner.counts().files, FILES.len());
|
||||
}
|
||||
|
||||
/// Opens a storage as a tab does: the persister, and the state of the engine it gives.
|
||||
fn open(storage: &Recording) -> (Persister, Snapshot) {
|
||||
let mut persister = Persister::default();
|
||||
let restored = block_on(persister.restore(storage)).unwrap();
|
||||
let snapshot = Snapshot {
|
||||
files: restored.files.iter().map(|f| (f.id, f.clone())).collect(),
|
||||
order: restored.order,
|
||||
categories: restored.categories,
|
||||
};
|
||||
(persister, snapshot)
|
||||
}
|
||||
|
||||
/// Adds a file that has just been read to the state of an engine.
|
||||
fn add_file(snapshot: &mut Snapshot, path: &str) -> FileId {
|
||||
let data = std::fs::read(path).unwrap();
|
||||
let file = parse(&data, &mut snapshot.categories).unwrap();
|
||||
let id = file.id;
|
||||
snapshot.order.push(id);
|
||||
snapshot.files.insert(id, Rc::new(file));
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_files_saved_by_engines_with_other_categories_read_the_same() {
|
||||
// two engines that share a storage (two tabs) learn the names in their own order
|
||||
// a second engine opens the storage, so its categories come from what it read, in the
|
||||
// order it read them; it adds a file that has names of its own
|
||||
let storage = Recording::default();
|
||||
let mut first = snapshot_of(&["data/with_surface.gpx", "data/with_highway.gpx"]);
|
||||
let mut second = snapshot_of(&["data/with_highway.gpx", "data/with_surface.gpx"]);
|
||||
assert_ne!(first.categories, second.categories);
|
||||
for snapshot in [&first, &second] {
|
||||
block_on(Persister::default().save(&storage, snapshot.clone())).unwrap();
|
||||
}
|
||||
let first = snapshot_of(&["data/with_surface.gpx", "data/with_highway.gpx"]);
|
||||
block_on(Persister::default().save(&storage, first.clone())).unwrap();
|
||||
|
||||
let mut persister = Persister::default();
|
||||
let restored = block_on(persister.restore(&storage)).unwrap();
|
||||
assert_eq!(restored.unreadable, 0);
|
||||
assert_eq!(restored.files.len(), 4);
|
||||
// every file means what it did for the engine that saved it
|
||||
first.files.extend(second.files.drain());
|
||||
for file in &restored.files {
|
||||
let original = &first.files[&file.id];
|
||||
let in_first = first.order.contains(&file.id);
|
||||
let categories = if in_first {
|
||||
&first.categories
|
||||
} else {
|
||||
&second.categories
|
||||
let (mut persister, mut second) = open(&storage);
|
||||
let added = add_file(&mut second, "data/with_surface.gpx");
|
||||
block_on(persister.save(&storage, second.clone())).unwrap();
|
||||
|
||||
// the first engine reads it all again, and every file means what it did for its engine
|
||||
let (_, reader) = open(&storage);
|
||||
assert_eq!(reader.files.len(), 3);
|
||||
for (id, file) in &reader.files {
|
||||
let (original, categories) = match first.files.get(id) {
|
||||
Some(original) => (original, &first.categories),
|
||||
None => (&second.files[id], &second.categories),
|
||||
};
|
||||
assert_eq!(gpx(file, &restored.categories), gpx(original, categories));
|
||||
assert_eq!(gpx(file, &reader.categories), gpx(original, categories));
|
||||
}
|
||||
assert!(reader.files.contains_key(&added));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_head_follows_the_commits_of_the_persister() {
|
||||
let storage = Recording::default();
|
||||
let mut persister = Persister::default();
|
||||
assert_eq!(persister.head(), Revision(0));
|
||||
|
||||
let mut snapshot = snapshot_of(&FILES);
|
||||
block_on(persister.save(&storage, snapshot.clone())).unwrap();
|
||||
assert_eq!(persister.head(), Revision(1));
|
||||
assert_eq!(block_on(storage.head()).unwrap(), Revision(1));
|
||||
|
||||
// nothing to write is no commit
|
||||
block_on(persister.save(&storage, snapshot.clone())).unwrap();
|
||||
assert_eq!(persister.head(), Revision(1));
|
||||
assert_eq!(storage.batches.borrow().len(), 1);
|
||||
|
||||
let id = snapshot.order[0];
|
||||
let mut file = (*snapshot.files[&id]).clone();
|
||||
file.info.name = "renamed".into();
|
||||
snapshot.files.insert(id, Rc::new(file));
|
||||
block_on(persister.save(&storage, snapshot)).unwrap();
|
||||
assert_eq!(persister.head(), Revision(2));
|
||||
// a persister that opens the storage starts from its head
|
||||
assert_eq!(open(&storage).0.head(), Revision(2));
|
||||
assert_eq!(storage.batches.borrow()[1].expect_head, Revision(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_tab_that_is_behind_cannot_write() {
|
||||
// two tabs open the same file; one edits a point, the other renames the file
|
||||
let storage = Recording::default();
|
||||
block_on(Persister::default().save(&storage, snapshot_of(&["data/simple.gpx"]))).unwrap();
|
||||
let ((mut a, mut snap_a), (mut b, mut snap_b)) = (open(&storage), open(&storage));
|
||||
let id = snap_a.order[0];
|
||||
|
||||
let mut file = (*snap_a.files[&id]).clone();
|
||||
file.trk[0].trkseg[0].update(0, |p| p.ele += 100.0);
|
||||
snap_a.files.insert(id, Rc::new(file));
|
||||
block_on(a.save(&storage, snap_a.clone())).unwrap();
|
||||
let stored = block_on(storage.load()).unwrap();
|
||||
|
||||
let mut file = (*snap_b.files[&id]).clone();
|
||||
file.info.name = "renamed by B".into();
|
||||
snap_b.files.insert(id, Rc::new(file));
|
||||
assert_eq!(
|
||||
block_on(b.save(&storage, snap_b)),
|
||||
Err(CommitError::Stale { head: Revision(2) })
|
||||
);
|
||||
// nothing was written, the file is still readable, and the persister did not move
|
||||
assert_eq!(block_on(storage.load()).unwrap(), stored);
|
||||
assert_eq!(b.head(), Revision(1));
|
||||
let (_, reader) = open(&storage);
|
||||
assert_eq!(reader.files.len(), 1);
|
||||
assert_eq!(reader.files[&id].info.name, snap_a.files[&id].info.name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_tab_that_is_behind_reads_the_storage_again_and_writes_again() {
|
||||
let storage = Recording::default();
|
||||
block_on(Persister::default().save(&storage, snapshot_of(&["data/simple.gpx"]))).unwrap();
|
||||
let ((mut a, mut snap_a), (mut b, snap_b)) = (open(&storage), open(&storage));
|
||||
let id = snap_a.order[0];
|
||||
assert!(!block_on(b.is_behind(&storage)).unwrap());
|
||||
|
||||
let mut file = (*snap_a.files[&id]).clone();
|
||||
file.info.name = "renamed by A".into();
|
||||
snap_a.files.insert(id, Rc::new(file));
|
||||
block_on(a.save(&storage, snap_a)).unwrap();
|
||||
assert!(block_on(b.is_behind(&storage)).unwrap());
|
||||
assert!(!block_on(a.is_behind(&storage)).unwrap());
|
||||
|
||||
let restored = block_on(b.restore(&storage)).unwrap();
|
||||
assert_eq!(restored.files[0].info.name, "renamed by A");
|
||||
assert!(!block_on(b.is_behind(&storage)).unwrap());
|
||||
let mut snapshot = snap_b;
|
||||
snapshot.files.insert(id, restored.files[0].clone());
|
||||
assert!(block_on(b.save(&storage, snapshot)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user