oxgraph-snapshot 0.2.0

Topology-agnostic byte-level snapshot container.
Documentation
//! Property test: the no-`alloc` writer ([`SnapshotPlan::write_into`]) and the
//! `alloc`-gated owning builder ([`SnapshotBuilder::finish`]) emit byte-for-byte
//! identical snapshots for the same logical sections, and the result re-opens.
//!
//! The builder delegates to the plan today, so they cannot diverge — this test
//! pins that invariant against future refactors that might give the builder its
//! own encoding path.

use std::collections::BTreeSet;

use oxgraph_snapshot::{
    PendingSection, PlanError, Snapshot, SnapshotBuilder, SnapshotError, SnapshotPlan,
};
use proptest::{prelude::*, test_runner::TestCaseError};

/// Converts writer/builder results into proptest failures.
fn prop_plan<T>(result: Result<T, PlanError>) -> Result<T, TestCaseError> {
    result.map_err(|error| TestCaseError::fail(error.to_string()))
}

/// Converts snapshot open results into proptest failures.
fn prop_open<T>(result: Result<T, SnapshotError>) -> Result<T, TestCaseError> {
    result.map_err(|error| TestCaseError::fail(error.to_string()))
}

/// One generated section: a unique kind, a version, an alignment log2 within
/// the v1 cap, and a bounded payload.
#[derive(Clone, Debug)]
struct GenSection {
    /// Section kind tag.
    kind: u32,
    /// Section version.
    version: u32,
    /// Payload alignment as `log2`, within the v1 cap.
    alignment_log2: u8,
    /// Section payload bytes.
    payload: Vec<u8>,
}

prop_compose! {
    fn gen_section()(
        kind in 0u32..64,
        version in 0u32..4,
        alignment_log2 in 0u8..=4,
        payload in proptest::collection::vec(any::<u8>(), 0..32),
    ) -> GenSection {
        GenSection { kind, version, alignment_log2, payload }
    }
}

proptest! {
    #![proptest_config(ProptestConfig {
        failure_persistence: None,
        ..ProptestConfig::default()
    })]

    #[test]
    fn writer_and_builder_emit_identical_bytes(
        sections in proptest::collection::vec(gen_section(), 0..8),
    ) {
        // Deduplicate kinds: both encoders reject duplicates, and we want to
        // exercise the success path here.
        let mut seen = BTreeSet::new();
        let unique: Vec<GenSection> = sections
            .into_iter()
            .filter(|section| seen.insert(section.kind))
            .collect();

        // Build via the owning builder.
        let mut builder = SnapshotBuilder::new();
        for section in &unique {
            prop_plan(builder.add_section(
                section.kind,
                section.version,
                section.alignment_log2,
                section.payload.clone(),
            ).map(|_| ()))?;
        }
        let from_builder = prop_plan(builder.finish())?;

        // Build via the no-alloc plan into a heap buffer.
        let pending: Vec<PendingSection<'_>> = unique
            .iter()
            .map(|section| PendingSection {
                kind: section.kind,
                version: section.version,
                alignment_log2: section.alignment_log2,
                payload: section.payload.as_slice(),
            })
            .collect();
        let plan = prop_plan(SnapshotPlan::new(&pending))?;
        let needed = prop_plan(plan.encoded_len())?;
        let mut from_plan = vec![0u8; needed];
        let written = prop_plan(plan.write_into(&mut from_plan))?;
        prop_assert_eq!(written, needed);

        // The two encoders must agree byte for byte.
        prop_assert_eq!(&from_builder, &from_plan);

        // And the result re-opens and exposes the same sections.
        let snapshot = prop_open(Snapshot::open(&from_builder))?;
        prop_assert_eq!(snapshot.section_count(), unique.len());
        for section in &unique {
            let Some(view) = snapshot.section(section.kind) else {
                return Err(TestCaseError::fail("section missing after open"));
            };
            prop_assert_eq!(view.bytes(), section.payload.as_slice());
            prop_assert_eq!(view.version(), section.version);
        }
    }
}