use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::audit::AnalyzeGuard;
fn real_analyze_binary() -> PathBuf {
if let Some(pinned) = std::env::var_os("TRUSTY_ANALYZE_BIN") {
let pinned = PathBuf::from(pinned);
assert!(
pinned.exists(),
"TRUSTY_ANALYZE_BIN={} does not exist",
pinned.display()
);
return pinned;
}
let exe = std::env::current_exe().expect("resolve the test executable");
let candidate = exe
.parent()
.and_then(Path::parent)
.map(|profile| profile.join("trusty-analyze"))
.expect("a target/<profile>/deps layout");
assert!(
candidate.exists(),
"{} is not built — run `cargo build -p trusty-analyze` before \
`cargo test -p tga -- --include-ignored`, or point TRUSTY_ANALYZE_BIN at a copy",
candidate.display()
);
candidate
}
async fn reachable_trusty_search() -> String {
let url =
std::env::var("TRUSTY_SEARCH_URL").unwrap_or_else(|_| "http://127.0.0.1:7878".to_string());
let reachable =
trusty_common::daemon_guard::probe_once(&format!("{}/health", url.trim_end_matches('/')))
.await;
assert!(
reachable,
"trusty-search is not answering at {url} — start it (`trusty-search start`) before \
`cargo test -p tga -- --include-ignored`, or point TRUSTY_SEARCH_URL at a running one. \
It cannot be stubbed: trusty-analyze speaks h2c to it with prior knowledge."
);
url
}
fn wrapper_for(dir: &Path, binary: &Path, search_url: &str, pid_file: &Path) -> String {
use std::os::unix::fs::PermissionsExt as _;
let script = format!(
"#!/bin/sh\n\
echo $$ > {pid_file}\n\
export TRUSTY_SEARCH_URL={search_url}\n\
export TRUSTY_ANALYZER_FACTS={dir}/facts.redb\n\
export TRUSTY_DATA_DIR_OVERRIDE={dir}\n\
exec {binary} \"$@\" > {dir}/analyze.log 2>&1\n",
pid_file = pid_file.display(),
dir = dir.display(),
binary = binary.display(),
);
let path = dir.join("analyze-wrapper");
std::fs::write(&path, script).expect("write the wrapper");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))
.expect("make the wrapper executable");
path.to_str().expect("a UTF-8 temp path").to_string()
}
fn daemon_socket_under(dir: &Path) -> PathBuf {
dir.join("trusty-analyze").join("trusty-analyze.sock")
}
struct KillOnDrop(PathBuf);
impl Drop for KillOnDrop {
fn drop(&mut self) {
let Ok(raw) = std::fs::read_to_string(&self.0) else {
return;
};
let Ok(pid) = raw.trim().parse::<u32>() else {
return;
};
let _ = std::process::Command::new("kill")
.arg("-TERM")
.arg(pid.to_string())
.status();
}
}
#[ignore = "needs `cargo build -p trusty-analyze`; run with --include-ignored"]
#[tokio::test]
async fn the_real_analyze_binary_refuses_the_audit_when_trusty_search_is_down() {
let binary = real_analyze_binary();
let dir = tempfile::tempdir().expect("create a temp dir");
let pid_file = dir.path().join("analyze.pid");
let _reaper = KillOnDrop(pid_file.clone());
let dead_search = format!("http://127.0.0.1:{}", super::tests::free_port());
let wrapper = wrapper_for(dir.path(), &binary, &dead_search, &pid_file);
let guard = AnalyzeGuard {
socket: daemon_socket_under(dir.path()),
binary: wrapper,
startup_timeout: Duration::from_secs(5),
poll_interval: Duration::from_millis(200),
};
let err = crate::audit::ensure_analyze_daemon_with(&guard)
.await
.expect_err("the real binary cannot serve without trusty-search");
assert!(
err.cause.contains("did not answer healthy"),
"the spawn succeeded and the readiness poll is what refused; got: {}",
err.cause
);
assert!(
err.to_string().contains("trusty-search start"),
"the refusal must name the fix: {err}"
);
}
#[ignore = "needs `cargo build -p trusty-analyze` and a running trusty-search; run with --include-ignored"]
#[tokio::test]
async fn the_real_analyze_binary_satisfies_the_preflight_end_to_end() {
let binary = real_analyze_binary();
let search = reachable_trusty_search().await;
let dir = tempfile::tempdir().expect("create a temp dir");
let pid_file = dir.path().join("analyze.pid");
let _reaper = KillOnDrop(pid_file.clone());
let wrapper = wrapper_for(dir.path(), &binary, &search, &pid_file);
let guard = AnalyzeGuard {
socket: daemon_socket_under(dir.path()),
binary: wrapper,
startup_timeout: Duration::from_secs(60),
poll_interval: Duration::from_millis(250),
};
if let Err(e) = crate::audit::ensure_analyze_daemon_with(&guard).await {
let log = std::fs::read_to_string(dir.path().join("analyze.log")).unwrap_or_default();
panic!("the real binary must satisfy the preflight: {e}\n--- daemon log ---\n{log}");
}
let already_up = AnalyzeGuard {
binary: "/nonexistent/trusty-analyze".to_string(),
..guard
};
crate::audit::ensure_analyze_daemon_with(&already_up)
.await
.expect("a daemon this guard just started must satisfy the next run without a spawn");
}
fn real_search_binary() -> PathBuf {
if let Some(pinned) = std::env::var_os(crate::audit::ENV_SEARCH_BIN) {
let pinned = PathBuf::from(pinned);
assert!(
pinned.exists(),
"TRUSTY_SEARCH_BIN={} does not exist",
pinned.display()
);
return pinned;
}
let exe = std::env::current_exe().expect("resolve the test executable");
let candidate = exe
.parent()
.and_then(Path::parent)
.map(|profile| profile.join("trusty-search"))
.expect("a target/<profile>/deps layout");
assert!(
candidate.exists(),
"{} is not built — run `cargo build -p trusty-search` before \
`cargo test -p tga -- --include-ignored`, or point TRUSTY_SEARCH_BIN at a copy",
candidate.display()
);
candidate
}
#[ignore = "needs `cargo build -p trusty-search`; run with --include-ignored"]
#[test]
fn the_real_search_binary_still_takes_index_path_and_name() {
let binary = real_search_binary();
let output = std::process::Command::new(&binary)
.args(["index", "--help"])
.output()
.expect("run `trusty-search index --help`");
assert!(output.status.success(), "`index --help` exited non-zero");
let help = String::from_utf8_lossy(&output.stdout);
assert!(
help.contains("--name"),
"`--name` is what binds the index to the id trusty-review looks up:\n{help}"
);
assert!(
help.contains("[PATH]") || help.contains("<PATH>"),
"the positional checkout path is still the first argument:\n{help}"
);
}
#[ignore = "needs `cargo build -p trusty-search` and a running trusty-search; run with --include-ignored"]
#[tokio::test]
async fn the_real_search_binary_exits_non_zero_for_an_unknown_index() {
let binary = real_search_binary();
let _ = reachable_trusty_search().await;
let unknown = format!("tga-audit-no-such-index-{}", std::process::id());
let args = super::repo_index::probe_args(&unknown);
let output = std::process::Command::new(&binary)
.args(&args)
.output()
.expect("run `trusty-search index-status <unknown>`");
assert!(
!output.status.success(),
"an unknown index must not report success — stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
#[ignore = "needs `cargo build -p trusty-search`; run with --include-ignored"]
#[test]
fn the_real_search_binary_still_takes_start_foreground() {
let binary = real_search_binary();
let output = std::process::Command::new(&binary)
.args(["start", "--help"])
.output()
.expect("run `trusty-search start --help`");
assert!(output.status.success(), "`start --help` exited non-zero");
let help = String::from_utf8_lossy(&output.stdout);
assert!(
help.contains("--foreground"),
"`--foreground` is what stops the child re-spawning itself:\n{help}"
);
}
#[ignore = "needs a running trusty-search; run with --include-ignored"]
#[tokio::test]
async fn the_real_search_daemon_satisfies_the_preflight_without_a_spawn() {
let _ = reachable_trusty_search().await;
let resolved = crate::audit::SearchGuard::from_env();
let guard = crate::audit::SearchGuard {
binary: "/nonexistent/trusty-search".to_string(),
startup_timeout: Duration::from_secs(2),
poll_interval: Duration::from_millis(200),
..resolved
};
crate::audit::ensure_search_daemon_with(&guard)
.await
.unwrap_or_else(|e| {
panic!(
"a running trusty-search must satisfy the preflight at the resolved address \
`{}`: {e}",
guard.url
)
});
}