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