use std::path::Path;
use io_pimdir::{PimdirSourceStore, PimdirStore};
use io_replica::{
change::ReplicaWriteOp,
client::ReplicaStorage,
collection::ReplicaCollectionId,
hub::ReplicaSourceId,
object::{ReplicaHash, ReplicaObject},
placement::{
ReplicaBase, ReplicaFlags, ReplicaHandle, ReplicaLevel, ReplicaLinkId, ReplicaMeta,
ReplicaPlacement, ReplicaSortKey, ReplicaStatus,
},
};
const CONTACTS: &str = "contacts";
const LINK: &str = "uid:a";
fn store(dir: &Path, source: &str) -> PimdirSourceStore {
let store = PimdirStore::open(dir).unwrap().for_source(source);
store.ensure_collection(CONTACTS, "text/vcard").unwrap();
store
}
fn body(store: &PimdirSourceStore, bytes: &[u8]) -> ReplicaWriteOp {
ReplicaWriteOp::StoreObject {
object: ReplicaObject {
hash: store.hash(bytes),
size: bytes.len(),
},
body: Some(bytes.to_vec()),
}
}
fn card(
store: &PimdirSourceStore,
handle: &str,
object: &[u8],
base: Option<&[u8]>,
) -> ReplicaWriteOp {
ReplicaWriteOp::UpsertPlacement(ReplicaPlacement {
collection: ReplicaCollectionId(CONTACTS.into()),
handle: ReplicaHandle(handle.into()),
link_id: Some(ReplicaLinkId(LINK.into())),
object: Some(store.hash(object)),
level: ReplicaLevel::Full,
meta: Some(ReplicaMeta(r#"{"v":1}"#.into())),
sort_key: ReplicaSortKey("k".into()),
flags: ReplicaFlags::default(),
status: ReplicaStatus::Clean,
conflict_revision: None,
conflict_object: None,
base: base.map(|bytes| ReplicaBase {
flags: ReplicaFlags::default(),
revision: Some("r".into()),
object: Some(store.hash(bytes)),
}),
origin: None,
})
}
fn agreement(store: &PimdirSourceStore, source: &str) -> Option<ReplicaHash> {
store
.item_bindings(CONTACTS, LINK)
.unwrap()
.into_iter()
.find(|(id, _)| id == &ReplicaSourceId(source.into()))
.map(|(_, binding)| binding.shared_object)
.unwrap()
}
fn blob_of(dir: &Path, hash: &ReplicaHash) -> std::path::PathBuf {
dir.join("objects")
.join(&hash.0[0..2])
.join(&hash.0[2..4])
.join(&hash.0)
}
#[test]
fn a_body_named_only_by_an_agreement_point_is_collectable() {
let dir = tempfile::tempdir().unwrap();
let mut left = store(dir.path(), "left");
let mut right = store(dir.path(), "right");
left.write(vec![
body(&left, b"one"),
card(&left, "left-1.vcf", b"one", Some(b"one")),
])
.unwrap();
right
.write(vec![
body(&right, b"one"),
card(&right, "right-1.vcf", b"one", None),
])
.unwrap();
left.write(vec![
body(&left, b"two"),
card(&left, "left-1.vcf", b"two", Some(b"two")),
])
.unwrap();
let one = left.hash(b"one");
let two = left.hash(b"two");
assert_eq!(agreement(&right, "right"), Some(one.clone()));
assert_eq!(agreement(&left, "left"), Some(two.clone()));
assert!(
left.refcount_drift().unwrap().is_empty(),
"counting the agreement point would put the write path and the \
five-column recomputation at odds about this body"
);
assert!(
blob_of(dir.path(), &one).is_file(),
"still there, since no write reclaims (spec §5)"
);
let collected = left.collect_garbage().unwrap();
assert_eq!(
(collected.objects, collected.blobs),
(1, 1),
"the agreement point held no pin, so the body it names is taken"
);
assert!(!blob_of(dir.path(), &one).is_file(), "the body is gone");
assert!(
blob_of(dir.path(), &two).is_file(),
"the shared body is still referenced and untouched"
);
drop(left);
drop(right);
let reopened = store(dir.path(), "right");
assert_eq!(agreement(&reopened, "right"), Some(one));
assert!(reopened.refcount_drift().unwrap().is_empty());
}
#[test]
fn the_conflict_body_is_pinned_where_the_agreement_point_is_not() {
let dir = tempfile::tempdir().unwrap();
let mut left = store(dir.path(), "left");
left.write(vec![
body(&left, b"one"),
body(&left, b"remote"),
ReplicaWriteOp::UpsertPlacement(ReplicaPlacement {
collection: ReplicaCollectionId(CONTACTS.into()),
handle: ReplicaHandle("left-1.vcf".into()),
link_id: Some(ReplicaLinkId(LINK.into())),
object: Some(left.hash(b"one")),
level: ReplicaLevel::Full,
meta: Some(ReplicaMeta(r#"{"v":1}"#.into())),
sort_key: ReplicaSortKey("k".into()),
flags: ReplicaFlags::default(),
status: ReplicaStatus::Conflict,
conflict_revision: Some("r-remote".into()),
conflict_object: Some(left.hash(b"remote")),
base: Some(ReplicaBase {
flags: ReplicaFlags::default(),
revision: Some("r".into()),
object: Some(left.hash(b"one")),
}),
origin: None,
}),
])
.unwrap();
let remote = left.hash(b"remote");
assert!(left.refcount_drift().unwrap().is_empty());
let collected = left.collect_garbage().unwrap();
assert_eq!((collected.objects, collected.blobs), (0, 0));
assert_eq!(
std::fs::read(blob_of(dir.path(), &remote)).unwrap(),
b"remote",
"the diverging body is readable as bytes, not merely present"
);
}