boundgraph-core 0.1.0

Core primitives for the BoundGraph runtime: content-addressed digests, work orders, context, condensates, evidence, and the ledger.
Documentation
//! Property tests for US-0101.
//!
//! The claim under test is the one everything else inherits: a digest is a
//! *function* of the value. Equal values always share a digest; any change to
//! any field always changes it.

use std::collections::{BTreeMap, HashMap};

use boundgraph_core::{Canonical, Digest, from_canonical_cbor, to_canonical_cbor};
use proptest::prelude::*;
use serde::{Deserialize, Serialize};

/// A value with enough shape to exercise nesting, maps, options and bytes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct Specimen {
    name: String,
    count: u64,
    flag: bool,
    tags: Vec<String>,
    attrs: BTreeMap<String, String>,
    note: Option<String>,
    blob: Vec<u8>,
}

fn specimen() -> impl Strategy<Value = Specimen> {
    (
        ".{0,24}",
        any::<u64>(),
        any::<bool>(),
        prop::collection::vec(".{0,8}", 0..4),
        prop::collection::btree_map(".{0,8}", ".{0,8}", 0..4),
        prop::option::of(".{0,16}"),
        prop::collection::vec(any::<u8>(), 0..24),
    )
        .prop_map(|(name, count, flag, tags, attrs, note, blob)| Specimen {
            name,
            count,
            flag,
            tags,
            attrs,
            note,
            blob,
        })
}

proptest! {
    /// Equal values share a digest — the deduplication guarantee.
    #[test]
    fn equal_values_share_a_digest(s in specimen()) {
        let clone = s.clone();
        prop_assert_eq!(s.digest().unwrap(), clone.digest().unwrap());
    }

    /// Encoding is a pure function, not affected by call order or history.
    #[test]
    fn encoding_is_deterministic(s in specimen()) {
        let first = to_canonical_cbor(&s).unwrap();
        let second = to_canonical_cbor(&s).unwrap();
        prop_assert_eq!(first, second);
    }

    /// Round-tripping preserves both the value and its identity.
    #[test]
    fn round_trip_preserves_value_and_digest(s in specimen()) {
        let bytes = to_canonical_cbor(&s).unwrap();
        let back: Specimen = from_canonical_cbor(&bytes).unwrap();
        prop_assert_eq!(&s, &back);
        prop_assert_eq!(s.digest().unwrap(), back.digest().unwrap());
    }

    /// Changing any single field changes the digest.
    #[test]
    fn any_field_change_changes_the_digest(s in specimen()) {
        let base = s.digest().unwrap();

        let mut m = s.clone();
        m.name.push('!');
        prop_assert_ne!(base, m.digest().unwrap());

        let mut m = s.clone();
        m.count = m.count.wrapping_add(1);
        prop_assert_ne!(base, m.digest().unwrap());

        let mut m = s.clone();
        m.flag = !m.flag;
        prop_assert_ne!(base, m.digest().unwrap());

        let mut m = s.clone();
        m.tags.push("extra".into());
        prop_assert_ne!(base, m.digest().unwrap());

        let mut m = s.clone();
        m.attrs.insert("added-key".into(), "v".into());
        prop_assert_ne!(base, m.digest().unwrap());

        let mut m = s.clone();
        m.note = match m.note {
            None => Some(String::new()),
            Some(_) => None,
        };
        prop_assert_ne!(base, m.digest().unwrap());

        let mut m = s.clone();
        m.blob.push(0);
        prop_assert_ne!(base, m.digest().unwrap());
    }

    /// Iteration order must not affect identity.
    ///
    /// `HashMap` is the real test here: its iteration order is not the
    /// insertion order and is not stable across builds, so if canonicalisation
    /// did not sort keys this would fail. (A `BTreeMap` would pass trivially,
    /// since it always iterates sorted — which is why it is not used here.)
    /// Keys come from a `BTreeMap` strategy so they are guaranteed unique;
    /// with duplicate keys the two maps would differ in *content*, not order.
    #[test]
    fn iteration_order_does_not_affect_identity(
        entries in prop::collection::btree_map(".{1,6}", ".{0,6}", 1..6)
    ) {
        let forward: HashMap<String, String> =
            entries.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
        let reverse: HashMap<String, String> =
            entries.iter().rev().map(|(k, v)| (k.clone(), v.clone())).collect();

        prop_assert_eq!(forward.len(), reverse.len());
        prop_assert_eq!(forward.digest().unwrap(), reverse.digest().unwrap());
    }

    /// Hex is a lossless, round-trippable rendering of a digest.
    #[test]
    fn digest_hex_round_trips(bytes in prop::collection::vec(any::<u8>(), 0..64)) {
        let d = Digest::of(&bytes);
        let hex = d.to_hex();
        prop_assert_eq!(hex.len(), 64);
        prop_assert_eq!(hex.parse::<Digest>().unwrap(), d);
    }

    /// Distinct inputs give distinct digests (collision resistance in practice).
    #[test]
    fn distinct_inputs_give_distinct_digests(
        a in prop::collection::vec(any::<u8>(), 0..32),
        b in prop::collection::vec(any::<u8>(), 0..32),
    ) {
        prop_assume!(a != b);
        prop_assert_ne!(Digest::of(&a), Digest::of(&b));
    }
}