1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
//! For testing your extension.
use std::time::Duration;
use crate::{CallbackMessage, Context, State, Value};
#[cfg(feature = "call-context")]
use crate::{ArmaCallContext, Caller, Mission, Server, Source};
/// Wrapper around [`crate::Extension`] used for testing.
pub struct Extension(crate::Extension);
const BUFFER_SIZE: libc::size_t = 10240; // The sized used by Arma 3 as of 2021-12-30
#[derive(Debug, PartialEq, Eq)]
/// Result of an event handler
pub enum Result<T, E> {
/// an event has been handled and the handler is done, the value of T is the return value of the event handler
Ok(T),
/// the handler has encountered an error, the value of T is the return value of the event handler
Err(E),
/// an event is handled but the handler is not done and should receive another event
Continue,
/// the handler reached the specified timeout
Timeout,
}
impl<T, E> Result<T, E> {
/// Returns true if the result is an ok result
pub fn is_ok(&self) -> bool {
matches!(self, Self::Ok(_))
}
/// Returns true if the result is an error
pub fn is_err(&self) -> bool {
matches!(self, Self::Err(_))
}
/// Returns true if the result is a continue result
pub fn is_continue(&self) -> bool {
matches!(self, Self::Continue)
}
/// Returns true if the result is a timeout result
pub fn is_timeout(&self) -> bool {
matches!(self, Self::Timeout)
}
}
impl Extension {
/// Create a new testing Extension
pub fn new(ext: crate::Extension) -> Self {
Self(ext)
}
#[must_use]
/// Returns a context for simulating interactions with Arma
pub fn context(&self) -> Context {
self.0.context().with_buffer_size(BUFFER_SIZE)
}
#[must_use]
/// Get a reference to the extensions state container
pub fn state(&self) -> &State {
&self.0.group.state
}
#[cfg(feature = "call-context")]
#[must_use]
/// Call a function with Arma call context.
///
/// # Safety
/// This function is unsafe because it interacts with the C API.
pub unsafe fn call_with_context(
&self,
function: &str,
args: Option<Vec<String>>,
caller: Caller,
source: Source,
mission: Mission,
server: Server,
) -> (String, libc::c_int) {
self.set_call_context(ArmaCallContext::new(caller, source, mission, server));
self.handle_call(function, args)
}
#[must_use]
/// Call a function without Arma call context.
///
/// # Safety
/// This function is unsafe because it interacts with the C API.
///
/// # Note
/// If the `call-context` feature is enabled, this function passes default values for each field.
pub unsafe fn call(&self, function: &str, args: Option<Vec<String>>) -> (String, libc::c_int) {
#[cfg(feature = "call-context")]
self.set_call_context(ArmaCallContext::default());
self.handle_call(function, args)
}
#[cfg(feature = "call-context")]
fn set_call_context(&self, ctx: ArmaCallContext) {
self.0.call_ctx.replace(ctx);
}
unsafe fn handle_call(
&self,
function: &str,
args: Option<Vec<String>>,
) -> (String, libc::c_int) {
let mut output = [0; BUFFER_SIZE];
let len = args.as_ref().map(|a| a.len().try_into().unwrap());
let mut args_pointer = args.map(|v| {
v.into_iter()
.map(|s| std::ffi::CString::new(s).unwrap().into_raw())
.collect::<Vec<*mut i8>>()
});
let res = self.0.group.handle(
self.context(),
function,
output.as_mut_ptr(),
BUFFER_SIZE,
args_pointer.as_mut().map(Vec::as_mut_ptr),
len,
);
if let Some(args) = args_pointer {
for arg in args {
let _ = std::ffi::CString::from_raw(arg);
}
}
(
std::ffi::CStr::from_ptr(output.as_ptr())
.to_str()
.unwrap()
.to_string(),
res,
)
}
/// Create a callback handler
///
/// Returns a Result from the handler if the callback was handled,
/// or `Result::Timeout` if either no event was received, or the handler
/// returned `Result::Continue` until the timeout was reached.
///
/// The handler must return a Result indicating the callback was handled to exit
/// `Result::Continue` will continue to provide events to the handler until another variant is returned
pub fn callback_handler<F, T, E>(&self, handler: F, timeout: Duration) -> Result<T, E>
where
F: Fn(&str, &str, Option<Value>) -> Result<T, E>,
{
let (_, rx) = &self.0.callback_channel;
let deadline = std::time::Instant::now() + timeout;
loop {
match rx.recv_deadline(deadline) {
Ok(CallbackMessage::Call(name, func, data)) => match handler(&name, &func, data) {
Result::Ok(value) => return Result::Ok(value),
Result::Err(error) => return Result::Err(error),
Result::Timeout => return Result::Timeout,
Result::Continue => {}
},
_ => return Result::Timeout,
}
}
}
}
// Annoying to exist, but necessary to prevent duplicate code in the readme
#[doc(hidden)]
pub mod test {
use crate::{arma, Extension, Group};
use crate as arma_rs;
#[arma]
pub fn init() -> Extension {
Extension::build()
.group(
"hello",
Group::new()
.command("english", hello::english)
)
.group(
"welcome",
Group::new()
.command("english", welcome::english)
)
.group("timer", timer::group())
.finish()
}
mod hello {
pub fn english() -> &'static str {
"hello"
}
}
mod welcome {
pub fn english(name: String) -> String {
format!("Welcome {name}")
}
}
mod timer {
use std::{thread, time::Duration};
use crate::{Context, Group};
pub fn sleep(ctx: Context, duration: u64, id: String) {
thread::spawn(move || {
thread::sleep(Duration::from_secs(duration));
let _ = ctx.callback_data("timer:sleep", "done", Some(id));
});
}
pub fn group() -> Group {
Group::new().command("sleep", sleep)
}
}
}