use crate::record::distributed_region::ReplicaInfo;
use crate::security::SecurityContext;
use crate::types::symbol::Symbol;
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssignmentStrategy {
Full,
Striped,
MinimumK,
Weighted,
}
#[derive(Debug)]
pub struct SymbolAssigner {
strategy: AssignmentStrategy,
}
impl SymbolAssigner {
#[inline]
#[must_use]
pub const fn new(strategy: AssignmentStrategy) -> Self {
Self { strategy }
}
#[inline]
#[must_use]
pub const fn strategy(&self) -> AssignmentStrategy {
self.strategy
}
#[must_use]
pub fn assign(
&self,
symbols: &[Symbol],
replicas: &[ReplicaInfo],
security_context: &SecurityContext,
region_id: Option<&str>,
k: u16,
) -> Vec<ReplicaAssignment> {
if replicas.is_empty() || symbols.is_empty() {
return Vec::new();
}
let authorized_replicas: Vec<&ReplicaInfo> = replicas
.iter()
.filter(|replica| security_context.is_replica_authorized(&replica.id, region_id))
.collect();
if authorized_replicas.is_empty() {
return Vec::new();
}
match self.strategy {
AssignmentStrategy::Full => Self::assign_full(symbols, &authorized_replicas, k),
AssignmentStrategy::Striped => Self::assign_striped(symbols, &authorized_replicas, k),
AssignmentStrategy::MinimumK => {
Self::assign_minimum_k(symbols, &authorized_replicas, k)
}
AssignmentStrategy::Weighted => Self::assign_weighted(symbols, &authorized_replicas, k),
}
}
fn assign_full(
symbols: &[Symbol],
replicas: &[&ReplicaInfo],
k: u16,
) -> Vec<ReplicaAssignment> {
let k_usize = k as usize;
let all_indices: Vec<usize> = (0..symbols.len()).collect();
replicas
.iter()
.map(|r| ReplicaAssignment::from_indices(r, all_indices.clone(), k_usize))
.collect()
}
fn assign_striped(
symbols: &[Symbol],
replicas: &[&ReplicaInfo],
k: u16,
) -> Vec<ReplicaAssignment> {
let k_usize = k as usize;
let n = replicas.len();
let mut assignments: Vec<Vec<usize>> = vec![Vec::new(); n];
for (i, _) in symbols.iter().enumerate() {
assignments[i % n].push(i);
}
replicas
.iter()
.enumerate()
.map(|(i, r)| ReplicaAssignment::from_indices(r, assignments[i].clone(), k_usize)) .collect()
}
fn assign_minimum_k(
symbols: &[Symbol],
replicas: &[&ReplicaInfo],
k: u16,
) -> Vec<ReplicaAssignment> {
let k_usize = k as usize;
replicas
.iter()
.enumerate()
.map(|(replica_idx, r)| {
let mut indices: BTreeSet<usize> = BTreeSet::new();
let symbol_len = symbols.len();
if symbol_len > 0 {
for j in 0..std::cmp::min(k_usize, symbol_len) {
let idx =
(replica_idx * symbol_len / replicas.len().max(1) + j) % symbol_len;
indices.insert(idx);
}
let mut fill = 0;
while indices.len() < k_usize && fill < symbol_len {
indices.insert(fill);
fill += 1;
}
}
let symbol_indices: Vec<usize> = indices.into_iter().collect();
ReplicaAssignment::from_indices(r, symbol_indices, k_usize)
})
.collect()
}
fn assign_weighted(
symbols: &[Symbol],
replicas: &[&ReplicaInfo],
k: u16,
) -> Vec<ReplicaAssignment> {
let mut assignments: Vec<Vec<usize>> = vec![Vec::new(); replicas.len()];
let mut assigned_counts = vec![0_u64; replicas.len()];
for (symbol_idx, _) in symbols.iter().enumerate() {
let mut best_idx = 0usize;
let mut best_projected_total =
u64::from(replicas[best_idx].symbol_count) + assigned_counts[best_idx];
for candidate_idx in 1..replicas.len() {
let candidate_projected_total = u64::from(replicas[candidate_idx].symbol_count)
+ assigned_counts[candidate_idx];
if candidate_projected_total < best_projected_total
|| (candidate_projected_total == best_projected_total
&& assigned_counts[candidate_idx] < assigned_counts[best_idx])
{
best_idx = candidate_idx;
best_projected_total = candidate_projected_total;
}
}
assignments[best_idx].push(symbol_idx);
assigned_counts[best_idx] += 1;
}
replicas
.iter()
.enumerate()
.map(|(replica_idx, replica)| {
ReplicaAssignment::from_indices(
replica,
assignments[replica_idx].clone(),
k as usize,
)
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplicaAssignment {
pub replica_id: String,
pub symbol_indices: Vec<usize>,
pub can_decode: bool,
}
impl ReplicaAssignment {
fn from_indices(replica: &ReplicaInfo, symbol_indices: Vec<usize>, k_usize: usize) -> Self {
Self {
replica_id: replica.id.clone(),
can_decode: symbol_indices.len() >= k_usize,
symbol_indices,
}
}
}
#[cfg(all(test, feature = "legacy-internal-test-harnesses"))]
mod tests {
#![allow(
clippy::pedantic,
clippy::nursery,
clippy::expect_fun_call,
clippy::map_unwrap_or,
clippy::cast_possible_wrap,
clippy::future_not_send
)]
use super::*;
fn create_test_replicas(count: usize) -> Vec<ReplicaInfo> {
(0..count)
.map(|i| ReplicaInfo::new(&format!("r{i}"), &format!("addr{i}")))
.collect()
}
fn create_test_replicas_with_symbol_counts(symbol_counts: &[u32]) -> Vec<ReplicaInfo> {
symbol_counts
.iter()
.enumerate()
.map(|(i, &symbol_count)| {
let mut replica = ReplicaInfo::new(&format!("r{i}"), &format!("addr{i}"));
replica.symbol_count = symbol_count;
replica
})
.collect()
}
fn create_test_symbols(count: usize) -> Vec<Symbol> {
(0..count)
.map(|i| Symbol::new_for_test(1, 0, i as u32, &[0u8; 128]))
.collect()
}
trait AuthorizedAssignForTests {
fn assign_authorized(
&self,
symbols: &[Symbol],
replicas: &[ReplicaInfo],
k: u16,
) -> Vec<ReplicaAssignment>;
}
impl AuthorizedAssignForTests for SymbolAssigner {
fn assign_authorized(
&self,
symbols: &[Symbol],
replicas: &[ReplicaInfo],
k: u16,
) -> Vec<ReplicaAssignment> {
let mut security_context = SecurityContext::for_testing(42);
for replica in replicas {
security_context
.authorize_replica(&replica.id, None)
.expect("test replica identifiers should be authorizable");
}
SymbolAssigner::assign(self, symbols, replicas, &security_context, None, k)
}
}
#[test]
fn full_assignment_all_replicas_get_all() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let symbols = create_test_symbols(10);
let replicas = create_test_replicas(3);
let security_context = SecurityContext::for_testing(42);
let assignments = assigner.assign(&symbols, &replicas, &security_context, None, 5);
assert_eq!(assignments.len(), 3);
for assignment in &assignments {
assert_eq!(assignment.symbol_indices.len(), 10);
assert!(assignment.can_decode);
}
}
#[test]
fn striped_assignment_distributes_evenly() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Striped);
let symbols = create_test_symbols(9);
let replicas = create_test_replicas(3);
let assignments = assigner.assign_authorized(&symbols, &replicas, 5);
for assignment in &assignments {
assert_eq!(assignment.symbol_indices.len(), 3);
}
}
#[test]
fn striped_no_overlap() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Striped);
let symbols = create_test_symbols(12);
let replicas = create_test_replicas(3);
let assignments = assigner.assign_authorized(&symbols, &replicas, 4);
let mut all: Vec<usize> = Vec::new();
for a in &assignments {
all.extend_from_slice(&a.symbol_indices);
}
all.sort_unstable();
all.dedup();
assert_eq!(all.len(), 12, "all symbols should be assigned exactly once");
}
#[test]
fn minimum_k_assignment() {
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let symbols = create_test_symbols(15);
let replicas = create_test_replicas(3);
let assignments = assigner.assign_authorized(&symbols, &replicas, 10);
for assignment in &assignments {
assert!(
assignment.symbol_indices.len() >= 10,
"replica {} got {} symbols, need >= 10",
assignment.replica_id,
assignment.symbol_indices.len()
);
assert!(assignment.can_decode);
}
}
#[test]
fn empty_symbols_returns_empty() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let symbols: Vec<Symbol> = vec![];
let replicas = create_test_replicas(3);
let assignments = assigner.assign_authorized(&symbols, &replicas, 5);
assert!(assignments.is_empty());
}
#[test]
fn empty_replicas_returns_empty() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let symbols = create_test_symbols(10);
let replicas: Vec<ReplicaInfo> = vec![];
let assignments = assigner.assign_authorized(&symbols, &replicas, 5);
assert!(assignments.is_empty());
}
#[test]
fn weighted_prefers_less_loaded_replicas() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Weighted);
let symbols = create_test_symbols(18);
let replicas = create_test_replicas_with_symbol_counts(&[0, 4, 9]);
let assignments = assigner.assign_authorized(&symbols, &replicas, 3);
let counts: Vec<_> = assignments
.iter()
.map(|assignment| assignment.symbol_indices.len())
.collect();
assert_eq!(counts.iter().sum::<usize>(), symbols.len());
assert!(
counts[0] > counts[1],
"lighter replica should get more symbols"
);
assert!(
counts[1] > counts[2],
"heaviest replica should get the fewest symbols"
);
let mut all_indices: Vec<_> = assignments
.iter()
.flat_map(|assignment| assignment.symbol_indices.iter().copied())
.collect();
all_indices.sort_unstable();
all_indices.dedup();
assert_eq!(
all_indices.len(),
symbols.len(),
"weighted assignment must not duplicate symbols"
);
}
#[test]
fn weighted_equal_loads_balance_like_striping() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Weighted);
let symbols = create_test_symbols(10);
let replicas = create_test_replicas_with_symbol_counts(&[2, 2, 2]);
let assignments = assigner.assign_authorized(&symbols, &replicas, 3);
let counts: Vec<_> = assignments
.iter()
.map(|assignment| assignment.symbol_indices.len())
.collect();
let min = counts.iter().copied().min().unwrap_or(0);
let max = counts.iter().copied().max().unwrap_or(0);
assert_eq!(counts.iter().sum::<usize>(), symbols.len());
assert!(
max - min <= 1,
"equal loads should distribute nearly evenly, got {counts:?}"
);
}
#[test]
fn metamorphic_weighted_equal_loads_match_striped_assignment() {
let weighted = SymbolAssigner::new(AssignmentStrategy::Weighted);
let striped = SymbolAssigner::new(AssignmentStrategy::Striped);
let replicas = create_test_replicas_with_symbol_counts(&[7, 7, 7, 7]);
for symbol_count in [1_usize, 2, 3, 4, 7, 11, 17] {
let symbols = create_test_symbols(symbol_count);
let weighted_plan = weighted.assign_authorized(&symbols, &replicas, 99);
let striped_plan = striped.assign_authorized(&symbols, &replicas, 99);
assert_eq!(
weighted_plan, striped_plan,
"with equal existing loads, weighted assignment should reduce to striped \
round-robin for {symbol_count} symbols"
);
}
}
#[test]
fn weighted_avoids_heavier_replica_until_projected_loads_match() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Weighted);
let symbols = create_test_symbols(2);
let replicas = create_test_replicas_with_symbol_counts(&[0, 100]);
let assignments = assigner.assign_authorized(&symbols, &replicas, 1);
let counts: Vec<_> = assignments
.iter()
.map(|assignment| assignment.symbol_indices.len())
.collect();
assert_eq!(counts, vec![2, 0]);
}
#[test]
fn weighted_handles_near_u32_max_existing_symbol_counts() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Weighted);
let symbols = create_test_symbols(3);
let replicas = create_test_replicas_with_symbol_counts(&[u32::MAX, u32::MAX - 1]);
let assignments = assigner.assign_authorized(&symbols, &replicas, 2);
assert_eq!(assignments[0].symbol_indices, vec![1]);
assert_eq!(assignments[1].symbol_indices, vec![0, 2]);
let mut assigned_once: Vec<_> = assignments
.iter()
.flat_map(|assignment| assignment.symbol_indices.iter().copied())
.collect();
assigned_once.sort_unstable();
assert_eq!(assigned_once, vec![0, 1, 2]);
}
#[test]
fn metamorphic_weighted_assignment_invariant_under_uniform_load_shift() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Weighted);
let symbols = create_test_symbols(37);
let baseline_replicas = create_test_replicas_with_symbol_counts(&[0, 3, 9, 3]);
let shifted_replicas =
create_test_replicas_with_symbol_counts(&[10_000, 10_003, 10_009, 10_003]);
let baseline = assigner.assign_authorized(&symbols, &baseline_replicas, 4);
let shifted = assigner.assign_authorized(&symbols, &shifted_replicas, 4);
assert_eq!(
baseline, shifted,
"weighted assignment should depend on relative projected load; adding \
the same constant to every replica must not change the plan"
);
}
#[test]
fn full_more_replicas_than_symbols() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let symbols = create_test_symbols(3);
let replicas = create_test_replicas(10);
let assignments = assigner.assign_authorized(&symbols, &replicas, 2);
assert_eq!(assignments.len(), 10);
for a in &assignments {
assert_eq!(a.symbol_indices.len(), 3);
assert!(a.can_decode);
}
}
#[test]
fn full_single_symbol() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let symbols = create_test_symbols(1);
let replicas = create_test_replicas(3);
let assignments = assigner.assign_authorized(&symbols, &replicas, 1);
for a in &assignments {
assert_eq!(a.symbol_indices.len(), 1);
assert!(a.can_decode);
}
}
#[test]
fn full_k_greater_than_symbol_count() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let symbols = create_test_symbols(5);
let replicas = create_test_replicas(2);
let assignments = assigner.assign_authorized(&symbols, &replicas, 10);
for a in &assignments {
assert_eq!(a.symbol_indices.len(), 5);
assert!(!a.can_decode);
}
}
#[test]
fn striped_uneven_distribution() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Striped);
let symbols = create_test_symbols(10);
let replicas = create_test_replicas(3);
let assignments = assigner.assign_authorized(&symbols, &replicas, 3);
let total: usize = assignments.iter().map(|a| a.symbol_indices.len()).sum();
assert_eq!(total, 10, "all symbols assigned");
for a in &assignments {
assert!(!a.symbol_indices.is_empty());
assert!(a.symbol_indices.len() <= 4);
}
}
#[test]
fn striped_single_replica() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Striped);
let symbols = create_test_symbols(5);
let replicas = create_test_replicas(1);
let assignments = assigner.assign_authorized(&symbols, &replicas, 3);
assert_eq!(assignments.len(), 1);
assert_eq!(assignments[0].symbol_indices.len(), 5);
assert!(assignments[0].can_decode);
}
#[test]
fn striped_more_replicas_than_symbols() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Striped);
let symbols = create_test_symbols(3);
let replicas = create_test_replicas(5);
let assignments = assigner.assign_authorized(&symbols, &replicas, 2);
let total: usize = assignments.iter().map(|a| a.symbol_indices.len()).sum();
assert_eq!(total, 3);
let nonempty = assignments
.iter()
.filter(|a| !a.symbol_indices.is_empty())
.count();
assert_eq!(nonempty, 3);
}
#[test]
fn striped_assignment_preserves_existing_residue_classes_when_symbols_extend() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Striped);
let replicas = create_test_replicas(4);
let base_symbols = create_test_symbols(11);
let extended_symbols = create_test_symbols(base_symbols.len() + replicas.len() * 2);
let base_plan = assigner.assign_authorized(&base_symbols, &replicas, 99);
let extended_plan = assigner.assign_authorized(&extended_symbols, &replicas, 99);
assert_eq!(base_plan.len(), extended_plan.len());
for (replica_idx, (base, extended)) in base_plan.iter().zip(&extended_plan).enumerate() {
assert_eq!(base.replica_id, extended.replica_id);
let preserved_indices: Vec<_> = extended
.symbol_indices
.iter()
.copied()
.filter(|&idx| idx < base_symbols.len())
.collect();
assert_eq!(
preserved_indices, base.symbol_indices,
"extending symbols must not reshuffle existing striped assignments"
);
let appended_indices: Vec<_> = extended
.symbol_indices
.iter()
.copied()
.filter(|&idx| idx >= base_symbols.len())
.collect();
assert_eq!(
appended_indices.len(),
2,
"two complete replica periods should add two symbols per replica"
);
for idx in &extended.symbol_indices {
assert_eq!(
idx % replicas.len(),
replica_idx,
"striped assignment must keep replica residue classes stable"
);
}
}
}
#[test]
fn minimum_k_single_replica() {
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let symbols = create_test_symbols(10);
let replicas = create_test_replicas(1);
let assignments = assigner.assign_authorized(&symbols, &replicas, 5);
assert_eq!(assignments.len(), 1);
assert!(assignments[0].symbol_indices.len() >= 5);
assert!(assignments[0].can_decode);
}
#[test]
fn minimum_k_k_equals_symbol_count() {
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let symbols = create_test_symbols(5);
let replicas = create_test_replicas(3);
let assignments = assigner.assign_authorized(&symbols, &replicas, 5);
for a in &assignments {
assert_eq!(a.symbol_indices.len(), 5);
assert!(a.can_decode);
}
}
#[test]
fn minimum_k_k_greater_than_symbols() {
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let symbols = create_test_symbols(5);
let replicas = create_test_replicas(2);
let assignments = assigner.assign_authorized(&symbols, &replicas, 10);
for a in &assignments {
assert!(!a.can_decode);
}
}
#[test]
fn minimum_k_no_duplicate_indices() {
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let symbols = create_test_symbols(20);
let replicas = create_test_replicas(4);
let assignments = assigner.assign_authorized(&symbols, &replicas, 8);
for a in &assignments {
let mut sorted = a.symbol_indices.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
sorted.len(),
a.symbol_indices.len(),
"no duplicate indices for replica {}",
a.replica_id
);
}
}
#[test]
fn strategy_accessor() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Striped);
assert_eq!(assigner.strategy(), AssignmentStrategy::Striped);
}
#[test]
fn replica_authorization_filters_unauthorized_replicas() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let symbols = create_test_symbols(5);
let security_context = SecurityContext::for_testing(42);
let mut replicas = vec![
ReplicaInfo::new("replica-auth-1", "addr1"), ReplicaInfo::new("node-auth-2", "addr2"), ReplicaInfo::new("r3", "addr3"), ReplicaInfo::new("invalid-test", "addr4"), ReplicaInfo::new("rogue-replica", "addr5"), ReplicaInfo::new("", "addr6"), ];
let assignments = assigner.assign(&symbols, &replicas, &security_context, None, 3);
assert_eq!(assignments.len(), 3);
let replica_ids: Vec<_> = assignments.iter().map(|a| &a.replica_id).collect();
assert!(replica_ids.contains(&&"replica-auth-1".to_string()));
assert!(replica_ids.contains(&&"node-auth-2".to_string()));
assert!(replica_ids.contains(&&"r3".to_string()));
assert!(!replica_ids.contains(&&"invalid-test".to_string()));
assert!(!replica_ids.contains(&&"rogue-replica".to_string()));
assert!(!replica_ids.contains(&&"".to_string()));
}
#[test]
fn all_unauthorized_replicas_returns_empty() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let symbols = create_test_symbols(5);
let security_context = SecurityContext::for_testing(42);
let replicas = vec![
ReplicaInfo::new("test-replica", "addr1"), ReplicaInfo::new("rogue-node", "addr2"), ReplicaInfo::new("", "addr3"), ];
let assignments = assigner.assign(&symbols, &replicas, &security_context, None, 3);
assert!(assignments.is_empty());
}
#[test]
fn replica_authorization_preserves_assignment_strategy_semantics() {
let symbols = create_test_symbols(12);
let security_context = SecurityContext::for_testing(42);
let replicas = vec![
ReplicaInfo::new("replica-1", "addr1"),
ReplicaInfo::new("replica-2", "addr2"),
ReplicaInfo::new("replica-3", "addr3"),
];
let striped = SymbolAssigner::new(AssignmentStrategy::Striped);
let assignments = striped.assign(&symbols, &replicas, &security_context, None, 4);
let mut all: Vec<usize> = Vec::new();
for a in &assignments {
all.extend_from_slice(&a.symbol_indices);
}
all.sort_unstable();
all.dedup();
assert_eq!(all.len(), 12, "all symbols should be assigned exactly once");
assert_eq!(
assignments.len(),
3,
"all authorized replicas should get assignments"
);
}
#[test]
fn both_empty_returns_empty() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let security_context = SecurityContext::for_testing(42);
let assignments = assigner.assign(&[], &[], &security_context, None, 5);
assert!(assignments.is_empty());
}
#[test]
fn full_k_zero() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let symbols = create_test_symbols(5);
let replicas = create_test_replicas(2);
let assignments = assigner.assign_authorized(&symbols, &replicas, 0);
for a in &assignments {
assert!(a.can_decode);
}
}
#[test]
fn assignment_strategy_conformance_matrix() {
let symbols = create_test_symbols(8);
let replicas = create_test_replicas(3);
let expected_all_indices: Vec<_> = (0..symbols.len()).collect();
for strategy in [
AssignmentStrategy::Full,
AssignmentStrategy::Striped,
AssignmentStrategy::MinimumK,
AssignmentStrategy::Weighted,
] {
let plan = SymbolAssigner::new(strategy).assign_authorized(&symbols, &replicas, 4);
assert_eq!(
plan.len(),
replicas.len(),
"{strategy:?}: one assignment per replica"
);
for assignment in &plan {
assert!(
assignment
.symbol_indices
.iter()
.all(|&idx| idx < symbols.len()),
"{strategy:?}: assigned indices must stay within the symbol set"
);
let mut per_replica = assignment.symbol_indices.clone();
per_replica.sort_unstable();
per_replica.dedup();
assert_eq!(
per_replica.len(),
assignment.symbol_indices.len(),
"{strategy:?}: per-replica assignments must not duplicate indices"
);
}
match strategy {
AssignmentStrategy::Full => {
for assignment in &plan {
assert_eq!(assignment.symbol_indices, expected_all_indices);
assert!(
assignment.can_decode,
"full replication with 8 symbols and k=4 must decode everywhere"
);
}
}
AssignmentStrategy::Striped => {
let mut assigned_once: Vec<_> = plan
.iter()
.flat_map(|assignment| assignment.symbol_indices.iter().copied())
.collect();
assigned_once.sort_unstable();
assert_eq!(
assigned_once, expected_all_indices,
"striping must assign every symbol exactly once"
);
assert!(
plan.iter().all(|assignment| !assignment.can_decode),
"8 symbols striped over 3 replicas stays below k=4 per replica"
);
}
AssignmentStrategy::MinimumK => {
for assignment in &plan {
assert_eq!(assignment.symbol_indices.len(), 4);
assert!(
assignment.can_decode,
"minimum-k must provide k symbols per replica"
);
}
}
AssignmentStrategy::Weighted => {
let mut assigned_once: Vec<_> = plan
.iter()
.flat_map(|assignment| assignment.symbol_indices.iter().copied())
.collect();
assigned_once.sort_unstable();
assert_eq!(
assigned_once, expected_all_indices,
"weighted assignment must assign every symbol exactly once"
);
}
}
}
}
#[test]
fn assignment_strategy_debug_clone_copy_eq() {
let s = AssignmentStrategy::Striped;
let dbg = format!("{s:?}");
assert!(dbg.contains("Striped"), "{dbg}");
let copied = s;
let cloned = s;
assert_eq!(copied, cloned);
assert_ne!(s, AssignmentStrategy::Full);
}
#[test]
fn replica_assignment_debug_clone() {
let ra = ReplicaAssignment {
replica_id: "r0".to_string(),
symbol_indices: vec![0, 1, 2],
can_decode: true,
};
let dbg = format!("{ra:?}");
assert!(dbg.contains("ReplicaAssignment"), "{dbg}");
let cloned = ra;
assert_eq!(cloned.replica_id, "r0");
assert_eq!(cloned.symbol_indices, [0, 1, 2]);
}
fn golden_replicas() -> Vec<ReplicaInfo> {
create_test_replicas_with_symbol_counts(&[10, 5, 20])
}
fn golden_symbols() -> Vec<Symbol> {
create_test_symbols(8)
}
const GOLDEN_K: u16 = 4;
#[test]
fn golden_plan_full_strategy() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Full);
let plan = assigner.assign_authorized(&golden_symbols(), &golden_replicas(), GOLDEN_K);
insta::assert_debug_snapshot!(plan);
}
#[test]
fn golden_plan_striped_strategy() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Striped);
let plan = assigner.assign_authorized(&golden_symbols(), &golden_replicas(), GOLDEN_K);
insta::assert_debug_snapshot!(plan);
}
#[test]
fn golden_plan_minimum_k_strategy() {
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let plan = assigner.assign_authorized(&golden_symbols(), &golden_replicas(), GOLDEN_K);
insta::assert_debug_snapshot!(plan);
}
#[test]
fn golden_plan_weighted_strategy() {
let assigner = SymbolAssigner::new(AssignmentStrategy::Weighted);
let plan = assigner.assign_authorized(&golden_symbols(), &golden_replicas(), GOLDEN_K);
insta::assert_debug_snapshot!(plan);
}
#[test]
fn assign_minimum_k_returns_sorted_indices() {
let symbols = create_test_symbols(64);
let replicas = create_test_replicas(4);
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let plan = assigner.assign_authorized(&symbols, &replicas, 16);
for assignment in &plan {
let mut sorted = assignment.symbol_indices.clone();
sorted.sort_unstable();
assert_eq!(
assignment.symbol_indices, sorted,
"br-45xcbm: symbol_indices must be sorted (BTreeSet iteration order)"
);
}
}
#[test]
fn assign_minimum_k_is_deterministic_across_calls() {
let symbols = create_test_symbols(256);
let replicas = create_test_replicas(8);
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let p1 = assigner.assign_authorized(&symbols, &replicas, 64);
for _ in 0..8 {
let pn = assigner.assign_authorized(&symbols, &replicas, 64);
assert_eq!(p1, pn, "assign_minimum_k must be deterministic");
}
}
#[test]
fn assign_minimum_k_handles_k_10000_quickly() {
let k: u16 = 10_000;
let symbols = create_test_symbols(k as usize);
let replicas = create_test_replicas(8);
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let started = std::time::Instant::now();
let plan = assigner.assign_authorized(&symbols, &replicas, k);
let elapsed = started.elapsed();
assert_eq!(plan.len(), 8);
for assignment in &plan {
assert!(
assignment.can_decode,
"every replica must hold at least K={k} symbols"
);
assert_eq!(
assignment.symbol_indices.len(),
k as usize,
"each replica receives exactly K symbols"
);
}
assert!(
elapsed < std::time::Duration::from_secs(5),
"assign_minimum_k(K=10000) must complete in under 5s; took {elapsed:?} \
(regression: did dedup revert to O(K^2)?)"
);
}
#[test]
fn assign_minimum_k_when_k_equals_symbol_count() {
let symbols = create_test_symbols(32);
let replicas = create_test_replicas(2);
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let plan = assigner.assign_authorized(&symbols, &replicas, 32);
for assignment in &plan {
assert_eq!(assignment.symbol_indices.len(), 32);
for (i, idx) in assignment.symbol_indices.iter().enumerate() {
assert_eq!(*idx, i);
}
}
}
#[test]
fn assign_minimum_k_handles_empty_symbols() {
let symbols: Vec<Symbol> = Vec::new();
let replicas = create_test_replicas(3);
let assigner = SymbolAssigner::new(AssignmentStrategy::MinimumK);
let plan = assigner.assign_authorized(&symbols, &replicas, 4);
for assignment in &plan {
assert!(assignment.symbol_indices.is_empty());
assert!(!assignment.can_decode);
}
}
}