arma_rs/
testing.rs

1//! For testing your extension.
2
3use std::time::Duration;
4
5use crate::{CallbackMessage, Context, State, Value};
6
7use crate::{ArmaCallContext, Caller, Mission, Server, Source};
8
9/// Wrapper around [`crate::Extension`] used for testing.
10pub struct Extension(crate::Extension);
11
12const BUFFER_SIZE: libc::size_t = 10240; // The sized used by Arma 3 as of 2021-12-30
13
14#[derive(Debug, PartialEq, Eq)]
15/// Result of an event handler
16pub enum Result<T, E> {
17    /// an event has been handled and the handler is done, the value of T is the return value of the event handler
18    Ok(T),
19    /// the handler has encountered an error, the value of T is the return value of the event handler
20    Err(E),
21    /// an event is handled but the handler is not done and should receive another event
22    Continue,
23    /// the handler reached the specified timeout
24    Timeout,
25}
26
27impl<T, E> Result<T, E> {
28    /// Returns true if the result is an ok result
29    pub fn is_ok(&self) -> bool {
30        matches!(self, Self::Ok(_))
31    }
32
33    /// Returns true if the result is an error
34    pub fn is_err(&self) -> bool {
35        matches!(self, Self::Err(_))
36    }
37
38    /// Returns true if the result is a continue result
39    pub fn is_continue(&self) -> bool {
40        matches!(self, Self::Continue)
41    }
42
43    /// Returns true if the result is a timeout result
44    pub fn is_timeout(&self) -> bool {
45        matches!(self, Self::Timeout)
46    }
47}
48
49impl Extension {
50    /// Create a new testing Extension
51    pub fn new(ext: crate::Extension) -> Self {
52        Self(ext)
53    }
54
55    #[must_use]
56    /// Returns a context for simulating interactions with Arma
57    pub fn context(&self) -> Context {
58        self.0.context().with_buffer_size(BUFFER_SIZE)
59    }
60
61    #[must_use]
62    /// Get a reference to the extensions state container
63    pub fn state(&self) -> &State {
64        &self.0.group.state
65    }
66
67    #[must_use]
68    #[allow(clippy::too_many_arguments)]
69    /// Call a function with Arma call context.
70    ///
71    /// # Safety
72    /// This function is unsafe because it interacts with the C API.
73    pub fn call_with_context(
74        &self,
75        function: &str,
76        args: Option<Vec<String>>,
77        caller: Caller,
78        source: Source,
79        mission: Mission,
80        server: Server,
81        remote_exec_owner: i16,
82    ) -> (String, libc::c_int) {
83        self.0.context_manager.replace(Some(ArmaCallContext::new(
84            caller,
85            source,
86            mission,
87            server,
88            remote_exec_owner,
89        )));
90        unsafe { self.handle_call(function, args) }
91    }
92
93    #[must_use]
94    /// Call a function without Arma call context.
95    ///
96    /// # Safety
97    /// This function is unsafe because it interacts with the C API.
98    ///
99    /// # Note
100    /// If the `call-context` feature is enabled, this function passes default values for each field.
101    pub fn call(&self, function: &str, args: Option<Vec<String>>) -> (String, libc::c_int) {
102        self.0.context_manager.replace(None);
103        unsafe { self.handle_call(function, args) }
104    }
105
106    unsafe fn handle_call(
107        &self,
108        function: &str,
109        args: Option<Vec<String>>,
110    ) -> (String, libc::c_int) {
111        let mut output = [0; BUFFER_SIZE];
112        let len = args.as_ref().map(|a| a.len().try_into().unwrap());
113        let mut args_pointer = args.map(|v| {
114            v.into_iter()
115                .map(|s| std::ffi::CString::new(s).unwrap().into_raw())
116                .collect::<Vec<*mut i8>>()
117        });
118        let res = self.0.group.handle(
119            self.context(),
120            &self.0.context_manager,
121            function,
122            output.as_mut_ptr(),
123            BUFFER_SIZE,
124            args_pointer.as_mut().map(Vec::as_mut_ptr),
125            len,
126        );
127        if let Some(args) = args_pointer {
128            for arg in args {
129                let _ = std::ffi::CString::from_raw(arg);
130            }
131        }
132        (
133            std::ffi::CStr::from_ptr(output.as_ptr())
134                .to_str()
135                .unwrap()
136                .to_string(),
137            res,
138        )
139    }
140
141    /// Create a callback handler
142    ///
143    /// Returns a Result from the handler if the callback was handled,
144    /// or `Result::Timeout` if either no event was received, or the handler
145    /// returned `Result::Continue` until the timeout was reached.
146    ///
147    /// The handler must return a Result indicating the callback was handled to exit
148    /// `Result::Continue` will continue to provide events to the handler until another variant is returned
149    pub fn callback_handler<F, T, E>(&self, handler: F, timeout: Duration) -> Result<T, E>
150    where
151        F: Fn(&str, &str, Option<Value>) -> Result<T, E>,
152    {
153        let (_, rx) = &self.0.callback_channel;
154        let deadline = std::time::Instant::now() + timeout;
155        loop {
156            match rx.recv_deadline(deadline) {
157                Ok(CallbackMessage::Call(name, func, data)) => match handler(&name, &func, data) {
158                    Result::Ok(value) => return Result::Ok(value),
159                    Result::Err(error) => return Result::Err(error),
160                    Result::Timeout => return Result::Timeout,
161                    Result::Continue => {}
162                },
163                _ => return Result::Timeout,
164            }
165        }
166    }
167}