#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)]
use prikk_error::Result;
use prikk_object::ObjectId;
use super::{
DEFAULT_SYNC_SUMMARY_MAX_REF_COUNT, DEFAULT_SYNC_SUMMARY_MAX_TOTAL_BYTES,
SyncRefComparisonState, SyncSummaryRefEntry, build_sync_summary, compare_sync_summary,
decode_sync_summary,
};
use crate::patch_set_digest::{PatchSetDigest, compute_patch_set_digest};
use crate::sync_negotiation::sync_test_support::{
cleanup, fresh_repo, publish_branch, publish_received, publish_tag,
};
const REF_COUNT: usize = DEFAULT_SYNC_SUMMARY_MAX_REF_COUNT;
const TOTAL_BYTES: usize = DEFAULT_SYNC_SUMMARY_MAX_TOTAL_BYTES;
#[test]
fn an_empty_repository_encodes_a_summary_declaring_zero_refs() -> Result<()> {
let layout = fresh_repo("sync-summary-empty")?;
let bytes = build_sync_summary(&layout)?;
let entries = decode_sync_summary(&bytes, TOTAL_BYTES, REF_COUNT)?;
assert!(entries.is_empty());
cleanup(&layout);
Ok(())
}
#[test]
fn a_summary_round_trips_one_branchs_digest_and_count() -> Result<()> {
let layout = fresh_repo("sync-summary-round-trip")?;
let p1 = ObjectId::from_bytes([0x11; 32]);
let p2 = ObjectId::from_bytes([0x12; 32]);
publish_branch(&layout, "heads/main", vec![p1, p2])?;
let bytes = build_sync_summary(&layout)?;
let entries = decode_sync_summary(&bytes, TOTAL_BYTES, REF_COUNT)?;
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].ref_name, "heads/main");
assert_eq!(entries[0].patch_count, 2);
let expected_digest = compute_patch_set_digest(&{
let mut sorted = vec![p1, p2];
sorted.sort_unstable();
sorted
})?;
assert_eq!(entries[0].digest, expected_digest);
cleanup(&layout);
Ok(())
}
#[test]
fn a_ref_count_over_the_limit_is_rejected_before_allocating() {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"PSYNCSU1");
bytes.extend_from_slice(&(DEFAULT_SYNC_SUMMARY_MAX_REF_COUNT as u64 + 1).to_be_bytes());
let result = decode_sync_summary(&bytes, TOTAL_BYTES, DEFAULT_SYNC_SUMMARY_MAX_REF_COUNT);
let message = match result {
Ok(_) => panic!("an over-limit declared ref count must be rejected"),
Err(error) => error.to_string(),
};
assert!(
message.contains("over the configured limit"),
"expected the declared-count refusal, got: {message}"
);
}
#[test]
fn oversized_total_bytes_is_refused_before_parsing() -> Result<()> {
let layout = fresh_repo("sync-summary-oversized")?;
publish_branch(
&layout,
"heads/main",
vec![ObjectId::from_bytes([0x22; 32])],
)?;
let bytes = build_sync_summary(&layout)?;
let result = decode_sync_summary(&bytes, bytes.len() - 1, REF_COUNT);
let message = match result {
Ok(_) => panic!("an oversized summary must be refused before decoding"),
Err(error) => error.to_string(),
};
assert!(
message.contains("over the configured limit"),
"expected the total-byte-length refusal, got: {message}"
);
cleanup(&layout);
Ok(())
}
#[test]
fn a_summary_omits_tags_and_remotes() -> Result<()> {
let layout = fresh_repo("sync-summary-branches-only")?;
let block_id = publish_branch(
&layout,
"heads/main",
vec![ObjectId::from_bytes([0x21; 32])],
)?;
publish_tag(&layout, "tags/v1", "v1", block_id)?;
publish_received(&layout, "remotes/origin/heads/main", block_id)?;
let bytes = build_sync_summary(&layout)?;
let entries = decode_sync_summary(&bytes, TOTAL_BYTES, REF_COUNT)?;
assert_eq!(entries.len(), 1, "only the branch ref must appear");
assert_eq!(entries[0].ref_name, "heads/main");
cleanup(&layout);
Ok(())
}
fn remote_entry(ref_name: &str, digest: PatchSetDigest, patch_count: u64) -> SyncSummaryRefEntry {
SyncSummaryRefEntry {
ref_name: ref_name.to_string(),
digest,
patch_count,
}
}
#[test]
fn compare_reports_in_sync_when_digests_agree() -> Result<()> {
let layout = fresh_repo("sync-compare-in-sync")?;
let p1 = ObjectId::from_bytes([0x41; 32]);
publish_branch(&layout, "heads/main", vec![p1])?;
let digest = compute_patch_set_digest(&[p1])?;
let comparisons = compare_sync_summary(&layout, &[remote_entry("heads/main", digest, 1)])?;
assert_eq!(comparisons.len(), 1);
assert_eq!(comparisons[0].ref_name, "heads/main");
assert_eq!(comparisons[0].state, SyncRefComparisonState::InSync);
cleanup(&layout);
Ok(())
}
#[test]
fn compare_reports_differs_when_digests_disagree() -> Result<()> {
let layout = fresh_repo("sync-compare-differs")?;
let p1 = ObjectId::from_bytes([0x42; 32]);
publish_branch(&layout, "heads/main", vec![p1])?;
let wrong_digest = compute_patch_set_digest(&[ObjectId::from_bytes([0x43; 32])])?;
let comparisons =
compare_sync_summary(&layout, &[remote_entry("heads/main", wrong_digest, 1)])?;
assert_eq!(comparisons.len(), 1);
assert_eq!(comparisons[0].state, SyncRefComparisonState::Differs);
cleanup(&layout);
Ok(())
}
#[test]
fn compare_reports_asymmetric_refs_not_a_refusal() -> Result<()> {
let layout = fresh_repo("sync-compare-asymmetric")?;
publish_branch(
&layout,
"heads/local-only",
vec![ObjectId::from_bytes([0x44; 32])],
)?;
let remote_digest = compute_patch_set_digest(&[ObjectId::from_bytes([0x45; 32])])?;
let comparisons = compare_sync_summary(
&layout,
&[remote_entry("heads/remote-only", remote_digest, 1)],
)?;
assert_eq!(comparisons.len(), 2);
let local = comparisons
.iter()
.find(|c| c.ref_name == "heads/local-only")
.expect("heads/local-only must be reported");
assert_eq!(local.state, SyncRefComparisonState::LocalOnly);
let remote = comparisons
.iter()
.find(|c| c.ref_name == "heads/remote-only")
.expect("heads/remote-only must be reported");
assert_eq!(remote.state, SyncRefComparisonState::RemoteOnly);
cleanup(&layout);
Ok(())
}