#[expect(clippy::unwrap_used)]
pub async fn setup_test_env() -> (
tokio::sync::MutexGuard<'static, ()>,
wiremock::MockServer,
tempfile::TempDir,
) {
let lock = super::ENV_LOCK.lock().await;
let mock = wiremock::MockServer::start().await;
let tmp = tempfile::TempDir::new().unwrap();
setup_config(&tmp, &mock.uri());
(lock, mock, tmp)
}
#[expect(clippy::unwrap_used)]
pub fn setup_config(tmp: &tempfile::TempDir, server_url: &str) {
let config_dir = tmp.path().join("bzr");
std::fs::create_dir_all(&config_dir).unwrap();
let config_content = format!(
r#"
default_server = "test"
[servers.test]
url = "{server_url}"
api_key = "test-key"
auth_method = "header"
api_mode = "rest"
"#,
);
std::fs::write(config_dir.join("config.toml"), config_content).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&config_dir, std::fs::Permissions::from_mode(0o700)).unwrap();
std::fs::set_permissions(
config_dir.join("config.toml"),
std::fs::Permissions::from_mode(0o600),
)
.unwrap();
}
unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
}
#[cfg(unix)]
#[expect(clippy::unwrap_used)]
pub async fn capture_stdout<F, T>(f: F) -> (T, String)
where
F: std::future::Future<Output = T>,
{
use std::io::{Read, Seek, Write};
use std::os::unix::io::AsRawFd;
extern "C" {
fn dup(fd: std::ffi::c_int) -> std::ffi::c_int;
fn dup2(oldfd: std::ffi::c_int, newfd: std::ffi::c_int) -> std::ffi::c_int;
fn close(fd: std::ffi::c_int) -> std::ffi::c_int;
}
let tmp = tempfile::NamedTempFile::new().unwrap();
let tmp_fd = tmp.as_file().as_raw_fd();
let saved_stdout = unsafe { dup(1) };
assert!(saved_stdout >= 0, "dup(1) failed");
unsafe {
dup2(tmp_fd, 1);
}
let result = f.await;
std::io::stdout().flush().unwrap();
unsafe {
dup2(saved_stdout, 1);
close(saved_stdout);
}
let mut captured = String::new();
let mut file = tmp.reopen().unwrap();
file.seek(std::io::SeekFrom::Start(0)).unwrap();
file.read_to_string(&mut captured).unwrap();
(result, captured)
}
#[expect(
clippy::panic,
reason = "test helper: unrecoverable if output is not JSON"
)]
pub fn extract_json(output: &str) -> serde_json::Value {
if let Ok(v) = serde_json::from_str(output) {
return v;
}
for (i, ch) in output.char_indices() {
if ch == '[' || ch == '{' {
if let Some(v) = try_parse_from(&output[i..], ch) {
return v;
}
}
}
panic!("no valid JSON found in captured output: {output}");
}
fn try_parse_from(slice: &str, opener: char) -> Option<serde_json::Value> {
if let Ok(v) = serde_json::from_str(slice) {
return Some(v);
}
let closing = if opener == '[' { ']' } else { '}' };
for (j, jch) in slice.char_indices().rev() {
if jch == closing {
if let Ok(v) = serde_json::from_str(&slice[..=j]) {
return Some(v);
}
}
}
None
}
pub fn xmlrpc_bug_response(id: i64, summary: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<methodResponse><params><param><value><struct>
<member><name>bugs</name><value><array><data>
<value><struct>
<member><name>id</name><value><int>{id}</int></value></member>
<member><name>summary</name><value><string>{summary}</string></value></member>
<member><name>status</name><value><string>NEW</string></value></member>
<member><name>product</name><value><string>TestProduct</string></value></member>
<member><name>component</name><value><string>General</string></value></member>
<member><name>assigned_to</name><value><string>dev@example.com</string></value></member>
<member><name>priority</name><value><string>P1</string></value></member>
<member><name>severity</name><value><string>normal</string></value></member>
<member><name>keywords</name><value><array><data></data></array></value></member>
<member><name>blocks</name><value><array><data></data></array></value></member>
<member><name>depends_on</name><value><array><data></data></array></value></member>
<member><name>cc</name><value><array><data></data></array></value></member>
</struct></value>
</data></array></value></member>
</struct></value></param></params></methodResponse>"#
)
}
#[cfg(test)]
#[path = "test_helpers_tests.rs"]
mod tests;