use std::fmt;
#[cfg(feature = "lab-stack-traces")]
pub fn capture_stack_trace() -> String {
use std::fmt::Write;
let bt = backtrace::Backtrace::new();
let mut output = String::new();
let _ = writeln!(&mut output, "Stack trace:");
for (i, frame) in bt.frames().iter().enumerate() {
for symbol in frame.symbols() {
if let Some(name) = symbol.name() {
let _ = write!(&mut output, " {}: ", i);
if let Ok(demangled) = rustc_demangle::try_demangle(&name.to_string()) {
let _ = write!(&mut output, "{}", demangled);
} else {
let _ = write!(&mut output, "{}", name);
}
if let (Some(filename), Some(lineno)) = (symbol.filename(), symbol.lineno()) {
let _ = write!(&mut output, " at {}:{}", filename.display(), lineno);
}
let _ = writeln!(&mut output);
}
}
}
output
}
#[cfg(not(feature = "lab-stack-traces"))]
pub fn capture_stack_trace() -> String {
"Stack trace capture disabled (enable 'lab-stack-traces' feature)".to_string()
}
#[derive(Debug, Clone)]
pub struct StackTrace {
trace: String,
}
impl StackTrace {
pub fn capture() -> Self {
Self {
trace: capture_stack_trace(),
}
}
pub fn as_str(&self) -> &str {
&self.trace
}
pub fn compact(&self) -> String {
let lines: Vec<&str> = self.trace.lines().collect();
if lines.len() <= 5 {
return self.trace.clone();
}
let mut result = String::new();
for line in lines.iter().take(5) {
result.push_str(line);
result.push('\n');
}
result.push_str(&format!("... ({} more frames)\n", lines.len() - 5));
result
}
pub fn frame_count(&self) -> usize {
self.trace.lines().count()
}
pub fn filter_frames(&self, pattern: &str) -> Self {
let filtered_lines: Vec<&str> = self
.trace
.lines()
.filter(|line| line.contains(pattern))
.collect();
Self {
trace: filtered_lines.join("\n"),
}
}
}
impl fmt::Display for StackTrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.trace)
}
}
impl From<StackTrace> for String {
fn from(trace: StackTrace) -> String {
trace.trace
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::pedantic,
clippy::nursery,
clippy::expect_fun_call,
clippy::map_unwrap_or,
clippy::cast_possible_wrap,
clippy::future_not_send
)]
use super::*;
#[test]
fn test_capture_stack_trace() {
let trace = capture_stack_trace();
assert!(!trace.is_empty());
#[cfg(feature = "lab-stack-traces")]
assert!(trace.contains("Stack trace:"));
#[cfg(not(feature = "lab-stack-traces"))]
assert!(trace.contains("disabled"));
}
#[test]
fn test_stack_trace_wrapper() {
let trace = StackTrace::capture();
assert!(!trace.as_str().is_empty());
let displayed = format!("{}", trace);
assert_eq!(displayed, trace.as_str());
let compact = trace.compact();
assert!(!compact.is_empty());
let frame_count = trace.frame_count();
assert!(frame_count > 0);
}
#[test]
fn test_stack_trace_from_deep_call() {
fn level_1() -> StackTrace {
level_2()
}
fn level_2() -> StackTrace {
level_3()
}
fn level_3() -> StackTrace {
StackTrace::capture()
}
let trace = level_1();
#[cfg(feature = "lab-stack-traces")]
{
let trace_str = trace.as_str();
assert!(
trace_str.contains("level_1")
|| trace_str.contains("level_2")
|| trace_str.contains("level_3")
);
assert!(trace.frame_count() > 1);
}
#[cfg(not(feature = "lab-stack-traces"))]
assert_eq!(trace.frame_count(), 1);
}
#[test]
fn test_compact_formatting() {
let long_trace = StackTrace {
trace: (0..10)
.map(|i| format!(" {}: function_{}", i, i))
.collect::<Vec<_>>()
.join("\n"),
};
let compact = long_trace.compact();
if long_trace.frame_count() > 5 {
assert!(compact.contains("... ("));
assert!(compact.contains("more frames)"));
}
}
#[test]
fn test_filter_frames() {
let trace = StackTrace {
trace: "Stack trace:\n 0: my_function\n 1: std::panic\n 2: other_function\n 3: std::thread".to_string(),
};
let filtered = trace.filter_frames("std::");
assert!(filtered.as_str().contains("std::panic"));
assert!(filtered.as_str().contains("std::thread"));
assert!(!filtered.as_str().contains("my_function"));
assert!(!filtered.as_str().contains("other_function"));
}
#[test]
fn test_feature_flag_behavior() {
let trace = capture_stack_trace();
#[cfg(feature = "lab-stack-traces")]
{
assert!(trace.starts_with("Stack trace:"));
assert!(trace.lines().count() > 1);
}
#[cfg(not(feature = "lab-stack-traces"))]
{
assert_eq!(
trace,
"Stack trace capture disabled (enable 'lab-stack-traces' feature)"
);
}
}
#[test]
fn test_repeated_capture_preserves_contract() {
for _ in 0..10 {
let trace = capture_stack_trace();
assert!(!trace.is_empty());
#[cfg(feature = "lab-stack-traces")]
assert!(trace.starts_with("Stack trace:"));
#[cfg(not(feature = "lab-stack-traces"))]
assert_eq!(
trace,
"Stack trace capture disabled (enable 'lab-stack-traces' feature)"
);
}
}
#[test]
fn test_string_conversion() {
let trace = StackTrace::capture();
let as_string: String = trace.clone().into();
assert_eq!(as_string, trace.as_str());
}
#[test]
fn test_debug_formatting() {
let trace = StackTrace::capture();
let debug_str = format!("{:?}", trace);
assert!(debug_str.contains("StackTrace"));
assert!(debug_str.contains("trace:"));
}
}