mod fixtures;
use insta::assert_snapshot;
use profile_inspect::analysis::{CallerCalleeAnalyzer, CpuAnalyzer, HeapAnalyzer, ProfileDiffer};
use profile_inspect::ir::{FrameCategory, FrameId};
use regex::Regex;
#[test]
fn test_cpu_analyzer_basic() {
let profile = fixtures::create_simple_cpu_profile();
let analyzer = CpuAnalyzer::new();
let analysis = analyzer.analyze(&profile);
assert_eq!(analysis.total_time, 10000);
assert_eq!(analysis.total_samples, 13);
assert!(!analysis.functions.is_empty());
let breakdown = &analysis.category_breakdown;
let breakdown_total = breakdown.app
+ breakdown.deps
+ breakdown.node_internal
+ breakdown.v8_internal
+ breakdown.native;
assert_eq!(breakdown_total, 10000);
}
#[test]
fn test_cpu_analyzer_default_hides_internals() {
let profile = fixtures::create_simple_cpu_profile();
let analyzer = CpuAnalyzer::new(); let analysis = analyzer.analyze(&profile);
for func in &analysis.functions {
assert!(
!func.category.is_internal(),
"Function {} should not be internal",
func.name
);
}
}
#[test]
fn test_cpu_analyzer_include_internals() {
let profile = fixtures::create_simple_cpu_profile();
let analyzer = CpuAnalyzer::new().include_internals(true);
let analysis = analyzer.analyze(&profile);
let has_internal = analysis.functions.iter().any(|f| f.category.is_internal());
assert!(
has_internal,
"Should include internal frames when include_internals is true"
);
}
#[test]
fn test_cpu_analyzer_min_percent_filter() {
let profile = fixtures::create_simple_cpu_profile();
let analyzer = CpuAnalyzer::new().min_percent(50.0);
let analysis = analyzer.analyze(&profile);
for func in &analysis.functions {
let pct = func.self_percent(analysis.total_time);
assert!(
pct >= 50.0,
"Function {} has {}% but min is 50%",
func.name,
pct
);
}
}
#[test]
fn test_cpu_analyzer_top_n() {
let profile = fixtures::create_simple_cpu_profile();
let analyzer = CpuAnalyzer::new().include_internals(true).top_n(2);
let analysis = analyzer.analyze(&profile);
assert!(
analysis.functions.len() <= 2,
"Should have at most 2 functions"
);
}
#[test]
fn test_cpu_analyzer_category_filter() {
let profile = fixtures::create_simple_cpu_profile();
let analyzer = CpuAnalyzer::new().filter_categories(vec![FrameCategory::App]);
let analysis = analyzer.analyze(&profile);
for func in &analysis.functions {
assert_eq!(
func.category,
FrameCategory::App,
"Function {} should be App category",
func.name
);
}
}
#[test]
fn test_cpu_analyzer_focus_pattern() {
let profile = fixtures::create_simple_cpu_profile();
let pattern = Regex::new("compute").unwrap();
let analyzer = CpuAnalyzer::new().focus(pattern);
let analysis = analyzer.analyze(&profile);
assert!(!analysis.functions.is_empty());
for func in &analysis.functions {
assert!(
func.name.contains("compute") || func.location.contains("compute"),
"Function {} should match 'compute'",
func.name
);
}
}
#[test]
fn test_cpu_analyzer_exclude_pattern() {
let profile = fixtures::create_simple_cpu_profile();
let pattern = Regex::new("main").unwrap();
let analyzer = CpuAnalyzer::new().exclude(pattern);
let analysis = analyzer.analyze(&profile);
for func in &analysis.functions {
assert!(
!func.name.contains("main"),
"Function {} should not match 'main'",
func.name
);
}
}
#[test]
fn test_cpu_analyzer_gc_tracking() {
let profile = fixtures::create_gc_heavy_profile();
let analyzer = CpuAnalyzer::new().include_internals(true);
let analysis = analyzer.analyze(&profile);
assert!(analysis.gc_time > 0, "Should detect GC time");
let gc_pct = (analysis.gc_time as f64 / analysis.total_time as f64) * 100.0;
assert!(gc_pct > 10.0, "GC should be >10%, got {:.1}%", gc_pct);
assert!(analysis.gc_analysis.is_some(), "Should have GC analysis");
let gc = analysis.gc_analysis.as_ref().unwrap();
assert!(gc.sample_count > 0, "Should have GC samples");
assert!(
!gc.allocation_hotspots.is_empty(),
"Should identify allocation hotspots"
);
}
#[test]
fn test_cpu_analyzer_recursive_detection() {
let profile = fixtures::create_recursive_profile();
let analyzer = CpuAnalyzer::new();
let analysis = analyzer.analyze(&profile);
assert!(
!analysis.recursive_functions.is_empty(),
"Should detect recursive functions"
);
let recursive_names: Vec<&str> = analysis
.recursive_functions
.iter()
.map(|f| f.name.as_str())
.collect();
assert!(
recursive_names.contains(&"factorial") || recursive_names.contains(&"fibonacci"),
"Should detect factorial or fibonacci as recursive"
);
for func in &analysis.recursive_functions {
if func.name == "fibonacci" {
assert!(func.max_depth >= 3, "Fibonacci should have depth >= 3");
}
}
}
#[test]
fn test_cpu_analyzer_phase_analysis() {
let profile = fixtures::create_simple_cpu_profile();
let analyzer = CpuAnalyzer::new();
let analysis = analyzer.analyze(&profile);
assert!(
analysis.phase_analysis.is_some(),
"Should have phase analysis"
);
let phases = analysis.phase_analysis.as_ref().unwrap();
assert!(
phases.startup.sample_count > 0,
"Startup phase should have samples"
);
assert!(
phases.steady_state.sample_count > 0,
"Steady state should have samples"
);
assert_eq!(
phases.startup.sample_count + phases.steady_state.sample_count,
analysis.total_samples,
"Phases should account for all samples"
);
}
#[test]
fn test_cpu_analyzer_hot_paths() {
let profile = fixtures::create_simple_cpu_profile();
let analyzer = CpuAnalyzer::new();
let analysis = analyzer.analyze(&profile);
assert!(!analysis.hot_paths.is_empty(), "Should have hot paths");
for window in analysis.hot_paths.windows(2) {
assert!(
window[0].time >= window[1].time,
"Hot paths should be sorted by CPU time"
);
}
}
#[test]
fn test_cpu_analyzer_package_stats() {
let profile = fixtures::create_multi_package_profile();
let analyzer = CpuAnalyzer::new();
let analysis = analyzer.analyze(&profile);
assert!(
!analysis.package_stats.is_empty(),
"Should have package stats"
);
let packages: Vec<&str> = analysis
.package_stats
.iter()
.map(|p| p.package.as_str())
.collect();
assert!(
packages.iter().any(|p| p.contains("lodash")),
"Should identify lodash package"
);
}
#[test]
fn test_cpu_analysis_snapshot() {
let profile = fixtures::create_simple_cpu_profile();
let analyzer = CpuAnalyzer::new();
let analysis = analyzer.analyze(&profile);
let summary = format!(
"Total time: {}us\n\
Total samples: {}\n\
Functions count: {}\n\
Category breakdown:\n\
App: {}us ({:.1}%)\n\
Deps: {}us ({:.1}%)\n\
NodeInternal: {}us ({:.1}%)\n\
V8Internal: {}us ({:.1}%)\n\
Native: {}us ({:.1}%)\n\
GC time: {}us ({:.1}%)\n\
Hot paths: {}\n\
Recursive functions: {}",
analysis.total_time,
analysis.total_samples,
analysis.functions.len(),
analysis.category_breakdown.app,
analysis.category_breakdown.percent(FrameCategory::App),
analysis.category_breakdown.deps,
analysis.category_breakdown.percent(FrameCategory::Deps),
analysis.category_breakdown.node_internal,
analysis
.category_breakdown
.percent(FrameCategory::NodeInternal),
analysis.category_breakdown.v8_internal,
analysis
.category_breakdown
.percent(FrameCategory::V8Internal),
analysis.category_breakdown.native,
analysis.category_breakdown.percent(FrameCategory::Native),
analysis.gc_time,
if analysis.total_time > 0 {
(analysis.gc_time as f64 / analysis.total_time as f64) * 100.0
} else {
0.0
},
analysis.hot_paths.len(),
analysis.recursive_functions.len(),
);
assert_snapshot!(summary);
}
#[test]
fn test_heap_analyzer_basic() {
let profile = fixtures::create_simple_heap_profile();
let analyzer = HeapAnalyzer::new();
let analysis = analyzer.analyze(&profile);
assert_eq!(analysis.total_size, 18432);
assert_eq!(analysis.total_allocations, 6);
assert!(!analysis.functions.is_empty());
}
#[test]
fn test_heap_analyzer_category_breakdown() {
let profile = fixtures::create_simple_heap_profile();
let analyzer = HeapAnalyzer::new().include_internals(true);
let analysis = analyzer.analyze(&profile);
let breakdown = &analysis.category_breakdown;
assert!(
breakdown.app > 0 || breakdown.deps > 0,
"Should have app or deps allocations"
);
}
#[test]
fn test_heap_analyzer_default_hides_internals() {
let profile = fixtures::create_simple_heap_profile();
let analyzer = HeapAnalyzer::new();
let analysis = analyzer.analyze(&profile);
for func in &analysis.functions {
assert!(
!func.category.is_internal(),
"Function {} should not be internal",
func.name
);
}
}
#[test]
fn test_heap_analysis_snapshot() {
let profile = fixtures::create_simple_heap_profile();
let analyzer = HeapAnalyzer::new().include_internals(true);
let analysis = analyzer.analyze(&profile);
let summary = format!(
"Total size: {} bytes\n\
Total allocations: {}\n\
Functions count: {}\n\
Category breakdown:\n\
App: {} bytes\n\
Deps: {} bytes\n\
NodeInternal: {} bytes\n\
V8Internal: {} bytes\n\
Native: {} bytes",
analysis.total_size,
analysis.total_allocations,
analysis.functions.len(),
analysis.category_breakdown.app,
analysis.category_breakdown.deps,
analysis.category_breakdown.node_internal,
analysis.category_breakdown.v8_internal,
analysis.category_breakdown.native,
);
assert_snapshot!(summary);
}
#[test]
fn test_caller_callee_basic() {
let profile = fixtures::create_simple_cpu_profile();
let analyzer = CallerCalleeAnalyzer::new();
let analysis = analyzer.analyze(&profile, FrameId(1));
assert!(analysis.is_some(), "Should find analysis for compute");
let analysis = analysis.unwrap();
assert_eq!(analysis.target_name, "compute");
assert!(!analysis.callers.is_empty(), "Should have callers");
assert!(
analysis.callers.iter().any(|c| c.name == "main"),
"main should be a caller"
);
}
#[test]
fn test_caller_callee_find_by_name() {
let profile = fixtures::create_simple_cpu_profile();
let frame = CallerCalleeAnalyzer::find_frame_by_name(&profile, "compute");
assert!(frame.is_some(), "Should find compute frame");
assert_eq!(frame.unwrap().name, "compute");
let not_found = CallerCalleeAnalyzer::find_frame_by_name(&profile, "nonexistent");
assert!(not_found.is_none(), "Should not find nonexistent frame");
}
#[test]
fn test_profile_differ_basic() {
let before = fixtures::create_diff_profile_before();
let after = fixtures::create_diff_profile_after();
let differ = ProfileDiffer::new();
let diff = differ.diff(&before, &after);
assert_eq!(diff.before_total, 5000);
assert_eq!(diff.after_total, 4000);
assert!(!diff.improvements.is_empty(), "Should have improvements");
let has_slow_improvement = diff.improvements.iter().any(|d| d.name == "slowFunction");
assert!(
has_slow_improvement,
"slowFunction should be an improvement"
);
assert!(!diff.regressions.is_empty(), "Should have regressions");
let has_fast_regression = diff.regressions.iter().any(|d| d.name == "fastFunction");
assert!(has_fast_regression, "fastFunction should be a regression");
let has_new = diff.new_functions.iter().any(|f| f.name == "newFunction");
assert!(has_new, "Should detect newFunction");
let has_removed = diff
.removed_functions
.iter()
.any(|f| f.name == "removedFunction");
assert!(has_removed, "Should detect removedFunction");
}
#[test]
fn test_profile_differ_min_delta() {
let before = fixtures::create_diff_profile_before();
let after = fixtures::create_diff_profile_after();
let differ = ProfileDiffer::new().min_delta_percent(100.0);
let diff = differ.diff(&before, &after);
for regression in &diff.regressions {
assert!(
regression.delta_percent.abs() >= 100.0,
"Regression {} has {}% but min is 100%",
regression.name,
regression.delta_percent
);
}
}
#[test]
fn test_profile_differ_snapshot() {
let before = fixtures::create_diff_profile_before();
let after = fixtures::create_diff_profile_after();
let differ = ProfileDiffer::new().min_delta_percent(1.0);
let diff = differ.diff(&before, &after);
let summary = format!(
"Before total: {}us\n\
After total: {}us\n\
Overall delta: {:.1}%\n\
Regressions: {}\n\
Improvements: {}\n\
New functions: {}\n\
Removed functions: {}",
diff.before_total,
diff.after_total,
diff.overall_delta_percent,
diff.regressions
.iter()
.map(|r| format!("{} ({:+.1}%)", r.name, r.delta_percent))
.collect::<Vec<_>>()
.join(", "),
diff.improvements
.iter()
.map(|i| format!("{} ({:+.1}%)", i.name, i.delta_percent))
.collect::<Vec<_>>()
.join(", "),
diff.new_functions
.iter()
.map(|f| f.name.as_str())
.collect::<Vec<_>>()
.join(", "),
diff.removed_functions
.iter()
.map(|f| f.name.as_str())
.collect::<Vec<_>>()
.join(", "),
);
assert_snapshot!(summary);
}