use std::ffi::OsStr;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
use std::time::{Duration, Instant};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
const POLL_INTERVAL: Duration = Duration::from_millis(20);
pub struct CliInvoker {
cmd: Command,
stdin: Option<Vec<u8>>,
timeout: Duration,
}
impl CliInvoker {
pub fn new() -> Self {
Self {
cmd: Command::new(pbfhogg_bin()),
stdin: None,
timeout: DEFAULT_TIMEOUT,
}
}
pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
self.cmd.arg(arg);
self
}
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.cmd.args(args);
self
}
pub fn stdin_bytes(mut self, bytes: impl Into<Vec<u8>>) -> Self {
self.stdin = Some(bytes.into());
self
}
pub fn timeout(mut self, t: Duration) -> Self {
self.timeout = t;
self
}
pub fn run(mut self) -> CliOutput {
if self.stdin.is_some() {
self.cmd.stdin(Stdio::piped());
}
self.cmd.stdout(Stdio::piped());
self.cmd.stderr(Stdio::piped());
let mut child = self.cmd.spawn().expect("spawn pbfhogg binary");
if let Some(bytes) = self.stdin {
use std::io::Write;
let mut stdin = child.stdin.take().expect("stdin piped");
stdin.write_all(&bytes).expect("write stdin");
drop(stdin);
}
let mut stdout = child.stdout.take().expect("stdout piped");
let mut stderr = child.stderr.take().expect("stderr piped");
let stdout_thread = std::thread::spawn(move || {
let mut buf = Vec::new();
stdout.read_to_end(&mut buf).ok();
buf
});
let stderr_thread = std::thread::spawn(move || {
let mut buf = Vec::new();
stderr.read_to_end(&mut buf).ok();
buf
});
let start = Instant::now();
let status: ExitStatus = loop {
match child.try_wait() {
Ok(Some(s)) => break s,
Ok(None) => {
if start.elapsed() > self.timeout {
child.kill().ok();
child.wait().ok();
let stderr_buf = stderr_thread.join().unwrap_or_default();
panic!(
"pbfhogg timed out after {:?}; stderr:\n{}",
self.timeout,
String::from_utf8_lossy(&stderr_buf),
);
}
std::thread::sleep(POLL_INTERVAL);
}
Err(e) => panic!("error waiting for pbfhogg child: {e}"),
}
};
let stdout_buf = stdout_thread.join().unwrap_or_default();
let stderr_buf = stderr_thread.join().unwrap_or_default();
CliOutput {
status,
stdout: stdout_buf,
stderr: stderr_buf,
}
}
pub fn assert_success(self) -> CliOutput {
let out = self.run();
assert!(
out.status.success(),
"pbfhogg exited non-zero ({}); stderr:\n{}",
out.status,
out.stderr_str(),
);
out
}
pub fn assert_failure(self) -> CliOutput {
let out = self.run();
assert!(
!out.status.success(),
"pbfhogg exited successfully but test expected failure; stdout:\n{}\nstderr:\n{}",
out.stdout_str(),
out.stderr_str(),
);
out
}
}
impl Default for CliInvoker {
fn default() -> Self {
Self::new()
}
}
pub struct CliOutput {
pub status: ExitStatus,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
impl CliOutput {
pub fn stderr_str(&self) -> String {
String::from_utf8_lossy(&self.stderr).into_owned()
}
pub fn stdout_str(&self) -> String {
String::from_utf8_lossy(&self.stdout).into_owned()
}
pub fn assert_stderr_contains(&self, needle: &str) -> &Self {
let haystack = self.stderr_str();
assert!(
haystack.contains(needle),
"stderr did not contain {needle:?}; stderr was:\n{haystack}",
);
self
}
pub fn assert_stdout_contains(&self, needle: &str) -> &Self {
let haystack = self.stdout_str();
assert!(
haystack.contains(needle),
"stdout did not contain {needle:?}; stdout was:\n{haystack}",
);
self
}
pub fn is_o_direct_unsupported(&self) -> bool {
if self.status.success() {
return false;
}
let msg = self.stderr_str();
msg.contains("Invalid argument") || msg.contains("EINVAL")
}
pub fn is_uring_unsupported(&self) -> bool {
if self.status.success() {
return false;
}
let msg = self.stderr_str();
msg.contains("RLIMIT_MEMLOCK")
|| msg.contains("kernel does not support")
|| msg.contains("not supported")
}
}
fn pbfhogg_bin() -> PathBuf {
if let Some(d) = std::env::var_os("BROKKR_TEST_BIN_DIR") {
let bin = PathBuf::from(d).join("pbfhogg");
assert!(
bin.exists(),
"pbfhogg binary not found at {} (from BROKKR_TEST_BIN_DIR). \
The brokkr sweep should have built it; check the sweep's \
build_packages config.",
bin.display(),
);
return bin;
}
let target = std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target"));
let profile = if cfg!(debug_assertions) { "debug" } else { "release" };
let bin = target.join(profile).join("pbfhogg");
assert!(
bin.exists(),
"pbfhogg binary not found at {}. \
Run `brokkr check` (or `cargo build -p pbfhogg-cli`) first. \
Integration tests that drive the CLI rely on the binary \
being built as part of the workspace test run.",
bin.display(),
);
bin
}
pub fn path_arg(p: &Path) -> &OsStr {
p.as_os_str()
}