#![allow(clippy::unwrap_used, clippy::expect_used)]
pub(crate) static HOME_ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub(crate) struct AuditLogGuard {
path: std::path::PathBuf,
}
impl AuditLogGuard {
pub(crate) fn redirect(dir: &std::path::Path) -> Self {
let path = dir.join("audit.jsonl");
crate::request_log::TEST_AUDIT_ROUTE.with(|slot| *slot.borrow_mut() = Some(path.clone()));
Self { path }
}
pub(crate) fn records(&self) -> Vec<crate::request_log::LogRecord> {
match std::fs::read_to_string(&self.path) {
Ok(text) => text
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect(),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Vec::new(),
Err(err) => panic!("failed to read {}: {err}", self.path.display()),
}
}
pub(crate) fn verdicts(&self) -> Vec<String> {
self.records()
.iter()
.map(|record| record.context.get("verdict").cloned().unwrap_or_default())
.collect()
}
}
impl Drop for AuditLogGuard {
fn drop(&mut self) {
crate::request_log::TEST_AUDIT_ROUTE.with(|slot| *slot.borrow_mut() = None);
}
}
#[cfg(test)]
mod audit_log_guard_tests {
use super::AuditLogGuard;
#[test]
fn records_reads_as_empty_before_anything_writes() {
let dir = tempfile::tempdir().unwrap();
let guard = AuditLogGuard::redirect(dir.path());
assert!(guard.records().is_empty());
}
#[test]
fn records_panics_when_the_read_fails_for_a_reason_other_than_not_found() {
let dir = tempfile::tempdir().unwrap();
let guard = AuditLogGuard::redirect(dir.path());
std::fs::create_dir(dir.path().join("audit.jsonl")).unwrap();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| guard.records()));
assert!(result.is_err());
}
}
#[derive(Clone, Default)]
struct CaptureWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for CaptureWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for CaptureWriter {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
fn keep_every_dispatcher_consulted() {
static KEEP: std::sync::OnceLock<tracing::Dispatch> = std::sync::OnceLock::new();
KEEP.get_or_init(|| tracing::Dispatch::new(tracing::subscriber::NoSubscriber::default()));
}
pub(crate) fn capture_at(level: tracing::Level, f: impl FnOnce()) -> String {
keep_every_dispatcher_consulted();
let writer = CaptureWriter::default();
let subscriber = tracing_subscriber::fmt()
.with_max_level(level)
.with_ansi(false)
.with_writer(writer.clone())
.finish();
tracing::subscriber::with_default(subscriber, f);
let logs = String::from_utf8_lossy(&writer.0.lock().unwrap()).into_owned();
logs
}
pub(crate) async fn capture_future_at<F: std::future::Future>(
level: tracing::Level,
future: F,
) -> (F::Output, String) {
use tracing::instrument::WithSubscriber;
keep_every_dispatcher_consulted();
let writer = CaptureWriter::default();
let subscriber = tracing_subscriber::fmt()
.with_max_level(level)
.with_ansi(false)
.with_writer(writer.clone())
.finish();
let result = future.with_subscriber(subscriber).await;
let logs = String::from_utf8_lossy(&writer.0.lock().unwrap()).into_owned();
(result, logs)
}
pub(crate) mod failing_io {
pub(crate) struct FailingWriter;
impl std::io::Write for FailingWriter {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("simulated write failure"))
}
fn flush(&mut self) -> std::io::Result<()> {
Err(std::io::Error::other("simulated flush failure"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn flush_returns_error() {
let mut w = FailingWriter;
let err = w.flush().unwrap_err();
assert!(err.to_string().contains("simulated flush failure"));
}
}
}
pub(crate) mod env {
use std::collections::HashMap;
#[derive(Debug, Default, Clone)]
pub(crate) struct MapEnv(HashMap<String, String>);
impl MapEnv {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn with(mut self, key: &str, value: &str) -> Self {
self.0.insert(key.to_string(), value.to_string());
self
}
}
impl crate::utils::env::EnvSource for MapEnv {
fn var(&self, key: &str) -> Option<String> {
self.0.get(key).cloned()
}
}
#[allow(clippy::unwrap_used)]
pub(crate) fn secret_file(contents: &str) -> (tempfile::TempDir, String) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret");
std::fs::write(&path, contents).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
}
let path = path.to_str().unwrap().to_string();
(dir, path)
}
}
pub(crate) mod atlassian_env {
use std::sync::MutexGuard;
pub(crate) struct AtlassianEnvGuard {
_guard: MutexGuard<'static, ()>,
prev_home: Option<String>,
prev_xdg: Option<String>,
prev_url: Option<String>,
prev_email: Option<String>,
prev_token: Option<String>,
_tmp: tempfile::TempDir,
}
impl AtlassianEnvGuard {
pub(crate) fn new(instance_url: &str, email: &str, token: &str) -> Self {
let guard = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tmp = tempfile::tempdir().unwrap();
let prev_home = std::env::var("HOME").ok();
let prev_xdg = std::env::var("XDG_CONFIG_HOME").ok();
let prev_url = std::env::var("ATLASSIAN_INSTANCE_URL").ok();
let prev_email = std::env::var("ATLASSIAN_EMAIL").ok();
let prev_token = std::env::var("ATLASSIAN_API_TOKEN").ok();
std::env::set_var("HOME", tmp.path());
std::env::set_var("XDG_CONFIG_HOME", tmp.path().join("xdg"));
std::env::set_var("ATLASSIAN_INSTANCE_URL", instance_url);
std::env::set_var("ATLASSIAN_EMAIL", email);
std::env::set_var("ATLASSIAN_API_TOKEN", token);
Self {
_guard: guard,
prev_home,
prev_xdg,
prev_url,
prev_email,
prev_token,
_tmp: tmp,
}
}
}
impl Drop for AtlassianEnvGuard {
fn drop(&mut self) {
restore("HOME", self.prev_home.as_deref());
restore("XDG_CONFIG_HOME", self.prev_xdg.as_deref());
restore("ATLASSIAN_INSTANCE_URL", self.prev_url.as_deref());
restore("ATLASSIAN_EMAIL", self.prev_email.as_deref());
restore("ATLASSIAN_API_TOKEN", self.prev_token.as_deref());
}
}
fn restore(key: &str, prev: Option<&str>) {
match prev {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
}
#[cfg(unix)]
pub(crate) mod shim {
use std::path::Path;
use std::sync::{Mutex, MutexGuard};
const ETXTBSY: i32 = 26;
static SHIM_LOCK: Mutex<()> = Mutex::new(());
pub(crate) fn shim_lock() -> MutexGuard<'static, ()> {
SHIM_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn retry_on_etxtbsy<T>(
mut f: impl FnMut() -> anyhow::Result<T>,
) -> anyhow::Result<T> {
use std::time::Duration;
const MAX_ATTEMPTS: u32 = 8;
let mut backoff = Duration::from_millis(5);
for _ in 1..MAX_ATTEMPTS {
match f() {
Err(e) if is_etxtbsy(&e) => {
std::thread::sleep(backoff);
backoff = backoff.saturating_mul(2);
}
other => return other,
}
}
f()
}
pub(crate) async fn retry_on_etxtbsy_async<T, F, Fut>(mut f: F) -> anyhow::Result<T>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = anyhow::Result<T>>,
{
use std::time::Duration;
const MAX_ATTEMPTS: u32 = 8;
let mut backoff = Duration::from_millis(5);
for _ in 1..MAX_ATTEMPTS {
match f().await {
Err(e) if is_etxtbsy(&e) => {
tokio::time::sleep(backoff).await;
backoff = backoff.saturating_mul(2);
}
other => return other,
}
}
f().await
}
fn is_etxtbsy(err: &anyhow::Error) -> bool {
err.chain().any(|cause| {
cause
.downcast_ref::<std::io::Error>()
.and_then(std::io::Error::raw_os_error)
== Some(ETXTBSY)
})
}
pub(crate) fn write_exec_script(path: &Path, script: &str) {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o755)
.open(path)
.unwrap();
file.write_all(script.as_bytes()).unwrap();
file.sync_all().unwrap();
drop(file);
}
const WARMUP_ENV: &str = "OMNI_DEV_SHIM_WARMUP";
pub(crate) fn write_warmed_exec_script(path: &Path, script: &str) {
let (shebang, body) = script
.split_once('\n')
.expect("a warmed shim script must start with a shebang line");
assert!(
shebang.starts_with("#!"),
"a warmed shim script must start with a shebang line, got {shebang:?}"
);
write_exec_script(
path,
&format!("{shebang}\n[ -n \"${WARMUP_ENV}\" ] && exit 0\n{body}"),
);
retry_on_etxtbsy(|| {
let status = std::process::Command::new(path)
.env(WARMUP_ENV, "1")
.status()?;
anyhow::ensure!(status.success(), "shim warm-up exited with {status}");
Ok(())
})
.expect("shim warm-up exec");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn warmed_script_exits_on_warmup_without_running_its_body() {
let tmp = tempfile::TempDir::new().unwrap();
let marker = tmp.path().join("ran");
let shim = tmp.path().join("shim");
write_warmed_exec_script(&shim, &format!("#!/bin/sh\n: > '{}'\n", marker.display()));
assert!(
!marker.exists(),
"the warm-up exec must not run the script body"
);
}
#[test]
fn warmed_script_runs_its_body_on_a_normal_exec() {
let tmp = tempfile::TempDir::new().unwrap();
let marker = tmp.path().join("ran");
let shim = tmp.path().join("shim");
write_warmed_exec_script(&shim, &format!("#!/bin/sh\n: > '{}'\n", marker.display()));
let status =
retry_on_etxtbsy(|| Ok(std::process::Command::new(&shim).status()?)).unwrap();
assert!(status.success());
assert!(marker.exists(), "a normal exec must run the script body");
}
#[test]
#[should_panic(expected = "shebang")]
fn warmed_script_without_a_shebang_is_rejected() {
let tmp = tempfile::TempDir::new().unwrap();
write_warmed_exec_script(&tmp.path().join("shim"), "echo hi\n");
}
fn etxtbsy_error() -> anyhow::Error {
anyhow::Error::new(std::io::Error::from_raw_os_error(ETXTBSY))
.context("failed to run the shim")
}
#[test]
fn is_etxtbsy_sees_a_wrapped_text_file_busy() {
assert!(is_etxtbsy(&etxtbsy_error()));
}
#[test]
fn is_etxtbsy_rejects_other_errors() {
let enoent = anyhow::Error::new(std::io::Error::from_raw_os_error(2))
.context("failed to run the shim");
assert!(!is_etxtbsy(&enoent));
assert!(!is_etxtbsy(&anyhow::anyhow!("plain error, no io source")));
}
#[test]
fn retry_on_etxtbsy_succeeds_after_transient_failures() {
let mut calls = 0;
let out = retry_on_etxtbsy(|| {
calls += 1;
if calls <= 3 {
Err(etxtbsy_error())
} else {
Ok(calls)
}
})
.unwrap();
assert_eq!(out, 4, "should succeed on the 4th attempt");
assert_eq!(calls, 4);
}
#[test]
fn retry_on_etxtbsy_returns_a_non_etxtbsy_error_without_retrying() {
let mut calls = 0;
let result: anyhow::Result<()> = retry_on_etxtbsy(|| {
calls += 1;
Err(anyhow::anyhow!("a real failure"))
});
assert!(result.is_err());
assert_eq!(calls, 1, "a non-ETXTBSY error must not be retried");
}
#[test]
fn retry_on_etxtbsy_gives_up_after_the_budget_and_returns_the_last_error() {
let mut calls = 0;
let result: anyhow::Result<()> = retry_on_etxtbsy(|| {
calls += 1;
Err(etxtbsy_error())
});
assert!(is_etxtbsy(&result.unwrap_err()));
assert_eq!(calls, 8, "should try MAX_ATTEMPTS times, then give up");
}
#[tokio::test]
async fn retry_on_etxtbsy_async_succeeds_after_transient_failures() {
let mut calls = 0;
let out = retry_on_etxtbsy_async(|| {
calls += 1;
let calls = calls;
async move {
if calls <= 3 {
Err(etxtbsy_error())
} else {
Ok(calls)
}
}
})
.await
.unwrap();
assert_eq!(out, 4, "should succeed on the 4th attempt");
assert_eq!(calls, 4);
}
#[tokio::test]
async fn retry_on_etxtbsy_async_returns_a_non_etxtbsy_error_without_retrying() {
let mut calls = 0;
let result: anyhow::Result<()> = retry_on_etxtbsy_async(|| {
calls += 1;
async { Err(anyhow::anyhow!("a real failure")) }
})
.await;
assert!(result.is_err());
assert_eq!(calls, 1, "a non-ETXTBSY error must not be retried");
}
#[tokio::test]
async fn retry_on_etxtbsy_async_gives_up_after_the_budget_and_returns_the_last_error() {
let mut calls = 0;
let result: anyhow::Result<()> = retry_on_etxtbsy_async(|| {
calls += 1;
async { Err(etxtbsy_error()) }
})
.await;
assert!(is_etxtbsy(&result.unwrap_err()));
assert_eq!(calls, 8, "should try MAX_ATTEMPTS times, then give up");
}
}
}
pub(crate) mod git_repo {
use std::process::Command;
pub(crate) fn commit_files(files: &[(&str, &[u8])]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let run = |args: &[&str]| {
let out = Command::new("git")
.arg("-C")
.arg(dir.path())
.args(args)
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "{stderr}");
};
run(&["init", "-q"]);
for (path, bytes) in files {
std::fs::write(dir.path().join(path), bytes).unwrap();
}
run(&["add", "."]);
run(&[
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.com",
"commit",
"-qm",
"test: fixture",
]);
dir
}
}