use autoitx::AutoIt;
use std::path::PathBuf;
pub fn mock_dylib() -> PathBuf {
let exe = std::env::current_exe().expect("test binary has a path");
let deps = exe.parent().expect("test binary lives in deps/");
let profile = deps.parent().expect("deps/ has a parent");
let name = if cfg!(windows) {
"xtask_mock_dll.dll"
} else if cfg!(target_os = "macos") {
"libxtask_mock_dll.dylib"
} else {
"libxtask_mock_dll.so"
};
let candidates = [deps.join(name), profile.join(name)];
candidates
.iter()
.find(|p| p.is_file())
.cloned()
.unwrap_or_else(|| {
panic!(
"mock library not found. Looked in:\n {}\n {}\n\
Run `cargo build -p xtask-mock-dll`.",
candidates[0].display(),
candidates[1].display(),
)
})
}
static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub struct Harness {
#[expect(dead_code, reason = "held for its Drop; the lock is the point")]
guard: std::sync::MutexGuard<'static, ()>,
pub ai: AutoIt,
#[expect(dead_code, reason = "must outlive the function pointers below")]
lib: libloading::Library,
take_log: unsafe extern "system" fn(*mut u8, i32) -> i32,
set_next_string: unsafe extern "system" fn(*const u16),
set_next_int: unsafe extern "system" fn(i32),
push_int: unsafe extern "system" fn(i32),
set_next_error: unsafe extern "system" fn(i32),
}
impl Harness {
pub fn new() -> Self {
let guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let path = mock_dylib();
let lib = unsafe { libloading::Library::new(&path) }.expect("open mock");
let (reset, take_log, set_next_string, set_next_int, push_int, set_next_error) = unsafe {
(
*lib.get(b"MOCK_reset\0").expect("MOCK_reset"),
*lib.get(b"MOCK_take_log\0").expect("MOCK_take_log"),
*lib.get(b"MOCK_set_next_string\0").expect("set_next_string"),
*lib.get(b"MOCK_set_next_int\0").expect("set_next_int"),
*lib.get(b"MOCK_push_int\0").expect("push_int"),
*lib.get(b"MOCK_set_next_error\0").expect("set_next_error"),
)
};
let reset: unsafe extern "system" fn() = reset;
unsafe { reset() };
let ai = AutoIt::builder()
.dll_path(&path)
.build()
.expect("AutoIt should load against the mock");
unsafe { reset() };
Self {
guard,
ai,
lib,
take_log,
set_next_string,
set_next_int,
push_int,
set_next_error,
}
}
pub fn log(&self) -> String {
self.calls().join("\n")
}
pub fn calls(&self) -> Vec<String> {
let needed = unsafe { (self.take_log)(std::ptr::null_mut(), 0) };
let mut buf = vec![0u8; needed.max(1) as usize];
unsafe { (self.take_log)(buf.as_mut_ptr(), needed) };
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..end])
.lines()
.filter(|l| !l.is_empty() && !l.starts_with("AU3_error("))
.map(str::to_owned)
.collect()
}
pub fn call_names(&self) -> Vec<String> {
self.calls()
.iter()
.map(|c| c.split('(').next().unwrap_or_default().to_owned())
.collect()
}
pub fn script_string(&self, s: &str) {
let w: Vec<u16> = s.encode_utf16().chain(std::iter::once(0)).collect();
unsafe { (self.set_next_string)(w.as_ptr()) };
}
pub fn script_int(&self, v: i32) {
unsafe { (self.set_next_int)(v) };
}
pub fn script_ints(&self, values: &[i32]) {
for v in values {
unsafe { (self.push_int)(*v) };
}
}
pub fn script_error(&self, code: i32) {
unsafe { (self.set_next_error)(code) };
}
}