#[cfg(feature = "time_profiling")]
use std::env;
#[cfg(feature = "time_profiling")]
use std::{str::FromStr, time::Duration};
#[cfg(feature = "time_profiling")]
use thag_profiler::{
clear_profile_config_cache, debug_log, disable_profiling, fn_name, profiled,
profiling::{
DebugLevel, ProfileCapability, ProfileConfiguration, ProfilePaths, ProfileStats,
ProfileType, build_stack, clean_function_name, extract_path, get_profile_config,
get_reg_desc_name, get_time_path, is_profiled_function, register_profiled_function,
set_profile_config,
},
};
#[cfg(feature = "full_profiling")]
use thag_profiler::{
profiling::{get_memory_detail_dealloc_path, get_memory_detail_path, get_memory_path},
safe_alloc,
};
#[cfg(feature = "time_profiling")]
fn test_profile_capability() {
let none = ProfileCapability::NONE;
let time = ProfileCapability::TIME;
let memory = ProfileCapability::MEMORY;
let both = ProfileCapability::BOTH;
assert_eq!(both.0, time.0 | memory.0);
assert!(time.supports(ProfileType::Time));
assert!(!time.supports(ProfileType::Memory));
assert!(!time.supports(ProfileType::Both));
assert!(time.supports(ProfileType::None));
assert!(memory.supports(ProfileType::Memory));
assert!(!memory.supports(ProfileType::Time));
assert!(!memory.supports(ProfileType::Both));
assert!(both.supports(ProfileType::Time));
assert!(both.supports(ProfileType::Memory));
assert!(both.supports(ProfileType::Both));
assert!(none.supports(ProfileType::None));
assert!(!none.supports(ProfileType::Time));
assert_eq!(
ProfileCapability::from_profile_type(ProfileType::Time).0,
time.0
);
assert_eq!(
ProfileCapability::from_profile_type(ProfileType::Memory).0,
memory.0
);
assert_eq!(
ProfileCapability::from_profile_type(ProfileType::Both).0,
both.0
);
assert_eq!(
ProfileCapability::from_profile_type(ProfileType::None).0,
none.0
);
assert_eq!(both.intersection(ProfileType::Time).0, time.0);
assert_eq!(both.intersection(ProfileType::Memory).0, memory.0);
assert_eq!(both.intersection(ProfileType::Both).0, both.0);
assert_eq!(both.intersection(ProfileType::None).0, none.0);
let available = ProfileCapability::available();
#[cfg(all(feature = "time_profiling", not(feature = "full_profiling")))]
assert_eq!(available.0, time.0);
#[cfg(feature = "full_profiling")]
assert_eq!(available.0, both.0);
}
#[cfg(feature = "time_profiling")]
fn test_profile_type() {
assert_eq!(ProfileType::from_str("time"), Ok(ProfileType::Time));
assert_eq!(ProfileType::from_str("memory"), Ok(ProfileType::Memory));
assert_eq!(ProfileType::from_str("both"), Ok(ProfileType::Both));
assert_eq!(ProfileType::from_str("none"), Ok(ProfileType::None));
assert!(ProfileType::from_str("invalid").is_err());
assert_eq!(ProfileType::Time.to_string(), "time");
assert_eq!(ProfileType::Memory.to_string(), "memory");
assert_eq!(ProfileType::Both.to_string(), "both");
assert_eq!(ProfileType::None.to_string(), "none");
}
#[cfg(feature = "time_profiling")]
fn test_debug_level() {
assert_eq!(DebugLevel::from_str("none").unwrap(), DebugLevel::None);
assert_eq!(DebugLevel::from_str("quiet").unwrap(), DebugLevel::Quiet);
assert_eq!(
DebugLevel::from_str("announce").unwrap(),
DebugLevel::Announce
);
assert!(DebugLevel::from_str("invalid").is_err());
assert_eq!(DebugLevel::from_str("QUIET").unwrap(), DebugLevel::Quiet);
assert_eq!(
DebugLevel::from_str("Announce").unwrap(),
DebugLevel::Announce
);
assert_eq!(DebugLevel::from_str(" none ").unwrap(), DebugLevel::None);
assert_eq!(DebugLevel::None.to_string(), "none");
assert_eq!(DebugLevel::Quiet.to_string(), "quiet");
assert_eq!(DebugLevel::Announce.to_string(), "announce");
assert_eq!(DebugLevel::default(), DebugLevel::None);
}
#[cfg(feature = "time_profiling")]
fn test_profile_configuration() {
let config1 = ProfileConfiguration::try_from(["time", "", "quiet"].as_slice()).unwrap();
assert_eq!(config1.profile_type(), Some(ProfileType::Time));
assert_eq!(config1.debug_level(), Some(DebugLevel::Quiet));
let config2 =
ProfileConfiguration::try_from(["both", "./output", "announce", "true"].as_slice())
.unwrap();
assert_eq!(config2.profile_type(), Some(ProfileType::Both));
assert_eq!(config2.debug_level(), Some(DebugLevel::Announce));
assert!(config2.is_detailed_memory());
let invalid1 = ProfileConfiguration::try_from(["invalid", "", "quiet"].as_slice());
assert!(invalid1.is_err());
let invalid2 = ProfileConfiguration::try_from(["time", "", "quiet", "true"].as_slice());
assert!(
invalid2.is_err(),
"time profile with detailed_memory should not be allowed"
);
let mut config = config1;
config.set_profile_type(Some(ProfileType::Both));
assert_eq!(config.profile_type(), Some(ProfileType::Both));
let display_str = config.to_string();
assert!(display_str.contains("Profile Config:"));
assert!(display_str.contains("Profile Type: Both"));
}
#[cfg(feature = "time_profiling")]
fn test_env_config_parsing() {
let original_var = env::var("THAG_PROFILER").ok();
unsafe { env::remove_var("THAG_PROFILER") };
clear_profile_config_cache();
let config1 = get_profile_config();
assert!(!config1.is_enabled());
unsafe { env::set_var("THAG_PROFILER", "time,,quiet") };
clear_profile_config_cache();
let config2 = get_profile_config();
assert!(config2.is_enabled());
assert_eq!(config2.profile_type(), Some(ProfileType::Time));
assert_eq!(config2.debug_level(), Some(DebugLevel::Quiet));
unsafe { env::set_var("THAG_PROFILER", "both,./output,announce,true") };
clear_profile_config_cache();
let config3 = get_profile_config();
assert!(config3.is_enabled());
assert_eq!(config3.profile_type(), Some(ProfileType::Both));
assert_eq!(config3.debug_level(), Some(DebugLevel::Announce));
assert!(config3.is_detailed_memory());
unsafe { env::set_var("THAG_PROFILER", "time,,none") };
let config4 = get_profile_config();
assert_eq!(config4.profile_type(), Some(ProfileType::Both));
clear_profile_config_cache();
let config5 = get_profile_config();
assert_eq!(config5.profile_type(), Some(ProfileType::Time));
let mut custom_config = config5;
custom_config.set_profile_type(Some(ProfileType::Memory));
set_profile_config(custom_config);
let config6 = get_profile_config();
assert_eq!(config6.profile_type(), Some(ProfileType::Memory));
if let Some(value) = original_var {
unsafe { env::set_var("THAG_PROFILER", value) };
} else {
unsafe { env::remove_var("THAG_PROFILER") };
}
clear_profile_config_cache();
}
#[cfg(feature = "time_profiling")]
fn test_profile_file_paths() {
let paths = ProfilePaths::get();
assert!(
get_time_path().unwrap().ends_with(".folded"),
"Time path should end with .folded"
);
#[cfg(feature = "full_profiling")]
assert!(
get_memory_path().unwrap().ends_with("-memory.folded"),
"Memory path should end with -memory.folded"
);
assert!(
paths.debug_log.ends_with("-debug.log"),
"Debug log path should end with -debug.log"
);
let timestamp = &paths.timestamp;
assert_eq!(
timestamp.len(),
15,
"Timestamp should be YYYYMMDD-HHMMSS format"
);
assert_eq!(
timestamp.chars().nth(8).unwrap(),
'-',
"Timestamp should have a dash at position 8"
);
#[cfg(feature = "time_profiling")]
{
use thag_profiler::profiling::get_time_path;
let time_path = get_time_path().expect("Should be able to get time path");
assert!(
time_path.ends_with(".folded"),
"Time path should end with .folded"
);
}
#[cfg(feature = "full_profiling")]
{
let memory_path = get_memory_path().expect("Should be able to get memory path");
assert!(
memory_path.ends_with("-memory.folded"),
"Memory path should end with -memory.folded"
);
let detail_path =
get_memory_detail_path().expect("Should be able to get memory detail path");
assert!(
detail_path.ends_with("-memory_detail.folded"),
"Detail path should end with -memory_detail.folded"
);
let dealloc_path =
get_memory_detail_dealloc_path().expect("Should be able to get dealloc path");
assert!(
dealloc_path.ends_with("-memory_detail_dealloc.folded"),
"Dealloc path should end with -memory_detail_dealloc.folded"
);
}
}
#[cfg(feature = "time_profiling")]
fn test_function_registry() {
let test_prefix = "test_registry_";
let func1 = format!("{test_prefix}func1");
let func2 = format!("{test_prefix}func2");
let path1 = format!("{test_prefix}module;{func1}");
let path2 = format!("{test_prefix}module;{func2}");
register_profiled_function(&path1, &func1);
register_profiled_function(&path2, &func2);
assert!(is_profiled_function(&path1), "func1 should be registered");
assert!(is_profiled_function(&path2), "func2 should be registered");
assert!(
!is_profiled_function("unknown_function"),
"Unknown function should not be registered"
);
assert_eq!(get_reg_desc_name(&path1), Some(func1.clone()));
assert_eq!(get_reg_desc_name(&path2), Some(func2));
assert_eq!(get_reg_desc_name("unknown_function"), None);
let stack = vec![path1, "middleware".to_string(), "main".to_string()];
let path = extract_path(&stack, None);
assert!(!path.is_empty(), "Path should not be empty");
let path_vec = vec!["module".to_string(), func1.clone()];
let section_name = Some("section".to_string());
let stack_str = build_stack(&path_vec, section_name.as_ref(), ";");
assert!(
stack_str.contains(&func1),
"Stack should contain function name"
);
assert!(
stack_str.contains("section"),
"Stack should contain section name"
);
}
#[cfg(feature = "time_profiling")]
fn test_string_cleaning() {
let mut name1 = "module::function::{{closure}}".to_string();
assert_eq!(clean_function_name(&mut name1), "module::function");
let mut name2 = "module::function::h1a2b3c4d".to_string();
assert_eq!(clean_function_name(&mut name2), "module::function");
let mut name3 = "module::::function".to_string();
assert_eq!(clean_function_name(&mut name3), "module::function");
let mut name4 = "module::function::{{closure}}::h1a2b3c4d".to_string();
assert_eq!(clean_function_name(&mut name4), "module::function");
}
#[cfg(all(feature = "time_profiling", not(feature = "full_profiling")))]
#[enable_profiling(time)]
fn test_profile_creation() {
let profile = Profile::new(
Some("test_section"),
Some("test_function"),
ProfileType::Time,
false, false, file!(),
Some(100), Some(200), );
assert!(
profile.is_some(),
"Profile should be created when profiling is enabled"
);
if let Some(profile) = profile {
assert_eq!(profile.get_profile_type(), ProfileType::Time);
assert!(!profile.is_detailed_memory());
assert_eq!(profile.section_name(), Some("test_section".to_string()));
let path = profile.path();
assert!(!path.is_empty(), "Path should not be empty");
#[cfg(feature = "full_profiling")]
{
let memory_profile = Profile::new(
Some("memory_section"),
Some("memory_function"),
ProfileType::Memory,
false,
true, file!(),
Some(300),
Some(400),
);
assert!(memory_profile.is_some(), "Memory profile should be created");
if let Some(profile) = memory_profile {
assert_eq!(profile.get_profile_type(), ProfileType::Memory);
assert!(profile.is_detailed_memory());
}
}
}
disable_profiling();
let no_profile = Profile::new(
Some("disabled_section"),
Some("disabled_function"),
ProfileType::Time,
false,
false,
file!(),
None,
None,
);
assert!(
no_profile.is_none(),
"Profile should not be created when profiling is disabled"
);
}
#[cfg(feature = "time_profiling")]
fn test_profile_stats() {
let mut stats = ProfileStats::default();
stats.record("function1", 2, Duration::from_micros(100));
stats.record("function1", 4, Duration::from_micros(200));
stats.record("function2", 5, Duration::from_micros(150));
assert_eq!(*stats.calls.get("function1").unwrap(), 6);
assert_eq!(*stats.calls.get("function2").unwrap(), 5);
assert_eq!(*stats.total_time.get("function1").unwrap(), 300);
assert_eq!(*stats.total_time.get("function2").unwrap(), 150);
}
#[cfg(feature = "full_profiling")]
#[profiled(mem_summary)]
#[fn_name]
fn test_stack_extraction() {
safe_alloc! {
let callstack = thag_profiler::mem_tracking::extract_callstack_with_recursion_check(&["test_profiling".to_string()]).expect("Probable recursion detected.");
let callstack: Vec<&String> = callstack.iter().map(|(_, _, name, _, _)| name)
.collect();
assert!(!callstack.is_empty(), "Callstack should not be empty");
assert!(
callstack[0].contains(fn_name),
"First frame should be the current function name"
);
};
}
#[cfg(feature = "time_profiling")]
#[profiled]
#[allow(unused_assignments)]
fn test_profiled_function() {
let closure = || {
debug_log!("Inside profiled function");
#[cfg(feature = "debug_logging")]
let start = std::time::Instant::now();
#[allow(unused_variables)]
let mut sum = 0;
for i in 0..1000 {
sum += i;
}
debug_log!("Work took {} micros", start.elapsed().as_micros());
};
#[cfg(feature = "full_profiling")]
safe_alloc!(closure());
#[cfg(not(feature = "full_profiling"))]
closure();
}
#[test]
#[cfg(feature = "time_profiling")]
fn test_profiling_full_sequence() {
let original_var = env::var("THAG_PROFILER").ok();
unsafe { env::set_var("THAG_PROFILER", "time,,announce") };
clear_profile_config_cache();
println!("Starting profiling module tests");
println!("Testing profile capability...");
test_profile_capability();
println!("Testing profile type...");
test_profile_type();
println!("Testing debug level...");
test_debug_level();
println!("Testing profile configuration...");
test_profile_configuration();
println!("Testing environment config parsing...");
test_env_config_parsing();
println!("Testing profile file paths...");
test_profile_file_paths();
println!("Testing function registry...");
test_function_registry();
println!("Testing string cleaning...");
test_string_cleaning();
#[cfg(not(feature = "full_profiling"))]
{
println!("Testing profile creation...");
test_profile_creation();
}
println!("Testing profile stats...");
test_profile_stats();
#[cfg(feature = "full_profiling")]
{
enable_memory_profiling_for_test();
#[thag_profiler::enable_profiling(memory, function(mem_summary))]
fn enable_memory_profiling_for_test() {
eprintln!("In test_profiling_full_sequence::enable_memory_profiling_for_test");
println!("Testing stack extraction (full_profiling only)...");
test_stack_extraction();
}
}
println!("Testing profiled function attribute...");
test_profiled_function();
disable_profiling();
if let Some(value) = original_var {
unsafe { env::set_var("THAG_PROFILER", value) };
} else {
unsafe { env::remove_var("THAG_PROFILER") };
}
clear_profile_config_cache();
println!("All profiling tests passed!");
}