use profile_inspect::ir::{FrameCategory, FrameKind};
use profile_inspect::parser::{CpuProfileParser, HeapProfileParser};
use std::path::Path;
const MINIMAL_CPU_PROFILE: &str = r#"{
"nodes": [
{
"id": 1,
"callFrame": {
"functionName": "(root)",
"scriptId": "0",
"url": "",
"lineNumber": -1,
"columnNumber": -1
},
"children": [2]
},
{
"id": 2,
"callFrame": {
"functionName": "main",
"scriptId": "1",
"url": "/src/index.js",
"lineNumber": 10,
"columnNumber": 5
},
"children": [3],
"hitCount": 5
},
{
"id": 3,
"callFrame": {
"functionName": "compute",
"scriptId": "2",
"url": "/src/utils.js",
"lineNumber": 20,
"columnNumber": 1
},
"hitCount": 10
}
],
"samples": [2, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 2, 3, 3, 3],
"timeDeltas": [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000],
"startTime": 0,
"endTime": 15000
}"#;
const GC_CPU_PROFILE: &str = r#"{
"nodes": [
{
"id": 1,
"callFrame": {
"functionName": "(root)",
"scriptId": "0",
"url": "",
"lineNumber": -1,
"columnNumber": -1
},
"children": [2, 3]
},
{
"id": 2,
"callFrame": {
"functionName": "main",
"scriptId": "1",
"url": "/src/index.js",
"lineNumber": 10,
"columnNumber": 5
},
"hitCount": 5
},
{
"id": 3,
"callFrame": {
"functionName": "(garbage collector)",
"scriptId": "0",
"url": "",
"lineNumber": -1,
"columnNumber": -1
},
"hitCount": 2
}
],
"samples": [2, 2, 3, 2, 3, 2, 2],
"timeDeltas": [1000, 1000, 500, 1000, 500, 1000, 1000],
"startTime": 0,
"endTime": 6000
}"#;
const DEPS_CPU_PROFILE: &str = r#"{
"nodes": [
{
"id": 1,
"callFrame": {
"functionName": "(root)",
"scriptId": "0",
"url": "",
"lineNumber": -1,
"columnNumber": -1
},
"children": [2]
},
{
"id": 2,
"callFrame": {
"functionName": "main",
"scriptId": "1",
"url": "/project/src/index.js",
"lineNumber": 1,
"columnNumber": 1
},
"children": [3]
},
{
"id": 3,
"callFrame": {
"functionName": "map",
"scriptId": "2",
"url": "/project/node_modules/lodash/map.js",
"lineNumber": 100,
"columnNumber": 5
},
"hitCount": 10
}
],
"samples": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
"timeDeltas": [100, 100, 100, 100, 100, 100, 100, 100, 100, 100],
"startTime": 0,
"endTime": 1000
}"#;
#[test]
fn test_parse_minimal_cpu_profile() {
let parser = CpuProfileParser::default();
let result = parser.parse_str(MINIMAL_CPU_PROFILE, Some("test.cpuprofile".to_string()));
assert!(result.is_ok(), "Should parse minimal profile");
let profile = result.unwrap();
assert_eq!(profile.frames.len(), 3);
assert!(!profile.samples.is_empty());
assert_eq!(profile.source_file, Some("test.cpuprofile".to_string()));
assert!(profile.duration_us.is_some());
assert_eq!(profile.duration_us.unwrap(), 15000);
let main_frame = profile.frames.iter().find(|f| f.name == "main");
assert!(main_frame.is_some());
let main = main_frame.unwrap();
assert_eq!(main.file, Some("/src/index.js".to_string()));
assert_eq!(main.line, Some(11)); assert_eq!(main.col, Some(6)); assert_eq!(main.kind, FrameKind::Function);
assert_eq!(main.category, FrameCategory::App);
}
#[test]
fn test_parse_gc_frames() {
let parser = CpuProfileParser::default();
let profile = parser.parse_str(GC_CPU_PROFILE, None).unwrap();
let gc_frame = profile
.frames
.iter()
.find(|f| f.name == "(garbage collector)");
assert!(gc_frame.is_some(), "Should have GC frame");
let gc = gc_frame.unwrap();
assert_eq!(gc.kind, FrameKind::GC);
assert_eq!(gc.category, FrameCategory::V8Internal);
}
#[test]
fn test_parse_deps_classification() {
let parser = CpuProfileParser::default();
let profile = parser.parse_str(DEPS_CPU_PROFILE, None).unwrap();
let map_frame = profile.frames.iter().find(|f| f.name == "map");
assert!(map_frame.is_some(), "Should have map frame");
let map = map_frame.unwrap();
assert_eq!(map.category, FrameCategory::Deps);
assert!(map.file.as_ref().unwrap().contains("node_modules"));
}
#[test]
fn test_parse_stack_reconstruction() {
let parser = CpuProfileParser::default();
let profile = parser.parse_str(MINIMAL_CPU_PROFILE, None).unwrap();
assert!(!profile.stacks.is_empty());
let compute_stack = profile.stacks.iter().find(|s| {
s.frames
.iter()
.any(|&fid| profile.get_frame(fid).is_some_and(|f| f.name == "compute"))
});
assert!(compute_stack.is_some(), "Should have stack with compute");
let stack = compute_stack.unwrap();
assert!(stack.frames.len() >= 2);
}
#[test]
fn test_parse_invalid_json() {
let parser = CpuProfileParser::default();
let result = parser.parse_str("not valid json", None);
assert!(result.is_err());
}
#[test]
fn test_parse_empty_samples() {
let json = r#"{
"nodes": [
{
"id": 1,
"callFrame": {
"functionName": "(root)",
"scriptId": "0",
"url": "",
"lineNumber": -1,
"columnNumber": -1
}
}
],
"samples": [],
"timeDeltas": [],
"startTime": 0,
"endTime": 0
}"#;
let parser = CpuProfileParser::default();
let result = parser.parse_str(json, None);
assert!(result.is_ok());
let profile = result.unwrap();
assert!(profile.samples.is_empty());
assert_eq!(profile.total_weight(), 0);
}
const MINIMAL_HEAP_PROFILE: &str = r#"{
"head": {
"callFrame": {
"functionName": "(root)",
"scriptId": "0",
"url": "",
"lineNumber": -1,
"columnNumber": -1
},
"selfSize": 0,
"id": 1,
"children": [
{
"callFrame": {
"functionName": "allocate",
"scriptId": "1",
"url": "/src/alloc.js",
"lineNumber": 10,
"columnNumber": 5
},
"selfSize": 1024,
"id": 2,
"children": []
},
{
"callFrame": {
"functionName": "createBuffer",
"scriptId": "2",
"url": "/src/buffer.js",
"lineNumber": 20,
"columnNumber": 1
},
"selfSize": 4096,
"id": 3,
"children": []
}
]
}
}"#;
#[test]
fn test_parse_minimal_heap_profile() {
let parser = HeapProfileParser::default();
let result = parser.parse_str(MINIMAL_HEAP_PROFILE, Some("test.heapprofile".to_string()));
assert!(result.is_ok(), "Should parse minimal heap profile");
let profile = result.unwrap();
assert!(!profile.frames.is_empty());
assert_eq!(profile.source_file, Some("test.heapprofile".to_string()));
assert!(profile.duration_us.is_none());
assert!(profile.total_weight() > 0);
}
#[test]
fn test_parse_heap_invalid_json() {
let parser = HeapProfileParser::default();
let result = parser.parse_str("not valid json", None);
assert!(result.is_err());
}
#[test]
fn test_parse_real_cpu_profile() {
let path = Path::new("test-runner-profile");
if !path.exists() {
return; }
let cpu_file = std::fs::read_dir(path)
.unwrap()
.filter_map(Result::ok)
.find(|e| e.path().extension().is_some_and(|ext| ext == "cpuprofile"));
if let Some(entry) = cpu_file {
let parser = CpuProfileParser::default();
let result = parser.parse_file(&entry.path());
assert!(
result.is_ok(),
"Should parse real CPU profile: {:?}",
entry.path()
);
let profile = result.unwrap();
assert!(!profile.frames.is_empty(), "Should have frames");
assert!(!profile.samples.is_empty(), "Should have samples");
assert!(profile.total_weight() > 0, "Should have non-zero weight");
}
}
#[test]
fn test_parse_real_heap_profile() {
let path = Path::new("test-runner-profile");
if !path.exists() {
return; }
let heap_file = std::fs::read_dir(path)
.unwrap()
.filter_map(Result::ok)
.find(|e| e.path().extension().is_some_and(|ext| ext == "heapprofile"));
if let Some(entry) = heap_file {
let parser = HeapProfileParser::default();
let result = parser.parse_file(&entry.path());
assert!(
result.is_ok(),
"Should parse real heap profile: {:?}",
entry.path()
);
let profile = result.unwrap();
assert!(!profile.frames.is_empty(), "Should have frames");
assert!(
profile.total_weight() > 0,
"Should have non-zero allocation size"
);
}
}