use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use crate::error::{err_number, VBError, VBResult};
use super::appactivate::{AppActivateRecord, AppActivateRequest};
use super::backend::InteractionBackend;
use super::inputbox::{InputBoxRecord, InputBoxRequest};
use super::msgbox::{MsgBoxButton, MsgBoxRecord, MsgBoxRequest};
use super::sendkeys::{SendKeysRecord, SendKeysRequest};
use super::shell::{ShellRecord, ShellRequest};
pub struct MemoryBackend {
command_args: Vec<String>,
break_requested: Cell<bool>,
msgbox_responses: RefCell<VecDeque<MsgBoxButton>>,
msgbox_requests: RefCell<Vec<MsgBoxRecord>>,
input_responses: RefCell<VecDeque<String>>,
input_requests: RefCell<Vec<InputBoxRecord>>,
activate_responses: RefCell<VecDeque<bool>>,
activate_requests: RefCell<Vec<AppActivateRecord>>,
shell_responses: RefCell<VecDeque<f64>>,
shell_requests: RefCell<Vec<ShellRecord>>,
sendkeys_responses: RefCell<VecDeque<bool>>,
sendkeys_requests: RefCell<Vec<SendKeysRecord>>,
next_task_id: Cell<f64>,
}
impl MemoryBackend {
pub fn new() -> Self {
Self {
command_args: Vec::new(),
break_requested: Cell::new(false),
msgbox_responses: RefCell::new(VecDeque::new()),
msgbox_requests: RefCell::new(Vec::new()),
input_responses: RefCell::new(VecDeque::new()),
input_requests: RefCell::new(Vec::new()),
activate_responses: RefCell::new(VecDeque::new()),
activate_requests: RefCell::new(Vec::new()),
shell_responses: RefCell::new(VecDeque::new()),
shell_requests: RefCell::new(Vec::new()),
sendkeys_responses: RefCell::new(VecDeque::new()),
sendkeys_requests: RefCell::new(Vec::new()),
next_task_id: Cell::new(1.0),
}
}
pub fn with_args(args: Vec<String>) -> Self {
let mut backend = Self::new();
backend.command_args = args;
backend
}
pub fn with_msgbox_responses(responses: impl IntoIterator<Item = MsgBoxButton>) -> Self {
let mut backend = Self::new();
backend.msgbox_responses = RefCell::new(responses.into_iter().collect());
backend
}
pub fn with_input_responses(responses: impl IntoIterator<Item = impl Into<String>>) -> Self {
let mut backend = Self::new();
backend.input_responses = RefCell::new(
responses
.into_iter()
.map(Into::into)
.collect::<Vec<_>>()
.into(),
);
backend
}
pub fn with_activate_responses(responses: impl IntoIterator<Item = bool>) -> Self {
let mut backend = Self::new();
backend.activate_responses = RefCell::new(responses.into_iter().collect());
backend
}
pub fn with_shell_responses(responses: impl IntoIterator<Item = f64>) -> Self {
let mut backend = Self::new();
backend.shell_responses = RefCell::new(responses.into_iter().collect());
backend
}
pub fn with_sendkeys_responses(responses: impl IntoIterator<Item = bool>) -> Self {
let mut backend = Self::new();
backend.sendkeys_responses = RefCell::new(responses.into_iter().collect());
backend
}
pub fn set_command_args(&mut self, args: Vec<String>) {
self.command_args = args;
}
pub fn break_requested(&self) -> bool {
self.break_requested.get()
}
pub fn push_msgbox_response(&self, button: MsgBoxButton) {
self.msgbox_responses.borrow_mut().push_back(button);
}
pub fn extend_msgbox_responses(&self, responses: impl IntoIterator<Item = MsgBoxButton>) {
self.msgbox_responses.borrow_mut().extend(responses);
}
pub fn clear_msgbox_responses(&self) {
self.msgbox_responses.borrow_mut().clear();
}
pub fn pending_msgbox_responses(&self) -> usize {
self.msgbox_responses.borrow().len()
}
pub fn msgbox_requests(&self) -> Vec<MsgBoxRecord> {
self.msgbox_requests.borrow().clone()
}
pub fn take_msgbox_requests(&self) -> Vec<MsgBoxRecord> {
std::mem::take(&mut *self.msgbox_requests.borrow_mut())
}
pub fn push_input_response(&self, response: impl Into<String>) {
self.input_responses.borrow_mut().push_back(response.into());
}
pub fn extend_input_responses(&self, responses: impl IntoIterator<Item = impl Into<String>>) {
self.input_responses
.borrow_mut()
.extend(responses.into_iter().map(Into::into));
}
pub fn clear_input_responses(&self) {
self.input_responses.borrow_mut().clear();
}
pub fn pending_input_responses(&self) -> usize {
self.input_responses.borrow().len()
}
pub fn inputbox_requests(&self) -> Vec<InputBoxRecord> {
self.input_requests.borrow().clone()
}
pub fn take_inputbox_requests(&self) -> Vec<InputBoxRecord> {
std::mem::take(&mut *self.input_requests.borrow_mut())
}
pub fn push_activate_response(&self, activated: bool) {
self.activate_responses.borrow_mut().push_back(activated);
}
pub fn extend_activate_responses(&self, responses: impl IntoIterator<Item = bool>) {
self.activate_responses.borrow_mut().extend(responses);
}
pub fn clear_activate_responses(&self) {
self.activate_responses.borrow_mut().clear();
}
pub fn pending_activate_responses(&self) -> usize {
self.activate_responses.borrow().len()
}
pub fn appactivate_requests(&self) -> Vec<AppActivateRecord> {
self.activate_requests.borrow().clone()
}
pub fn take_appactivate_requests(&self) -> Vec<AppActivateRecord> {
std::mem::take(&mut *self.activate_requests.borrow_mut())
}
pub fn push_shell_response(&self, task_id: f64) {
self.shell_responses.borrow_mut().push_back(task_id);
}
pub fn extend_shell_responses(&self, responses: impl IntoIterator<Item = f64>) {
self.shell_responses.borrow_mut().extend(responses);
}
pub fn clear_shell_responses(&self) {
self.shell_responses.borrow_mut().clear();
}
pub fn pending_shell_responses(&self) -> usize {
self.shell_responses.borrow().len()
}
pub fn shell_requests(&self) -> Vec<ShellRecord> {
self.shell_requests.borrow().clone()
}
pub fn take_shell_requests(&self) -> Vec<ShellRecord> {
std::mem::take(&mut *self.shell_requests.borrow_mut())
}
pub fn push_sendkeys_response(&self, delivered: bool) {
self.sendkeys_responses.borrow_mut().push_back(delivered);
}
pub fn extend_sendkeys_responses(&self, responses: impl IntoIterator<Item = bool>) {
self.sendkeys_responses.borrow_mut().extend(responses);
}
pub fn clear_sendkeys_responses(&self) {
self.sendkeys_responses.borrow_mut().clear();
}
pub fn pending_sendkeys_responses(&self) -> usize {
self.sendkeys_responses.borrow().len()
}
pub fn sendkeys_requests(&self) -> Vec<SendKeysRecord> {
self.sendkeys_requests.borrow().clone()
}
pub fn take_sendkeys_requests(&self) -> Vec<SendKeysRecord> {
std::mem::take(&mut *self.sendkeys_requests.borrow_mut())
}
}
impl Default for MemoryBackend {
fn default() -> Self {
Self::new()
}
}
impl InteractionBackend for MemoryBackend {
fn command_args(&self) -> Vec<String> {
self.command_args.clone()
}
fn do_events(&self) -> i16 {
0
}
fn beep(&self) {
}
fn stop(&self) {
self.break_requested.set(true);
}
fn msg_box(&self, request: &MsgBoxRequest) -> VBResult<MsgBoxButton> {
self.msgbox_requests
.borrow_mut()
.push(MsgBoxRecord::of(request));
match self.msgbox_responses.borrow_mut().pop_front() {
None => Ok(request.default_button_value()),
Some(response) if request.offered_buttons().contains(&response) => Ok(response),
Some(response) => Err(VBError::with_description(
err_number::INVALID_PROCEDURE_CALL,
format!(
"MsgBox response mismatch: dialog offers {} but the queued response \
is {}",
offered_list(request.offered_buttons()),
response.name(),
),
)),
}
}
fn input_box(&self, request: &InputBoxRequest) -> VBResult<String> {
self.input_requests
.borrow_mut()
.push(InputBoxRecord::of(request));
Ok(self
.input_responses
.borrow_mut()
.pop_front()
.unwrap_or_else(|| request.default_response.clone()))
}
fn app_activate(&self, request: &AppActivateRequest) -> VBResult<()> {
self.activate_requests
.borrow_mut()
.push(AppActivateRecord::of(request));
match self.activate_responses.borrow_mut().pop_front() {
None | Some(true) => Ok(()),
Some(false) => Err(VBError::with_description(
err_number::INVALID_PROCEDURE_CALL,
format!(
"Invalid procedure call or argument: AppActivate found no window \
titled \"{}\"",
request.title,
),
)),
}
}
fn shell(&self, request: &ShellRequest) -> VBResult<f64> {
self.shell_requests
.borrow_mut()
.push(ShellRecord::of(request));
Ok(self
.shell_responses
.borrow_mut()
.pop_front()
.unwrap_or_else(|| {
let task_id = self.next_task_id.get();
self.next_task_id.set(task_id + 1.0);
task_id
}))
}
fn send_keys(&self, request: &SendKeysRequest) -> VBResult<()> {
self.sendkeys_requests
.borrow_mut()
.push(SendKeysRecord::of(request));
match self.sendkeys_responses.borrow_mut().pop_front() {
None | Some(true) => Ok(()),
Some(false) => Err(VBError::with_description(
err_number::INVALID_PROCEDURE_CALL,
format!(
"Invalid procedure call or argument: SendKeys could not deliver \"{}\"",
request.keys,
),
)),
}
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
fn offered_list(buttons: &[MsgBoxButton]) -> String {
buttons
.iter()
.map(|b| b.name())
.collect::<Vec<_>>()
.join("|")
}
#[cfg(test)]
mod tests {
use super::super::shell::WindowStyle;
use super::*;
#[test]
fn empty_by_default() {
let backend = MemoryBackend::new();
assert!(backend.command_args().is_empty());
}
#[test]
fn with_args_returns_injected() {
let backend = MemoryBackend::with_args(vec!["--debug".into(), "file.txt".into()]);
assert_eq!(backend.command_args(), vec!["--debug", "file.txt"]);
}
#[test]
fn set_command_args_replaces() {
let mut backend = MemoryBackend::new();
backend.set_command_args(vec!["/server:localhost".into()]);
assert_eq!(backend.command_args(), vec!["/server:localhost"]);
}
#[test]
fn do_events_returns_zero() {
let backend = MemoryBackend::new();
assert_eq!(backend.do_events(), 0i16);
}
#[test]
fn stop_sets_break_requested() {
let backend = MemoryBackend::new();
assert!(!backend.break_requested());
backend.stop();
assert!(backend.break_requested());
}
#[test]
fn empty_queue_answers_default_button() {
let backend = MemoryBackend::new();
let request = MsgBoxRequest::parse("save?", 4 + 32 + 256).unwrap(); let answer = backend.msg_box(&request).unwrap();
assert_eq!(answer, MsgBoxButton::No);
}
#[test]
fn queued_response_is_returned_in_order() {
let backend = MemoryBackend::with_msgbox_responses([MsgBoxButton::Yes, MsgBoxButton::No]);
let request = MsgBoxRequest::parse("again?", 4).unwrap();
assert_eq!(backend.msg_box(&request).unwrap(), MsgBoxButton::Yes);
assert_eq!(backend.msg_box(&request).unwrap(), MsgBoxButton::No);
assert_eq!(backend.msg_box(&request).unwrap(), MsgBoxButton::Yes);
}
#[test]
fn incompatible_response_is_reported_not_coerced() {
let backend = MemoryBackend::with_msgbox_responses([MsgBoxButton::Cancel]);
let request = MsgBoxRequest::parse("overwrite?", 4).unwrap();
let err = backend.msg_box(&request).unwrap_err();
assert_eq!(err.number, err_number::INVALID_PROCEDURE_CALL);
assert!(err.description.contains("Yes|No"), "{}", err.description);
assert!(err.description.contains("Cancel"), "{}", err.description);
}
#[test]
fn matching_response_for_okcancel_works() {
let backend = MemoryBackend::with_msgbox_responses([MsgBoxButton::Cancel]);
let request = MsgBoxRequest::parse("continue?", 1).unwrap();
assert_eq!(backend.msg_box(&request).unwrap(), MsgBoxButton::Cancel);
}
#[test]
fn requests_are_recorded_even_when_rejected() {
let backend = MemoryBackend::with_msgbox_responses([MsgBoxButton::Ignore]);
let request = MsgBoxRequest::parse("retry?", 5)
.unwrap()
.with_title(Some("Ops".into()));
let _ = backend.msg_box(&request);
let requests = backend.take_msgbox_requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].prompt, "retry?");
assert_eq!(requests[0].title.as_deref(), Some("Ops"));
assert_eq!(
requests[0].offered_buttons,
vec![MsgBoxButton::Retry, MsgBoxButton::Cancel]
);
assert!(backend.msgbox_requests().is_empty());
}
#[test]
fn response_queue_helpers_round_trip() {
let backend = MemoryBackend::new();
backend.push_msgbox_response(MsgBoxButton::Abort);
backend.extend_msgbox_responses([MsgBoxButton::Retry]);
assert_eq!(backend.pending_msgbox_responses(), 2);
backend.clear_msgbox_responses();
assert_eq!(backend.pending_msgbox_responses(), 0);
}
#[test]
fn empty_input_queue_answers_default_response() {
let backend = MemoryBackend::new();
let request = InputBoxRequest::new("name?").with_default("Arthur");
let answer = backend.input_box(&request).unwrap();
assert_eq!(answer, "Arthur");
}
#[test]
fn empty_input_queue_without_default_returns_empty_string() {
let backend = MemoryBackend::new();
let answer = backend.input_box(&InputBoxRequest::new("name?")).unwrap();
assert_eq!(answer, "");
}
#[test]
fn queued_input_responses_are_returned_in_order() {
let backend = MemoryBackend::with_input_responses(["first", "second", ""]);
let request = InputBoxRequest::new("value?").with_default("default");
assert_eq!(backend.input_box(&request).unwrap(), "first");
assert_eq!(backend.input_box(&request).unwrap(), "second");
assert_eq!(backend.input_box(&request).unwrap(), "");
assert_eq!(backend.input_box(&request).unwrap(), "default");
}
#[test]
fn input_requests_are_recorded() {
let backend = MemoryBackend::new();
backend
.input_box(
&InputBoxRequest::new("port?")
.with_title(Some("Config".into()))
.with_default("8080")
.with_position(100, 200),
)
.unwrap();
let requests = backend.take_inputbox_requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].prompt, "port?");
assert_eq!(requests[0].title.as_deref(), Some("Config"));
assert_eq!(requests[0].default_response, "8080");
assert_eq!(requests[0].xpos, Some(100));
assert_eq!(requests[0].ypos, Some(200));
assert!(backend.inputbox_requests().is_empty());
}
#[test]
fn input_response_queue_helpers_round_trip() {
let backend = MemoryBackend::new();
backend.push_input_response("a");
backend.extend_input_responses(["b", "c"]);
assert_eq!(backend.pending_input_responses(), 3);
backend.clear_input_responses();
assert_eq!(backend.pending_input_responses(), 0);
}
#[test]
fn empty_activate_queue_succeeds() {
let backend = MemoryBackend::new();
assert!(backend
.app_activate(&AppActivateRequest::new("Calculator"))
.is_ok());
}
#[test]
fn queued_activate_outcomes_are_returned_in_order() {
let backend = MemoryBackend::with_activate_responses([true, false, true]);
let request = AppActivateRequest::new("Notepad");
assert!(backend.app_activate(&request).is_ok());
assert!(backend.app_activate(&request).is_err());
assert!(backend.app_activate(&request).is_ok());
assert!(backend.app_activate(&request).is_ok());
}
#[test]
fn failed_activation_is_error_5_describing_the_title() {
let backend = MemoryBackend::with_activate_responses([false]);
let request = AppActivateRequest::new("Missing Window").with_wait(true);
let err = backend.app_activate(&request).unwrap_err();
assert_eq!(err.number, err_number::INVALID_PROCEDURE_CALL);
assert!(
err.description.contains("Missing Window"),
"{}",
err.description
);
}
#[test]
fn activate_requests_are_recorded_even_when_rejected() {
let backend = MemoryBackend::with_activate_responses([false]);
let _ = backend.app_activate(&AppActivateRequest::new("Calc"));
let requests = backend.take_appactivate_requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].title, "Calc");
assert!(!requests[0].wait);
assert!(backend.appactivate_requests().is_empty());
}
#[test]
fn activate_response_queue_helpers_round_trip() {
let backend = MemoryBackend::new();
backend.push_activate_response(true);
backend.extend_activate_responses([false, true]);
assert_eq!(backend.pending_activate_responses(), 3);
backend.clear_activate_responses();
assert_eq!(backend.pending_activate_responses(), 0);
}
#[test]
fn empty_shell_queue_hands_out_synthetic_task_ids() {
let backend = MemoryBackend::new();
let request = ShellRequest::new("notepad.exe");
assert_eq!(backend.shell(&request).unwrap(), 1.0);
assert_eq!(backend.shell(&request).unwrap(), 2.0);
assert!(backend.shell(&request).unwrap() > 0.0);
}
#[test]
fn queued_shell_responses_are_returned_in_order() {
let backend = MemoryBackend::with_shell_responses([4242.0, 7.0]);
let request = ShellRequest::new("calc.exe");
assert_eq!(backend.shell(&request).unwrap(), 4242.0);
assert_eq!(backend.shell(&request).unwrap(), 7.0);
assert_eq!(backend.shell(&request).unwrap(), 1.0);
}
#[test]
fn shell_requests_are_recorded() {
let backend = MemoryBackend::with_shell_responses([99.0]);
backend
.shell(
&ShellRequest::new(r#""C:\Program Files\App.exe" /flag"#)
.with_window_style(WindowStyle::MaximizedFocus),
)
.unwrap();
let requests = backend.take_shell_requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].pathname, r#""C:\Program Files\App.exe" /flag"#);
assert_eq!(requests[0].window_style, WindowStyle::MaximizedFocus);
assert!(backend.shell_requests().is_empty());
}
#[test]
fn shell_requests_are_recorded_even_with_an_empty_list() {
let backend = MemoryBackend::new();
let _ = backend.shell(&ShellRequest::new("backup.bat"));
let requests = backend.shell_requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].pathname, "backup.bat");
assert_eq!(requests[0].window_style, WindowStyle::MinimizedFocus);
}
#[test]
fn shell_response_queue_helpers_round_trip() {
let backend = MemoryBackend::new();
backend.push_shell_response(10.0);
backend.extend_shell_responses([20.0, 30.0]);
assert_eq!(backend.pending_shell_responses(), 3);
backend.clear_shell_responses();
assert_eq!(backend.pending_shell_responses(), 0);
}
#[test]
fn empty_sendkeys_queue_succeeds_without_touching_the_os() {
let backend = MemoryBackend::new();
assert!(backend
.send_keys(&SendKeysRequest::parse("Hello{ENTER}", false).unwrap())
.is_ok());
}
#[test]
fn sent_keys_are_stored_in_a_list_and_returned_as_requested() {
let backend = MemoryBackend::new();
backend
.send_keys(&SendKeysRequest::parse("User{TAB}", true).unwrap())
.unwrap();
backend
.send_keys(&SendKeysRequest::parse("{ENTER}", false).unwrap())
.unwrap();
let requests = backend.take_sendkeys_requests();
assert_eq!(requests.len(), 2);
assert_eq!(requests[0].keys, "User{TAB}");
assert!(requests[0].wait);
assert_eq!(requests[1].keys, "{ENTER}");
assert!(!requests[1].wait);
assert!(backend.sendkeys_requests().is_empty());
}
#[test]
fn queued_sendkeys_outcomes_are_returned_in_order() {
let backend = MemoryBackend::with_sendkeys_responses([true, false, true]);
let request = SendKeysRequest::parse("^c", false).unwrap();
assert!(backend.send_keys(&request).is_ok());
assert!(backend.send_keys(&request).is_err());
assert!(backend.send_keys(&request).is_ok());
assert!(backend.send_keys(&request).is_ok());
}
#[test]
fn failed_delivery_is_error_5_describing_the_keys() {
let backend = MemoryBackend::with_sendkeys_responses([false]);
let request = SendKeysRequest::parse("%{F4}", true).unwrap();
let err = backend.send_keys(&request).unwrap_err();
assert_eq!(err.number, err_number::INVALID_PROCEDURE_CALL);
assert!(err.description.contains("%{F4}"), "{}", err.description);
}
#[test]
fn sendkeys_requests_are_recorded_even_when_rejected() {
let backend = MemoryBackend::with_sendkeys_responses([false]);
let _ = backend.send_keys(&SendKeysRequest::parse("hi", false).unwrap());
let requests = backend.take_sendkeys_requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].keys, "hi");
assert!(!requests[0].wait);
}
#[test]
fn sendkeys_response_queue_helpers_round_trip() {
let backend = MemoryBackend::new();
backend.push_sendkeys_response(true);
backend.extend_sendkeys_responses([false, true]);
assert_eq!(backend.pending_sendkeys_responses(), 3);
backend.clear_sendkeys_responses();
assert_eq!(backend.pending_sendkeys_responses(), 0);
}
}