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
//! What can go wrong.
use crate::Selector;
use std::time::Duration;
/// The result type used throughout this crate.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// An automation failure.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// The AutoItX3 DLL could not be found or loaded.
///
/// The inner error lists every path tried — see
/// [`autoitx_sys::LoadError`].
#[error(transparent)]
Load(#[from] autoitx_sys::LoadError),
/// An AutoIt call set its error flag.
///
/// AutoIt's error reporting is a single thread-global integer with no
/// message, so this carries the function name to make it locatable.
#[error("{func} failed (AutoIt error code {code})")]
AutoItFailed {
/// The AU3 function that failed.
func: &'static str,
/// AutoIt's error code.
code: i32,
},
/// No window matched.
#[error("no window matched {selector}")]
WindowNotFound {
/// The selector that matched nothing.
selector: Box<Selector>,
},
/// The window was found, but it has no such control.
///
/// Named separately from [`WindowNotFound`](Self::WindowNotFound) because
/// the two have different fixes: a wrong window selector versus a wrong
/// control identifier.
#[cfg(any(windows, feature = "mock-loader", docsrs))]
#[error("window {selector} has no control matching {control}")]
ControlNotFound {
/// The window that was found.
selector: Box<Selector>,
/// The control identifier that matched nothing.
control: crate::Control,
},
/// An operation did not complete in time.
#[error("{operation} timed out after {waited:?}")]
Timeout {
/// What was being waited for.
operation: &'static str,
/// How long it waited.
waited: Duration,
},
/// A selector could not be built or parsed.
#[error(transparent)]
Selector(#[from] crate::selector::SelectorError),
/// A key sequence could not be parsed.
#[error(transparent)]
Keys(#[from] crate::keys::KeyParseError),
/// A string argument contained an interior NUL.
///
/// Win32 strings are NUL-terminated, so this would silently truncate.
/// Truncating a window title or a password is worse than failing.
#[error("{what} contains an interior NUL byte at index {at}, which Win32 strings cannot carry")]
InteriorNul {
/// Which argument was at fault.
what: &'static str,
/// Byte offset of the NUL.
at: usize,
},
/// A returned string exceeded the configured limit.
///
/// AutoIt never reports how large a buffer it needed, so the safe layer
/// grows and retries. This is the ceiling on that growth.
#[error("{func} returned more than {limit} bytes of text")]
StringTooLarge {
/// The AU3 function that produced it.
func: &'static str,
/// The configured ceiling.
limit: usize,
},
/// The process-wide automation lock could not be acquired.
///
/// Another process holds it. Automation cannot safely interleave — two
/// robots sending keystrokes at once fight over focus — so this is a
/// failure, not something to work around.
#[error("timed out after {waited:?} waiting for the global automation mutex {name:?}")]
GlobalMutexTimeout {
/// The named mutex.
name: String,
/// How long it waited.
waited: Duration,
},
/// A macOS privacy permission has not been granted.
///
/// Carries a copy-pasteable hint rather than only the fact, because without
/// the Accessibility grant every AX call fails in a way that reads exactly
/// like "window not found" — and chasing that costs an hour.
#[cfg(target_os = "macos")]
#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))]
#[error("{permission:?} permission not granted — {hint}")]
PermissionDenied {
/// Which permission is missing.
permission: crate::ext::macos::Permission,
/// What to do about it, with the System Settings deep link.
hint: String,
},
/// A key sequence names a key this platform does not have.
///
/// `{PRINTSCREEN}` on macOS, for instance. Reported rather than skipped:
/// pressing nothing and returning success would leave the automation
/// believing it took a screenshot.
#[error("this platform ({platform}) has no {key} key")]
UnsupportedKey {
/// The key name, as written.
key: String,
/// Which platform lacks it.
platform: &'static str,
},
/// A platform call did not succeed.
///
/// The OS refused, or could not answer. Distinct from a *capability* gap,
/// which is a compile error rather than a variant here: this is the same
/// operation the platform normally performs, failing this time.
#[error("{platform} could not {operation}")]
Platform {
/// What was attempted, as a verb phrase.
operation: &'static str,
/// Which platform refused.
platform: &'static str,
},
/// An underlying I/O failure, e.g. launching a program.
#[error(transparent)]
Io(#[from] std::io::Error),
}
impl Error {
/// Builds a [`Error::WindowNotFound`].
#[must_use]
pub fn window_not_found(selector: &Selector) -> Self {
Self::WindowNotFound {
selector: Box::new(selector.clone()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn window_not_found_names_the_selector() {
let sel = Selector::from("[CLASS:Chrome_WidgetWin_1;TITLE:Acme Invoices]");
let msg = Error::window_not_found(&sel).to_string();
assert!(msg.contains("Chrome_WidgetWin_1"), "{msg}");
assert!(msg.contains("Acme Invoices"), "{msg}");
}
#[test]
fn timeout_reports_what_and_how_long() {
let msg = Error::Timeout {
operation: "win_wait_active",
waited: Duration::from_secs(30),
}
.to_string();
assert!(msg.contains("win_wait_active"), "{msg}");
assert!(msg.contains("30s"), "{msg}");
}
#[test]
fn error_does_not_grow_unboundedly() {
// Not a performance claim — every operation here is an OS round trip
// measured in milliseconds, so a few words in the Result are noise.
// The point is to notice if someone inlines a collection into a
// variant. `Selector` is boxed for that reason; the current 56 bytes
// come from `autoitx_sys::LoadError`, which is cold and carries the
// full list of searched paths.
const CEILING: usize = 64;
assert!(
size_of::<Error>() <= CEILING,
"Error grew to {} bytes (ceiling {CEILING}) — did a variant gain a \
collection field? Box it.",
size_of::<Error>()
);
}
}