//! Integration tests for the LLVM COFF helper commands. use std::env; use std::fs; use std::path::{Path, PathBuf}; use assert_cmd::Command; use predicates::prelude::*; use serde_json::Value; use tempfile::{TempDir, tempdir}; #[derive(Clone, Copy)] struct ToolCase { binary: &'static str, backend: &'static str, } const LLVM_TOOL_CASES: &[ToolCase] = &[ ToolCase { binary: "llvmreadobj", backend: "llvm-readobj", }, ToolCase { binary: "llvmobjdump", backend: "llvm-objdump", }, ToolCase { binary: "llvmnm", backend: "llvm-nm", }, ]; fn llvm_command(binary: &str) -> Command { Command::cargo_bin(binary).expect("binary") } #[test] fn help_mentions_examples_backend_and_shared_output_flags() { for case in LLVM_TOOL_CASES { let mut command = llvm_command(case.binary); command .arg("--help") .assert() .success() .stdout(predicate::str::contains("Examples")) .stdout(predicate::str::contains("--llvm-bin-dir")) .stdout(predicate::str::contains("--llvm-arg")) .stdout(predicate::str::contains("--json")) .stdout(predicate::str::contains("--toon")); } } #[test] fn json_output_exposes_common_top_level_contract() { for case in LLVM_TOOL_CASES { let backend_dir = fake_backend_dir(LLVM_TOOL_CASES); let input_dir = tempdir().expect("input tempdir"); let object = write_coff_placeholder(input_dir.path(), case.binary); let mut command = llvm_command(case.binary); add_fake_backend(&mut command, backend_dir.path()); let output = command .arg("--json") .arg(&object) .assert() .success() .get_output() .stdout .clone(); let json = serde_json::from_slice::(&output).expect("json output"); assert_common_json_contract(case.binary, &json); assert!( json["files"] .as_array() .is_some_and(|files| !files.is_empty()), "{}: expected at least one file entry in {json}", case.binary ); } } #[test] fn stdin_lines_feed_multiple_files() { for case in LLVM_TOOL_CASES { let backend_dir = fake_backend_dir(LLVM_TOOL_CASES); let input_dir = tempdir().expect("input tempdir"); let first = write_coff_placeholder(input_dir.path(), &format!("{}-first", case.binary)); let second = write_coff_placeholder(input_dir.path(), &format!("{}-second", case.binary)); let stdin = format!("{}\n{}\n", first.display(), second.display()); let mut command = llvm_command(case.binary); add_fake_backend(&mut command, backend_dir.path()); let output = command .arg("--json") .write_stdin(stdin) .assert() .success() .get_output() .stdout .clone(); let json = serde_json::from_slice::(&output).expect("json output"); assert_common_json_contract(case.binary, &json); let files = json["files"].as_array().expect("files array"); assert_eq!( files.len(), 2, "{}: expected stdin line input to produce two file entries in {json}", case.binary ); } } #[test] fn objdump_symbol_and_address_flags_are_recorded_in_backend_argv() { let backend_dir = fake_backend_dir(LLVM_TOOL_CASES); let input_dir = tempdir().expect("input tempdir"); let object = write_coff_placeholder(input_dir.path(), "objdump-argv"); let mut command = llvm_command("llvmobjdump"); add_fake_backend(&mut command, backend_dir.path()); let output = command .arg("--symbol") .arg("main") .arg("--start-address") .arg("0x0") .arg("--stop-address") .arg("0x20") .arg("--json") .arg(&object) .assert() .success() .get_output() .stdout .clone(); let json = serde_json::from_slice::(&output).expect("json output"); let argv = json["backend"]["argv"] .as_array() .expect("backend argv") .iter() .filter_map(Value::as_str) .collect::>(); assert!(argv.contains(&"--disassemble-symbols=main")); assert!(argv.contains(&"--start-address=0x0")); assert!(argv.contains(&"--stop-address=0x20")); } #[test] fn missing_backend_fails_with_actionable_message() { for case in LLVM_TOOL_CASES { let empty_backend_dir = tempdir().expect("backend tempdir"); let input_dir = tempdir().expect("input tempdir"); let object = write_coff_placeholder(input_dir.path(), case.binary); let mut command = llvm_command(case.binary); command .arg("--llvm-bin-dir") .arg(empty_backend_dir.path()) .arg(&object) .assert() .failure() .stderr(predicate::str::contains(case.backend)) .stderr(predicate::str::contains("--llvm-bin-dir")) .stderr( predicate::str::contains("backend") .or(predicate::str::contains("LLVM")) .or(predicate::str::contains("not found")), ); } } fn assert_common_json_contract(binary: &str, json: &Value) { assert!( json.get("backend").is_some(), "{binary}: missing top-level backend in {json}" ); assert!( json.get("summary").is_some(), "{binary}: missing top-level summary in {json}" ); assert!( json.get("files").and_then(Value::as_array).is_some(), "{binary}: missing top-level files array in {json}" ); assert!( json.get("parse_warnings") .and_then(Value::as_array) .is_some(), "{binary}: missing top-level parse_warnings array in {json}" ); } fn add_fake_backend(command: &mut Command, backend_dir: &Path) { command.arg("--llvm-bin-dir").arg(backend_dir); for arg in fake_backend_args() { command.arg("--llvm-arg").arg(arg); } } fn fake_backend_dir(cases: &[ToolCase]) -> TempDir { let dir = tempdir().expect("backend tempdir"); let shell = host_shell(); for case in cases { for file_name in backend_file_names(case.backend) { let destination = dir.path().join(file_name); fs::copy(&shell, &destination).expect("copy fake backend executable"); make_executable(&destination); } } dir } fn host_shell() -> PathBuf { if cfg!(windows) { env::var_os("COMSPEC").map_or_else( || PathBuf::from(r"C:\Windows\System32\cmd.exe"), PathBuf::from, ) } else { PathBuf::from("/bin/sh") } } fn backend_file_names(backend: &str) -> Vec { if cfg!(windows) { vec![backend.to_owned(), format!("{backend}.exe")] } else { vec![backend.to_owned()] } } fn fake_backend_args() -> Vec<&'static str> { if cfg!(windows) { vec!["/D", "/S", "/C", "echo fake llvm coff output"] } else { vec!["-c", "printf 'fake llvm coff output\\n'"] } } fn write_coff_placeholder(dir: &Path, stem: &str) -> PathBuf { let path = dir.join(format!("{stem}.obj")); fs::write(&path, b"MZ fake COFF placeholder\n").expect("write object placeholder"); path } #[cfg(unix)] fn make_executable(path: &Path) { use std::os::unix::fs::PermissionsExt as _; let mut permissions = fs::metadata(path) .expect("fake backend metadata") .permissions(); permissions.set_mode(0o755); fs::set_permissions(path, permissions).expect("fake backend permissions"); } #[cfg(not(unix))] const fn make_executable(_path: &Path) {}