// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 use crate::sequence::{BlockSequence, BlockSequenceError}; use crate::{BlockId, KvbmSequenceHashProvider, SequenceHash}; use super::ExternalBlockAssignments; const TEST_BLOCK_SIZE: u32 = 4; /// Helper to create a BlockSequence with a given number of complete blocks and optional partial. fn create_test_sequence(num_complete_blocks: usize, partial_tokens: usize) -> BlockSequence { let total_tokens = num_complete_blocks * TEST_BLOCK_SIZE as usize + partial_tokens; let tokens: Vec = (0..total_tokens as u32).collect(); BlockSequence::new(tokens, TEST_BLOCK_SIZE, None) } /// Helper to get the expected sequence hashes from a BlockSequence. fn get_expected_hashes(seq: &BlockSequence) -> Vec { seq.blocks() .iter() .map(|b| b.kvbm_sequence_hash()) .collect() } // ========================================================================= // Test Cases: Aligned sequences (no partial block) // ========================================================================= #[test] fn test_aligned_0_blocks_0_block_ids() { let seq = create_test_sequence(0, 0); let mut assignments = ExternalBlockAssignments::new(0); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..0); assert_eq!(assignments.assigned_count(), 0); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_aligned_1_block_0_block_ids() { let seq = create_test_sequence(1, 0); let mut assignments = ExternalBlockAssignments::new(0); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..0); assert_eq!(assignments.assigned_count(), 0); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_aligned_1_block_1_block_id() { let seq = create_test_sequence(1, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100]).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..1); assert_eq!(assignments.assigned_count(), 1); let (id, hash) = assignments.get_assigned(0).unwrap(); assert_eq!(id, 100); assert_eq!(hash, expected_hashes[0]); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_aligned_1_block_2_block_ids() { let seq = create_test_sequence(1, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200]).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..1); assert_eq!(assignments.assigned_count(), 1); let (id, hash) = assignments.get_assigned(0).unwrap(); assert_eq!(id, 100); assert_eq!(hash, expected_hashes[0]); assert_eq!(assignments.unassigned_count(), 1); assert_eq!(assignments.unassigned_iter().collect::>(), vec![200]); } #[test] fn test_aligned_3_blocks_3_block_ids() { let seq = create_test_sequence(3, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..3); assert_eq!(assignments.assigned_count(), 3); for (i, expected_hash) in expected_hashes.iter().enumerate() { let (id, hash) = assignments.get_assigned(i).unwrap(); assert_eq!(id, (i + 1) * 100); assert_eq!(hash, *expected_hash); } assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_aligned_3_blocks_1_block_id() { let seq = create_test_sequence(3, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100]).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..1); assert_eq!(assignments.assigned_count(), 1); let (id, hash) = assignments.get_assigned(0).unwrap(); assert_eq!(id, 100); assert_eq!(hash, expected_hashes[0]); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_aligned_3_blocks_5_block_ids() { let seq = create_test_sequence(3, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..3); assert_eq!(assignments.assigned_count(), 3); for (i, expected_hash) in expected_hashes.iter().enumerate() { let (id, hash) = assignments.get_assigned(i).unwrap(); assert_eq!(id, (i + 1) * 100); assert_eq!(hash, *expected_hash); } assert_eq!(assignments.unassigned_count(), 2); assert_eq!( assignments.unassigned_iter().collect::>(), vec![400, 500] ); } // ========================================================================= // Test Cases: Sequences with partial (dangling) block // ========================================================================= #[test] fn test_partial_0_complete_2_partial_0_block_ids() { let seq = create_test_sequence(0, 2); let mut assignments = ExternalBlockAssignments::new(0); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..0); assert_eq!(assignments.assigned_count(), 0); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_partial_2_complete_1_partial_2_block_ids() { let seq = create_test_sequence(2, 1); let expected_hashes = get_expected_hashes(&seq); assert_eq!(expected_hashes.len(), 2); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200]).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..2); assert_eq!(assignments.assigned_count(), 2); let (id0, hash0) = assignments.get_assigned(0).unwrap(); assert_eq!((id0, hash0), (100, expected_hashes[0])); let (id1, hash1) = assignments.get_assigned(1).unwrap(); assert_eq!((id1, hash1), (200, expected_hashes[1])); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_partial_2_complete_3_partial_4_block_ids() { let seq = create_test_sequence(2, 3); let expected_hashes = get_expected_hashes(&seq); assert_eq!(expected_hashes.len(), 2); let mut assignments = ExternalBlockAssignments::new(0); assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..2); assert_eq!(assignments.assigned_count(), 2); assert_eq!(assignments.unassigned_count(), 2); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400] ); } #[test] fn test_partial_3_complete_2_partial_1_block_id() { let seq = create_test_sequence(3, 2); let expected_hashes = get_expected_hashes(&seq); assert_eq!(expected_hashes.len(), 3); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100]).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..1); assert_eq!(assignments.assigned_count(), 1); let (id, hash) = assignments.get_assigned(0).unwrap(); assert_eq!((id, hash), (100, expected_hashes[0])); assert_eq!(assignments.unassigned_count(), 0); } // ========================================================================= // Test Cases: Multiple calls (incremental assignment) // ========================================================================= #[test] fn test_incremental_assignment_aligned() { let seq = create_test_sequence(4, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); // First call: assign first 2 blocks assignments.extend_block_ids(vec![100, 200]).unwrap(); let range_1 = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_1, 0..2); assert_eq!(assignments.assigned_count(), 2); // Second call: assign next 2 blocks (100, 200 are known prefix → skipped) assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); let range_2 = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_2, 2..4); assert_eq!(assignments.assigned_count(), 4); for (i, expected_hash) in expected_hashes.iter().enumerate() { let (id, hash) = assignments.get_assigned(i).unwrap(); assert_eq!(id, (i + 1) * 100); assert_eq!(hash, *expected_hash); } assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_incremental_assignment_with_excess() { let seq = create_test_sequence(3, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); // First call: assign first 2 blocks assignments.extend_block_ids(vec![100, 200]).unwrap(); let range_1 = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_1, 0..2); // Second call: 100, 200 skipped, 300 assigned, 400, 500 unassigned assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); let range_2 = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_2, 2..3); assert_eq!(assignments.assigned_count(), 3); let (id, hash) = assignments.get_assigned(2).unwrap(); assert_eq!((id, hash), (300, expected_hashes[2])); assert_eq!( assignments.unassigned_iter().collect::>(), vec![400, 500] ); } #[test] fn test_incremental_assignment_partial_then_excess() { let seq = create_test_sequence(2, 1); let expected_hashes = get_expected_hashes(&seq); assert_eq!(expected_hashes.len(), 2); let mut assignments = ExternalBlockAssignments::new(0); // First call: assign 1 block assignments.extend_block_ids(vec![100]).unwrap(); let range_1 = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_1, 0..1); // Second call: 100 skipped, 200 assigned, 300, 400 unassigned assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); let range_2 = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_2, 1..2); let (id, hash) = assignments.get_assigned(1).unwrap(); assert_eq!((id, hash), (200, expected_hashes[1])); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400] ); } #[test] fn test_all_blocks_already_assigned_extra_goes_to_unassigned() { let seq = create_test_sequence(2, 0); let mut assignments = ExternalBlockAssignments::new(0); // Assign all blocks assignments.extend_block_ids(vec![100, 200]).unwrap(); let range_1 = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_1, 0..2); // All new go to unassigned (100, 200 are skipped) assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); let range_2 = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_2, 2..2); assert_eq!(assignments.assigned_count(), 2); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400] ); } // ========================================================================= // Test Cases: Edge cases // ========================================================================= #[test] fn test_empty_slot_receives_block_ids() { let seq = create_test_sequence(0, 0); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..0); assert_eq!(assignments.assigned_count(), 0); assert_eq!( assignments.unassigned_iter().collect::>(), vec![100, 200, 300] ); } #[test] fn test_only_partial_tokens_receives_block_ids() { let seq = create_test_sequence(0, 3); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200]).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..0); assert_eq!(assignments.assigned_count(), 0); assert_eq!( assignments.unassigned_iter().collect::>(), vec![100, 200] ); } #[test] fn test_large_sequence_exact_match() { let seq = create_test_sequence(10, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); let block_ids: Vec = (0..10).map(|i| (i + 1) * 100).collect(); assignments.extend_block_ids(block_ids).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..10); assert_eq!(assignments.assigned_count(), 10); for (i, expected_hash) in expected_hashes.iter().enumerate() { let (id, hash) = assignments.get_assigned(i).unwrap(); assert_eq!(id, (i + 1) * 100); assert_eq!(hash, *expected_hash); } assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_verify_hash_block_id_pairing_order() { let seq = create_test_sequence(5, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments .extend_block_ids(vec![999, 888, 777, 666, 555]) .unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..5); assert_eq!(assignments.get_assigned(0).unwrap().0, 999); assert_eq!(assignments.get_assigned(1).unwrap().0, 888); assert_eq!(assignments.get_assigned(2).unwrap().0, 777); assert_eq!(assignments.get_assigned(3).unwrap().0, 666); assert_eq!(assignments.get_assigned(4).unwrap().0, 555); for (i, expected_hash) in expected_hashes.iter().enumerate() { assert_eq!(assignments.get_assigned(i).unwrap().1, *expected_hash); } } // ========================================================================= // Cartesian product test // ========================================================================= #[test] fn test_cartesian_product_combinations() { let num_blocks_options = [0, 1, 3, 5]; let partial_options = [0, 1, 3]; for &num_blocks in &num_blocks_options { for &partial in &partial_options { let seq = create_test_sequence(num_blocks, partial); let expected_hashes = get_expected_hashes(&seq); let available_blocks = expected_hashes.len(); // Test with 0 block_ids { let mut assignments = ExternalBlockAssignments::new(0); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..0); assert_eq!(assignments.assigned_count(), 0); assert_eq!(assignments.unassigned_count(), 0); } // Test with fewer block_ids than available blocks if available_blocks > 1 { let fewer = available_blocks / 2; let block_ids: Vec = (0..fewer).collect(); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(block_ids).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..fewer); assert_eq!(assignments.assigned_count(), fewer); assert_eq!(assignments.unassigned_count(), 0); for (i, expected_hash) in expected_hashes.iter().enumerate().take(fewer) { let (id, hash) = assignments.get_assigned(i).unwrap(); assert_eq!(id, i); assert_eq!(hash, *expected_hash); } } // Test with exact number of block_ids if available_blocks > 0 { let block_ids: Vec = (0..available_blocks).collect(); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(block_ids).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..available_blocks); assert_eq!(assignments.assigned_count(), available_blocks); assert_eq!(assignments.unassigned_count(), 0); } // Test with more block_ids than available blocks { let excess = 3; let total_ids = available_blocks + excess; let block_ids: Vec = (0..total_ids).collect(); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(block_ids).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..available_blocks); assert_eq!(assignments.assigned_count(), available_blocks); assert_eq!(assignments.unassigned_count(), excess); let expected_unassigned: Vec = (available_blocks..total_ids).collect(); assert_eq!( assignments.unassigned_iter().collect::>(), expected_unassigned ); } } } } // ========================================================================= // Test Cases: Previously unassigned blocks (FIFO behavior) // ========================================================================= #[test] fn test_unassigned_blocks_applied_before_new_blocks() { let mut seq = create_test_sequence(5, 0); let mut assignments = ExternalBlockAssignments::new(0); // Assign 5 blocks + 2 excess assignments .extend_block_ids(vec![100, 200, 300, 400, 500, 600, 700]) .unwrap(); let range_1 = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_1, 0..5); assert_eq!(assignments.assigned_count(), 5); assert_eq!( assignments.unassigned_iter().collect::>(), vec![600, 700] ); // Add 2 more complete blocks via token extension for token in 20..28u32 { seq.append_token(token).unwrap(); } assert_eq!(seq.blocks().len(), 7); // Flush with new block_ids — unassigned (600, 700) should be assigned first assignments .extend_block_ids(vec![100, 200, 300, 400, 500, 600, 700, 800, 900]) .unwrap(); let range_2 = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_2, 5..7); assert_eq!(assignments.assigned_count(), 7); // Verify 600, 700 were assigned to positions 5, 6 assert_eq!(assignments.get_assigned(5).unwrap().0, 600); assert_eq!(assignments.get_assigned(6).unwrap().0, 700); // 800, 900 should be unassigned assert_eq!( assignments.unassigned_iter().collect::>(), vec![800, 900] ); } #[test] fn test_unassigned_blocks_with_new_blocks_all_assigned() { let mut seq = create_test_sequence(3, 0); let mut assignments = ExternalBlockAssignments::new(0); // Assign 3 + 1 excess assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.unassigned_iter().collect::>(), vec![400]); // Add 3 more blocks for token in 12..24u32 { seq.append_token(token).unwrap(); } assert_eq!(seq.blocks().len(), 6); // 400 (unassigned) + 500, 600 (new) should all fit assignments .extend_block_ids(vec![100, 200, 300, 400, 500, 600]) .unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 3..6); assert_eq!(assignments.assigned_count(), 6); assert_eq!(assignments.get_assigned(3).unwrap().0, 400); assert_eq!(assignments.get_assigned(4).unwrap().0, 500); assert_eq!(assignments.get_assigned(5).unwrap().0, 600); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_unassigned_blocks_no_new_space() { let seq = create_test_sequence(2, 0); let mut assignments = ExternalBlockAssignments::new(0); // Assign 2 + 2 excess assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400] ); // No new space — all new go to unassigned assignments .extend_block_ids(vec![100, 200, 300, 400, 500, 600]) .unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 2..2); assert_eq!(assignments.assigned_count(), 2); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400, 500, 600] ); } #[test] fn test_unassigned_blocks_partial_space() { let mut seq = create_test_sequence(3, 0); let mut assignments = ExternalBlockAssignments::new(0); // Assign 3 + 2 excess assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!( assignments.unassigned_iter().collect::>(), vec![400, 500] ); // Add 1 more block for token in 12..16u32 { seq.append_token(token).unwrap(); } assert_eq!(seq.blocks().len(), 4); // Only 1 spot: 400 assigned, 500+new remain unassigned assignments .extend_block_ids(vec![100, 200, 300, 400, 500, 600, 700, 800]) .unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 3..4); assert_eq!(assignments.get_assigned(3).unwrap().0, 400); assert_eq!( assignments.unassigned_iter().collect::>(), vec![500, 600, 700, 800] ); } #[test] fn test_multiple_rounds_of_unassigned_accumulation() { let mut seq = create_test_sequence(2, 0); let mut assignments = ExternalBlockAssignments::new(0); // Round 1: 2 assigned, 2 unassigned assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400] ); // Round 2: no space, add 2 more assignments .extend_block_ids(vec![100, 200, 300, 400, 500, 600]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400, 500, 600] ); // Round 3: still no space, add 1 more assignments .extend_block_ids(vec![100, 200, 300, 400, 500, 600, 700]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400, 500, 600, 700] ); // Now add space for 3 more blocks for token in 8..20u32 { seq.append_token(token).unwrap(); } assert_eq!(seq.blocks().len(), 5); // Flush — all IDs already known, first 3 unassigned (300, 400, 500) get assigned let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 2..5); assert_eq!(assignments.assigned_count(), 5); assert_eq!(assignments.get_assigned(2).unwrap().0, 300); assert_eq!(assignments.get_assigned(3).unwrap().0, 400); assert_eq!(assignments.get_assigned(4).unwrap().0, 500); assert_eq!( assignments.unassigned_iter().collect::>(), vec![600, 700] ); } #[test] fn test_unassigned_blocks_ordering_preserved() { let mut seq = create_test_sequence(1, 0); let mut assignments = ExternalBlockAssignments::new(0); // 10 assigned, rest unassigned in FIFO order assignments .extend_block_ids(vec![10, 20, 30, 40, 50, 60]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.get_assigned(0).unwrap().0, 10); assert_eq!( assignments.unassigned_iter().collect::>(), vec![20, 30, 40, 50, 60] ); // Add 2 more blocks for token in 4..12u32 { seq.append_token(token).unwrap(); } // Flush — all IDs already known, 20, 30 assigned (FIFO) assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.get_assigned(1).unwrap().0, 20); assert_eq!(assignments.get_assigned(2).unwrap().0, 30); assert_eq!( assignments.unassigned_iter().collect::>(), vec![40, 50, 60] ); // Add 1 more block + 1 new ID for token in 12..16u32 { seq.append_token(token).unwrap(); } assignments .extend_block_ids(vec![10, 20, 30, 40, 50, 60, 70]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // 40 should be assigned (FIFO), not 70 assert_eq!(assignments.get_assigned(3).unwrap().0, 40); assert_eq!( assignments.unassigned_iter().collect::>(), vec![50, 60, 70] ); } #[test] fn test_unassigned_blocks_with_partial_token_block() { let mut seq = create_test_sequence(2, 2); let mut assignments = ExternalBlockAssignments::new(0); // 2 assigned, 2 unassigned assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400] ); // Complete the partial block (need 2 more tokens) seq.append_token(10).unwrap(); seq.append_token(11).unwrap(); assert_eq!(seq.blocks().len(), 3); // 300 should be assigned first (FIFO), 500 is new assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 2..3); assert_eq!(assignments.get_assigned(2).unwrap().0, 300); assert_eq!( assignments.unassigned_iter().collect::>(), vec![400, 500] ); } #[test] fn test_unassigned_blocks_exactly_fill_new_space() { let mut seq = create_test_sequence(2, 0); let mut assignments = ExternalBlockAssignments::new(0); // 2 assigned, 3 unassigned assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400, 500] ); // Add exactly 3 more blocks for token in 8..20u32 { seq.append_token(token).unwrap(); } assert_eq!(seq.blocks().len(), 5); // Flush — all IDs already known, unassigned exactly fill the space let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 2..5); assert_eq!(assignments.assigned_count(), 5); assert_eq!(assignments.get_assigned(2).unwrap().0, 300); assert_eq!(assignments.get_assigned(3).unwrap().0, 400); assert_eq!(assignments.get_assigned(4).unwrap().0, 500); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_empty_unassigned_with_new_blocks() { let mut seq = create_test_sequence(3, 0); let mut assignments = ExternalBlockAssignments::new(0); // Assign exactly the right number assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.unassigned_count(), 0); // Add more space for token in 12..16u32 { seq.append_token(token).unwrap(); } // New blocks with no previous unassigned assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 3..4); assert_eq!(assignments.get_assigned(3).unwrap().0, 400); assert_eq!(assignments.unassigned_count(), 0); } // ========================================================================= // New tests: Prefix validation (OrderingViolation) // ========================================================================= #[test] fn test_ordering_violation_known_after_new() { let seq = create_test_sequence(3, 0); let mut assignments = ExternalBlockAssignments::new(0); // Assign first block assignments.extend_block_ids(vec![100]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // Now try to extend with [200, 100] — 200 is new, then 100 is known → error let result = assignments.extend_block_ids(vec![200, 100]); assert!(result.is_err()); match result.unwrap_err() { BlockSequenceError::OrderingViolation { known_id, new_id, known_index, first_new_index, } => { assert_eq!(known_id, 100); assert_eq!(new_id, 200); assert_eq!(known_index, 1); assert_eq!(first_new_index, 0); } other => panic!("expected OrderingViolation, got: {other:?}"), } // Verify atomicity — state unchanged assert_eq!(assignments.assigned_count(), 1); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_ordering_violation_no_state_change() { let seq = create_test_sequence(5, 0); let mut assignments = ExternalBlockAssignments::new(0); // Assign first 2 assignments.extend_block_ids(vec![100, 200]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // Error case: [300, 100] — 300 new, then 100 known let result = assignments.extend_block_ids(vec![300, 100]); assert!(result.is_err()); // State should be exactly as before the failed call assert_eq!(assignments.assigned_count(), 2); assert_eq!(assignments.unassigned_count(), 0); assert_eq!(assignments.get_assigned(0).unwrap().0, 100); assert_eq!(assignments.get_assigned(1).unwrap().0, 200); } // ========================================================================= // New tests: All duplicates (re-extending same IDs is a no-op) // ========================================================================= #[test] fn test_all_duplicates_noop() { let seq = create_test_sequence(3, 0); let mut assignments = ExternalBlockAssignments::new(0); // Assign all 3 assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // Re-extend with same IDs — should be a no-op (all known, skipped) assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); // No new unassigned means assign_pending produces an empty range let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 3..3); // Empty range, no new assignments assert_eq!(assignments.assigned_count(), 3); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_duplicates_with_unassigned() { let seq = create_test_sequence(2, 0); let mut assignments = ExternalBlockAssignments::new(0); // 2 assigned + 2 unassigned assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // Re-extend with same — no-op (300, 400 are known in unassigned) let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 2..2); assert_eq!(assignments.assigned_count(), 2); assert_eq!( assignments.unassigned_iter().collect::>(), vec![300, 400] ); } #[test] fn test_shorter_sequence_slice_does_not_shrink_assigned() { let seq = create_test_sequence(5, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); // Assign 3 blocks using the full sequence assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.assigned_count(), 3); // Re-extend with same IDs but a shorter slice — should be a no-op, // not shrink assigned or move anything to unassigned. let range = assignments.assign_pending(&seq.blocks()[..2]).unwrap(); assert_eq!(range, 3..3); assert_eq!(assignments.assigned_count(), 3); assert_eq!(assignments.unassigned_count(), 0); // All three original assignments are intact for (i, &expected_hash) in expected_hashes.iter().enumerate().take(3) { let (id, hash) = assignments.get_assigned(i).unwrap(); assert_eq!(id, (i + 1) * 100); assert_eq!(hash, expected_hash); } } // ========================================================================= // New tests: Offset // ========================================================================= #[test] fn test_offset_assignments() { let seq = create_test_sequence(10, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(5); assert_eq!(assignments.offset(), 5); assert_eq!(assignments.next_position(), 5); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); // Should assign to positions 5, 6, 7 assert_eq!(range, 0..3); assert_eq!(assignments.assigned_count(), 3); assert_eq!(assignments.next_position(), 8); let (id0, hash0) = assignments.get_assigned(0).unwrap(); assert_eq!(id0, 100); assert_eq!(hash0, expected_hashes[5]); let (id1, hash1) = assignments.get_assigned(1).unwrap(); assert_eq!(id1, 200); assert_eq!(hash1, expected_hashes[6]); let (id2, hash2) = assignments.get_assigned(2).unwrap(); assert_eq!(id2, 300); assert_eq!(hash2, expected_hashes[7]); } #[test] fn test_offset_with_excess() { let seq = create_test_sequence(8, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(5); // 3 blocks available at offset 5 (positions 5, 6, 7), 5 block_ids → 2 unassigned assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..3); assert_eq!(assignments.assigned_count(), 3); assert_eq!(assignments.get_assigned(0).unwrap().1, expected_hashes[5]); assert_eq!( assignments.unassigned_iter().collect::>(), vec![400, 500] ); } // ========================================================================= // New tests: Multi-instance (two ExternalBlockAssignments on same blocks) // ========================================================================= #[test] fn test_multi_instance_different_offsets() { let seq = create_test_sequence(10, 0); let expected_hashes = get_expected_hashes(&seq); let mut g1 = ExternalBlockAssignments::new(0); let mut g2 = ExternalBlockAssignments::new(5); // G1 assigns positions 0..3 g1.extend_block_ids(vec![10, 20, 30]).unwrap(); let range_g1 = g1.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_g1, 0..3); // G2 assigns positions 5..8 g2.extend_block_ids(vec![110, 120, 130]).unwrap(); let range_g2 = g2.assign_pending(seq.blocks()).unwrap(); assert_eq!(range_g2, 0..3); // Verify G1 hashes assert_eq!(g1.get_assigned(0).unwrap().1, expected_hashes[0]); assert_eq!(g1.get_assigned(1).unwrap().1, expected_hashes[1]); assert_eq!(g1.get_assigned(2).unwrap().1, expected_hashes[2]); // Verify G2 hashes assert_eq!(g2.get_assigned(0).unwrap().1, expected_hashes[5]); assert_eq!(g2.get_assigned(1).unwrap().1, expected_hashes[6]); assert_eq!(g2.get_assigned(2).unwrap().1, expected_hashes[7]); // They are independent — no cross-interference assert_eq!(g1.assigned_count(), 3); assert_eq!(g2.assigned_count(), 3); } // ========================================================================= // New tests: Token extension + flush // ========================================================================= #[test] fn test_token_extension_then_flush() { let mut seq = create_test_sequence(2, 2); let mut assignments = ExternalBlockAssignments::new(0); // 2 blocks available, 3 block_ids → 1 unassigned assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.assigned_count(), 2); assert_eq!(assignments.unassigned_iter().collect::>(), vec![300]); // Complete the partial block seq.append_token(10).unwrap(); seq.append_token(11).unwrap(); assert_eq!(seq.blocks().len(), 3); // Flush — unassigned 300 gets assigned let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 2..3); assert_eq!(assignments.assigned_count(), 3); assert_eq!(assignments.get_assigned(2).unwrap().0, 300); assert_eq!(assignments.unassigned_count(), 0); } #[test] fn test_extend_tokens_creates_new_blocks() { let mut seq = create_test_sequence(1, 0); let mut assignments = ExternalBlockAssignments::new(0); // Assign 1 block + 2 excess assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.assigned_count(), 1); assert_eq!( assignments.unassigned_iter().collect::>(), vec![200, 300] ); // Extend tokens to create 2 more blocks let new_range = seq.extend_tokens((4..12).collect()).unwrap(); assert!(new_range.is_some()); assert_eq!(seq.blocks().len(), 3); // Flush let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 1..3); assert_eq!(assignments.assigned_count(), 3); assert_eq!(assignments.get_assigned(1).unwrap().0, 200); assert_eq!(assignments.get_assigned(2).unwrap().0, 300); assert_eq!(assignments.unassigned_count(), 0); } // ========================================================================= // New tests: clear() // ========================================================================= #[test] fn test_clear_preserves_offset() { let seq = create_test_sequence(5, 0); let mut assignments = ExternalBlockAssignments::new(3); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.assigned_count(), 2); // positions 3, 4 assert_eq!(assignments.unassigned_count(), 1); // 300 assignments.clear(); assert_eq!(assignments.offset(), 3); assert_eq!(assignments.assigned_count(), 0); assert_eq!(assignments.unassigned_count(), 0); assert_eq!(assignments.next_position(), 3); } // ========================================================================= // New tests: Position mismatch // ========================================================================= #[test] fn test_assign_with_nonzero_offset() { // Create a sequence with blocks at positions 0, 1, 2. // Use offset=1 so assignments start at sequence_blocks[1]. // Verifies that the position validation passes for valid data // and offset-based indexing works correctly. let seq = create_test_sequence(3, 0); let mut assignments = ExternalBlockAssignments::new(1); assignments.extend_block_ids(vec![100, 200]).unwrap(); let range = assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..2); // Position 1 and 2 should be assigned assert_eq!( assignments.get_assigned(0).unwrap().1, get_expected_hashes(&seq)[1] ); assert_eq!( assignments.get_assigned(1).unwrap().1, get_expected_hashes(&seq)[2] ); } // ========================================================================= // New tests: contains() // ========================================================================= #[test] fn test_contains() { let seq = create_test_sequence(2, 0); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // 100, 200 are assigned assert!(assignments.contains(&100)); assert!(assignments.contains(&200)); // 300 is unassigned assert!(assignments.contains(&300)); // 400 is unknown assert!(!assignments.contains(&400)); } // ========================================================================= // New tests: assigned_iter() // ========================================================================= #[test] fn test_assigned_iter() { let seq = create_test_sequence(3, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); let assigned: Vec<(BlockId, SequenceHash)> = assignments.assigned_iter().collect(); assert_eq!(assigned.len(), 3); assert_eq!(assigned[0], (100, expected_hashes[0])); assert_eq!(assigned[1], (200, expected_hashes[1])); assert_eq!(assigned[2], (300, expected_hashes[2])); } // ========================================================================= // New tests: BlockSequence // ========================================================================= #[test] fn test_block_sequence_new() { let seq = BlockSequence::new(vec![0, 1, 2, 3, 4, 5, 6, 7], 4, None); assert_eq!(seq.blocks().len(), 2); assert_eq!(seq.block_size(), 4); assert_eq!(seq.total_tokens(), 8); } #[test] fn test_block_sequence_all_sequence_hashes() { let seq = BlockSequence::new(vec![0, 1, 2, 3, 4, 5, 6, 7], 4, None); let hashes = seq.all_sequence_hashes(); assert_eq!(hashes.len(), 2); // Hashes should match what we'd get from blocks directly let expected: Vec<_> = seq .blocks() .iter() .map(|b| b.kvbm_sequence_hash()) .collect(); assert_eq!(hashes, expected); } #[test] fn test_block_sequence_extend_tokens() { let mut seq = BlockSequence::new(vec![0, 1, 2, 3], 4, None); assert_eq!(seq.blocks().len(), 1); let result = seq.extend_tokens(vec![4, 5, 6, 7, 8, 9]).unwrap(); assert_eq!(result, Some(1..2)); // One more block completed assert_eq!(seq.blocks().len(), 2); assert_eq!(seq.total_tokens(), 10); // 10 total, 2 partial remaining } #[test] fn test_block_sequence_append_token() { let mut seq = BlockSequence::new(vec![0, 1, 2], 4, None); assert_eq!(seq.blocks().len(), 0); // Doesn't complete a block let result = seq.append_token(3).unwrap(); assert_eq!(result, Some(0)); // Completed first block assert_eq!(seq.blocks().len(), 1); } #[test] fn test_block_sequence_with_salt() { let seq1 = BlockSequence::new(vec![0, 1, 2, 3], 4, None); let seq2 = BlockSequence::new(vec![0, 1, 2, 3], 4, Some(42)); // Different salts should produce different hashes let hashes1 = seq1.all_sequence_hashes(); let hashes2 = seq2.all_sequence_hashes(); assert_ne!(hashes1, hashes2); } // ========================================================================= // Positional access methods // ========================================================================= #[test] fn test_assigned_positions_empty() { let assignments = ExternalBlockAssignments::new(0); assert_eq!(assignments.assigned_positions(), 0..0); } #[test] fn test_assigned_positions_with_offset() { let seq = create_test_sequence(10, 0); let mut assignments = ExternalBlockAssignments::new(3); assignments.extend_block_ids(vec![100, 200]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.assigned_positions(), 3..5); } #[test] fn test_pending_positions_empty() { let assignments = ExternalBlockAssignments::new(0); assert_eq!(assignments.pending_positions(), 0..0); } #[test] fn test_pending_positions_with_assigned_and_unassigned() { let seq = create_test_sequence(2, 0); let mut assignments = ExternalBlockAssignments::new(0); assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // 2 assigned (positions 0,1), 2 pending (positions 2,3) assert_eq!(assignments.pending_positions(), 2..4); } #[test] fn test_pending_positions_with_offset() { let seq = create_test_sequence(10, 0); let mut assignments = ExternalBlockAssignments::new(5); assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // offset=5, 4 block_ids, 5 blocks available at positions 5..10 → 4 assigned // no pending assert_eq!(assignments.assigned_positions(), 5..9); assert_eq!(assignments.pending_positions(), 9..9); } #[test] fn test_get_at_position_in_range() { let seq = create_test_sequence(5, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(2); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // Assigned at positions 2, 3, 4 let (id, hash) = assignments.get_at_position(2).unwrap(); assert_eq!(id, 100); assert_eq!(hash, expected_hashes[2]); let (id, hash) = assignments.get_at_position(4).unwrap(); assert_eq!(id, 300); assert_eq!(hash, expected_hashes[4]); } #[test] fn test_get_at_position_out_of_range() { let seq = create_test_sequence(5, 0); let mut assignments = ExternalBlockAssignments::new(2); assignments.extend_block_ids(vec![100, 200]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // Assigned at positions 2, 3 assert!(assignments.get_at_position(0).is_none()); // before offset assert!(assignments.get_at_position(1).is_none()); // before offset assert!(assignments.get_at_position(4).is_none()); // past end assert!(assignments.get_at_position(100).is_none()); // way past end } #[test] fn test_get_pending_at_position_in_range() { let seq = create_test_sequence(2, 0); let mut assignments = ExternalBlockAssignments::new(0); assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // 2 assigned, 3 pending at positions 2, 3, 4 assert_eq!(assignments.get_pending_at_position(2), Some(300)); assert_eq!(assignments.get_pending_at_position(3), Some(400)); assert_eq!(assignments.get_pending_at_position(4), Some(500)); } #[test] fn test_get_pending_at_position_out_of_range() { let seq = create_test_sequence(2, 0); let mut assignments = ExternalBlockAssignments::new(0); assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); // 2 assigned, 2 pending at positions 2, 3 assert!(assignments.get_pending_at_position(0).is_none()); // assigned range assert!(assignments.get_pending_at_position(1).is_none()); // assigned range assert!(assignments.get_pending_at_position(4).is_none()); // past end assert!(assignments.get_pending_at_position(100).is_none()); } // ========================================================================= // zip_assigned tests // ========================================================================= use super::{zip_assigned, zip_assigned_pending}; #[test] fn test_zip_assigned_full_overlap() { let seq = create_test_sequence(5, 0); let mut a = ExternalBlockAssignments::new(0); let mut b = ExternalBlockAssignments::new(0); a.extend_block_ids(vec![10, 20, 30]).unwrap(); a.assign_pending(seq.blocks()).unwrap(); b.extend_block_ids(vec![110, 120, 130]).unwrap(); b.assign_pending(seq.blocks()).unwrap(); let pairs = zip_assigned(&a, &b); assert_eq!(pairs, vec![(0, 10, 110), (1, 20, 120), (2, 30, 130)]); } #[test] fn test_zip_assigned_partial_overlap() { let seq = create_test_sequence(10, 0); let mut a = ExternalBlockAssignments::new(0); let mut b = ExternalBlockAssignments::new(2); a.extend_block_ids(vec![10, 20, 30, 40]).unwrap(); a.assign_pending(seq.blocks()).unwrap(); b.extend_block_ids(vec![110, 120, 130]).unwrap(); b.assign_pending(seq.blocks()).unwrap(); // a: positions 0..4, b: positions 2..5 → overlap 2..4 let pairs = zip_assigned(&a, &b); assert_eq!(pairs, vec![(2, 30, 110), (3, 40, 120)]); } #[test] fn test_zip_assigned_no_overlap() { let seq = create_test_sequence(10, 0); let mut a = ExternalBlockAssignments::new(0); let mut b = ExternalBlockAssignments::new(5); a.extend_block_ids(vec![10, 20]).unwrap(); a.assign_pending(seq.blocks()).unwrap(); b.extend_block_ids(vec![110, 120]).unwrap(); b.assign_pending(seq.blocks()).unwrap(); // a: positions 0..2, b: positions 5..7 → no overlap let pairs = zip_assigned(&a, &b); assert!(pairs.is_empty()); } #[test] fn test_zip_assigned_either_empty() { let seq = create_test_sequence(5, 0); let a = ExternalBlockAssignments::new(0); let mut b = ExternalBlockAssignments::new(0); b.extend_block_ids(vec![110, 120]).unwrap(); b.assign_pending(seq.blocks()).unwrap(); assert!(zip_assigned(&a, &b).is_empty()); assert!(zip_assigned(&b, &a).is_empty()); } #[test] fn test_zip_assigned_both_empty() { let a = ExternalBlockAssignments::new(0); let b = ExternalBlockAssignments::new(0); assert!(zip_assigned(&a, &b).is_empty()); } // ========================================================================= // zip_assigned_pending tests // ========================================================================= #[test] fn test_zip_assigned_pending_full_overlap() { let seq = create_test_sequence(5, 0); let mut src = ExternalBlockAssignments::new(2); let mut dst = ExternalBlockAssignments::new(0); // src: assigned at positions 2, 3, 4 src.extend_block_ids(vec![10, 20, 30]).unwrap(); src.assign_pending(seq.blocks()).unwrap(); // dst: only sees first 2 blocks → 2 assigned at 0, 1 + 3 pending at 2, 3, 4 dst.extend_block_ids(vec![110, 120, 130, 140, 150]).unwrap(); dst.assign_pending(&seq.blocks()[..2]).unwrap(); assert_eq!(dst.assigned_count(), 2); assert_eq!(dst.unassigned_count(), 3); let pairs = zip_assigned_pending(&src, &dst); assert_eq!(pairs, vec![(2, 10, 130), (3, 20, 140), (4, 30, 150)]); } #[test] fn test_zip_assigned_pending_partial_overlap() { let seq = create_test_sequence(10, 0); let mut src = ExternalBlockAssignments::new(2); let mut dst = ExternalBlockAssignments::new(0); // src: assigned at 2, 3, 4, 5 src.extend_block_ids(vec![10, 20, 30, 40]).unwrap(); src.assign_pending(seq.blocks()).unwrap(); // dst: only sees first 3 blocks → 3 assigned at 0, 1, 2 + 2 pending at 3, 4 dst.extend_block_ids(vec![110, 120, 130, 140, 150]).unwrap(); dst.assign_pending(&seq.blocks()[..3]).unwrap(); assert_eq!(dst.assigned_count(), 3); assert_eq!(dst.unassigned_count(), 2); // src assigned: 2..6, dst pending: 3..5 → overlap 3..5 let pairs = zip_assigned_pending(&src, &dst); assert_eq!(pairs, vec![(3, 20, 140), (4, 30, 150)]); } #[test] fn test_zip_assigned_pending_no_overlap() { let seq = create_test_sequence(10, 0); let mut src = ExternalBlockAssignments::new(0); let mut dst = ExternalBlockAssignments::new(0); // src: assigned at 0, 1 src.extend_block_ids(vec![10, 20]).unwrap(); src.assign_pending(seq.blocks()).unwrap(); // dst: 5 assigned at 0..5 + 2 pending at 5, 6 dst.extend_block_ids(vec![110, 120, 130, 140, 150, 160, 170]) .unwrap(); dst.assign_pending(seq.blocks()).unwrap(); // src assigned: 0..2, dst pending: 5..7 → no overlap let pairs = zip_assigned_pending(&src, &dst); assert!(pairs.is_empty()); } #[test] fn test_zip_assigned_pending_either_empty() { let seq = create_test_sequence(5, 0); let src = ExternalBlockAssignments::new(0); // no assigned let mut dst = ExternalBlockAssignments::new(0); dst.extend_block_ids(vec![110, 120, 130]).unwrap(); dst.assign_pending(seq.blocks()).unwrap(); // src has no assigned blocks assert!(zip_assigned_pending(&src, &dst).is_empty()); // dst has no pending blocks let mut src2 = ExternalBlockAssignments::new(0); src2.extend_block_ids(vec![10, 20]).unwrap(); src2.assign_pending(seq.blocks()).unwrap(); let dst2 = ExternalBlockAssignments::new(0); assert!(zip_assigned_pending(&src2, &dst2).is_empty()); } // ========================================================================= // Onboard scenario test (G2 → G1 transfer) // ========================================================================= #[test] fn test_onboard_scenario_g2_to_g1() { // Sequence: 5 complete blocks (positions 0..5) let seq = create_test_sequence(5, 0); // Step 1 — G1 matches positions 0..2 let mut g1 = ExternalBlockAssignments::new(0); g1.extend_block_ids(vec![1000, 1001]).unwrap(); g1.assign_pending(seq.blocks()).unwrap(); assert_eq!(g1.assigned_positions(), 0..2); assert_eq!(g1.assigned_count(), 2); // Step 2 — G2 matches positions 2..5 let mut g2 = ExternalBlockAssignments::new(2); g2.extend_block_ids(vec![2002, 2003, 2004]).unwrap(); g2.assign_pending(seq.blocks()).unwrap(); assert_eq!(g2.assigned_positions(), 2..5); assert_eq!(g2.assigned_count(), 3); // Step 3 — Queue new block_ids for G1 (don't assign yet — blocks not transferred) g1.extend_block_ids(vec![1000, 1001, 1002, 1003, 1004]) .unwrap(); assert_eq!(g1.assigned_count(), 2); // still only 2 assigned assert_eq!(g1.unassigned_count(), 3); // 1002, 1003, 1004 pending assert_eq!(g1.pending_positions(), 2..5); // Step 4 — Compute onboard pairs (before physical transfer) let pairs = zip_assigned_pending(&g2, &g1); assert_eq!( pairs, vec![(2, 2002, 1002), (3, 2003, 1003), (4, 2004, 1004)] ); // Step 5 — After physical transfer, assign pending against the full sequence g1.assign_pending(seq.blocks()).unwrap(); assert_eq!(g1.assigned_count(), 5); assert_eq!(g1.unassigned_count(), 0); assert_eq!(g1.assigned_positions(), 0..5); // Step 6 — Verify assigned overlap matches let assigned_pairs = zip_assigned(&g2, &g1); assert_eq!( assigned_pairs, vec![(2, 2002, 1002), (3, 2003, 1003), (4, 2004, 1004)] ); } // ========================================================================= // New tests: stage_pending + commit_staged (two-step assignment) // ========================================================================= #[test] fn test_stage_pending_basic() { let seq = create_test_sequence(3, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); // Stage — should move unassigned → staged with hashes let staged_range = assignments.stage_pending(seq.blocks()).unwrap(); assert_eq!(staged_range, 0..3); assert_eq!(assignments.staged_count(), 3); assert_eq!(assignments.unassigned_count(), 0); assert_eq!(assignments.assigned_count(), 0); // Verify staged contents for (i, expected_hash) in expected_hashes.iter().enumerate() { let (id, hash) = assignments.get_staged(i).unwrap(); assert_eq!(id, (i + 1) * 100); assert_eq!(hash, *expected_hash); } } #[test] fn test_commit_staged_basic() { let seq = create_test_sequence(3, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); // Stage then commit assignments.stage_pending(seq.blocks()).unwrap(); let assigned_range = assignments.commit_staged(); assert_eq!(assigned_range, 0..3); assert_eq!(assignments.assigned_count(), 3); assert_eq!(assignments.staged_count(), 0); assert_eq!(assignments.unassigned_count(), 0); // Verify assigned contents for (i, expected_hash) in expected_hashes.iter().enumerate() { let (id, hash) = assignments.get_assigned(i).unwrap(); assert_eq!(id, (i + 1) * 100); assert_eq!(hash, *expected_hash); } } #[test] fn test_stage_pending_partial() { let seq = create_test_sequence(2, 0); let mut assignments = ExternalBlockAssignments::new(0); // 5 block_ids but only 2 sequence blocks assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); let staged_range = assignments.stage_pending(seq.blocks()).unwrap(); assert_eq!(staged_range, 0..2); assert_eq!(assignments.staged_count(), 2); assert_eq!(assignments.unassigned_count(), 3); } #[test] fn test_stage_pending_then_commit_then_stage_more() { let seq = create_test_sequence(5, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); // Stage first 3 (only 3 blocks available at first) let range1 = assignments.stage_pending(&seq.blocks()[..3]).unwrap(); assert_eq!(range1, 0..3); assert_eq!(assignments.staged_count(), 3); assert_eq!(assignments.unassigned_count(), 2); // Commit them let assigned_range = assignments.commit_staged(); assert_eq!(assigned_range, 0..3); assert_eq!(assignments.assigned_count(), 3); assert_eq!(assignments.staged_count(), 0); // Stage the remaining 2 let range2 = assignments.stage_pending(seq.blocks()).unwrap(); assert_eq!(range2, 0..2); assert_eq!(assignments.staged_count(), 2); assert_eq!(assignments.unassigned_count(), 0); // Commit them let assigned_range2 = assignments.commit_staged(); assert_eq!(assigned_range2, 3..5); assert_eq!(assignments.assigned_count(), 5); // Verify all assigned for (i, expected_hash) in expected_hashes.iter().enumerate() { let (id, hash) = assignments.get_assigned(i).unwrap(); assert_eq!(id, (i + 1) * 100); assert_eq!(hash, *expected_hash); } } #[test] fn test_stage_pending_empty_unassigned() { let seq = create_test_sequence(3, 0); let mut assignments = ExternalBlockAssignments::new(0); // No block_ids → nothing to stage let range = assignments.stage_pending(seq.blocks()).unwrap(); assert_eq!(range, 0..0); assert_eq!(assignments.staged_count(), 0); } #[test] fn test_commit_staged_empty() { let mut assignments = ExternalBlockAssignments::new(0); // Nothing staged → empty range let range = assignments.commit_staged(); assert_eq!(range, 0..0); } // ========================================================================= // New tests: staged_positions // ========================================================================= #[test] fn test_staged_positions_empty() { let assignments = ExternalBlockAssignments::new(0); assert_eq!(assignments.staged_positions(), 0..0); } #[test] fn test_staged_positions_after_stage() { let seq = create_test_sequence(5, 0); let mut assignments = ExternalBlockAssignments::new(0); // Pre-assign first 2 assignments.extend_block_ids(vec![100, 200]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.assigned_count(), 2); // Add 3 more and stage them assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); assignments.stage_pending(seq.blocks()).unwrap(); assert_eq!(assignments.staged_count(), 3); // Staged positions should be 2..5 assert_eq!(assignments.staged_positions(), 2..5); assert_eq!(assignments.assigned_positions(), 0..2); } #[test] fn test_staged_positions_with_offset() { let seq = create_test_sequence(10, 0); let mut assignments = ExternalBlockAssignments::new(3); assignments.extend_block_ids(vec![100, 200]).unwrap(); assignments.assign_pending(seq.blocks()).unwrap(); assert_eq!(assignments.assigned_count(), 2); assignments .extend_block_ids(vec![100, 200, 300, 400]) .unwrap(); assignments.stage_pending(seq.blocks()).unwrap(); assert_eq!(assignments.staged_count(), 2); // offset=3, 2 assigned → staged starts at 5, 2 staged → staged ends at 7 assert_eq!(assignments.staged_positions(), 5..7); } // ========================================================================= // New tests: staged_iter // ========================================================================= #[test] fn test_staged_iter() { let seq = create_test_sequence(3, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.stage_pending(seq.blocks()).unwrap(); let staged: Vec<(BlockId, SequenceHash)> = assignments.staged_iter().collect(); assert_eq!(staged.len(), 3); assert_eq!(staged[0], (100, expected_hashes[0])); assert_eq!(staged[1], (200, expected_hashes[1])); assert_eq!(staged[2], (300, expected_hashes[2])); } // ========================================================================= // New tests: take_staged // ========================================================================= #[test] fn test_take_staged() { let seq = create_test_sequence(3, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); assignments.stage_pending(seq.blocks()).unwrap(); assert_eq!(assignments.staged_count(), 3); let drained = assignments.take_staged(); assert_eq!(drained.len(), 3); assert_eq!(assignments.staged_count(), 0); assert_eq!(drained[0], (100, expected_hashes[0])); assert_eq!(drained[1], (200, expected_hashes[1])); assert_eq!(drained[2], (300, expected_hashes[2])); } // ========================================================================= // New tests: next_position with staged blocks // ========================================================================= #[test] fn test_next_position_accounts_for_staged() { let seq = create_test_sequence(5, 0); let mut assignments = ExternalBlockAssignments::new(0); assignments.extend_block_ids(vec![100, 200, 300]).unwrap(); // Before staging assert_eq!(assignments.next_position(), 0); // After staging (3 staged, 0 assigned) assignments.stage_pending(seq.blocks()).unwrap(); assert_eq!(assignments.next_position(), 3); assert_eq!(assignments.assigned_count(), 0); assert_eq!(assignments.staged_count(), 3); // After committing (0 staged, 3 assigned) assignments.commit_staged(); assert_eq!(assignments.next_position(), 3); assert_eq!(assignments.assigned_count(), 3); assert_eq!(assignments.staged_count(), 0); } #[test] fn test_pending_positions_accounts_for_staged() { let seq = create_test_sequence(3, 0); let mut assignments = ExternalBlockAssignments::new(0); assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); // Stage 3, leaving 2 unassigned assignments.stage_pending(seq.blocks()).unwrap(); assert_eq!(assignments.staged_count(), 3); assert_eq!(assignments.unassigned_count(), 2); // Pending positions should be after staged blocks assert_eq!(assignments.pending_positions(), 3..5); } // ========================================================================= // New tests: extend_assigned (direct insert to assigned) // ========================================================================= #[test] fn test_extend_assigned_basic() { let seq = create_test_sequence(3, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); // Build (BlockId, SequenceHash) pairs let items: Vec<(BlockId, SequenceHash)> = vec![ (100, expected_hashes[0]), (200, expected_hashes[1]), (300, expected_hashes[2]), ]; let count = assignments.extend_assigned(items).unwrap(); assert_eq!(count, 3); assert_eq!(assignments.assigned_count(), 3); assert_eq!(assignments.staged_count(), 0); assert_eq!(assignments.unassigned_count(), 0); for (i, expected_hash) in expected_hashes.iter().enumerate() { let (id, hash) = assignments.get_assigned(i).unwrap(); assert_eq!(id, (i + 1) * 100); assert_eq!(hash, *expected_hash); } } #[test] fn test_extend_assigned_then_stage_pending() { let seq = create_test_sequence(5, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); // Direct-assign first 2 let items: Vec<(BlockId, SequenceHash)> = vec![(100, expected_hashes[0]), (200, expected_hashes[1])]; assignments.extend_assigned(items).unwrap(); assert_eq!(assignments.assigned_count(), 2); // Enqueue 3 more, stage, commit assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); assignments.stage_pending(seq.blocks()).unwrap(); assignments.commit_staged(); assert_eq!(assignments.assigned_count(), 5); // Verify all in order for (i, expected_hash) in expected_hashes.iter().enumerate() { let (id, hash) = assignments.get_assigned(i).unwrap(); assert_eq!(id, (i + 1) * 100); assert_eq!(hash, *expected_hash); } } #[test] fn test_extend_assigned_duplicate_detection() { let seq = create_test_sequence(2, 0); let expected_hashes = get_expected_hashes(&seq); let mut assignments = ExternalBlockAssignments::new(0); // Assign 1 block assignments .extend_assigned(vec![(100, expected_hashes[0])]) .unwrap(); // Try to assign with duplicate block_id → error let result = assignments.extend_assigned(vec![(100, expected_hashes[1])]); assert!(result.is_err()); match result.unwrap_err() { BlockSequenceError::DuplicateBlockId { block_id } => { assert_eq!(block_id, 100); } other => panic!("expected DuplicateBlockId, got: {other:?}"), } // Original assignment unchanged assert_eq!(assignments.assigned_count(), 1); } #[test] fn test_extend_assigned_empty() { let mut assignments = ExternalBlockAssignments::new(0); let count = assignments.extend_assigned(Vec::new()).unwrap(); assert_eq!(count, 0); assert_eq!(assignments.assigned_count(), 0); } // ========================================================================= // New tests: contains checks all three collections // ========================================================================= #[test] fn test_contains_with_staged() { let seq = create_test_sequence(3, 0); let mut assignments = ExternalBlockAssignments::new(0); assignments .extend_block_ids(vec![100, 200, 300, 400, 500]) .unwrap(); // Assign first 2 assignments.assign_pending(&seq.blocks()[..2]).unwrap(); assert_eq!(assignments.assigned_count(), 2); // Stage 1 (block at position 2) assignments.stage_pending(seq.blocks()).unwrap(); assert_eq!(assignments.staged_count(), 1); assert_eq!(assignments.unassigned_count(), 2); // 100, 200 in assigned assert!(assignments.contains(&100)); assert!(assignments.contains(&200)); // 300 in staged assert!(assignments.contains(&300)); // 400, 500 in unassigned assert!(assignments.contains(&400)); assert!(assignments.contains(&500)); // 600 nowhere assert!(!assignments.contains(&600)); } // ========================================================================= // New tests: Debug impl // ========================================================================= #[test] fn test_debug_impl() { let assignments = ExternalBlockAssignments::new(5); let debug_str = format!("{assignments:?}"); assert!(debug_str.contains("ExternalBlockAssignments")); assert!(debug_str.contains("offset")); }