From 4b412c143703414ef76c43102bfa1d2134f73bb8 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Fri, 6 Mar 2026 21:24:50 +0000 Subject: [PATCH] Enable crashtracking ffi example tests in CI --- examples/ffi/crashtracking.c | 79 ++++++++++++------ tools/src/bin/ffi_test.rs | 158 ++++++++++++++++++++++++++--------- 2 files changed, 174 insertions(+), 63 deletions(-) diff --git a/examples/ffi/crashtracking.c b/examples/ffi/crashtracking.c index ba4d22b88a..447f38039c 100644 --- a/examples/ffi/crashtracking.c +++ b/examples/ffi/crashtracking.c @@ -6,13 +6,12 @@ #include #include #include +#include -#define INIT_FROM_SLICE(s) {.ptr = s.ptr, .len = s.len} +#define INIT_FROM_SLICE(s) \ + { .ptr = s.ptr, .len = s.len } -void example_segfault_handler(int signal) { - printf("Segmentation fault caught. Signal number: %d\n", signal); - exit(-1); -} +static ddog_CharSlice slice(const char *s) { return (ddog_CharSlice){.ptr = s, .len = strlen(s)}; } void handle_result(ddog_VoidResult result) { if (result.tag == DDOG_VOID_RESULT_ERR) { @@ -34,30 +33,58 @@ uintptr_t handle_uintptr_t_result(ddog_crasht_Result_Usize result) { } int main(int argc, char **argv) { - if (signal(SIGSEGV, example_segfault_handler) == SIG_ERR) { - perror("Error setting up signal handler"); - return -1; + // Receiver binary path: CLI arg > env var > hardcoded default + const char *receiver_path = NULL; + if (argc >= 2) { + receiver_path = argv[1]; + } else { + receiver_path = getenv("DDOG_CRASHT_TEST_RECEIVER"); + } + if (!receiver_path || receiver_path[0] == '\0') { + receiver_path = "/tmp/libdatadog/bin/libdatadog-crashtracking-receiver"; + } + + // Output directory: env var > hardcoded default + const char *output_dir = getenv("DDOG_CRASHT_TEST_OUTPUT_DIR"); + if (!output_dir || output_dir[0] == '\0') { + output_dir = "/tmp/crashreports"; + } + + // Build output file paths + char report_path[512]; + char stderr_path[512]; + char stdout_path[512]; + snprintf(report_path, sizeof(report_path), "%s/crashreport.json", output_dir); + snprintf(stderr_path, sizeof(stderr_path), "%s/stderr.txt", output_dir); + snprintf(stdout_path, sizeof(stdout_path), "%s/stdout.txt", output_dir); + + // Forward the dynamic-linker search path to the receiver process. +#ifdef __APPLE__ + const char *ld_search_path_var = "DYLD_LIBRARY_PATH"; +#else + const char *ld_search_path_var = "LD_LIBRARY_PATH"; +#endif + const char *ld_library_path = getenv(ld_search_path_var); + ddog_crasht_EnvVar env_vars[1]; + ddog_crasht_Slice_EnvVar env_slice = {.ptr = NULL, .len = 0}; + if (ld_library_path && ld_library_path[0] != '\0') { + env_vars[0].key = slice(ld_search_path_var); + env_vars[0].val = slice(ld_library_path); + env_slice.ptr = env_vars; + env_slice.len = 1; } ddog_crasht_ReceiverConfig receiver_config = { .args = {}, - .env = {}, - //.path_to_receiver_binary = DDOG_CHARSLICE_C("SET ME TO THE ACTUAL PATH ON YOUR MACHINE"), - // E.g. on my machine, where I run ./build-profiling-ffi.sh /tmp/libdatadog - .path_to_receiver_binary = - DDOG_CHARSLICE_C("/tmp/libdatadog/bin/libdatadog-crashtracking-receiver"), - .optional_stderr_filename = DDOG_CHARSLICE_C("/tmp/crashreports/stderr.txt"), - .optional_stdout_filename = DDOG_CHARSLICE_C("/tmp/crashreports/stdout.txt"), + .env = env_slice, + .path_to_receiver_binary = slice(receiver_path), + .optional_stderr_filename = slice(stderr_path), + .optional_stdout_filename = slice(stdout_path), }; - struct ddog_Endpoint *endpoint = - ddog_endpoint_from_filename(DDOG_CHARSLICE_C("/tmp/crashreports/crashreport.json")); - // Alternatively: - // struct ddog_Endpoint * endpoint = - // ddog_endpoint_from_url(DDOG_CHARSLICE_C("http://localhost:8126")); + struct ddog_Endpoint *endpoint = ddog_endpoint_from_filename(slice(report_path)); // Get the default signals and explicitly use them. - // We could also pass an empty list here, which would also use the default signals. struct ddog_crasht_Slice_CInt signals = ddog_crasht_default_signals(); ddog_crasht_Config config = { .create_alt_stack = false, @@ -79,8 +106,10 @@ int main(int argc, char **argv) { handle_result(ddog_crasht_begin_op(DDOG_CRASHT_OP_TYPES_PROFILER_COLLECTING_SAMPLE)); handle_uintptr_t_result(ddog_crasht_insert_span_id(0, 42)); handle_uintptr_t_result(ddog_crasht_insert_trace_id(1, 1)); - handle_uintptr_t_result(ddog_crasht_insert_additional_tag(DDOG_CHARSLICE_C("This is a very informative extra bit of info"))); - handle_uintptr_t_result(ddog_crasht_insert_additional_tag(DDOG_CHARSLICE_C("This message will for sure help us debug the crash"))); + handle_uintptr_t_result(ddog_crasht_insert_additional_tag( + DDOG_CHARSLICE_C("This is a very informative extra bit of info"))); + handle_uintptr_t_result(ddog_crasht_insert_additional_tag( + DDOG_CHARSLICE_C("This message will for sure help us debug the crash"))); #ifdef EXPLICIT_RAISE_SEGV // Test raising SEGV explicitly, to ensure chaining works @@ -91,8 +120,8 @@ int main(int argc, char **argv) { char *bug = NULL; *bug = 42; - // At this point, we expect the following files to be written into /tmp/crashreports - // foo.txt foo.txt.telemetry stderr.txt stdout.txt + // The crash handler should intercept the SIGSEGV, invoke the receiver, + // and write the crash report to output_dir before the process terminates. return 0; } diff --git a/tools/src/bin/ffi_test.rs b/tools/src/bin/ffi_test.rs index 6881da0a0f..749021e967 100644 --- a/tools/src/bin/ffi_test.rs +++ b/tools/src/bin/ffi_test.rs @@ -121,58 +121,102 @@ fn skip_examples() -> &'static HashMap<&'static str, &'static str> { static MAP: OnceLock> = OnceLock::new(); MAP.get_or_init(|| { HashMap::from([ - ("crashtracking", "intentionally crashes"), ("exporter", "requires CLI arguments"), ("exporter_manager", "Flaky because SIGPIPE thing"), ]) }) } +struct ExpectedCrash { + /// Expected Unix signal number (ex 11 for SIGSEGV). + #[cfg(unix)] + signal: i32, + /// File that should exist in the work directory after the crash, + /// confirming the crash handler ran successfully + output_file: &'static str, +} + +fn expected_crashes() -> &'static HashMap<&'static str, ExpectedCrash> { + static MAP: OnceLock> = OnceLock::new(); + MAP.get_or_init(|| { + HashMap::from([( + "crashtracking", + ExpectedCrash { + #[cfg(unix)] + signal: 11, // SIGSEGV + output_file: "crashreport.json", + }, + )]) + }) +} + +/// Locate the crashtracking receiver binary and library directory. +/// +/// They may live in either "release/" (local/ffi_test build) or "artifacts/" +/// (CI pre-built). Returns `(receiver_binary_path, lib_dir)` +fn find_receiver_paths(project_root: &Path) -> (PathBuf, PathBuf) { + let make_paths = |dir: &str| { + let base = project_root.join(dir); + ( + base.join("bin").join("libdatadog-crashtracking-receiver"), + base.join("lib"), + ) + }; + ["release", "artifacts"] + .iter() + .map(|dir| make_paths(dir)) + .find(|(bin, _)| bin.exists()) + .unwrap_or_else(|| make_paths("release")) +} + +/// Build the library search-path env var for the receiver process. +/// +/// The C test binary is dynamically linked against libdatadog_profiling.{so,dylib} +/// which is not on the system library path. Returns `(var_name, value)` +fn library_search_path_env(lib_dir: &Path) -> (String, String) { + #[cfg(target_os = "macos")] + let search_path_var = "DYLD_LIBRARY_PATH"; + #[cfg(not(target_os = "macos"))] + let search_path_var = "LD_LIBRARY_PATH"; + + let lib_path = match std::env::var(search_path_var) { + Ok(existing) if !existing.is_empty() => { + format!("{}:{}", lib_dir.display(), existing) + } + _ => lib_dir.display().to_string(), + }; + (search_path_var.to_string(), lib_path) +} + /// Per-test environment variables. The runner sets these before spawning /// the test executable so that tests which need external resources (e.g. the /// receiver binary) can find them without hard-coding paths. -fn per_test_env(name: &str, project_root: &Path) -> Vec<(String, String)> { +fn per_test_env(name: &str, project_root: &Path, work_dir: &Path) -> Vec<(String, String)> { match name { - "crashtracking_unhandled_exception" => { - // The receiver binary and shared library may live in either - // "release/" (local/ffi_test build) or "artifacts/" (CI pre-built). - // Check both and use whichever exists. - let make_paths = |dir: &str| { - let base = project_root.join(dir); + "crashtracking" => { + let (receiver, lib_dir) = find_receiver_paths(project_root); + let (search_var, search_val) = library_search_path_env(&lib_dir); + vec![ ( - base.join("bin").join("libdatadog-crashtracking-receiver"), - base.join("lib"), - ) - }; - let (receiver, lib_dir) = ["release", "artifacts"] - .iter() - .map(|dir| make_paths(dir)) - .find(|(bin, _)| bin.exists()) - .unwrap_or_else(|| make_paths("release")); - - // The C test binary is dynamically linked against libdatadog_profiling.{so,dylib} - // which is not on the system library path. Set the platform-specific linker - // search path so the binary can load, and the C test forwards it via getenv() - // into the receiver's explicit execve environment. - // Linux → LD_LIBRARY_PATH - // macOS → DYLD_LIBRARY_PATH - #[cfg(target_os = "macos")] - let search_path_var = "DYLD_LIBRARY_PATH"; - #[cfg(not(target_os = "macos"))] - let search_path_var = "LD_LIBRARY_PATH"; - - let lib_path = match std::env::var(search_path_var) { - Ok(existing) if !existing.is_empty() => { - format!("{}:{}", lib_dir.display(), existing) - } - _ => lib_dir.display().to_string(), - }; + "DDOG_CRASHT_TEST_RECEIVER".to_string(), + receiver.display().to_string(), + ), + ( + "DDOG_CRASHT_TEST_OUTPUT_DIR".to_string(), + work_dir.display().to_string(), + ), + (search_var, search_val), + ] + } + "crashtracking_unhandled_exception" => { + let (receiver, lib_dir) = find_receiver_paths(project_root); + let (search_var, search_val) = library_search_path_env(&lib_dir); vec![ ( "DDOG_CRASHT_TEST_RECEIVER".to_string(), receiver.display().to_string(), ), - (search_path_var.to_string(), lib_path), + (search_var, search_val), ] } _ => vec![], @@ -428,9 +472,46 @@ fn format_exit_status(status: &std::process::ExitStatus) -> String { fn determine_status( exit_status: Option, is_expected_failure: bool, + expected_crash: Option<&ExpectedCrash>, + work_dir: &Path, ) -> TestStatus { match exit_status { Some(status) => { + // Check for expected crash first + if let Some(crash) = expected_crash { + let crashed_with_expected_signal = { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + status.signal() == Some(crash.signal) + } + #[cfg(not(unix))] + { + !status.success() + } + }; + + if crashed_with_expected_signal { + let output_path = work_dir.join(crash.output_file); + if output_path.exists() { + return TestStatus::Passed; + } + return TestStatus::Failed(format!( + "crashed as expected but output file '{}' not found", + crash.output_file + )); + } + if status.success() { + return TestStatus::Failed( + "expected crash but process exited successfully".to_string(), + ); + } + return TestStatus::Failed(format!( + "expected crash signal but got {}", + format_exit_status(&status) + )); + } + let success = status.success(); match (success, is_expected_failure) { (true, false) => TestStatus::Passed, @@ -451,7 +532,8 @@ fn run_test( timeout: Duration, ) -> TestResult { let is_expected_failure = expected_failures().contains_key(name); - let env_vars = per_test_env(name, project_root); + let expected_crash = expected_crashes().get(name); + let env_vars = per_test_env(name, project_root, work_dir); let start = Instant::now(); let child = match spawn_test(exe_path, work_dir, &env_vars) { @@ -467,7 +549,7 @@ fn run_test( }; let (exit_status, output) = wait_with_output(child, timeout); - let status = determine_status(exit_status, is_expected_failure); + let status = determine_status(exit_status, is_expected_failure, expected_crash, work_dir); TestResult { name: name.to_string(),