liteboxfs 0.1.0

A modern POSIX filesystem in a SQLite database
Documentation
use xpct::{
    be_ok, contain_element, equal, expect, fields, have_len, match_elements, match_fields, not,
};

use crate::{
    block::{
        adapter::ChunkingBlockStoreAdapter,
        store::{BlockStore, DataStore},
        types::{Block, FileId, FileOffset},
    },
    hash::BlockHasher,
    testing::MemoryBlockStore,
};

use super::{BlockSignature, BlockStoreTestExt, DataBlock, TEST_BLOCK_SIZE};

#[test]
fn truncate_to_len_larger_than_file_is_noop() {
    let mut block_store = MemoryBlockStore::new();
    let file_id = FileId::from(1);

    let DataBlock {
        id: block_id,
        signature,
        ..
    } = expect!(block_store.insert_data(file_id, 0))
        .to(be_ok())
        .into_inner();

    let mut data_store = ChunkingBlockStoreAdapter::without_chunking(block_store);

    let block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner();

    let block_list = expect!(data_store.truncate(file_id, block_list, 16))
        .to(be_ok())
        .into_inner()
        .into_iter()
        .collect::<Vec<_>>();

    expect!(&block_list).to(have_len(1));
    expect!(&block_list).to(contain_element(Block {
        id: block_id,
        signature: signature.clone(),
    }));

    let actual_block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner()
        .into_iter()
        .collect::<Vec<_>>();

    expect!(block_list).to(equal(actual_block_list));
}

#[test]
fn truncate_to_zero_removes_all_blocks() {
    let mut block_store = MemoryBlockStore::new();
    let file_id = FileId::from(1);

    expect!(block_store.insert_data(file_id, 0)).to(be_ok());

    let mut data_store = ChunkingBlockStoreAdapter::without_chunking(block_store);

    let block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner();

    let block_list = expect!(data_store.truncate(file_id, block_list, 0))
        .to(be_ok())
        .into_inner()
        .into_iter()
        .collect::<Vec<_>>();

    expect!(&block_list).to(have_len(0));

    let actual_block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner()
        .into_iter()
        .collect::<Vec<_>>();

    expect!(&actual_block_list).to(have_len(0));
}

#[test]
fn truncate_splits_data_blocks() {
    let mut block_store = MemoryBlockStore::new();
    let file_id = FileId::from(1);

    let DataBlock {
        id: block_id,
        bytes,
        ..
    } = expect!(block_store.insert_data(file_id, 0))
        .to(be_ok())
        .into_inner();

    let mut data_store = ChunkingBlockStoreAdapter::without_chunking(block_store);

    let truncate_len = TEST_BLOCK_SIZE / 2;

    let block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner();

    let block_list = expect!(data_store.truncate(file_id, block_list, truncate_len as FileOffset))
        .to(be_ok())
        .into_inner()
        .into_iter()
        .collect::<Vec<_>>();

    expect!(&block_list).to(have_len(1));

    let expected_hash = BlockHasher::hash(bytes.get(0..truncate_len).unwrap());

    expect!(block_list.clone()).to(match_elements([match_fields(fields!(Block {
        id: not(equal(block_id)),
        signature: equal(BlockSignature::Data {
            hash: expected_hash.clone(),
            len: truncate_len,
        }),
    }))]));

    let actual_block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner()
        .into_iter()
        .collect::<Vec<_>>();

    expect!(block_list).to(equal(actual_block_list));
}

#[test]
fn truncate_at_exact_block_boundary() {
    let mut block_store = MemoryBlockStore::new();
    let file_id = FileId::from(1);

    let DataBlock {
        id: first_block_id,
        signature: first_signature,
        ..
    } = expect!(block_store.insert_data(file_id, 0))
        .to(be_ok())
        .into_inner();

    expect!(block_store.insert_data(file_id, 1)).to(be_ok());

    let mut data_store = ChunkingBlockStoreAdapter::without_chunking(block_store);

    let block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner();

    expect!(&block_list).to(have_len(2));

    let block_list =
        expect!(data_store.truncate(file_id, block_list, TEST_BLOCK_SIZE as FileOffset))
            .to(be_ok())
            .into_inner()
            .into_iter()
            .collect::<Vec<_>>();

    expect!(&block_list).to(have_len(1));
    expect!(&block_list).to(contain_element(Block {
        id: first_block_id,
        signature: first_signature,
    }));

    let actual_block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner()
        .into_iter()
        .collect::<Vec<_>>();

    expect!(block_list).to(equal(actual_block_list));
}

#[test]
fn truncate_empty_file_to_zero_is_noop() {
    let mut block_store = MemoryBlockStore::new();
    let file_id = FileId::from(1);

    // Insert a block, then remove it to create an "empty" file that exists
    expect!(block_store.insert_data(file_id, 0)).to(be_ok());
    expect!(block_store.remove_blocks(file_id, 0..1)).to(be_ok());

    let mut data_store = ChunkingBlockStoreAdapter::without_chunking(block_store);

    let block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner();

    expect!(&block_list).to(have_len(0));

    // Truncate empty file to 0 - should be no-op
    let block_list = expect!(data_store.truncate(file_id, block_list, 0))
        .to(be_ok())
        .into_inner()
        .into_iter()
        .collect::<Vec<_>>();

    expect!(&block_list).to(have_len(0));

    let actual_block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner()
        .into_iter()
        .collect::<Vec<_>>();

    expect!(&actual_block_list).to(have_len(0));
}

#[test]
fn truncate_to_exact_current_length_preserves_blocks() {
    let mut block_store = MemoryBlockStore::new();
    let file_id = FileId::from(1);

    let DataBlock {
        id: block_id,
        signature,
        ..
    } = expect!(block_store.insert_data(file_id, 0))
        .to(be_ok())
        .into_inner();

    let mut data_store = ChunkingBlockStoreAdapter::without_chunking(block_store);

    let block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner();

    let block_list =
        expect!(data_store.truncate(file_id, block_list, TEST_BLOCK_SIZE as FileOffset))
            .to(be_ok())
            .into_inner()
            .into_iter()
            .collect::<Vec<_>>();

    expect!(&block_list).to(have_len(1));
    expect!(&block_list).to(contain_element(Block {
        id: block_id,
        signature: signature.clone(),
    }));

    let actual_block_list = expect!(data_store.list_blocks(file_id))
        .to(be_ok())
        .into_inner()
        .into_iter()
        .collect::<Vec<_>>();

    expect!(block_list).to(equal(actual_block_list));
}