Add tests, all passing

This commit is contained in:
Ellie Huxtable 2023-07-03 08:42:34 +01:00
parent f05fe5bd83
commit 73ad36bc15
6 changed files with 259 additions and 7 deletions

5
Cargo.lock generated
View File

@ -151,7 +151,12 @@ dependencies = [
"memchr", "memchr",
"minspan", "minspan",
"parse_duration", "parse_duration",
<<<<<<< HEAD
"rand 0.8.5", "rand 0.8.5",
=======
"pretty_assertions",
"rand",
>>>>>>> f788279b (Add tests, all passing)
"regex", "regex",
"reqwest", "reqwest",
"rmp", "rmp",

View File

@ -38,6 +38,7 @@ tokio = { version = "1", features = ["full"] }
uuid = { version = "1.3", features = ["v4", "serde"] } uuid = { version = "1.3", features = ["v4", "serde"] }
whoami = "1.1.2" whoami = "1.1.2"
typed-builder = "0.14.0" typed-builder = "0.14.0"
pretty_assertions = "1.3.0"
[workspace.dependencies.reqwest] [workspace.dependencies.reqwest]
version = "0.11" version = "0.11"

View File

@ -69,3 +69,4 @@ generic-array = { version = "0.14", optional = true, features = ["serde"] }
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
pretty_assertions = { workspace = true }

View File

@ -1,16 +1,20 @@
use atuin_common::record::RecordIndex;
// do a sync :O // do a sync :O
use eyre::Result; use eyre::Result;
use uuid::Uuid; use uuid::Uuid;
use super::store::Store;
use crate::{api_client::Client, settings::Settings}; use crate::{api_client::Client, settings::Settings};
use super::store::Store; use atuin_common::record::{Diff, RecordIndex};
pub async fn diff( #[derive(Debug, Eq, PartialEq)]
settings: &Settings, pub enum Operation {
store: &mut impl Store, // Either upload or download until the tail matches the below
) -> Result<Vec<(Uuid, String, Uuid)>> { Upload { tail: Uuid, host: Uuid, tag: String },
Download { tail: Uuid, host: Uuid, tag: String },
}
pub async fn diff(settings: &Settings, store: &mut impl Store) -> Result<Diff> {
let client = Client::new(&settings.sync_address, &settings.session_token)?; let client = Client::new(&settings.sync_address, &settings.session_token)?;
// First, build our own index // First, build our own index
@ -23,3 +27,234 @@ pub async fn diff(
Ok(diff) Ok(diff)
} }
// Take a diff, along with a local store, and resolve it into a set of operations.
// With the store as context, we can determine if a tail exists locally or not and therefore if it needs uploading or download.
// In theory this could be done as a part of the diffing stage, but it's easier to reason
// about and test this way
pub async fn operations(diff: Diff, store: &impl Store) -> Result<Vec<Operation>> {
let mut operations = Vec::with_capacity(diff.len());
for i in diff {
let (host, tag, tail) = i;
// First, try to fetch the tail
// If it exists locally, then that means we need to update the remote
// host until it has the same tail. Ie, upload.
// If it does not exist locally, that means remote is ahead of us.
// Therefore, we need to download until our local tail matches
let record = store.get(tail).await;
let op = if let Ok(_) = record {
// if local has the ID, then we should find the actual tail of this
// store, so we know what we need to update the remote to.
let tail = store
.last(host, tag.as_str())
.await?
.expect("failed to fetch last record, expected tag/host to exist");
// TODO(ellie) update the diffing so that it stores the context of the current tail
// that way, we can determine how much we need to upload.
// For now just keep uploading until tails match
Operation::Upload {
tail: tail.id,
host,
tag,
}
} else {
Operation::Download { tail, host, tag }
};
operations.push(op);
}
// sort them - purely so we have a stable testing order, and can rely on
// same input = same output
// We can sort by ID so long as we continue to use UUIDv7 or something
// with the same properties
operations.sort_by_key(|op| match op {
Operation::Upload { tail, host, .. } => ("upload", host.clone(), tail.clone()),
Operation::Download { tail, host, .. } => ("download", host.clone(), tail.clone()),
});
Ok(operations)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use atuin_common::record::{Diff, Record, RecordIndex};
use pretty_assertions::{assert_eq, assert_ne};
use uuid::Uuid;
use crate::record::{
sqlite_store::SqliteStore,
store::Store,
sync::{self, Operation},
};
fn test_record() -> Record {
Record::builder()
.host(atuin_common::utils::uuid_v7())
.version("v1".into())
.tag(atuin_common::utils::uuid_v7().simple().to_string())
.data(vec![0, 1, 2, 3])
.build()
}
// Take a list of local records, and a list of remote records.
// Return the local database, and a diff of local/remote, ready to build
// ops
async fn build_test_diff(
local_records: Vec<Record>,
remote_records: Vec<Record>,
) -> (SqliteStore, Diff) {
let local_store = SqliteStore::new(":memory:")
.await
.expect("failed to open in memory sqlite");
let remote_store = SqliteStore::new(":memory:")
.await
.expect("failed to open in memory sqlite"); // "remote"
for i in local_records {
local_store.push(&i).await.unwrap();
}
for i in remote_records {
remote_store.push(&i).await.unwrap();
}
let local_tails = local_store.tail_records().await.unwrap();
let local_index = RecordIndex::from(local_tails);
let remote_tails = remote_store.tail_records().await.unwrap();
let remote_index = RecordIndex::from(remote_tails);
let diff = local_index.diff(&remote_index);
(local_store, diff)
}
#[tokio::test]
async fn test_basic_diff() {
// a diff where local is ahead of remote. nothing else.
let record = test_record();
let (store, diff) = build_test_diff(vec![record.clone()], vec![]).await;
assert_eq!(diff.len(), 1);
let operations = sync::operations(diff, &store).await.unwrap();
assert_eq!(operations.len(), 1);
assert_eq!(
operations[0],
Operation::Upload {
host: record.host,
tag: record.tag,
tail: record.id
}
);
}
#[tokio::test]
async fn build_two_way_diff() {
// a diff where local is ahead of remote for one, and remote for
// another. One upload, one download
let shared_record = test_record();
let remote_ahead = test_record();
let local_ahead = shared_record.new_child(vec![1, 2, 3]);
let local = vec![shared_record.clone(), local_ahead.clone()]; // local knows about the already synced, and something newer in the same store
let remote = vec![shared_record.clone(), remote_ahead.clone()]; // remote knows about the already-synced, and one new record in a new store
let (store, diff) = build_test_diff(local, remote).await;
let operations = sync::operations(diff, &store).await.unwrap();
assert_eq!(operations.len(), 2);
assert_eq!(
operations,
vec![
Operation::Download {
tail: remote_ahead.id,
host: remote_ahead.host,
tag: remote_ahead.tag,
},
Operation::Upload {
tail: local_ahead.id,
host: local_ahead.host,
tag: local_ahead.tag,
},
]
);
}
#[tokio::test]
async fn build_complex_diff() {
// One shared, ahead but known only by remote
// One known only by local
// One known only by remote
let shared_record = test_record();
let remote_known = test_record();
let local_known = test_record();
let second_shared = test_record();
let second_shared_remote_ahead = second_shared.new_child(vec![1, 2, 3]);
let local_ahead = shared_record.new_child(vec![1, 2, 3]);
let local = vec![
shared_record.clone(),
second_shared.clone(),
local_known.clone(),
local_ahead.clone(),
];
let remote = vec![
shared_record.clone(),
second_shared.clone(),
second_shared_remote_ahead.clone(),
remote_known.clone(),
]; // remote knows about the already-synced, and one new record in a new store
let (store, diff) = build_test_diff(local, remote).await;
let operations = sync::operations(diff, &store).await.unwrap();
assert_eq!(operations.len(), 4);
assert_eq!(
operations,
vec![
Operation::Download {
tail: remote_known.id,
host: remote_known.host,
tag: remote_known.tag,
},
Operation::Download {
tail: second_shared_remote_ahead.id,
host: second_shared.host,
tag: second_shared.tag,
},
Operation::Upload {
tail: local_ahead.id,
host: local_ahead.host,
tag: local_ahead.tag,
},
Operation::Upload {
tail: local_known.id,
host: local_known.host,
tag: local_known.tag,
},
]
);
}
}

View File

@ -17,7 +17,13 @@ serde = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
typed-builder = { workspace = true } typed-builder = { workspace = true }
<<<<<<< HEAD
eyre = { workspace = true } eyre = { workspace = true }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"
=======
[dev-dependencies]
pretty_assertions = { workspace = true }
>>>>>>> f788279b (Add tests, all passing)

View File

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder; use typed_builder::TypedBuilder;
use uuid::Uuid; use uuid::Uuid;
<<<<<<< HEAD
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct DecryptedData(pub Vec<u8>); pub struct DecryptedData(pub Vec<u8>);
@ -13,6 +14,9 @@ pub struct EncryptedData {
pub data: String, pub data: String,
pub content_encryption_key: String, pub content_encryption_key: String,
} }
=======
pub type Diff = Vec<(Uuid, String, Uuid)>;
>>>>>>> f788279b (Add tests, all passing)
/// A single record stored inside of our local database /// A single record stored inside of our local database
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
@ -125,7 +129,7 @@ impl RecordIndex {
/// other machine has a different tail, it will be the differing tail. This is useful to /// other machine has a different tail, it will be the differing tail. This is useful to
/// check if the other index is ahead of us, or behind. /// check if the other index is ahead of us, or behind.
/// If the other index does not have the (host, tag) pair, then the other value will be None. /// If the other index does not have the (host, tag) pair, then the other value will be None.
pub fn diff(&self, other: &Self) -> Vec<(Uuid, String, Uuid)> { pub fn diff(&self, other: &Self) -> Diff {
let mut ret = Vec::new(); let mut ret = Vec::new();
// First, we check if other has everything that self has // First, we check if other has everything that self has