swactor/crates/datastore/tests/chunking_tests.rs
Zachery Aaron Shores-Chmielewski af1541695a
Some checks failed
ci/hello Job 'hello' completed
ci/smoke Pipeline 'smoke' success
ci/check Job 'check' completed
ci/clippy Job 'clippy' completed
ci/ci Pipeline 'ci' failure
feat: content addressed datastore (#41)
Content addressable datastore. Allows you to configure a node to store and stream large blobs of data, and retrieve them from any swactor-connected node.
Co-authored-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
Co-committed-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-02-15 17:03:31 +00:00

200 lines
7.6 KiB
Rust

//! Tests for the chunking engine — pure function contracts.
use std::collections::HashSet;
use swactor_datastore::chunking::{chunk_blob, reassemble_blob, verify_integrity, ChunkingError};
use swactor_datastore::types::ContentHash;
// ═══════════════════════════════════════════════════════════════════════════
// Scenario: basic chunking behaviour
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn small_file_fits_in_single_chunk() {
let data = b"tiny";
let (content_hash, manifest, chunks) = chunk_blob(data, 1024);
assert_eq!(chunks.len(), 1);
assert_eq!(manifest.chunks.len(), 1);
assert_eq!(content_hash, ContentHash::of(data));
// Single chunk's hash == hash of that chunk's data (which is the whole blob).
assert_eq!(manifest.chunks[0].hash, ContentHash::of(data));
}
#[test]
fn multi_chunk_blob_reassembles_to_original() {
let data: Vec<u8> = (0..300).map(|i| (i % 256) as u8).collect();
let chunk_size = 64;
let (_, manifest, chunks) = chunk_blob(&data, chunk_size);
assert!(manifest.chunks.len() >= 3);
let reassembled = reassemble_blob(&manifest, &chunks).unwrap();
assert_eq!(reassembled, data);
}
#[test]
fn last_chunk_is_smaller_when_not_aligned() {
let data = vec![0xAB; 100];
let (_, manifest, chunks) = chunk_blob(&data, 64);
assert_eq!(chunks.len(), 2);
assert_eq!(manifest.chunks[0].size, 64);
assert_eq!(manifest.chunks[1].size, 36);
assert_eq!(manifest.chunks[0].offset, 0);
assert_eq!(manifest.chunks[1].offset, 64);
}
#[test]
fn empty_blob_produces_empty_manifest() {
let (content_hash, manifest, chunks) = chunk_blob(b"", 1024);
assert!(chunks.is_empty());
assert!(manifest.chunks.is_empty());
assert_eq!(manifest.total_size, 0);
assert_eq!(content_hash, ContentHash::of(b""));
// Reassembly of empty manifest yields empty data.
let reassembled = reassemble_blob(&manifest, &chunks).unwrap();
assert!(reassembled.is_empty());
}
// ═══════════════════════════════════════════════════════════════════════════
// Scenario: reassembly failure modes
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn reassembly_with_missing_chunk_fails() {
// Use non-repeating data so each chunk has a unique hash.
let data: Vec<u8> = (0..128).collect();
let (_, manifest, mut chunks) = chunk_blob(&data, 64);
// Remove the last chunk.
chunks.pop();
let err = reassemble_blob(&manifest, &chunks).unwrap_err();
match err {
ChunkingError::MissingChunk { hash } => {
assert_eq!(hash, manifest.chunks.last().unwrap().hash);
}
other => panic!("expected MissingChunk, got: {other:?}"),
}
}
#[test]
fn reassembly_detects_wrong_content_hash() {
let data = vec![42; 128];
let (_, mut manifest, chunks) = chunk_blob(&data, 64);
// Corrupt the manifest's content hash.
manifest.content_hash = ContentHash::of(b"wrong");
let err = reassemble_blob(&manifest, &chunks).unwrap_err();
assert!(matches!(err, ChunkingError::HashMismatch { .. }));
}
// ═══════════════════════════════════════════════════════════════════════════
// Scenario: determinism and deduplication
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn identical_blobs_produce_identical_manifests() {
let data = b"deterministic input";
let (h1, m1, c1) = chunk_blob(data, 8);
let (h2, m2, c2) = chunk_blob(data, 8);
assert_eq!(h1, h2);
assert_eq!(m1, m2);
assert_eq!(c1.len(), c2.len());
for (a, b) in c1.iter().zip(c2.iter()) {
assert_eq!(a.0, b.0);
assert_eq!(a.1, b.1);
}
}
#[test]
fn deduplication_across_objects() {
// Two blobs that share a common prefix produce the same chunk hash for that prefix.
let shared_prefix = vec![0xBE; 64];
let mut blob_a = shared_prefix.clone();
blob_a.extend_from_slice(&[0xAA; 64]);
let mut blob_b = shared_prefix.clone();
blob_b.extend_from_slice(&[0xBB; 64]);
let (_, _, chunks_a) = chunk_blob(&blob_a, 64);
let (_, _, chunks_b) = chunk_blob(&blob_b, 64);
// First chunk should be identical (shared prefix).
assert_eq!(chunks_a[0].0, chunks_b[0].0);
// Second chunk should differ.
assert_ne!(chunks_a[1].0, chunks_b[1].0);
// A HashSet of all chunk hashes should have 3 unique entries (shared + 2 distinct).
let all_hashes: HashSet<_> = chunks_a
.iter()
.chain(chunks_b.iter())
.map(|(h, _)| *h)
.collect();
assert_eq!(all_hashes.len(), 3);
}
#[test]
fn verify_integrity_passes_for_correct_data() {
let data = b"check me";
let hash = ContentHash::of(data);
assert!(verify_integrity(data, &hash));
}
#[test]
fn verify_integrity_fails_for_wrong_data() {
let hash = ContentHash::of(b"original");
assert!(!verify_integrity(b"tampered", &hash));
}
// ═══════════════════════════════════════════════════════════════════════════
// Property-based tests
// ═══════════════════════════════════════════════════════════════════════════
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn chunk_then_reassemble_is_identity(
data in proptest::collection::vec(any::<u8>(), 0..8192),
chunk_size in 1u32..=512,
) {
let (_, manifest, chunks) = chunk_blob(&data, chunk_size);
let reassembled = reassemble_blob(&manifest, &chunks).unwrap();
prop_assert_eq!(data, reassembled);
}
}
proptest! {
#[test]
fn content_hash_matches_blake3_of_whole_blob(
data in proptest::collection::vec(any::<u8>(), 0..4096),
) {
let (content_hash, manifest, _) = chunk_blob(&data, 256);
let expected = ContentHash::of(&data);
prop_assert_eq!(content_hash, expected);
prop_assert_eq!(manifest.content_hash, expected);
}
}
proptest! {
#[test]
fn chunk_offsets_are_contiguous(
data in proptest::collection::vec(any::<u8>(), 1..4096),
chunk_size in 1u32..=256,
) {
let (_, manifest, _) = chunk_blob(&data, chunk_size);
let mut expected_offset = 0u64;
for chunk_ref in &manifest.chunks {
prop_assert_eq!(chunk_ref.offset, expected_offset);
expected_offset += chunk_ref.size as u64;
}
prop_assert_eq!(expected_offset, manifest.total_size);
}
}
}