acyclic-fs 0.1.5

Rust SDK for versioned workspaces and embedded native mounts
Documentation
use super::*;
use crate::foundation::{Digest, FileId};
use crate::kernel::tree_mutation::{TreeFormat, TreeMutation, TreeSemanticError};
use crate::kernel::{FileKind, LogicalName, NameEncoding, TreeEntry};
use crate::performance::WorkError;
use crate::storage::{ObjectKind, ObjectStoreError};

fn name(byte: u8) -> Result<LogicalName, crate::kernel::TreePageError> {
    LogicalName::new(NameEncoding::PosixBytes, vec![byte], 1)
}

fn entry(byte: u8) -> Result<TreeEntry, crate::kernel::TreePageError> {
    Ok(TreeEntry {
        name: name(byte)?,
        file_id: FileId::from_bytes([byte; 16]),
        kind: FileKind::Regular,
    })
}

fn child(byte: u8) -> Result<Child<LogicalName>, crate::kernel::TreePageError> {
    Ok(Child {
        first: name(byte)?,
        page: root(ObjectKind::TreePage, byte),
    })
}

fn root(kind: ObjectKind, byte: u8) -> ObjectId {
    ObjectId {
        kind,
        digest: Digest::from_bytes([byte; 32]),
    }
}

fn remove(byte: u8) -> Result<TreeMutation, crate::kernel::TreePageError> {
    Ok(TreeMutation::Remove {
        name: name(byte)?,
        expected_file_id: None,
    })
}

#[test]
fn admission_rejects_every_invalid_request_before_work() -> Result<(), Box<dyn std::error::Error>> {
    let limits = DecodeLimits::default();
    let mutation = remove(1)?;

    for (candidate_root, mutations, maximum, candidate_limits, expected) in [
        (
            root(ObjectKind::Blob, 1),
            vec![mutation.clone()],
            1,
            limits,
            "wrong-kind",
        ),
        (root(ObjectKind::TreePage, 1), vec![], 1, limits, "empty"),
        (
            root(ObjectKind::TreePage, 1),
            vec![mutation.clone()],
            0,
            limits,
            "too-many",
        ),
        (
            root(ObjectKind::TreePage, 1),
            vec![mutation.clone(), mutation.clone()],
            1,
            limits,
            "too-many",
        ),
        (
            root(ObjectKind::TreePage, 1),
            vec![mutation.clone()],
            1,
            DecodeLimits {
                maximum_page_items: 1,
                ..limits
            },
            "invalid-limits",
        ),
    ] {
        let failure = validate::<TreeFormat, TreeMutation>(
            candidate_root,
            &mutations,
            maximum,
            candidate_limits,
        )
        .err()
        .ok_or("invalid persistent-tree request was admitted")?;
        assert_eq!(*failure.work, WorkCounters::default());
        match expected {
            "wrong-kind" => assert!(matches!(failure.error, Error::WrongRootKind)),
            "empty" => assert!(matches!(failure.error, Error::Empty)),
            "too-many" => assert!(matches!(failure.error, Error::TooManyMutations)),
            "invalid-limits" => assert!(matches!(failure.error, Error::InvalidLimits)),
            _ => unreachable!(),
        }
    }

    validate::<TreeFormat, TreeMutation>(root(ObjectKind::TreePage, 1), &[mutation], 1, limits)?;
    Ok(())
}

#[test]
fn pages_clone_without_changing_authenticated_frontiers() -> Result<(), Box<dyn std::error::Error>>
{
    let leaf_entries = vec![entry(1)?, entry(2)?];
    let leaf = Page::<TreeFormat>::Leaf(leaf_entries.clone());
    assert!(matches!(leaf.clone(), Page::Leaf(entries) if entries == leaf_entries));

    let internal_children = vec![child(1)?, child(2)?];
    let internal = Page::<TreeFormat>::Internal(internal_children.clone());
    assert!(matches!(internal.clone(), Page::Internal(children) if children == internal_children));
    Ok(())
}

#[test]
fn chunking_is_bounded_by_items_bytes_and_encoder_contracts()
-> Result<(), Box<dyn std::error::Error>> {
    let items = [1_u8, 2, 3];
    let mut limits = DecodeLimits {
        maximum_page_items: 2,
        maximum_page_bytes: 64,
        ..DecodeLimits::default()
    };
    assert_eq!(
        page_chunk_end::<_, TreeSemanticError>(&items, 0, limits, |_| Ok(1))?,
        (2, 2)
    );
    assert_eq!(
        page_chunk_end::<_, TreeSemanticError>(&items, 2, limits, |_| Ok(1))?,
        (3, 1)
    );

    limits.maximum_page_items = 3;
    limits.maximum_page_bytes = 17;
    assert_eq!(
        page_chunk_end::<_, TreeSemanticError>(&items, 0, limits, |_| Ok(1))?,
        (2, 3)
    );

    limits.maximum_page_bytes = 15;
    assert!(matches!(
        page_chunk_end::<_, TreeSemanticError>(&items, 0, limits, |_| Ok(1)),
        Err(Error::PageItemTooLarge)
    ));
    limits.maximum_page_items = 0;
    limits.maximum_page_bytes = 64;
    assert!(matches!(
        page_chunk_end::<_, TreeSemanticError>(&items, 0, limits, |_| Ok(1)),
        Err(Error::PageItemTooLarge)
    ));
    limits.maximum_page_items = 3;
    assert!(matches!(
        page_chunk_end::<_, TreeSemanticError>(&items, 0, limits, |_| {
            Err(CanonicalDecodeError::Truncated)
        }),
        Err(Error::Decode(CanonicalDecodeError::Truncated))
    ));
    Ok(())
}

#[test]
fn search_unchanged_and_bounds_cover_every_frontier() -> Result<(), Box<dyn std::error::Error>> {
    let entries = [entry(2)?, entry(4)?, entry(6)?];
    assert_eq!(search::<TreeFormat>(&entries, &name(2)?).0, Ok(0));
    assert_eq!(search::<TreeFormat>(&entries, &name(4)?).0, Ok(1));
    assert_eq!(search::<TreeFormat>(&entries, &name(6)?).0, Ok(2));
    assert_eq!(search::<TreeFormat>(&entries, &name(1)?).0, Err(0));
    assert_eq!(search::<TreeFormat>(&entries, &name(3)?).0, Err(1));
    assert_eq!(search::<TreeFormat>(&entries, &name(7)?).0, Err(3));

    let children = [child(2)?, child(4)?];
    assert!(unchanged(&children, &children));
    assert!(!unchanged(&children[..1], &children));
    let mut changed = children.clone();
    changed[1].page = root(ObjectKind::TreePage, 9);
    assert!(!unchanged(&changed, &children));

    assert!(validate_leaf::<TreeFormat, TreeMutation>(&entries, None, None).is_ok());
    assert!(
        validate_leaf::<TreeFormat, TreeMutation>(&entries, Some(&name(2)?), Some(&name(7)?))
            .is_ok()
    );
    assert!(matches!(
        validate_leaf::<TreeFormat, TreeMutation>(&entries, Some(&name(3)?), None),
        Err(Error::ChildBoundsMismatch)
    ));
    assert!(matches!(
        validate_leaf::<TreeFormat, TreeMutation>(&entries, None, Some(&name(6)?)),
        Err(Error::ChildBoundsMismatch)
    ));
    assert!(validate_leaf::<TreeFormat, TreeMutation>(&[], None, Some(&name(1)?)).is_ok());

    assert!(validate_children::<TreeFormat, TreeMutation>(&children, None, None).is_ok());
    assert!(
        validate_children::<TreeFormat, TreeMutation>(&children, Some(&name(2)?), Some(&name(5)?))
            .is_ok()
    );
    assert!(matches!(
        validate_children::<TreeFormat, TreeMutation>(&children, Some(&name(3)?), None),
        Err(Error::ChildBoundsMismatch)
    ));
    assert!(matches!(
        validate_children::<TreeFormat, TreeMutation>(&children, None, Some(&name(4)?)),
        Err(Error::ChildBoundsMismatch)
    ));
    assert!(validate_children::<TreeFormat, TreeMutation>(&[], None, Some(&name(1)?)).is_ok());
    Ok(())
}

#[test]
fn lower_layer_error_translation_is_total() {
    assert!(matches!(
        map_io::<TreeSemanticError>(persistent_io::Error::AllocationFailed),
        Error::AllocationFailed
    ));
    assert!(matches!(
        map_io::<TreeSemanticError>(persistent_io::Error::Allocation(
            AllocationError::CapacityExceeded
        )),
        Error::Allocation(AllocationError::CapacityExceeded)
    ));
    assert!(matches!(
        map_io::<TreeSemanticError>(persistent_io::Error::Storage(ObjectStoreError::Missing)),
        Error::Storage(ObjectStoreError::Missing)
    ));
    assert!(matches!(
        map_io::<TreeSemanticError>(persistent_io::Error::Decode(
            CanonicalDecodeError::Truncated
        )),
        Error::Decode(CanonicalDecodeError::Truncated)
    ));
    assert!(matches!(
        map_io::<TreeSemanticError>(persistent_io::Error::Work(WorkError::Overflow)),
        Error::Work(WorkError::Overflow)
    ));
}