summa-core 2.0.0

Core async search engine library with WASM support
Documentation
use super::*;

fn encoded(codec: PostingCodec) -> Vec<u8> {
    let mut postings = PostingList::new();
    for doc in 0..257 {
        postings.push(doc * 2, doc % 7 + 1);
    }
    let list = BlockPostingList::from_posting_list_with_ratio_bounds(
        &postings,
        true,
        Some(&|doc| doc % 101 + 8),
        codec,
    )
    .unwrap();
    let mut bytes = Vec::new();
    list.serialize(&mut bytes).unwrap();
    bytes
}

#[test]
fn unaddressed_posting_trailers_are_rejected_before_read_or_merge() {
    for codec in [
        PostingCodec::Rounded,
        PostingCodec::Packed,
        PostingCodec::Pfor,
        PostingCodec::Simd4x,
    ] {
        let mut bytes = encoded(codec);
        bytes.insert(bytes.len() - FOOTER_V2_SIZE, 0);
        assert!(BlockPostingList::deserialize(&bytes).is_err());
        let mut output = vec![0xab];
        assert!(BlockPostingList::concatenate_streaming(&[(&bytes, 0)], &mut output).is_err());
        assert_eq!(output, [0xab]);
    }
}

#[test]
fn streaming_merge_rejects_corrupt_blocks_before_any_output() {
    for codec in [
        PostingCodec::Rounded,
        PostingCodec::Packed,
        PostingCodec::Pfor,
        PostingCodec::Simd4x,
    ] {
        let valid = encoded(codec);
        let footer = Footer::parse(&valid).unwrap();
        for (at, value) in [(6, 0xe1), (7, 33), (footer.l0_end(), 1)] {
            let mut corrupt = valid.clone();
            corrupt[at] = value;
            assert!(BlockPostingList::deserialize(&corrupt).is_err());
            for sources in [
                vec![(&corrupt[..], 0)],
                vec![(&valid[..], 0), (&corrupt[..], 1000)],
            ] {
                let mut output = vec![0xab];
                let result = BlockPostingList::concatenate_streaming(&sources, &mut output);
                assert!(
                    matches!(result, Err(crate::Error::Corruption(_))),
                    "codec={codec}, byte={at}: {result:?}"
                );
                assert_eq!(
                    output,
                    [0xab],
                    "invalid source must be rejected before writes"
                );
            }
        }
    }
}

#[test]
fn concatenation_rejects_overlapping_or_terminal_document_remapping() {
    let bytes = encoded(PostingCodec::Rounded);
    let list = BlockPostingList::deserialize(&bytes).unwrap();
    for offsets in [
        vec![u32::MAX - 511],
        vec![u32::MAX - 512],
        vec![0, 512],
        vec![1000, 0],
    ] {
        let sources: Vec<_> = offsets.iter().map(|&offset| (&bytes[..], offset)).collect();
        let mut output = vec![0xab];
        let result = BlockPostingList::concatenate_streaming(&sources, &mut output);
        assert!(
            matches!(result, Err(crate::Error::Corruption(_))),
            "offsets={offsets:?}: {result:?}"
        );
        assert_eq!(output, [0xab]);
        let sources: Vec<_> = offsets
            .iter()
            .map(|&offset| (list.clone(), offset))
            .collect();
        assert!(
            BlockPostingList::concatenate_blocks(&sources).is_err(),
            "offsets={offsets:?}"
        );
    }
}

#[test]
fn checked_streaming_merge_matches_encoded_copy_and_materialized_output() {
    for codec in [
        PostingCodec::Rounded,
        PostingCodec::Packed,
        PostingCodec::Pfor,
        PostingCodec::Simd4x,
    ] {
        let bytes = encoded(codec);
        let list = BlockPostingList::deserialize(&bytes).unwrap();
        let mut copied = Vec::new();
        BlockPostingList::concatenate_streaming(&[(&bytes, 0)], &mut copied).unwrap();
        assert_eq!(copied, bytes);
        let sources = [(list.clone(), 7), (list.clone(), 1000)];
        let merged = BlockPostingList::concatenate_blocks(&sources).unwrap();
        let mut expected = Vec::new();
        merged.serialize(&mut expected).unwrap();
        let mut output = Vec::new();
        let (count, length) =
            BlockPostingList::concatenate_streaming(&[(&bytes, 7), (&bytes, 1000)], &mut output)
                .unwrap();
        assert_eq!(count, 514);
        assert_eq!(length, output.len());
        assert_eq!(output, expected);
        let reopened = BlockPostingList::deserialize(&output).unwrap();
        assert_eq!(reopened.doc_count(), 514);
    }
}

#[test]
fn concatenation_checks_position_cursor_sum_before_output() {
    let mut bytes = encoded(PostingCodec::Rounded);
    let at = bytes.len() - FOOTER_V2_SIZE + 24;
    bytes[at..at + 8].copy_from_slice(&(1u64 << 63).to_le_bytes());
    let list = BlockPostingList::deserialize(&bytes).unwrap();
    let mut output = vec![0xab];
    let result =
        BlockPostingList::concatenate_streaming(&[(&bytes, 0), (&bytes, 1000)], &mut output);
    assert!(matches!(result, Err(crate::Error::Corruption(_))));
    assert_eq!(output, [0xab]);
    assert!(BlockPostingList::concatenate_blocks(&[(list.clone(), 0), (list, 1000)]).is_err());
}