use std::collections::{BTreeMap, BTreeSet};
use ed25519_dalek::{SigningKey, VerifyingKey};
use crate::blob::encodings::simplearchive::SimpleArchive;
use crate::inline::encodings::hash::Handle;
use crate::inline::Inline;
use crate::repo::{BlobStoreGet, BlobStoreMeta, CapabilityProofRead, StoreSnapshot};
use super::discovery::CollectionDiscoveryError;
use super::records::CollectionCommit;
use super::{
Collection, CollectionData, CollectionHandle, CollectionRead, CollectionRecord, CollectionStore,
};
pub type CommitPair = (CollectionData, Inline<Handle<SimpleArchive>>);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Scope {
Admitted,
All,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourceSurvey {
handle: CollectionHandle,
admitted: BTreeSet<CommitPair>,
unadmitted: BTreeSet<CommitPair>,
policy_readable: bool,
authors: usize,
admitted_authors: usize,
invalid: usize,
other_records: usize,
}
impl SourceSurvey {
pub fn handle(&self) -> CollectionHandle {
self.handle
}
pub fn admitted(&self) -> &BTreeSet<CommitPair> {
&self.admitted
}
pub fn unadmitted(&self) -> &BTreeSet<CommitPair> {
&self.unadmitted
}
pub fn policy_readable(&self) -> bool {
self.policy_readable
}
pub fn authors(&self) -> usize {
self.authors
}
pub fn admitted_authors(&self) -> usize {
self.admitted_authors
}
pub fn invalid_signatures(&self) -> usize {
self.invalid
}
pub fn other_records(&self) -> usize {
self.other_records
}
pub fn pairs(&self) -> usize {
self.admitted.len() + self.unadmitted.len()
}
pub fn content(&self, scope: Scope) -> impl Iterator<Item = &CommitPair> {
let unadmitted = match scope {
Scope::Admitted => None,
Scope::All => Some(self.unadmitted.iter()),
};
self.admitted.iter().chain(unadmitted.into_iter().flatten())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MigrationSurvey {
target: CollectionHandle,
signer: Option<VerifyingKey>,
sources: Vec<SourceSurvey>,
target_content: BTreeSet<CommitPair>,
target_by_signer: BTreeSet<CommitPair>,
target_other_records: usize,
target_invalid: usize,
}
impl MigrationSurvey {
pub fn target(&self) -> CollectionHandle {
self.target
}
pub fn signer(&self) -> Option<VerifyingKey> {
self.signer
}
pub fn sources(&self) -> &[SourceSurvey] {
&self.sources
}
pub fn holds(&self, pair: &CommitPair) -> bool {
self.target_content.contains(pair)
}
pub fn target_pairs(&self) -> usize {
self.target_content.len()
}
pub fn target_pairs_by_signer(&self) -> usize {
self.target_by_signer.len()
}
pub fn target_other_records(&self) -> usize {
self.target_other_records
}
pub fn target_invalid_signatures(&self) -> usize {
self.target_invalid
}
pub fn carried(&self, scope: Scope) -> BTreeSet<CommitPair> {
self.sources
.iter()
.flat_map(|source| source.content(scope))
.copied()
.collect()
}
pub fn missing(&self, scope: Scope) -> BTreeSet<CommitPair> {
self.carried(scope)
.into_iter()
.filter(|pair| !self.target_content.contains(pair))
.collect()
}
pub fn net_new(&self, scope: Scope) -> BTreeSet<CommitPair> {
if self.signer.is_none() {
return BTreeSet::new();
}
self.carried(scope)
.into_iter()
.filter(|pair| !self.target_by_signer.contains(pair))
.collect()
}
pub fn redundant(&self, scope: Scope) -> BTreeSet<CommitPair> {
self.net_new(scope)
.into_iter()
.filter(|pair| self.target_content.contains(pair))
.collect()
}
pub fn unsourced(&self) -> BTreeSet<CommitPair> {
let carried = self.carried(Scope::All);
self.target_content
.iter()
.filter(|pair| !carried.contains(pair))
.copied()
.collect()
}
pub fn unadmitted(&self) -> BTreeSet<CommitPair> {
self.sources
.iter()
.flat_map(|source| source.unadmitted().iter())
.copied()
.collect()
}
}
pub type SurveyError<E> = CollectionDiscoveryError<E>;
pub fn survey<S>(
snapshot: &S,
sources: &[CollectionHandle],
target: CollectionHandle,
signer: Option<VerifyingKey>,
) -> Result<MigrationSurvey, SurveyError<S::RecordsError>>
where
S: CollectionRead + StoreSnapshot + BlobStoreGet + CapabilityProofRead,
{
let wanted: BTreeSet<CollectionHandle> = sources
.iter()
.copied()
.filter(|source| *source != target)
.collect();
let mut held: BTreeMap<CollectionHandle, BTreeMap<CommitPair, BTreeSet<[u8; 32]>>> =
BTreeMap::new();
let mut invalid: BTreeMap<CollectionHandle, usize> = BTreeMap::new();
let mut other: BTreeMap<CollectionHandle, usize> = BTreeMap::new();
let mut target_content = BTreeSet::new();
let mut target_by_signer = BTreeSet::new();
let mut target_other = 0usize;
let mut target_invalid = 0usize;
let signer_bytes = signer.map(|key| key.to_bytes());
let records = snapshot
.records()
.map_err(CollectionDiscoveryError::Records)?;
for record in records {
let record = record.map_err(CollectionDiscoveryError::Records)?;
let collection = record.collection();
let is_target = collection == target;
let is_source = wanted.contains(&collection);
if !is_target && !is_source {
continue;
}
let CollectionRecord::Commit(commit) = &record else {
if is_target {
target_other += 1;
} else {
*other.entry(collection).or_default() += 1;
}
continue;
};
if commit.verify_strict().is_err() {
*invalid.entry(collection).or_default() += 1;
if is_target {
target_invalid += 1;
}
continue;
}
let pair = (commit.data(), commit.metadata());
if is_target {
target_content.insert(pair);
if Some(commit.public_key().raw) == signer_bytes {
target_by_signer.insert(pair);
}
continue;
}
held.entry(collection)
.or_default()
.entry(pair)
.or_default()
.insert(commit.public_key().raw);
}
let mut surveyed = Vec::new();
for source in sources.iter().copied() {
if source == target {
continue;
}
let content = held.remove(&source).unwrap_or_default();
let opened = Collection::<SimpleArchive>::open(snapshot, source).ok();
let policy_readable = opened.is_some();
let authors: BTreeSet<[u8; 32]> = content.values().flatten().copied().collect();
let mut admitted_authors: BTreeSet<[u8; 32]> = BTreeSet::new();
if let Some(collection) = opened {
for raw in &authors {
let admitted = VerifyingKey::from_bytes(raw)
.ok()
.and_then(|key| collection.writer_is_admitted(snapshot, key).ok())
.unwrap_or(false);
if admitted {
admitted_authors.insert(*raw);
}
}
}
let mut admitted = BTreeSet::new();
let mut unadmitted = BTreeSet::new();
for (pair, asserted_by) in content {
if asserted_by.iter().any(|raw| admitted_authors.contains(raw)) {
admitted.insert(pair);
} else {
unadmitted.insert(pair);
}
}
surveyed.push(SourceSurvey {
handle: source,
admitted,
unadmitted,
policy_readable,
authors: authors.len(),
admitted_authors: admitted_authors.len(),
invalid: invalid.remove(&source).unwrap_or_default(),
other_records: other.remove(&source).unwrap_or_default(),
});
}
Ok(MigrationSurvey {
target,
signer,
sources: surveyed,
target_content,
target_by_signer,
target_other_records: target_other,
target_invalid,
})
}
pub fn carry<S>(
store: &mut S,
target: CollectionHandle,
signer: &SigningKey,
pairs: &BTreeSet<CommitPair>,
) -> Result<usize, S::InsertError>
where
S: CollectionStore,
{
for (data, metadata) in pairs {
let commit = CollectionCommit::sign(signer, target, *data, *metadata);
store.insert(CollectionRecord::Commit(commit))?;
}
Ok(pairs.len())
}
pub fn unresident<S>(snapshot: &S, pairs: &BTreeSet<CommitPair>) -> Result<usize, S::MetaError>
where
S: BlobStoreMeta,
{
let mut missing = 0;
for (data, metadata) in pairs {
let data: Inline<Handle<SimpleArchive>> = Inline::new(data.raw);
if snapshot.metadata(data)?.is_none() || snapshot.metadata(*metadata)?.is_none() {
missing += 1;
}
}
Ok(missing)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collection::policy::{AdmissionPolicy, CollectionPolicy};
use crate::collection::records::{CollectionCommit, CollectionMerge};
use crate::collection::{grant_collection_write, CollectionStoreExt};
use crate::id::Id;
use crate::metadata;
use crate::prelude::entity;
use crate::repo::memoryrepo::MemoryRepo;
use crate::repo::SnapshotSource;
use crate::trible::Fragment;
fn signer(byte: u8) -> SigningKey {
SigningKey::from_bytes(&[byte; 32])
}
fn private(key: &SigningKey) -> CollectionPolicy {
CollectionPolicy::new(
AdmissionPolicy::direct(key.verifying_key()),
AdmissionPolicy::direct(key.verifying_key()),
)
}
fn row(tag: u8) -> Fragment {
entity! { metadata::tag: Id::new([tag; 16]).unwrap() }
}
fn record_count(store: &mut MemoryRepo) -> usize {
store
.snapshot()
.unwrap()
.records()
.unwrap()
.filter_map(Result::ok)
.count()
}
struct Cutover {
store: MemoryRepo,
old: Collection<SimpleArchive>,
new: Collection<SimpleArchive>,
old_key: SigningKey,
new_key: SigningKey,
}
fn cutover(rows: &[u8]) -> Cutover {
let old_key = signer(1);
let new_key = signer(2);
let mut store = MemoryRepo::default();
let old = store.collection("wiki", private(&old_key)).unwrap();
let new = store.collection("wiki", private(&new_key)).unwrap();
assert_ne!(old.handle(), new.handle(), "the re-mint under one name");
for tag in rows {
store.commit(old, &old_key, row(*tag)).unwrap();
}
Cutover {
store,
old,
new,
old_key,
new_key,
}
}
#[test]
fn a_carry_moves_content_and_then_reconciles_complete() {
let Cutover {
mut store,
old,
new,
new_key,
..
} = cutover(&[0x11, 0x22, 0x33]);
let snapshot = store.snapshot().unwrap();
let before = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
assert_eq!(before.sources()[0].admitted().len(), 3);
assert_eq!(before.missing(Scope::Admitted).len(), 3);
assert_eq!(before.net_new(Scope::Admitted).len(), 3);
assert_eq!(before.redundant(Scope::Admitted).len(), 0);
let appended = carry(
&mut store,
new.handle(),
&new_key,
&before.net_new(Scope::Admitted),
)
.unwrap();
assert_eq!(appended, 3);
let snapshot = store.snapshot().unwrap();
let after = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
assert_eq!(
after.missing(Scope::Admitted).len(),
0,
"nothing left behind"
);
assert_eq!(after.target_pairs(), 3);
assert_eq!(
super::super::generation::unreached_records(&snapshot, old.handle(), new.handle())
.unwrap(),
0
);
}
#[test]
fn the_net_new_figure_is_exactly_what_a_carry_appends() {
let Cutover {
mut store,
old,
new,
new_key,
..
} = cutover(&[0x11, 0x22, 0x33, 0x44]);
store.commit(new, &new_key, row(0x11)).unwrap();
let snapshot = store.snapshot().unwrap();
let predicted = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap()
.net_new(Scope::Admitted);
assert_eq!(predicted.len(), 3, "four pairs, one already committed here");
let before = record_count(&mut store);
carry(&mut store, new.handle(), &new_key, &predicted).unwrap();
assert_eq!(record_count(&mut store) - before, predicted.len());
}
#[test]
fn a_second_migration_appends_nothing() {
let Cutover {
mut store,
old,
new,
new_key,
..
} = cutover(&[0x11, 0x22, 0x33]);
let snapshot = store.snapshot().unwrap();
let first = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
carry(
&mut store,
new.handle(),
&new_key,
&first.net_new(Scope::Admitted),
)
.unwrap();
let settled = record_count(&mut store);
let snapshot = store.snapshot().unwrap();
let second = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
assert_eq!(
second.net_new(Scope::Admitted).len(),
0,
"a repeated migration has nothing to do"
);
carry(
&mut store,
new.handle(),
&new_key,
&second.carried(Scope::Admitted),
)
.unwrap();
assert_eq!(record_count(&mut store), settled);
}
#[test]
fn a_deliberately_incomplete_migration_is_detected_and_named() {
let Cutover {
mut store,
old,
new,
new_key,
..
} = cutover(&[0x11, 0x22, 0x33, 0x44, 0x55]);
let snapshot = store.snapshot().unwrap();
let full = survey(&snapshot, &[old.handle()], new.handle(), None).unwrap();
let everything = full.carried(Scope::Admitted);
assert_eq!(everything.len(), 5);
let partial: BTreeSet<CommitPair> = everything.iter().take(3).copied().collect();
let withheld: BTreeSet<CommitPair> = everything.difference(&partial).copied().collect();
carry(&mut store, new.handle(), &new_key, &partial).unwrap();
let snapshot = store.snapshot().unwrap();
let check = survey(&snapshot, &[old.handle()], new.handle(), None).unwrap();
let missing = check.missing(Scope::Admitted);
assert_eq!(missing.len(), 2, "the shortfall is counted");
assert_eq!(missing, withheld, "and it names exactly what is absent");
assert_eq!(check.sources()[0].admitted().len(), 5);
assert_eq!(check.target_pairs(), 3);
}
#[test]
fn the_wrong_key_appends_everything_and_gains_nothing_extra() {
let Cutover {
mut store,
old,
new,
new_key,
..
} = cutover(&[0x11, 0x22, 0x33, 0x44]);
for tag in [0x11, 0x22, 0x33] {
store.commit(new, &new_key, row(tag)).unwrap();
}
let stranger = signer(9);
let snapshot = store.snapshot().unwrap();
let right = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
let wrong = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(stranger.verifying_key()),
)
.unwrap();
assert_eq!(right.net_new(Scope::Admitted).len(), 1);
assert_eq!(right.redundant(Scope::Admitted).len(), 0);
assert_eq!(wrong.net_new(Scope::Admitted).len(), 4);
assert_eq!(
wrong.redundant(Scope::Admitted).len(),
3,
"three writes that add no content the target lacks"
);
assert_eq!(
right.missing(Scope::Admitted),
wrong.missing(Scope::Admitted)
);
let before = record_count(&mut store);
carry(
&mut store,
new.handle(),
&stranger,
&wrong.net_new(Scope::Admitted),
)
.unwrap();
assert_eq!(record_count(&mut store) - before, 4);
}
#[test]
fn content_the_source_never_admitted_is_separated_from_content_it_did() {
let Cutover {
mut store,
old,
new,
old_key,
new_key,
} = cutover(&[0x11, 0x22]);
let intruder = signer(7);
store.commit(old, &intruder, row(0x99)).unwrap();
assert!(!old
.writer_is_admitted(&store.snapshot().unwrap(), intruder.verifying_key())
.unwrap());
let snapshot = store.snapshot().unwrap();
let view = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
let source = &view.sources()[0];
assert_eq!(source.admitted().len(), 2);
assert_eq!(source.unadmitted().len(), 1);
assert_eq!(source.authors(), 2);
assert_eq!(source.admitted_authors(), 1);
assert!(source.policy_readable());
assert_eq!(view.net_new(Scope::Admitted).len(), 2, "the move");
assert_eq!(view.net_new(Scope::All).len(), 3, "the promotion");
grant_collection_write(&mut store, old.handle(), &old_key, intruder.verifying_key())
.unwrap();
let snapshot = store.snapshot().unwrap();
let view = survey(&snapshot, &[old.handle()], new.handle(), None).unwrap();
assert_eq!(view.sources()[0].admitted().len(), 3);
assert_eq!(view.sources()[0].unadmitted().len(), 0);
}
#[test]
fn every_author_is_decided_not_only_the_first_admitted_one() {
let Cutover {
mut store,
old,
new,
old_key,
new_key,
} = cutover(&[0x11]);
let intruder = signer(7);
store.commit(old, &intruder, row(0x11)).unwrap();
let snapshot = store.snapshot().unwrap();
let view = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
let source = &view.sources()[0];
assert_eq!(source.pairs(), 1, "one payload, two assertions");
assert_eq!(source.admitted().len(), 1, "an admitted author asserted it");
assert_eq!(source.authors(), 2, "and the excluded one is still counted");
assert_eq!(source.admitted_authors(), 1);
let _ = old_key;
}
#[test]
fn a_source_whose_policy_cannot_be_read_is_reported_not_refused() {
let Cutover {
mut store,
old,
new,
old_key,
new_key,
} = cutover(&[0x11]);
let phantom: CollectionHandle = Inline::new([0x5A; 32]);
let orphan = CollectionCommit::sign(
&old_key,
phantom,
crate::inline::Inline::new([0x11; 32]),
crate::inline::Inline::new([0x00; 32]),
);
store.insert(CollectionRecord::Commit(orphan)).unwrap();
let snapshot = store.snapshot().unwrap();
let view = survey(
&snapshot,
&[old.handle(), phantom],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
let unreadable = &view.sources()[1];
assert!(!unreadable.policy_readable());
assert_eq!(unreadable.admitted().len(), 0);
assert_eq!(unreadable.unadmitted().len(), 1);
assert_eq!(view.sources()[0].admitted().len(), 1);
assert_eq!(view.net_new(Scope::Admitted).len(), 1);
}
#[test]
fn a_record_kind_the_carry_does_not_model_is_skipped_and_counted() {
let Cutover {
mut store,
old,
new,
old_key,
new_key,
} = cutover(&[0x11, 0x22]);
let commits: Vec<CollectionCommit> = store
.snapshot()
.unwrap()
.records()
.unwrap()
.filter_map(Result::ok)
.filter_map(|record| match record {
CollectionRecord::Commit(commit) if commit.collection() == old.handle() => {
Some(commit)
}
_ => None,
})
.collect();
assert_eq!(commits.len(), 2);
let merge = CollectionMerge::sign(
&old_key,
old.handle(),
[commits[0].data(), commits[1].data()],
crate::inline::Inline::new([0xEE; 32]),
)
.unwrap();
store.insert(CollectionRecord::Merge(merge)).unwrap();
let snapshot = store.snapshot().unwrap();
let view = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
assert_eq!(view.sources()[0].other_records(), 1, "seen and set aside");
assert_eq!(view.sources()[0].pairs(), 2, "and not counted as content");
assert_eq!(view.net_new(Scope::Admitted).len(), 2);
}
#[test]
fn an_unverifiable_commit_is_skipped_rather_than_carried() {
let Cutover {
mut store,
old,
new,
new_key,
..
} = cutover(&[0x11]);
let genuine = CollectionCommit::sign(
&signer(3),
old.handle(),
Inline::new([0xAB; 32]),
Inline::new([0x00; 32]),
);
let (r, s) = genuine.signature();
let mut broken = s.raw;
broken[0] ^= 0x01;
let forged = CollectionCommit::from_parts(
genuine.collection(),
genuine.data(),
genuine.metadata(),
genuine.public_key(),
r,
Inline::new(broken),
);
store.insert(CollectionRecord::Commit(forged)).unwrap();
let snapshot = store.snapshot().unwrap();
let view = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
assert_eq!(view.sources()[0].invalid_signatures(), 1);
assert_eq!(view.sources()[0].pairs(), 1, "only the genuine row");
assert_eq!(view.net_new(Scope::Admitted).len(), 1);
}
#[test]
fn an_unverifiable_commit_in_the_target_leaves_its_content_missing() {
let Cutover {
mut store,
old,
new,
new_key,
..
} = cutover(&[0x11]);
let pair = {
let snapshot = store.snapshot().unwrap();
survey(&snapshot, &[old.handle()], new.handle(), None)
.unwrap()
.carried(Scope::Admitted)
.into_iter()
.next()
.unwrap()
};
let genuine = CollectionCommit::sign(&new_key, new.handle(), pair.0, pair.1);
let (r, s) = genuine.signature();
let mut broken = s.raw;
broken[0] ^= 0x01;
store
.insert(CollectionRecord::Commit(CollectionCommit::from_parts(
genuine.collection(),
genuine.data(),
genuine.metadata(),
genuine.public_key(),
r,
Inline::new(broken),
)))
.unwrap();
let snapshot = store.snapshot().unwrap();
let view = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
assert_eq!(view.target_invalid_signatures(), 1);
assert_eq!(view.target_pairs(), 0, "an invalid record asserts nothing");
assert_eq!(view.missing(Scope::Admitted).len(), 1);
assert_eq!(view.net_new(Scope::Admitted).len(), 1);
}
#[test]
fn many_sources_collapse_into_one_target() {
let a_key = signer(1);
let b_key = signer(2);
let live_key = signer(3);
let mut store = MemoryRepo::default();
let a = store.collection("wiki", private(&a_key)).unwrap();
let b = store.collection("wiki", private(&b_key)).unwrap();
let live = store.collection("wiki", private(&live_key)).unwrap();
store.commit(a, &a_key, row(0x11)).unwrap();
store.commit(a, &a_key, row(0x22)).unwrap();
store.commit(b, &b_key, row(0x22)).unwrap();
store.commit(b, &b_key, row(0x33)).unwrap();
let snapshot = store.snapshot().unwrap();
let view = survey(
&snapshot,
&[a.handle(), b.handle()],
live.handle(),
Some(live_key.verifying_key()),
)
.unwrap();
assert_eq!(view.sources().len(), 2);
assert_eq!(view.carried(Scope::Admitted).len(), 3);
assert_eq!(view.net_new(Scope::Admitted).len(), 3);
carry(
&mut store,
live.handle(),
&live_key,
&view.net_new(Scope::Admitted),
)
.unwrap();
let snapshot = store.snapshot().unwrap();
let after = survey(
&snapshot,
&[a.handle(), b.handle()],
live.handle(),
Some(live_key.verifying_key()),
)
.unwrap();
assert_eq!(after.missing(Scope::Admitted).len(), 0);
for source in [a.handle(), b.handle()] {
assert_eq!(
super::super::generation::unreached_records(&snapshot, source, live.handle())
.unwrap(),
0
);
}
}
#[test]
fn content_no_source_holds_is_reported_in_the_other_direction() {
let a_key = signer(1);
let b_key = signer(2);
let live_key = signer(3);
let mut store = MemoryRepo::default();
let a = store.collection("wiki", private(&a_key)).unwrap();
let b = store.collection("wiki", private(&b_key)).unwrap();
let live = store.collection("wiki", private(&live_key)).unwrap();
store.commit(a, &a_key, row(0x11)).unwrap();
store.commit(b, &b_key, row(0x22)).unwrap();
for tag in [0x11, 0x22, 0x33] {
store.commit(live, &live_key, row(tag)).unwrap();
}
let snapshot = store.snapshot().unwrap();
let both = survey(&snapshot, &[a.handle(), b.handle()], live.handle(), None).unwrap();
assert_eq!(both.unsourced().len(), 1, "only the introduced row");
let alone = survey(&snapshot, &[a.handle()], live.handle(), None).unwrap();
assert_eq!(alone.unsourced().len(), 2);
}
#[test]
fn a_source_equal_to_the_target_contributes_nothing() {
let Cutover {
mut store,
new,
new_key,
..
} = cutover(&[0x11]);
let snapshot = store.snapshot().unwrap();
let view = survey(
&snapshot,
&[new.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
assert!(view.sources().is_empty());
assert_eq!(view.net_new(Scope::All).len(), 0);
}
#[test]
fn a_commit_naming_absent_content_is_carried_and_counted() {
let Cutover {
mut store,
old,
new,
old_key,
new_key,
} = cutover(&[0x11]);
let absent = CollectionCommit::sign(
&old_key,
old.handle(),
Inline::new([0xC0; 32]),
Inline::new([0xC1; 32]),
);
store.insert(CollectionRecord::Commit(absent)).unwrap();
let snapshot = store.snapshot().unwrap();
let view = survey(
&snapshot,
&[old.handle()],
new.handle(),
Some(new_key.verifying_key()),
)
.unwrap();
let pairs = view.net_new(Scope::Admitted);
assert_eq!(pairs.len(), 2);
assert_eq!(
unresident(&snapshot, &pairs).unwrap(),
1,
"one of them names content this store does not hold"
);
carry(&mut store, new.handle(), &new_key, &pairs).unwrap();
let snapshot = store.snapshot().unwrap();
assert_eq!(
survey(&snapshot, &[old.handle()], new.handle(), None)
.unwrap()
.missing(Scope::Admitted)
.len(),
0
);
}
}