use super::*;
use crate::TestRuntime;
struct RetainEvenPolicy;
impl SlotReusePolicy for RetainEvenPolicy {
fn get_slots_to_retain(&self, active: &[SlotId]) -> HashSet<SlotId> {
active
.iter()
.copied()
.filter(|slot| slot.raw() % 2 == 0)
.collect()
}
fn are_compatible(&self, existing: SlotId, requested: SlotId) -> bool {
existing == requested
}
}
struct ParityPolicy;
impl SlotReusePolicy for ParityPolicy {
fn get_slots_to_retain(&self, active: &[SlotId]) -> HashSet<SlotId> {
let _ = active;
HashSet::default()
}
fn are_compatible(&self, existing: SlotId, requested: SlotId) -> bool {
existing.raw() % 2 == requested.raw() % 2
}
}
#[test]
fn exact_reuse_wins() {
let mut state = SubcomposeState::default();
state.register_active(SlotId::new(1), &[10], &[]);
state.dispose_or_reuse_starting_from_index(0);
assert_eq!(state.reusable(), &[10]);
let reused = state.take_node_from_reusables(SlotId::new(1));
assert_eq!(reused, Some(10));
}
#[test]
fn policy_based_compatibility() {
let mut state = SubcomposeState::new(Box::new(ParityPolicy));
state.register_active(SlotId::new(2), &[42], &[]);
state.dispose_or_reuse_starting_from_index(0);
assert_eq!(state.reusable(), &[42]);
let reused = state.take_node_from_reusables(SlotId::new(4));
assert_eq!(reused, Some(42));
}
#[test]
fn dispose_or_reuse_respects_policy() {
let mut state = SubcomposeState::new(Box::new(RetainEvenPolicy));
state.register_active(SlotId::new(1), &[10], &[]);
state.register_active(SlotId::new(2), &[11], &[]);
let disposed = state.dispose_or_reuse_starting_from_index(0);
assert!(disposed.is_empty());
assert_eq!(state.reusable(), &[10]);
assert_eq!(state.reusable_count, 1);
}
#[test]
fn dispose_from_middle_moves_trailing_slots() {
let mut state = SubcomposeState::default();
state.register_active(SlotId::new(1), &[10], &[]);
state.register_active(SlotId::new(2), &[20], &[]);
state.register_active(SlotId::new(3), &[30], &[]);
let disposed = state.dispose_or_reuse_starting_from_index(2);
assert!(disposed.is_empty());
assert_eq!(state.reusable(), &[30]);
assert_eq!(state.reusable_count, 1);
assert!(state.dispose_or_reuse_starting_from_index(5).is_empty());
}
#[test]
fn incompatible_reuse_is_rejected() {
let mut state = SubcomposeState::default();
state.register_active(SlotId::new(1), &[10], &[]);
state.dispose_or_reuse_starting_from_index(0);
assert_eq!(state.take_node_from_reusables(SlotId::new(2)), None);
assert_eq!(state.reusable(), &[10]);
}
#[test]
fn reordering_keyed_children_preserves_nodes() {
let mut state = SubcomposeState::default();
state.register_active(SlotId::new(1), &[11], &[]);
state.register_active(SlotId::new(2), &[22], &[]);
state.register_active(SlotId::new(3), &[33], &[]);
let disposed = state.dispose_or_reuse_starting_from_index(0);
assert!(disposed.is_empty());
assert_eq!(state.reusable().len(), 3);
let reordered = [SlotId::new(3), SlotId::new(1), SlotId::new(2)];
let mut reused_nodes = Vec::new();
for slot in reordered {
let node = state
.take_node_from_reusables(slot)
.expect("expected node for reordered slot");
reused_nodes.push(node);
state.register_active(slot, &[node], &[]);
}
assert_eq!(reused_nodes, vec![33, 11, 22]);
assert!(state.reusable().is_empty());
assert_eq!(state.reusable_count, 0);
}
#[test]
fn removing_slots_deactivates_scopes() {
let runtime = TestRuntime::new();
let scope_a = RecomposeScope::new_for_test(runtime.handle());
let scope_b = RecomposeScope::new_for_test(runtime.handle());
let mut state = SubcomposeState::default();
state.register_active(SlotId::new(1), &[10], std::slice::from_ref(&scope_a));
state.register_active(SlotId::new(2), &[20], std::slice::from_ref(&scope_b));
let disposed = state.dispose_or_reuse_starting_from_index(1);
assert!(disposed.is_empty());
assert!(scope_a.is_active());
assert!(!scope_b.is_active());
assert_eq!(state.reusable(), &[20]);
}
#[test]
fn draining_inactive_precomposed_returns_nodes() {
let mut state = SubcomposeState::default();
state.register_precomposed(SlotId::new(7), 77);
state.register_active(SlotId::new(8), &[88], &[]);
let disposed = state.drain_inactive_precomposed();
assert_eq!(disposed, vec![77]);
assert!(state.precomposed().is_empty());
}
#[test]
fn draining_inactive_precomposed_uses_current_pass_activation() {
let mut state = SubcomposeState::default();
state.begin_pass();
state.register_active(SlotId::new(1), &[10], &[]);
assert!(state.finish_pass().is_empty());
state.register_precomposed(SlotId::new(1), 99);
state.begin_pass();
let disposed = state.drain_inactive_precomposed();
assert_eq!(disposed, vec![99]);
assert!(state.precomposed().is_empty());
}
#[test]
fn finish_pass_disposes_inactive_slots() {
let mut state = SubcomposeState::default();
state.begin_pass();
state.register_active(SlotId::new(1), &[10], &[]);
assert!(state.finish_pass().is_empty());
state.begin_pass();
let disposed = state.finish_pass();
assert!(disposed.is_empty());
assert_eq!(state.reusable(), &[10]);
}
#[test]
fn finish_pass_keeps_active_slots() {
let mut state = SubcomposeState::default();
state.begin_pass();
state.register_active(SlotId::new(1), &[10], &[]);
state.register_active(SlotId::new(2), &[20], &[]);
let disposed = state.finish_pass();
assert!(disposed.is_empty());
assert!(state.reusable().is_empty());
}
#[test]
fn content_type_policy_allows_cross_slot_reuse() {
let policy = ContentTypeReusePolicy::new();
policy.set_content_type(SlotId::new(10), 1); policy.set_content_type(SlotId::new(20), 1); policy.set_content_type(SlotId::new(30), 2);
assert!(policy.are_compatible(SlotId::new(10), SlotId::new(20)));
assert!(policy.are_compatible(SlotId::new(20), SlotId::new(10)));
assert!(!policy.are_compatible(SlotId::new(10), SlotId::new(30)));
assert!(!policy.are_compatible(SlotId::new(20), SlotId::new(30)));
}
#[test]
fn content_type_policy_exact_match_always_wins() {
let policy = ContentTypeReusePolicy::new();
assert!(policy.are_compatible(SlotId::new(5), SlotId::new(5)));
policy.set_content_type(SlotId::new(5), 1);
assert!(policy.are_compatible(SlotId::new(5), SlotId::new(5)));
}
#[test]
fn content_type_policy_unregistered_slots_not_compatible() {
let policy = ContentTypeReusePolicy::new();
policy.set_content_type(SlotId::new(10), 1);
assert!(!policy.are_compatible(SlotId::new(10), SlotId::new(99)));
assert!(!policy.are_compatible(SlotId::new(99), SlotId::new(10)));
assert!(!policy.are_compatible(SlotId::new(88), SlotId::new(99)));
}
#[test]
fn content_type_reuse_in_subcompose_state() {
let policy = ContentTypeReusePolicy::new();
policy.set_content_type(SlotId::new(1), 100);
policy.set_content_type(SlotId::new(3), 100);
policy.set_content_type(SlotId::new(2), 200);
let mut state = SubcomposeState::new(Box::new(policy));
state.register_active(SlotId::new(1), &[10], &[]);
state.dispose_or_reuse_starting_from_index(0);
assert_eq!(state.reusable(), &[10]);
let reused = state.take_node_from_reusables(SlotId::new(3));
assert_eq!(
reused,
Some(10),
"Should reuse node across slots with same content type"
);
}
#[test]
fn content_type_none_clears_policy() {
let mut state = SubcomposeState::new(Box::new(ContentTypeReusePolicy::new()));
state.register_content_type(SlotId::new(1), 100);
state.register_active(SlotId::new(1), &[10], &[]);
assert_eq!(state.get_content_type(SlotId::new(1)), Some(100));
state.update_content_type(SlotId::new(1), None);
assert_eq!(state.get_content_type(SlotId::new(1)), None);
state.dispose_or_reuse_starting_from_index(0);
state.register_content_type(SlotId::new(2), 100);
let reused = state.take_node_from_reusables(SlotId::new(2));
assert_eq!(
reused, None,
"Untyped slot should not match typed slot 2 (type 100)"
);
}
#[test]
fn migrating_last_reusable_node_prunes_dead_slot_bookkeeping() {
let mut state = SubcomposeState::new(Box::new(ContentTypeReusePolicy::new()));
let slot_a = SlotId::new(1);
let slot_b = SlotId::new(2);
state.register_content_type(slot_a, 7);
state.register_content_type(slot_b, 7);
let _host = state.get_or_create_slots(slot_a);
let _callback = state.callback_holder(slot_a);
state.register_active(slot_a, &[10], &[]);
assert!(state.dispose_or_reuse_starting_from_index(0).is_empty());
assert!(state.slot_compositions.contains_key(&slot_a));
assert!(state.slot_callbacks.contains_key(&slot_a));
assert_eq!(state.get_content_type(slot_a), Some(7));
assert_eq!(state.reusable_count, 1);
let reused = state.take_node_from_reusables(slot_b);
assert_eq!(reused, Some(10));
assert!(!state.slot_compositions.contains_key(&slot_a));
assert!(!state.slot_callbacks.contains_key(&slot_a));
assert_eq!(state.get_content_type(slot_a), None);
assert_eq!(state.reusable_count, 0);
assert!(!state.reusable_node_counts.contains_key(&slot_a));
}
#[test]
fn finish_pass_disposes_overflow_slot_and_prunes_dead_slot_bookkeeping() {
let mut state = SubcomposeState::default();
let slot = SlotId::new(1);
state.max_reusable_per_type = 0;
state.register_content_type(slot, 9);
let _host = state.get_or_create_slots(slot);
let _callback = state.callback_holder(slot);
state.begin_pass();
state.register_active(slot, &[10], &[]);
assert!(state.finish_pass().is_empty());
assert!(state.slot_compositions.contains_key(&slot));
assert!(state.slot_callbacks.contains_key(&slot));
state.begin_pass();
let disposed = state.finish_pass();
assert_eq!(disposed, vec![10]);
assert!(!state.slot_compositions.contains_key(&slot));
assert!(!state.slot_callbacks.contains_key(&slot));
assert_eq!(state.get_content_type(slot), None);
assert_eq!(state.reusable_count, 0);
assert!(!state.reusable_node_counts.contains_key(&slot));
}