From e89dd8a6f79efff18bb5e59f7cc95e27ccca3a4b Mon Sep 17 00:00:00 2001 From: Laszlo Nagy Date: Tue, 21 Apr 2026 11:57:56 +0000 Subject: [PATCH] test(output): cover canonical, relative, and fallback path formats The existing path_format_config test only exercised the 'absolute' strategy. Add three tests for the remaining acceptance criteria: - canonical_path_format_resolves_symlinks: compiles via a symlinked directory (src/ -> real/) and asserts the canonical file field resolves through the symlink to the real path. - relative_file_format_is_relative_to_directory: invokes the compiler with an absolute source path and asserts 'file: relative' rewrites it relative to the formatted directory (which is absolute). - canonical_file_format_falls_back_for_missing_source: feeds the semantic subcommand an event referencing a source that does not exist on disk; asserts the entry is kept (not dropped) and file falls back to the unformatted path. The Windows \\?\ prefix stripping scenario is Windows-only and remains uncovered. Co-Authored-By: Claude Opus 4.7 (1M context) --- integration-tests/tests/cases/config.rs | 184 ++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/integration-tests/tests/cases/config.rs b/integration-tests/tests/cases/config.rs index 96785e48..a391b251 100644 --- a/integration-tests/tests/cases/config.rs +++ b/integration-tests/tests/cases/config.rs @@ -360,6 +360,190 @@ sources: Ok(()) } +/// With `file: canonical`, symlinked source paths are written as the resolved +/// real path (symlinks followed). +// Requirements: output-path-format +#[test] +#[cfg(target_family = "unix")] +#[cfg(has_preload_library)] +#[cfg(all(has_executable_compiler_c, has_executable_shell))] +fn canonical_path_format_resolves_symlinks() -> Result<()> { + use std::os::unix::fs::symlink; + + let env = TestEnvironment::new("canonical_symlinks")?; + + // Real source under real/; src/ is a symlink pointing at real/. Compiling + // via src/main.c records "src/main.c" in the event, which canonical must + // resolve back to .../real/main.c. + env.create_source_files(&[("real/main.c", "int main() { return 0; }")])?; + symlink(env.test_dir().join("real"), env.test_dir().join("src"))?; + + let build = format!("{} -c src/main.c -o src/main.o", COMPILER_C_PATH); + let script = env.create_shell_script("build.sh", &build)?; + + let config = format!( + r#" +schema: "4.1" + +intercept: + mode: preload + path: "{preload}" + +format: + paths: + directory: canonical + file: canonical +"#, + preload = PRELOAD_LIBRARY_PATH + ); + let config_path = env.test_dir().join("config.yaml"); + std::fs::write(&config_path, config)?; + + env.run_bear_success(&[ + "--output", + "compile_commands.json", + "--config", + config_path.to_str().unwrap(), + "--", + SHELL_PATH, + script.to_str().unwrap(), + ])?; + + let db = env.load_compilation_database("compile_commands.json")?; + let file = db + .entries() + .first() + .and_then(|e| e.get("file").and_then(|v| v.as_str())) + .expect("expected at least one entry") + .to_string(); + + assert!( + file.ends_with("/real/main.c"), + "canonical file field must resolve symlinks; got {file}, expected path ending with /real/main.c" + ); + assert!( + !file.contains("/src/"), + "canonical file field must not contain the symlink segment /src/; got {file}" + ); + + Ok(()) +} + +/// With `directory: absolute` and `file: relative`, an absolute source path +/// observed at interception is rewritten relative to the (formatted) directory. +// Requirements: output-path-format +#[test] +#[cfg(target_family = "unix")] +#[cfg(has_preload_library)] +#[cfg(all(has_executable_compiler_c, has_executable_shell))] +fn relative_file_format_is_relative_to_directory() -> Result<()> { + let env = TestEnvironment::new("relative_file_format")?; + + env.create_source_files(&[("src/main.c", "int main() { return 0; }")])?; + + // Compile using an absolute source path so the intercepted event records + // the absolute form; `file: relative` must then rewrite it to src/main.c. + let build = format!(r#"{} -c "$(pwd)/src/main.c" -o src/main.o"#, COMPILER_C_PATH); + let script = env.create_shell_script("build.sh", &build)?; + + let config = format!( + r#" +schema: "4.1" + +intercept: + mode: preload + path: "{preload}" + +format: + paths: + directory: absolute + file: relative +"#, + preload = PRELOAD_LIBRARY_PATH + ); + let config_path = env.test_dir().join("config.yaml"); + std::fs::write(&config_path, config)?; + + env.run_bear_success(&[ + "--output", + "compile_commands.json", + "--config", + config_path.to_str().unwrap(), + "--", + SHELL_PATH, + script.to_str().unwrap(), + ])?; + + let db = env.load_compilation_database("compile_commands.json")?; + let entry = db.entries().first().expect("expected at least one entry"); + let file = entry.get("file").and_then(|v| v.as_str()).unwrap_or(""); + let directory = entry.get("directory").and_then(|v| v.as_str()).unwrap_or(""); + + assert_eq!(file, "src/main.c", "file must be relative to directory, got {file:?}"); + assert!(std::path::Path::new(directory).is_absolute(), "directory must be absolute, got {directory:?}"); + + Ok(()) +} + +/// With `file: canonical` and a source that does not exist at output-write +/// time, Bear must not drop the entry: it falls back to the unformatted path +/// (and logs a warning). Exercised via the `semantic` subcommand so we can +/// feed a hand-crafted event referencing a ghost source. +// Requirements: output-path-format +#[test] +#[cfg(target_family = "unix")] +fn canonical_file_format_falls_back_for_missing_source() -> Result<()> { + use serde_json::json; + + let env = TestEnvironment::new("canonical_missing_fallback")?; + + // Working dir exists (so directory canonicalization succeeds and the entry + // is kept); the source file does not exist, so file canonicalization must + // fall back to the unformatted path. + let temp_dir = env.test_dir().to_str().unwrap().to_string(); + let event = json!({ + "pid": 12345, + "execution": { + "executable": COMPILER_C_PATH, + "arguments": [COMPILER_C_PATH, "-c", "ghost.c"], + "working_dir": temp_dir, + "environment": {} + } + }); + env.create_source_files(&[("events.json", &event.to_string())])?; + + let config = r#" +schema: "4.1" + +format: + paths: + directory: canonical + file: canonical +"#; + let config_path = env.test_dir().join("config.yaml"); + std::fs::write(&config_path, config)?; + + env.run_bear_success(&[ + "--config", + config_path.to_str().unwrap(), + "semantic", + "--input", + "events.json", + "--output", + "compile_commands.json", + ])?; + + let db = env.load_compilation_database("compile_commands.json")?; + let file = db + .entries() + .first() + .and_then(|e| e.get("file").and_then(|v| v.as_str())) + .expect("entry must survive the canonical-file fallback"); + assert_eq!(file, "ghost.c", "file field must fall back to the unformatted path; got {file:?}"); + + Ok(()) +} + /// Test invalid configuration handling /// Verifies Bear handles invalid config gracefully #[test]