use gc_lite::{GcError, GcHeap, GcPartitionId, GcRef, GcTrace, GcTraceCtx, gc_type_register};
#[derive(Debug, PartialEq, Clone)]
struct TestData {
value: i32,
name: String,
}
impl GcTrace for TestData {
fn trace(&self, _: &mut GcTraceCtx) {}
}
struct TestNode {
value: i32,
children: Vec<GcRef<TestNode>>,
}
impl GcTrace for TestNode {
fn trace(&self, tr: &mut GcTraceCtx) {
for child in &self.children {
tr.add(*child);
}
}
}
impl core::fmt::Debug for TestNode {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("TestNode")
.field("value", &self.value)
.field("children_count", &self.children.len())
.finish()
}
}
impl TestNode {
fn new(value: i32) -> Self {
Self {
value,
children: Vec::new(),
}
}
fn add_child(&mut self, child: GcRef<TestNode>) {
self.children.push(child);
}
}
gc_type_register! {
TestData, drop_pass = 0;
TestNode, drop_pass = 0;
}
#[test]
fn test_partition_creation_and_retrieval() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id1 = heap.create_partition();
let id2 = heap.create_partition();
assert_ne!(id1, id2);
assert_eq!(heap.partition_ids().len(), 2);
let partition = heap.partition(id1).unwrap();
assert_eq!(partition.memory_used(), 0);
}
#[test]
fn test_partition_removal() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
assert!(heap.partition(id).is_some());
assert_eq!(heap.partition_ids().len(), 1);
heap.remove_partition(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
assert!(heap.partition(id).is_none());
assert_eq!(heap.partition_ids().len(), 0);
}
#[test]
fn test_partition_gc_threshold() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
heap.set_memory_limit(1024);
assert_eq!(heap.gc_threshold(), 0);
heap.set_gc_threshold(512);
assert_eq!(heap.gc_threshold(), 512);
heap.set_gc_threshold(2048);
assert_eq!(heap.gc_threshold(), 819);
}
#[test]
fn test_allocation_fails_when_limit_exceeded() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
heap.set_memory_limit(256); let id = heap.create_partition();
let mut allocated_count = 0;
loop {
match unsafe {
heap.alloc_raw(
id,
TestData {
value: allocated_count,
name: format!("obj_{}", allocated_count),
},
)
} {
Ok(_) => {
allocated_count += 1;
}
Err((GcError::PartitionFull, _)) => {
break;
}
Err((err, _)) => {
panic!("Unexpected error: {:?}", err);
}
}
}
assert!(allocated_count > 0);
let result = unsafe {
heap.alloc_raw(
id,
TestData {
value: 999,
name: "should_fail".to_string(),
},
)
};
assert!(matches!(result, Err((GcError::PartitionFull, _))));
}
#[test]
fn test_set_memory_limit_above_used_memory() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let _obj1 = unsafe {
heap.alloc_raw(
id,
TestData {
value: 1,
name: "obj1".to_string(),
},
)
}
.unwrap();
let _obj2 = unsafe {
heap.alloc_raw(
id,
TestData {
value: 2,
name: "obj2".to_string(),
},
)
}
.unwrap();
let used_memory = heap.partition(id).unwrap().memory_used();
assert!(used_memory > 0);
let new_limit = used_memory + 512;
let applied = heap.set_memory_limit(new_limit);
assert_eq!(applied, new_limit);
assert_eq!(heap.memory_limit(), new_limit);
let _obj3 = unsafe {
heap.alloc_raw(
id,
TestData {
value: 3,
name: "obj3".to_string(),
},
)
}
.unwrap();
}
#[test]
fn test_set_memory_limit_below_used_memory() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let _obj1 = unsafe {
heap.alloc_raw(
id,
TestData {
value: 1,
name: "obj1".to_string(),
},
)
}
.unwrap();
let _obj2 = unsafe {
heap.alloc_raw(
id,
TestData {
value: 2,
name: "obj2".to_string(),
},
)
}
.unwrap();
let used_memory = heap.partition(id).unwrap().memory_used();
assert!(used_memory > 0);
let smaller_limit = used_memory - 1;
let applied_limit = heap.set_memory_limit(smaller_limit);
assert_eq!(applied_limit, used_memory);
assert_eq!(heap.memory_limit(), used_memory);
let result = unsafe {
heap.alloc_raw(
id,
TestData {
value: 3,
name: "should_fail".to_string(),
},
)
};
assert!(matches!(result, Err((GcError::PartitionFull, _))));
}
#[test]
fn test_set_unlimited_memory() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
heap.set_memory_limit(0);
assert_eq!(heap.memory_limit(), 0);
let mut allocated_count = 0;
loop {
match unsafe {
heap.alloc_raw(
id,
TestData {
value: allocated_count,
name: format!("obj_{}", allocated_count),
},
)
} {
Ok(_) => {
allocated_count += 1;
if allocated_count >= 100 {
break;
}
}
Err((err, _)) => {
panic!("Unexpected error with unlimited memory: {:?}", err);
}
}
}
assert_eq!(allocated_count, 100);
}
#[test]
fn test_object_allocation() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let initial_memory = heap.partition(id).unwrap().memory_used();
let obj: GcRef<TestData> = unsafe {
heap.alloc_raw(
id,
TestData {
value: 42,
name: "test".to_string(),
},
)
}
.unwrap();
let partition = heap.partition(id).unwrap();
let after_memory = partition.memory_used();
assert!(after_memory > initial_memory);
assert_eq!(obj.value, 42);
assert_eq!(obj.name, "test");
}
#[test]
fn test_memory_usage_increases_with_allocation() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let mut memory_after_each_alloc: Vec<usize> = Vec::new();
for i in 0..5 {
let _obj = unsafe {
heap.alloc_raw(
id,
TestData {
value: i,
name: format!("obj_{}", i),
},
)
}
.expect("allocation failed");
let memory = heap.partition(id).unwrap().memory_used();
memory_after_each_alloc.push(memory);
}
for i in 1..memory_after_each_alloc.len() {
assert!(
memory_after_each_alloc[i] > memory_after_each_alloc[i - 1],
"Memory should increase after each allocation"
);
}
let final_memory = heap.partition(id).unwrap().memory_used();
assert!(final_memory > 0);
unsafe {
heap.alloc_root_raw(
id,
TestData {
value: 100,
name: "root".to_string(),
},
)
}
.unwrap();
let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
assert!(freed > 0);
let after_gc_memory = heap.partition(id).unwrap().memory_used();
assert!(after_gc_memory < final_memory);
}
#[test]
fn test_multiple_object_allocation() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let mut objs: Vec<GcRef<TestData>> = Vec::new();
for i in 0..10 {
let obj = unsafe {
heap.alloc_raw(
id,
TestData {
value: i,
name: format!("obj_{}", i),
},
)
}
.expect("allocation failed");
objs.push(obj);
}
for (i, obj) in objs.iter().enumerate() {
assert_eq!(obj.value, i as i32);
assert_eq!(obj.name, format!("obj_{}", i));
}
let partition = heap.partition(id).unwrap();
assert!(partition.memory_used() > 0);
}
#[test]
fn test_partition_full_error() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
heap.set_memory_limit(512);
let id = heap.create_partition();
let mut result = unsafe {
heap.alloc_raw(
id,
TestData {
value: 0,
name: "test".to_string(),
},
)
};
let mut count = 0;
while let Ok(_obj) = result {
count += 1;
result = unsafe {
heap.alloc_raw(
id,
TestData {
value: count,
name: format!("test_{}", count),
},
)
};
}
assert!(matches!(result, Err((GcError::PartitionFull, _))));
assert!(count > 0);
}
#[test]
fn test_invalid_partition_allocation() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let invalid_id = GcPartitionId(9999);
let result = unsafe {
heap.alloc_raw(
invalid_id,
TestData {
value: 42,
name: "test".to_string(),
},
)
};
assert!(matches!(result, Err((GcError::PartitionNotFound, _))));
}
#[test]
fn test_root_object_management() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let obj = unsafe {
heap.alloc_root_raw(
id,
TestData {
value: 42,
name: "test".to_string(),
},
)
}
.unwrap();
assert!(obj.is_root());
}
#[test]
fn test_root_objects_preserve_during_gc() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let obj = unsafe {
heap.alloc_root_raw(
id,
TestData {
value: 42,
name: "test".to_string(),
},
)
}
.unwrap();
let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
assert_eq!(freed, 0);
assert_eq!(obj.value, 42);
}
#[test]
fn test_non_root_objects_collected() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let root_obj = unsafe {
heap.alloc_root_raw(
id,
TestData {
value: 1,
name: "root".to_string(),
},
)
}
.unwrap();
let _non_root_obj = unsafe {
heap.alloc_raw(
id,
TestData {
value: 2,
name: "non_root".to_string(),
},
)
}
.unwrap();
let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
assert!(freed > 0);
assert_eq!(root_obj.value, 1);
}
#[test]
fn test_manual_garbage_collection() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
for i in 0..5 {
if i < 2 {
let _obj = unsafe {
heap.alloc_root_raw(
id,
TestData {
value: i,
name: format!("obj_{}", i),
},
)
}
.unwrap();
} else {
let _obj = unsafe {
heap.alloc_raw(
id,
TestData {
value: i,
name: format!("obj_{}", i),
},
)
}
.unwrap();
}
}
let before = heap.partition(id).unwrap().memory_used();
let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
assert!(freed > 0);
let after = heap.partition(id).unwrap().memory_used();
assert!(after < before);
}
#[test]
fn test_circular_reference_handling() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let mut node1 = unsafe { heap.alloc_raw(id, TestNode::new(1)) }.unwrap();
let mut node2 = unsafe { heap.alloc_raw(id, TestNode::new(2)) }.unwrap();
node1.with_mut(&mut heap, |n| n.add_child(node2));
node2.with_mut(&mut heap, |n| n.add_child(node1));
let mut root = unsafe { heap.alloc_root_raw(id, TestNode::new(0)) }.unwrap();
root.with_mut(&mut heap, |n| n.add_child(node1));
root.with_mut(&mut heap, |n| n.children.clear());
let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
assert!(freed > 0);
let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
assert_eq!(freed, 0);
}
#[test]
fn test_weak_reference_creation_and_upgrade() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let obj = unsafe {
heap.alloc_root_raw(
id,
TestData {
value: 42,
name: "test".to_string(),
},
)
}
.unwrap();
let weak_ref = heap.downgrade(&obj);
let upgraded = weak_ref.upgrade(&heap);
assert!(upgraded.is_some());
let upgraded_ref = upgraded.unwrap();
assert_eq!(upgraded_ref.value, 42);
}
#[test]
fn test_weak_reference_after_collection() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let obj = unsafe {
heap.alloc_raw(
id,
TestData {
value: 42,
name: "test".to_string(),
},
)
}
.unwrap();
let weak_ref = heap.downgrade(&obj);
heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
let upgraded = weak_ref.upgrade(&heap);
assert!(upgraded.is_none());
}
#[test]
fn test_multiple_weak_references() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let id = heap.create_partition();
let obj = unsafe {
heap.alloc_root_raw(
id,
TestData {
value: 42,
name: "test".to_string(),
},
)
}
.unwrap();
let weak1 = heap.downgrade(&obj);
let weak2 = heap.downgrade(&obj);
let weak3 = heap.downgrade(&obj);
assert!(weak1.upgrade(&heap).is_some());
assert!(weak2.upgrade(&heap).is_some());
assert!(weak3.upgrade(&heap).is_some());
}
#[test]
fn test_weak_reference_after_partition_removal() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
heap.create_partition();
let child_id = heap.create_partition();
let obj = unsafe {
heap.alloc_raw(
child_id,
TestData {
value: 42,
name: "test".to_string(),
},
)
}
.unwrap();
let weak_ref = heap.downgrade(&obj);
assert!(weak_ref.upgrade(&heap).is_some());
heap.remove_partition(child_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
let upgraded = weak_ref.upgrade(&heap);
assert!(upgraded.is_none());
}
#[test]
fn test_contains_method() {
let mut heap1 = GcHeap::new(&GC_TYPE_REGISTRY);
let mut heap2 = GcHeap::new(&GC_TYPE_REGISTRY);
let id1 = heap1.create_partition();
let id2 = heap2.create_partition();
let obj1 = unsafe {
heap1.alloc_raw(
id1,
TestData {
value: 1,
name: "heap1".to_string(),
},
)
}
.unwrap();
let obj2 = unsafe {
heap2.alloc_raw(
id2,
TestData {
value: 2,
name: "heap2".to_string(),
},
)
}
.unwrap();
assert!(heap1.contains(obj1.node_ptr()));
assert!(!heap1.contains(obj2.node_ptr()));
assert!(heap2.contains(obj2.node_ptr()));
assert!(!heap2.contains(obj1.node_ptr()));
}