#![cfg(test)]
use crate::db;
use crate::pipeline::board::{BoardStore, Ticket, TicketComment, TicketParams, TicketPhase};
use crate::util::UnwrapPoison;
use crate::workspace::test_ws_named;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
static TEST_ROOT: OnceLock<PathBuf> = OnceLock::new();
#[cfg(unix)]
static TEST_ROOT_CREATOR_PID: OnceLock<libc::pid_t> = OnceLock::new();
pub fn env_lock() -> &'static std::sync::Mutex<()> {
static ENV_LOCK: OnceLock<std::sync::Mutex<()>> = OnceLock::new();
ENV_LOCK.get_or_init(|| std::sync::Mutex::new(()))
}
pub(crate) struct ProbeFile(pub PathBuf);
impl Drop for ProbeFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[must_use]
pub(crate) struct RetryPolicyGuard {
previous: Option<crate::retry::RetryPolicy>,
}
impl Drop for RetryPolicyGuard {
fn drop(&mut self) {
crate::retry::restore_test_retry_policy(self.previous.take());
}
}
pub(crate) fn install_test_retry_policy(policy: crate::retry::RetryPolicy) -> RetryPolicyGuard {
let previous = crate::retry::swap_test_retry_policy(policy);
RetryPolicyGuard { previous }
}
pub(crate) struct FakeProvider {
script: std::sync::Mutex<
std::collections::VecDeque<Result<crate::ChatResponse, crate::providers::ScopedCallError>>,
>,
pub request_fingerprints: std::sync::Mutex<Vec<String>>,
pub request_messages: std::sync::Mutex<Vec<String>>,
}
impl FakeProvider {
#[must_use]
pub(crate) fn new() -> Self {
Self {
script: std::sync::Mutex::new(std::collections::VecDeque::new()),
request_fingerprints: std::sync::Mutex::new(Vec::new()),
request_messages: std::sync::Mutex::new(Vec::new()),
}
}
#[must_use]
pub(crate) fn ok(self, text: &str) -> Self {
self.ok_with_finish(text, None)
}
#[must_use]
pub(crate) fn ok_with_finish(self, text: &str, finish_reason: Option<&str>) -> Self {
self.script
.lock()
.unwrap()
.push_back(Ok(crate::ChatResponse {
text: Some(text.to_string()),
finish_reason: finish_reason.map(str::to_string),
..crate::ChatResponse::default()
}));
self
}
#[must_use]
pub(crate) fn ok_reasoning_only(self, reasoning: &str, finish_reason: Option<&str>) -> Self {
self.script
.lock()
.unwrap()
.push_back(Ok(crate::ChatResponse {
text: None,
reasoning: Some(crate::Reasoning {
reasoning: Some(reasoning.to_string()),
reasoning_content: Some(reasoning.to_string()),
reasoning_details: None,
}),
finish_reason: finish_reason.map(str::to_string),
..crate::ChatResponse::default()
}));
self
}
#[must_use]
pub(crate) fn ok_reasoning_only_with_usage(
self,
reasoning: &str,
finish_reason: Option<&str>,
input_tokens: u64,
output_tokens: u64,
) -> Self {
self.script
.lock()
.unwrap()
.push_back(Ok(crate::ChatResponse {
text: None,
reasoning: Some(crate::Reasoning {
reasoning: Some(reasoning.to_string()),
reasoning_content: Some(reasoning.to_string()),
reasoning_details: None,
}),
finish_reason: finish_reason.map(str::to_string),
usage: Some(crate::ProviderUsage {
input_tokens: Some(input_tokens),
output_tokens: Some(output_tokens),
..crate::ProviderUsage::default()
}),
..crate::ChatResponse::default()
}));
self
}
#[must_use]
pub(crate) fn ok_with_usage(self, text: &str, input_tokens: u64, output_tokens: u64) -> Self {
self.script
.lock()
.unwrap()
.push_back(Ok(crate::ChatResponse {
text: Some(text.to_string()),
usage: Some(crate::ProviderUsage {
input_tokens: Some(input_tokens),
output_tokens: Some(output_tokens),
..crate::ProviderUsage::default()
}),
..crate::ChatResponse::default()
}));
self
}
#[must_use]
pub(crate) fn ok_tool_call(self, name: &str) -> Self {
self.script
.lock()
.unwrap()
.push_back(Ok(crate::ChatResponse {
text: None,
tool_calls: vec![crate::ToolCall {
id: "call_test".to_string(),
name: name.to_string(),
arguments: serde_json::json!({}),
}],
finish_reason: Some("tool_calls".to_string()),
..crate::ChatResponse::default()
}));
self
}
#[must_use]
pub(crate) fn ok_text_and_tool_calls(
self,
text: &str,
calls: &[(&str, serde_json::Value)],
) -> Self {
let tool_calls = calls
.iter()
.enumerate()
.map(|(i, (name, args))| crate::ToolCall {
id: format!("call_test_{i}"),
name: name.to_string(),
arguments: args.clone(),
})
.collect();
let text = Some(text.to_string());
self.script
.lock()
.unwrap()
.push_back(Ok(crate::ChatResponse {
text,
tool_calls,
finish_reason: Some("tool_calls".to_string()),
..crate::ChatResponse::default()
}));
self
}
#[must_use]
pub(crate) fn err(self, class: crate::retry::FailureClass, msg: &str) -> Self {
let inner = anyhow::anyhow!("{msg}");
let record = crate::retry::RetryFailureRecord::new_simple(class, &inner, None);
self.script
.lock()
.unwrap()
.push_back(Err(crate::providers::ScopedCallError::new(
inner, record, class,
)));
self
}
#[must_use]
pub(crate) fn err_http(self, status: u16, body: &str) -> Self {
let msg = format!("OpenRouter API error ({status}): {body}");
self.err(crate::retry::FailureClass::NonRetryable, &msg)
}
}
#[async_trait::async_trait]
impl crate::Provider for FakeProvider {
async fn chat_scoped(
&self,
request: crate::ChatRequest,
) -> Result<crate::ChatResponse, crate::providers::ScopedCallError> {
self.request_fingerprints
.lock()
.unwrap()
.push(format!("{request:?}"));
self.request_messages.lock().unwrap().push(
request
.messages
.iter()
.map(|m| format!("{m:?}"))
.collect::<Vec<_>>()
.join("\u{0}"),
);
self.script.lock().unwrap().pop_front().unwrap_or_else(|| {
Ok(crate::ChatResponse {
text: Some("unscripted default".to_string()),
..crate::ChatResponse::default()
})
})
}
async fn warmup(&self) -> anyhow::Result<()> {
Ok(())
}
}
#[must_use]
pub(crate) struct FakeProviderGuard {
previous: Option<Arc<dyn crate::Provider>>,
installed: Arc<dyn crate::Provider>,
}
impl Drop for FakeProviderGuard {
fn drop(&mut self) {
crate::providers::restore_provider_for_test_if(&self.installed, self.previous.take());
}
}
pub(crate) fn install_fake_provider(provider: Arc<dyn crate::Provider>) -> FakeProviderGuard {
let previous = crate::providers::swap_provider_for_test(provider.clone());
FakeProviderGuard {
previous,
installed: provider,
}
}
#[must_use]
pub(crate) struct RetrySeam {
_fake: FakeProviderGuard,
_retry: RetryPolicyGuard,
}
pub(crate) fn install_retry_seam(provider: impl crate::Provider + 'static) -> RetrySeam {
install_retry_seam_dyn(Arc::new(provider))
}
pub(crate) fn install_retry_seam_dyn(provider: Arc<dyn crate::Provider>) -> RetrySeam {
let retry = install_test_retry_policy(crate::retry::tiny_test_policy());
let fake = install_fake_provider(provider);
RetrySeam {
_fake: fake,
_retry: retry,
}
}
pub(crate) struct SpyChannel {
name: &'static str,
sent: Arc<std::sync::Mutex<Vec<crate::SendMessage>>>,
}
impl SpyChannel {
#[must_use]
pub(crate) fn new(
name: &'static str,
) -> (Self, Arc<std::sync::Mutex<Vec<crate::SendMessage>>>) {
let sent = Arc::new(std::sync::Mutex::new(Vec::new()));
(
Self {
name,
sent: Arc::clone(&sent),
},
sent,
)
}
}
#[async_trait::async_trait]
impl crate::Channel for SpyChannel {
async fn send(&self, message: &crate::SendMessage) -> anyhow::Result<()> {
self.sent.lock().unwrap_poison().push(message.clone());
Ok(())
}
async fn listen(
&self,
_tx: tokio::sync::mpsc::Sender<crate::ChannelMessage>,
) -> anyhow::Result<()> {
Ok(())
}
fn name(&self) -> &'static str {
self.name
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
pub(crate) fn install_test_log_store(store: crate::logs::LogStore) -> TestLogStoreGuard {
let previous = crate::stats::swap_test_log_store(Some(store));
TestLogStoreGuard { previous }
}
#[must_use]
pub(crate) struct TestLogStoreGuard {
previous: Option<crate::logs::LogStore>,
}
impl Drop for TestLogStoreGuard {
fn drop(&mut self) {
crate::stats::swap_test_log_store(self.previous.take());
}
}
pub struct EnvVarGuard {
_lock: std::sync::MutexGuard<'static, ()>,
key: String,
original: Option<std::ffi::OsString>,
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
unsafe {
match &self.original {
Some(val) => std::env::set_var(&self.key, val),
None => std::env::remove_var(&self.key),
}
}
}
}
#[must_use]
pub fn set_env_var(key: &str, value: Option<&str>) -> EnvVarGuard {
let guard = env_lock().lock().unwrap_poison();
let original = std::env::var_os(key);
unsafe {
match value {
Some(val) => std::env::set_var(key, val),
None => std::env::remove_var(key),
}
}
EnvVarGuard {
_lock: guard,
key: key.to_owned(),
original,
}
}
#[cfg(unix)]
pub fn make_executable(path: &Path) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, PermissionsExt::from_mode(0o755)).unwrap();
}
pub(crate) fn tiny_png_data_uri() -> String {
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
let img = image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 0, 0, 255]));
let mut buf = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
.expect("test PNG must encode");
format!("data:image/png;base64,{}", STANDARD.encode(&buf))
}
pub(crate) fn test_root() -> &'static PathBuf {
TEST_ROOT.get_or_init(|| {
let tmp = tempfile::TempDir::new().expect("failed to create test temp dir");
let path = tmp.path().to_path_buf();
std::mem::forget(tmp);
#[cfg(unix)]
register_test_root_cleanup();
path
})
}
#[cfg(unix)]
fn register_test_root_cleanup() {
TEST_ROOT_CREATOR_PID.get_or_init(|| unsafe { libc::getpid() });
unsafe {
libc::atexit(cleanup_test_root);
}
}
#[cfg(unix)]
extern "C" fn cleanup_test_root() {
let current_pid = unsafe { libc::getpid() };
if TEST_ROOT_CREATOR_PID.get().copied() != Some(current_pid) {
return;
}
if let Some(path) = TEST_ROOT.get() {
let _ = std::fs::remove_dir_all(path);
}
}
#[macro_export]
macro_rules! open_test_store {
($store:ty, $store_name:expr) => {{
let tmp = ::tempfile::TempDir::new().expect("temp dir for test store");
let store = <$store>::open(tmp.path()).await.unwrap_or_else(|e| {
::std::panic!(
"failed to open test {store_name} store: {e:?}",
store_name = $store_name
)
});
(store, tmp)
}};
}
pub(crate) async fn make_ticket(
store: &BoardStore,
ws: &crate::Workspace,
title: &str,
phase: TicketPhase,
) -> String {
TicketBuilder::new(store, ws)
.title(title)
.phase(phase)
.create()
.await
.unwrap_or_else(|e| panic!("make_ticket({title}, {phase}) failed: {e}"))
}
pub async fn expect_ticket(store: &BoardStore, id: &str) -> Ticket {
store
.get_ticket(id)
.await
.expect("BoardStore::get_ticket query failed")
.expect("expected ticket to exist")
}
pub async fn expect_ticket_phase(store: &BoardStore, id: &str) -> TicketPhase {
store
.get_ticket_phase(id)
.await
.expect("BoardStore::get_ticket_phase query failed")
.expect("expected ticket phase to exist")
}
pub(crate) struct TicketBuilder<'a> {
store: &'a BoardStore,
ws: crate::Workspace,
title: String,
desc: String,
phase: TicketPhase,
prereqs: Vec<String>,
reporter: String,
embedding: Option<Vec<u8>>,
priority: i64,
}
impl<'a> TicketBuilder<'a> {
pub(crate) fn new(store: &'a BoardStore, ws: &crate::Workspace) -> Self {
Self {
store,
ws: ws.clone(),
title: String::new(),
desc: "desc".into(),
phase: TicketPhase::Backlog,
prereqs: Vec::new(),
reporter: "test".into(),
embedding: None,
priority: 1,
}
}
pub(crate) fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub(crate) fn desc(mut self, desc: impl Into<String>) -> Self {
self.desc = desc.into();
self
}
pub(crate) fn phase(mut self, phase: TicketPhase) -> Self {
self.phase = phase;
self
}
pub(crate) fn prereqs(mut self, prereqs: &[String]) -> Self {
self.prereqs = prereqs.to_vec();
self
}
pub(crate) fn priority(mut self, priority: i64) -> Self {
self.priority = priority;
self
}
pub(crate) fn reporter(mut self, reporter: impl Into<String>) -> Self {
self.reporter = reporter.into();
self
}
pub(crate) async fn create(self) -> anyhow::Result<String> {
let (store, params) = self.into_parts();
store.create_ticket(¶ms).await
}
fn into_parts(self) -> (&'a BoardStore, TicketParams) {
(
self.store,
TicketParams {
title: self.title,
description: self.desc,
workspace_name: self.ws.name,
phase: self.phase,
prerequisites: self.prereqs,
reporter: self.reporter,
embedding: self.embedding,
priority: self.priority,
},
)
}
}
pub(crate) struct TicketFixture(Ticket);
impl TicketFixture {
pub(crate) fn new(id: &str, phase: TicketPhase) -> Self {
Self(Ticket {
id: id.into(),
title: "Test ticket".into(),
description: String::new(),
phase,
workspace_name: "test_ws".into(),
created_at: "2026-01-01T00:00:00Z".into(),
updated_at: "2026-01-01T00:00:00Z".into(),
comments: Vec::new(),
prerequisites: Vec::new(),
supersedes: None,
superseded_by: None,
commit_hash: None,
lines_added: None,
lines_removed: None,
reporter: "test".into(),
is_archived: false,
priority: 0,
reviewed_head: None,
reviewed_tree: None,
done_at: None,
bounce_count: 0,
})
}
pub(crate) fn title(mut self, title: &str) -> Self {
self.0.title = title.into();
self
}
pub(crate) fn workspace(mut self, workspace: &str) -> Self {
self.0.workspace_name = workspace.into();
self
}
pub(crate) fn archived(mut self, archived: bool) -> Self {
self.0.is_archived = archived;
self
}
pub(crate) fn created_at(mut self, created_at: &str) -> Self {
self.0.created_at = created_at.into();
self
}
pub(crate) fn done_at(mut self, done_at: Option<&str>) -> Self {
self.0.done_at = done_at.map(str::to_string);
self
}
pub(crate) fn comment(mut self, role: &str, content: &str, created_at: &str) -> Self {
self.0.comments.push(TicketComment {
role: role.into(),
content: content.into(),
created_at: created_at.into(),
});
self
}
pub(crate) fn build(self) -> Ticket {
self.0
}
}
pub(crate) struct JobRowBuilder<'a> {
conn: &'a crate::db::Connection,
id: String,
kind: String,
role: String,
workspace_name: String,
task: String,
user_name: Option<String>,
channel: Option<String>,
retry_count: Option<i64>,
timestamps: Option<String>,
}
impl<'a> JobRowBuilder<'a> {
pub(crate) fn new(
conn: &'a crate::db::Connection,
id: impl Into<String>,
kind: impl Into<String>,
role: impl Into<String>,
workspace_name: impl Into<String>,
) -> Self {
Self {
conn,
id: id.into(),
kind: kind.into(),
role: role.into(),
workspace_name: workspace_name.into(),
task: String::new(),
user_name: None,
channel: None,
retry_count: None,
timestamps: None,
}
}
pub(crate) fn task(mut self, task: impl Into<String>) -> Self {
self.task = task.into();
self
}
pub(crate) fn user_name(mut self, user_name: impl Into<String>) -> Self {
self.user_name = Some(user_name.into());
self
}
pub(crate) fn channel(mut self, channel: impl Into<String>) -> Self {
self.channel = Some(channel.into());
self
}
pub(crate) fn timestamps(mut self, timestamps: impl Into<String>) -> Self {
self.timestamps = Some(timestamps.into());
self
}
pub(crate) async fn insert(self) -> anyhow::Result<()> {
let Self {
conn,
id,
kind,
role,
workspace_name,
task,
user_name,
channel,
retry_count,
timestamps,
} = self;
let mut columns = vec!["id", "kind", "task", "workspace_name", "role"];
let mut values: Vec<crate::db::Value> = vec![
crate::db::Value::Text(id),
crate::db::Value::Text(kind),
crate::db::Value::Text(task),
crate::db::Value::Text(workspace_name),
crate::db::Value::Text(role),
];
if let Some(user_name) = user_name {
columns.push("user_name");
values.push(crate::db::Value::Text(user_name));
}
if let Some(channel) = channel {
columns.push("channel");
values.push(crate::db::Value::Text(channel));
}
if let Some(retry_count) = retry_count {
columns.push("retry_count");
values.push(crate::db::Value::Integer(retry_count));
}
let timestamps = timestamps.expect(
"JobRowBuilder::insert: `.timestamps()` is required — the helper never generates timestamps internally",
);
columns.push("created_at");
columns.push("updated_at");
values.push(crate::db::Value::Text(timestamps.clone()));
values.push(crate::db::Value::Text(timestamps));
let placeholders = vec!["?"; values.len()].join(", ");
let table = "jobs";
let sql = format!(
"INSERT INTO {table} ({}) VALUES ({placeholders})",
columns.join(", ")
);
conn.execute(&sql, values).await?;
Ok(())
}
}
pub async fn init_test_stores() {
static INIT: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new();
INIT.get_or_init(|| async {
let _ = crate::config::CONFIG.try_set_storage_root(test_root().clone());
crate::search_engine::init_global();
crate::pipeline::chronicle::init_global();
crate::db::init_all_stores()
.await
.expect("failed to initialize test stores (see chained error for per-store details)");
crate::pipeline::chronicle::start_subscriber();
})
.await;
}
pub async fn init_management_test_stores() {
init_test_stores().await;
let _ = crate::agent::message_router::init_global();
}
pub async fn create_test_workspace(path: &str, name: &str) -> crate::Workspace {
let now = crate::db::now();
crate::workspace::store()
.conn
.execute(
"INSERT INTO workspaces (name, path, created_at, updated_at, paused) \
VALUES (?1, ?2, ?3, ?4, ?5)",
db::params![name, path, now.clone(), now, 0],
)
.await
.expect("insert test workspace");
test_ws_named(path, name)
}
pub(crate) async fn seed_session_row(
conn: &crate::db::Connection,
agent_id: &str,
role: &str,
content: &str,
) {
conn.execute(
"INSERT INTO sessions (agent_id, role, content, created_at) VALUES (?1, ?2, ?3, ?4)",
db::params![agent_id, role, content, crate::db::now()],
)
.await
.unwrap();
}
pub(crate) fn init_temp_repo() -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().expect("create temp dir");
let repo_path = dir.path().to_path_buf();
let status = std::process::Command::new("git")
.args(["init"])
.current_dir(&repo_path)
.status()
.expect("git init");
assert!(status.success());
for (key, value) in [("user.name", "Test"), ("user.email", "test@test.com")] {
let status = std::process::Command::new("git")
.args(["config", key, value])
.current_dir(&repo_path)
.status()
.expect("git config");
assert!(status.success());
}
std::fs::write(repo_path.join("test.txt"), b"line1\nline2\nline3\n").expect("write test file");
let status = std::process::Command::new("git")
.args(["add", "-A"])
.current_dir(&repo_path)
.status()
.expect("git add");
assert!(status.success());
let status = std::process::Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(&repo_path)
.status()
.expect("git commit");
assert!(status.success());
(dir, repo_path)
}
pub(crate) fn rs_files_under(dir: &Path) -> Vec<PathBuf> {
fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
for entry in std::fs::read_dir(dir).expect("read source directory") {
let path = entry.expect("read directory entry").path();
if path.is_dir() {
walk(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
let mut files = Vec::new();
walk(dir, &mut files);
files
}
pub(crate) fn rel_source_path(manifest_dir: &Path, file: &Path) -> String {
file.strip_prefix(manifest_dir)
.expect("source files live under the manifest dir")
.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/")
}
#[cfg(test)]
mod env_var_guard_tests {
use super::*;
#[test]
fn sets_and_restores_to_absent() {
let guard = set_env_var("MAHBOT_TEST_SET_RESTORE", Some("hello"));
assert_eq!(std::env::var("MAHBOT_TEST_SET_RESTORE"), Ok("hello".into()));
drop(guard);
assert!(
std::env::var_os("MAHBOT_TEST_SET_RESTORE").is_none(),
"guard should restore env var to absent on drop"
);
}
#[test]
fn removes_env_var() {
unsafe {
std::env::set_var("MAHBOT_TEST_REMOVE", "present");
}
let guard = set_env_var("MAHBOT_TEST_REMOVE", None);
assert!(
std::env::var_os("MAHBOT_TEST_REMOVE").is_none(),
"set_env_var(key, None) should remove the variable"
);
drop(guard);
assert_eq!(
std::env::var("MAHBOT_TEST_REMOVE"),
Ok("present".into()),
"guard should restore the original value on drop"
);
unsafe {
std::env::remove_var("MAHBOT_TEST_REMOVE");
}
}
#[test]
fn captures_and_restores_original_value() {
unsafe {
std::env::set_var("MAHBOT_TEST_CAPTURE", "original");
}
let guard = set_env_var("MAHBOT_TEST_CAPTURE", Some("override"));
assert_eq!(std::env::var("MAHBOT_TEST_CAPTURE"), Ok("override".into()));
drop(guard);
assert_eq!(
std::env::var("MAHBOT_TEST_CAPTURE"),
Ok("original".into()),
"guard should restore the original value on drop"
);
unsafe {
std::env::remove_var("MAHBOT_TEST_CAPTURE");
}
}
#[test]
fn restores_on_panic() {
unsafe {
std::env::remove_var("MAHBOT_TEST_PANIC_ABSENT");
}
let result = std::panic::catch_unwind(|| {
let _guard = set_env_var("MAHBOT_TEST_PANIC_ABSENT", Some("panic-value"));
panic!("intentional panic");
});
assert!(result.is_err());
assert!(
std::env::var_os("MAHBOT_TEST_PANIC_ABSENT").is_none(),
"MAHBOT_TEST_PANIC_ABSENT should be absent after panic-restore"
);
}
#[test]
fn restores_original_on_panic() {
unsafe {
std::env::set_var("MAHBOT_TEST_PANIC_ORIGINAL", "original");
}
let result = std::panic::catch_unwind(|| {
let _guard = set_env_var("MAHBOT_TEST_PANIC_ORIGINAL", Some("panic-value"));
panic!("intentional panic");
});
assert!(result.is_err());
assert_eq!(
std::env::var("MAHBOT_TEST_PANIC_ORIGINAL"),
Ok("original".into()),
"should restore original value after panic"
);
unsafe {
std::env::remove_var("MAHBOT_TEST_PANIC_ORIGINAL");
}
}
}
#[cfg(test)]
mod retry_policy_guard_tests {
use super::*;
const TINY_MAX_ATTEMPTS: u32 = 3;
#[test]
#[serial_test::serial(provider)] fn installs_and_restores_on_drop() {
assert_eq!(
crate::retry::RetryPolicy::current().max_attempts,
crate::retry::DEFAULT_RETRY_MAX_ATTEMPTS
);
let guard = install_test_retry_policy(crate::retry::tiny_test_policy());
assert_eq!(
crate::retry::RetryPolicy::current().max_attempts,
TINY_MAX_ATTEMPTS
);
drop(guard);
assert_eq!(
crate::retry::RetryPolicy::current().max_attempts,
crate::retry::DEFAULT_RETRY_MAX_ATTEMPTS,
"guard must restore the pre-test override on drop"
);
}
#[test]
#[serial_test::serial(provider)] fn restores_on_panic() {
let result = std::panic::catch_unwind(|| {
let _guard = install_test_retry_policy(crate::retry::tiny_test_policy());
assert_eq!(
crate::retry::RetryPolicy::current().max_attempts,
TINY_MAX_ATTEMPTS
);
panic!("intentional panic while holding the policy guard");
});
assert!(result.is_err());
assert_eq!(
crate::retry::RetryPolicy::current().max_attempts,
crate::retry::DEFAULT_RETRY_MAX_ATTEMPTS,
"a panicking test must not leak the tiny policy into later tests"
);
}
}
fn noisy_image(width: u32, height: u32, format: image::ImageFormat) -> Vec<u8> {
use image::{ImageBuffer, Rgb};
let img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::from_fn(width, height, |x, y| {
let mut v = x.wrapping_mul(0x9E37_79B9) ^ y.wrapping_mul(0x85EB_CA6B);
v ^= v >> 13;
v ^= v << 17;
v ^= v >> 5;
Rgb([
(v & 0xFF) as u8,
((v >> 8) & 0xFF) as u8,
((v >> 16) & 0xFF) as u8,
])
});
let mut out = Vec::new();
image::DynamicImage::ImageRgb8(img)
.write_to(&mut std::io::Cursor::new(&mut out), format)
.unwrap();
out
}
pub fn noisy_png(width: u32, height: u32) -> Vec<u8> {
noisy_image(width, height, image::ImageFormat::Png)
}
pub fn noisy_jpeg(width: u32, height: u32) -> Vec<u8> {
noisy_image(width, height, image::ImageFormat::Jpeg)
}
pub fn canonical_without_verbatim_prefix(path: &Path) -> PathBuf {
crate::util::strip_verbatim_prefix(&std::fs::canonicalize(path).expect("canonicalize"))
}