Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 77 additions & 8 deletions libdd-crashtracker/src/crash_info/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,11 @@ impl ErrorDataBuilder {
if let Some(stack) = &mut self.stack {
stack.set_complete()?;
} else {
// With https://github.com/DataDog/libdatadog/pull/1076 it happens that stack trace are
// empty on musl based Linux (Alpine) because stack unwinding may not be able to unwind
// passed the signal handler. This by-passing for musl is temporary and needs a fix.
#[cfg(target_env = "musl")]
return Ok(());
#[cfg(not(target_env = "musl"))]
anyhow::bail!("Can't set non-existant stack complete");
// No frames were received, but we got the end marker. This can happen on
// certain platforms/contexts where stack unwinding fails to capture any
// frames (e.g., musl-based Linux). Initialize an empty but incomplete stack
// to indicate that stack collection did not succeed.
self.stack = Some(StackTrace::new_incomplete());
}
Ok(())
}
Expand Down Expand Up @@ -355,7 +353,15 @@ impl CrashInfoBuilder {
}

pub fn with_stack_set_complete(&mut self) -> anyhow::Result<()> {
self.error.with_stack_set_complete()
let had_no_frames = self.error.stack.is_none();
Comment thread
gleocadie marked this conversation as resolved.
self.error.with_stack_set_complete()?;
if had_no_frames {
self.with_log_message(
"No native stack frames received; stack unwinding may have failed".to_string(),
true,
)?;
}
Ok(())
}

pub fn with_thread(&mut self, thread: ThreadData) -> anyhow::Result<()> {
Expand Down Expand Up @@ -545,4 +551,67 @@ mod tests {
let report = builder.build().unwrap();
assert!(report.error.message.as_ref().unwrap().len() >= 10000);
}

#[test]
fn test_no_frames_is_incomplete() {
// When we receive an end stacktrace marker but no frames were collected,
// we should create an empty incomplete stack rather than erroring.
let mut builder = ErrorDataBuilder::new();
assert!(builder.stack.is_none());

// This should succeed and create an empty incomplete stack
let result = builder.with_stack_set_complete();
assert!(result.is_ok());

// Verify we now have a stack that is incomplete (no frames were captured)
assert!(builder.stack.is_some());
let stack = builder.stack.as_ref().unwrap();
assert!(stack.frames.is_empty());
assert!(stack.incomplete);
}

#[test]
fn test_with_stack_set_complete_with_frames() {
// When we have frames and call set_complete, it should mark them complete
let mut builder = ErrorDataBuilder::new();

// Add a frame (which creates an incomplete stack)
let frame = StackFrame::test_instance(1);
builder.with_stack_frame(frame, true).unwrap();
assert!(builder.stack.as_ref().unwrap().incomplete);

// Mark complete
builder.with_stack_set_complete().unwrap();

// Verify stack is now complete
let stack = builder.stack.as_ref().unwrap();
assert_eq!(stack.frames.len(), 1);
assert!(!stack.incomplete);
}

#[test]
fn test_crash_info_builder_empty_stack_is_incomplete() {
// When no frames were captured, the stack and CrashInfo should be marked
// incomplete to indicate that stack collection did not succeed.
let mut builder = CrashInfoBuilder::new();
builder.with_kind(ErrorKind::UnixSignal).unwrap();

// Simulate receiving BEGIN_STACKTRACE then END_STACKTRACE with no frames
builder.with_stack_set_complete().unwrap();

let crash_info = builder.build().unwrap();

// The stack should be empty and incomplete (no frames were captured)
assert!(crash_info.error.stack.frames.is_empty());
assert!(crash_info.error.stack.incomplete);

// The overall crash info should be marked complete
assert!(!crash_info.incomplete);

// A log message should be recorded noting that no frames were received
assert!(crash_info
.log_messages
.iter()
.any(|msg| msg.contains("No native stack frames received")));
}
}
90 changes: 90 additions & 0 deletions libdd-crashtracker/src/receiver/receive_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,4 +678,94 @@ mod tests {
.unwrap();
assert!(matches!(state, StdinState::Waiting));
}

#[test]
fn test_stacktrace_empty_workflow() {
// Test that receiving BEGIN_STACKTRACE followed by END_STACKTRACE
// (with no frames) creates an empty but complete stack
let mut builder = CrashInfoBuilder::new();
let mut config = None;

let mut state = StdinState::Waiting;

state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_BEGIN_STACKTRACE,
state,
&None,
)
.unwrap();
assert!(matches!(state, StdinState::StackTrace));

// End stacktrace immediately (no frames)
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_END_STACKTRACE,
state,
&None,
)
.unwrap();
assert!(matches!(state, StdinState::Waiting));

// Verify we have an empty but incomplete stack (no frames captured = stack unwinding
// failed)
let stack = builder.error.stack.as_ref().expect("Stack should exist");
assert!(stack.frames.is_empty());
assert!(
stack.incomplete,
"Stack should be marked incomplete when no frames were captured"
);

// Verify a log message was recorded about no frames
assert!(builder
.log_messages
.as_ref()
.map(|msgs| msgs
.iter()
.any(|msg| msg.contains("No native stack frames received")))
.unwrap_or(false));
}

#[test]
fn test_stacktrace_with_frames_workflow() {
let mut builder = CrashInfoBuilder::new();
let mut config = None;

let mut state = StdinState::Waiting;

// Begin stacktrace
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_BEGIN_STACKTRACE,
state,
&None,
)
.unwrap();
assert!(matches!(state, StdinState::StackTrace));

// Add a frame
let frame_json = r#"{"ip":"0x1234"}"#;
state = process_line(&mut builder, &mut config, frame_json, state, &None).unwrap();
assert!(matches!(state, StdinState::StackTrace));

// End stacktrace
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_END_STACKTRACE,
state,
&None,
)
.unwrap();
assert!(matches!(state, StdinState::Waiting));

// Verify we have a stack with one frame, marked complete
let stack = builder.error.stack.as_ref().expect("Stack should exist");
assert_eq!(stack.frames.len(), 1);
assert!(!stack.incomplete, "Stack should be marked complete");
assert_eq!(stack.frames[0].ip, Some("0x1234".to_string()));
}
}
Loading