#![allow(dead_code)]
use memscope_rs::{
analysis::relation_inference::{Relation, RelationGraphBuilder},
core::types::TrackKind::HeapOwner,
snapshot::types::ActiveAllocation,
};
use std::time::{SystemTime, UNIX_EPOCH};
fn make_alloc(ptr: usize, size: usize) -> ActiveAllocation {
ActiveAllocation {
ptr: Some(ptr),
kind: HeapOwner { ptr, size },
size,
allocated_at: 0,
var_name: None,
type_name: None,
thread_id: 0,
call_stack_hash: None,
module_path: None,
stack_ptr: None,
}
}
fn make_alloc_with_metadata(
ptr: usize,
size: usize,
allocated_at: u64,
call_stack_hash: Option<u64>,
) -> ActiveAllocation {
ActiveAllocation {
ptr: Some(ptr),
kind: HeapOwner { ptr, size },
size,
allocated_at,
var_name: None,
type_name: None,
thread_id: 0,
call_stack_hash,
module_path: None,
stack_ptr: None,
}
}
fn current_time_ns() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
}
#[test]
fn test_empty_allocations() {
let graph = RelationGraphBuilder::build(&[], None);
assert_eq!(graph.edge_count(), 0);
assert!(graph.all_nodes().is_empty());
}
#[test]
fn test_single_allocation() {
let v = vec![1i32, 2, 3];
let allocs = vec![make_alloc(v.as_ptr() as usize, v.capacity() * 4)];
let graph = RelationGraphBuilder::build(&allocs, None);
assert_eq!(
graph.edge_count(),
0,
"Single allocation with no references should have no edges"
);
}
#[test]
fn test_clone_detection_similar_content() {
let v1 = vec![1i32, 2, 3, 4, 5];
let v2 = vec![1i32, 2, 3, 4, 5];
let allocs = vec![
make_alloc(v1.as_ptr() as usize, v1.capacity() * 4),
make_alloc(v2.as_ptr() as usize, v2.capacity() * 4),
];
let graph = RelationGraphBuilder::build(&allocs, None);
let has_clone = graph.edges.iter().any(|e| e.relation == Relation::Clone);
assert!(
has_clone || graph.edge_count() <= 2,
"Similar Vecs should either be detected as Clone or have at most Owner edges"
);
}
#[test]
fn test_clone_detection_different_content() {
let v1 = vec![1i32, 2, 3, 4, 5];
let v2 = vec![100i32, 200, 300, 400, 500];
let allocs = vec![
make_alloc(v1.as_ptr() as usize, v1.capacity() * 4),
make_alloc(v2.as_ptr() as usize, v2.capacity() * 4),
];
let graph = RelationGraphBuilder::build(&allocs, None);
let has_clone = graph.edges.iter().any(|e| e.relation == Relation::Clone);
assert!(
!has_clone,
"Different content Vecs should NOT be detected as Clone"
);
}
#[test]
fn test_multiple_allocations_graph() {
let v1 = vec![1i32, 2, 3];
let v2 = vec![4i32, 5, 6];
let s1 = String::from("test");
let allocs = vec![
make_alloc(v1.as_ptr() as usize, v1.capacity() * 4),
make_alloc(v2.as_ptr() as usize, v2.capacity() * 4),
make_alloc(s1.as_ptr() as usize, s1.capacity()),
];
let graph = RelationGraphBuilder::build(&allocs, None);
assert!(
graph.edge_count() <= 6,
"With 3 allocations, should have at most 6 possible edges"
);
let nodes = graph.all_nodes();
assert!(nodes.len() <= 3, "Should reference at most 3 unique nodes");
}
#[test]
fn test_cycle_detection_in_graph() {
let mut graph = RelationGraphBuilder::build(&[], None);
graph.add_edge(0, 1, Relation::Owns);
graph.add_edge(1, 2, Relation::Slice);
graph.add_edge(2, 0, Relation::Clone);
let cycles = graph.detect_cycles();
assert!(!cycles.is_empty(), "Should detect cycle: 0 -> 1 -> 2 -> 0");
}
#[test]
fn test_no_false_positive_cycles() {
let mut graph = RelationGraphBuilder::build(&[], None);
graph.add_edge(0, 1, Relation::Owns);
graph.add_edge(1, 2, Relation::Slice);
graph.add_edge(0, 2, Relation::Clone);
let cycles = graph.detect_cycles();
assert!(
cycles.is_empty(),
"Should not detect cycle in DAG: 0 -> 1 -> 2, 0 -> 2"
);
}
#[test]
fn test_graph_add_edge() {
let mut graph = RelationGraphBuilder::build(&[], None);
graph.add_edge(0, 1, Relation::Owns);
graph.add_edge(1, 2, Relation::Slice);
assert_eq!(graph.edge_count(), 2);
assert_eq!(graph.all_nodes().len(), 3);
}
#[test]
#[cfg(not(target_os = "linux"))]
fn test_owner_detection_with_vec_metadata() {
let data = vec![0xDEu8; 256];
let data_ptr = data.as_ptr() as usize;
let mut metadata = [0u8; 24];
metadata[0..8].copy_from_slice(&data_ptr.to_le_bytes());
let meta_ptr = metadata.as_ptr() as usize;
let allocs = vec![make_alloc(meta_ptr, 24), make_alloc(data_ptr, 256)];
let graph = RelationGraphBuilder::build(&allocs, None);
let owner_edges: Vec<_> = graph
.edges
.iter()
.filter(|e| e.relation == Relation::Owns && e.from == 0 && e.to == 1)
.collect();
assert!(
!owner_edges.is_empty(),
"Should detect Owner relationship from metadata containing pointer to data buffer"
);
}
#[test]
fn test_box_pointer_in_memory() {
let boxed = Box::new(42i32);
let heap_ptr = Box::into_raw(boxed);
let allocs = vec![make_alloc(heap_ptr as usize, std::mem::size_of::<i32>())];
let graph = RelationGraphBuilder::build(&allocs, None);
println!(
"Box test: heap at 0x{:x}, edges: {}",
heap_ptr as usize,
graph.edge_count()
);
unsafe {
let _ = Box::from_raw(heap_ptr);
}
assert!(graph.edge_count() <= 1);
}
#[test]
fn test_box_vec_relationship() {
let boxed_vec = Box::new(vec![1i32, 2, 3]);
let vec_data_ptr = boxed_vec.as_ptr() as usize;
let box_ptr = Box::into_raw(boxed_vec) as usize;
let allocs = vec![
make_alloc(box_ptr, std::mem::size_of::<Vec<i32>>()),
make_alloc(vec_data_ptr, 3 * 4),
];
let graph = RelationGraphBuilder::build(&allocs, None);
println!(
"Box<Vec> test: box at 0x{:x}, vec data at 0x{:x}, edges: {:?}",
box_ptr, vec_data_ptr, graph.edges
);
unsafe {
let _ = Box::from_raw(box_ptr as *mut Vec<i32>);
}
assert!(graph.edge_count() <= 3);
}
#[test]
fn test_arc_inner_memory_layout() {
use std::sync::Arc;
let arc = Arc::new(42i32);
let inner_ptr = Arc::as_ptr(&arc) as usize;
let allocs = vec![make_alloc(inner_ptr, std::mem::size_of::<i32>() + 16)];
let graph = RelationGraphBuilder::build(&allocs, None);
println!(
"Arc inner test: at 0x{:x}, edges: {}",
inner_ptr,
graph.edge_count()
);
assert!(graph.edge_count() <= 2);
}
#[test]
fn test_similar_size_allocations() {
let v1 = [0u8; 100];
let v2 = [0u8; 100];
let v3 = [0u8; 100];
let allocs = vec![
make_alloc(v1.as_ptr() as usize, 100),
make_alloc(v2.as_ptr() as usize, 100),
make_alloc(v3.as_ptr() as usize, 100),
];
let graph = RelationGraphBuilder::build(&allocs, None);
println!(
"Similar size test: 3 allocations of 100 bytes, edges: {:?}",
graph.edges
);
let clone_count = graph
.edges
.iter()
.filter(|e| e.relation == Relation::Clone)
.count();
assert!(
clone_count <= 3,
"Should detect at most 3 clone relationships"
);
}
#[test]
fn test_different_size_allocations() {
let v1 = [0u8; 100];
let v2 = [0u8; 200];
let v3 = vec![0u8; 300];
let allocs = vec![
make_alloc(v1.as_ptr() as usize, 100),
make_alloc(v2.as_ptr() as usize, 200),
make_alloc(v3.as_ptr() as usize, 300),
];
let graph = RelationGraphBuilder::build(&allocs, None);
let has_clone = graph.edges.iter().any(|e| e.relation == Relation::Clone);
assert!(
!has_clone,
"Different size allocations should NOT be detected as Clone"
);
}
#[test]
fn test_string_vs_bytes() {
let s = String::from("hello world");
let bytes = s.as_bytes().to_vec();
let allocs = vec![
make_alloc(s.as_ptr() as usize, s.capacity()),
make_alloc(bytes.as_ptr() as usize, bytes.len()),
];
let graph = RelationGraphBuilder::build(&allocs, None);
println!(
"String vs bytes test: string at 0x{:x}, bytes at 0x{:x}, edges: {:?}",
s.as_ptr() as usize,
bytes.as_ptr() as usize,
graph.edges
);
assert!(graph.edge_count() <= 2);
}
#[test]
fn test_graph_all_nodes_dedup() {
let mut graph = RelationGraphBuilder::build(&[], None);
graph.add_edge(0, 1, Relation::Owns);
graph.add_edge(0, 2, Relation::Slice);
graph.add_edge(1, 2, Relation::Clone);
let nodes = graph.all_nodes();
assert_eq!(nodes.len(), 3, "Should have 3 unique nodes");
assert!(nodes.contains(&0));
assert!(nodes.contains(&1));
assert!(nodes.contains(&2));
}
#[test]
fn test_large_allocation_graph() {
let allocs: Vec<ActiveAllocation> = (0..100)
.map(|i| {
let v = [i as u8; 64];
make_alloc(v.as_ptr() as usize, 64)
})
.collect();
let graph = RelationGraphBuilder::build(&allocs, None);
println!(
"Large graph test: {} allocations, {} edges",
allocs.len(),
graph.edge_count()
);
assert!(
graph.edge_count() <= 1000,
"With max_clone_edges_per_node=10, should have at most 1000 edges"
);
}
#[test]
#[cfg(not(target_os = "linux"))]
fn test_realistic_clone_scenario_with_timestamps() {
let base_time = current_time_ns();
let stack_hash_1: u64 = 0x1234_5678;
let stack_hash_2: u64 = 0xABCD_EF00;
let v1 = [1i32, 2, 3, 4, 5];
let v2 = [1i32, 2, 3, 4, 5];
let v3 = [10i32, 20, 30, 40, 50];
let v4 = [1i32, 2, 3, 4, 5];
let allocs = vec![
make_alloc_with_metadata(v1.as_ptr() as usize, 20, base_time, Some(stack_hash_1)),
make_alloc_with_metadata(
v2.as_ptr() as usize,
20,
base_time + 1_000_000,
Some(stack_hash_1),
),
make_alloc_with_metadata(
v3.as_ptr() as usize,
20,
base_time + 2_000_000,
Some(stack_hash_1),
),
make_alloc_with_metadata(
v4.as_ptr() as usize,
20,
base_time + 3_000_000,
Some(stack_hash_2),
),
];
let graph = RelationGraphBuilder::build(&allocs, None);
println!("Realistic clone scenario: edges = {:?}", graph.edges);
let clone_edges: Vec<_> = graph
.edges
.iter()
.filter(|e| e.relation == Relation::Clone)
.collect();
assert!(
!clone_edges.is_empty(),
"Should detect at least 1 clone relationship (v1 and v2)"
);
assert!(
clone_edges.len() <= 3,
"Should not detect more than 3 clone relationships"
);
}
#[test]
#[cfg(not(target_os = "linux"))]
fn test_accuracy_metrics() {
struct AccuracyMetrics {
true_positives: usize,
false_positives: usize,
true_negatives: usize,
false_negatives: usize,
}
impl AccuracyMetrics {
fn precision(&self) -> f64 {
if self.true_positives + self.false_positives == 0 {
return 0.0;
}
self.true_positives as f64 / (self.true_positives + self.false_positives) as f64
}
fn recall(&self) -> f64 {
if self.true_positives + self.false_negatives == 0 {
return 0.0;
}
self.true_positives as f64 / (self.true_positives + self.false_negatives) as f64
}
fn f1_score(&self) -> f64 {
let p = self.precision();
let r = self.recall();
if p + r == 0.0 {
return 0.0;
}
2.0 * p * r / (p + r)
}
}
let v1 = [1i32, 2, 3];
let v2 = [1i32, 2, 3];
let v3 = [100i32, 200, 300];
let allocs = vec![
make_alloc(v1.as_ptr() as usize, 12),
make_alloc(v2.as_ptr() as usize, 12),
make_alloc(v3.as_ptr() as usize, 12),
];
let graph = RelationGraphBuilder::build(&allocs, None);
let expected_clones = vec![(0, 1)];
let detected_clones: Vec<(usize, usize)> = graph
.edges
.iter()
.filter(|e| e.relation == Relation::Clone)
.map(|e| (e.from, e.to))
.collect();
let mut metrics = AccuracyMetrics {
true_positives: 0,
false_positives: 0,
true_negatives: 0,
false_negatives: 0,
};
for expected in &expected_clones {
if detected_clones.contains(expected) {
metrics.true_positives += 1;
} else {
metrics.false_negatives += 1;
}
}
for detected in &detected_clones {
if !expected_clones.contains(detected) {
metrics.false_positives += 1;
}
}
let possible_pairs = 3;
metrics.true_negatives = possible_pairs - expected_clones.len() - metrics.false_positives;
println!("\n=== Accuracy Metrics ===");
println!("True Positives: {}", metrics.true_positives);
println!("False Positives: {}", metrics.false_positives);
println!("True Negatives: {}", metrics.true_negatives);
println!("False Negatives: {}", metrics.false_negatives);
println!("Precision: {:.2}%", metrics.precision() * 100.0);
println!("Recall: {:.2}%", metrics.recall() * 100.0);
println!("F1 Score: {:.2}%", metrics.f1_score() * 100.0);
assert!(
metrics.precision() >= 0.5,
"Precision should be at least 50%"
);
}
#[test]
fn test_owner_detection_accuracy() {
let boxed_vec = Box::new(vec![1i32, 2, 3, 4, 5]);
let vec_data_ptr = boxed_vec.as_ptr() as usize;
let box_ptr = Box::into_raw(boxed_vec) as usize;
let allocs = vec![make_alloc(box_ptr, 24), make_alloc(vec_data_ptr, 20)];
let graph = RelationGraphBuilder::build(&allocs, None);
let owner_edges: Vec<_> = graph
.edges
.iter()
.filter(|e| e.relation == Relation::Owns)
.collect();
println!("\n=== Owner Detection Accuracy ===");
println!("Box at 0x{:x}, Vec data at 0x{:x}", box_ptr, vec_data_ptr);
println!("Owner edges: {:?}", owner_edges);
let expected_owner = (0, 1);
let detected = owner_edges
.iter()
.any(|e| e.from == expected_owner.0 && e.to == expected_owner.1);
if detected {
println!("✅ Owner relationship correctly detected");
} else {
println!("⚠️ Owner relationship not detected (may need memory content access)");
}
unsafe {
let _ = Box::from_raw(box_ptr as *mut Vec<i32>);
}
assert!(graph.edge_count() <= 3);
}
#[test]
fn test_no_false_positive_owner_different_types() {
let v1 = [1i32, 2, 3];
let v2 = [4i32, 5, 6];
let allocs = vec![
make_alloc(v1.as_ptr() as usize, 12),
make_alloc(v2.as_ptr() as usize, 12),
];
let graph = RelationGraphBuilder::build(&allocs, None);
let owner_edges: Vec<_> = graph
.edges
.iter()
.filter(|e| e.relation == Relation::Owns)
.collect();
println!("\n=== No False Positive Owner Test ===");
println!("Two independent Vecs, no owner relationship expected");
println!("Owner edges detected: {:?}", owner_edges);
assert!(
owner_edges.is_empty() || owner_edges.len() <= 2,
"Independent allocations should not have Owner relationships (or at most 2)"
);
}
#[test]
fn test_performance_large_graph() {
use std::time::Instant;
let start = Instant::now();
let allocs: Vec<ActiveAllocation> = (0..1000)
.map(|i| {
let v = [i as u8; 64];
make_alloc(v.as_ptr() as usize, 64)
})
.collect();
let build_start = Instant::now();
let graph = RelationGraphBuilder::build(&allocs, None);
let build_duration = build_start.elapsed();
let total_duration = start.elapsed();
println!("\n=== Performance Test ===");
println!("Allocations: {}", allocs.len());
println!("Edges: {}", graph.edge_count());
println!("Build time: {:.2}ms", build_duration.as_secs_f64() * 1000.0);
println!("Total time: {:.2}ms", total_duration.as_secs_f64() * 1000.0);
assert!(
build_duration.as_millis() < 1000,
"Building graph for 1000 allocations should take < 1 second"
);
}