smix_error/lib.rs
1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3#![deny(rustdoc::broken_intra_doc_links)]
4
5//! smix-error — ExpectationFailure + FailureCode + AI-readable
6//! `to_prompt()` + `build_suggestions` (stone, cold path).
7//!
8//! `build_suggestions` behavior: threshold > 0.5, top 3, sort by score
9//! descending → field (name > id > text) → DFS index ascending.
10//!
11//! # Why a separate stone
12//!
13//! SDK / driver / runner-client all throw / catch `ExpectationFailure`. As
14//! the canonical failure type it must live in a leaf crate that everyone
15//! can depend on. Failure messages MUST be AI-readable — `to_prompt()`
16//! is the canonical render.
17
18#![doc(html_root_url = "https://docs.smix.dev/smix-error")]
19
20use serde::{Deserialize, Serialize};
21use smix_screen::{ElementSummary, ScreenFacts, WindowInfo, WindowKind};
22use smix_selector::{Selector, describe_selector};
23use std::fmt;
24
25/// All failure codes smix surfaces back to the SDK / MCP / CLI
26/// (SCREAMING_SNAKE_CASE wire).
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
29// New codes keep arriving — two in two releases — and on an exhaustive
30// enum each one is a major, because a consumer matching every arm stops
31// compiling. That is the wrong price for smix naming a failure more
32// precisely, and it was about to be paid for the second time.
33//
34// The cost lands once, here, in a major that was already owed: every
35// downstream `match` needs a catch-all arm from now on. What it buys is
36// that the next code is additive.
37//
38// The guard that made a new variant fail to compile lives inside this
39// crate now (see `the_vocabulary_is_pinned` below). From out here
40// `non_exhaustive` would have made it accept anything, so it moved
41// rather than being weakened.
42#[non_exhaustive]
43pub enum FailureCode {
44 /// Selector matched zero elements in the visible tree.
45 ElementNotFound,
46 /// Element matched but failed the visibility filter.
47 NotVisible,
48 /// Element matched but `enabled = false`.
49 NotEnabled,
50 /// Selector matched multiple elements (when uniqueness was required).
51 Ambiguous,
52 /// Operation exceeded the implicit-wait budget.
53 Timeout,
54 /// `expect` assertion (e.g. `toHaveText`) did not hold.
55 AssertionFailed,
56 /// Target app exited or never launched.
57 AppNotRunning,
58 /// Simulator device is not booted.
59 SimulatorNotBooted,
60 /// The touch was synthesised, and it did not land inside the
61 /// element the selector matched.
62 ///
63 /// Distinct from `ElementNotFound` because the two send a reader
64 /// somewhere different: not-found means fix the selector, missed
65 /// means the element was there and the touch went elsewhere — a
66 /// stale frame, or something moved between the tree fetch and the
67 /// tap.
68 TapMissed,
69 /// The screen is described in one coordinate space and the touch
70 /// would be delivered in another, so no aim can land where the tree
71 /// says the element is.
72 ///
73 /// Distinct from `TapMissed`, and the distinction is the whole
74 /// point: a miss says the element was there and the touch went
75 /// elsewhere, which invites another attempt with a better point.
76 /// There is no better point here — whatever is passed gets
77 /// recomputed against the app's frame and then read against the
78 /// device's. Retrying is the trap, and the consumer who found this
79 /// spent an afternoon in it.
80 CoordinateSpaceMismatch,
81 /// Catch-all for runner / driver / IO failures.
82 DriverError,
83 /// The device's capture path is under load and refusing frames for a
84 /// stated window — the screenshot pacer's circuit is open.
85 ///
86 /// Separate from [`Self::DriverError`] because the two want opposite
87 /// responses and only one of them is a defect. A driver error means
88 /// something is broken; this means "not now, try again shortly", and
89 /// a caller with time left can simply keep waiting. It arrived as a
90 /// `DriverError` until 7.1, which is how a consumer's release gate
91 /// met it: `FAIL [DRIVER_ERROR]` from `waitForAnimationToEnd`, a
92 /// verb whose entire meaning is to wait.
93 CaptureBackpressure,
94}
95
96/// Structured failure thrown by SDK matchers and driver calls. Shape
97/// is AI-feed-back-ready.
98///
99/// `selector` is the typed `Selector` rather than the wire
100/// `serde_json::Value` — callers raising failures from the driver / SDK
101/// side already have the typed selector, and `to_prompt` invokes
102/// `describe_selector` for stable rendering.
103///
104/// Serializes as `{ ok: false, code, message, selector, suggestions,
105/// visibleElements, hint, screenshot, deviceLog }`; the `ok = false`
106/// discriminant identifies the failure branch of a `Result` wire.
107#[derive(Clone, Debug, Serialize, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct ExpectationFailure {
110 /// Always `false` for failures. Round-trip discriminator for
111 /// `Result<T, ExpectationFailure>` wire.
112 pub ok: False,
113 /// Failure code discriminator.
114 pub code: FailureCode,
115 /// Human-readable summary (and AI-readable when fed into `to_prompt`).
116 pub message: String,
117 /// Originating selector, when one is available.
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub selector: Option<Selector>,
120 /// Ranked near-miss suggestions (edit-distance to label / id / etc.).
121 #[serde(default)]
122 pub suggestions: Vec<String>,
123 /// Snapshot of visible+enabled elements at failure time.
124 #[serde(default)]
125 pub visible_elements: Vec<ElementSummary>,
126 /// Optional one-line hint (e.g. "try `app.wait_for(...)` first").
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub hint: Option<String>,
129 /// The smix that produced this failure.
130 ///
131 /// Not decoration. A consumer wrote up two defects against smix
132 /// behaviour that had been fixed hours earlier, quoting an error
133 /// message the current build no longer emits — they had no way to
134 /// tell whether the smix in front of them contained the fix for
135 /// the thing they had just hit. A failure that names its version
136 /// makes that answerable from the failure itself.
137 ///
138 /// Set by [`ExpectationFailure::new`] from the crate version, so no
139 /// call site can forget it.
140 #[serde(default)]
141 pub smix_version: String,
142 /// base64-encoded PNG; omitted from default rendering to keep logs lean.
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub screenshot: Option<String>,
145 /// Last-N-lines of captured device log, folded into AI-readable
146 /// output for failure-window system context.
147 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub device_log: Vec<String>,
149 /// How many elements `visible_elements` was cut from.
150 ///
151 /// A list of ten read as the whole screen: a consumer built a
152 /// detector on it that called every Android failure blind, because
153 /// the ten were always the status bar. The total says it is a
154 /// sample.
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub visible_total: Option<usize>,
157 /// The windows the screen held when this failed, focused app first.
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
159 pub windows: Vec<WindowInfo>,
160 /// Windows the reader could not read. "The app is not in the tree"
161 /// and "the app's window could not be read" are different failures.
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub unreadable_windows: Option<u32>,
164}
165
166/// Newtype around `false` — used as the [`ExpectationFailure::ok`]
167/// discriminator. Serializes/deserializes as the JSON literal `false`.
168#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(transparent)]
170pub struct False(pub bool);
171
172/// Builder/Init form for ergonomic construction.
173#[derive(Default)]
174pub struct FailureInit {
175 /// Optional failure code (defaults to `DriverError` if `None`).
176 pub code: Option<FailureCode>,
177 /// Human-readable / AI-readable message.
178 pub message: String,
179 /// Originating selector, when one is available.
180 pub selector: Option<Selector>,
181 /// Pre-built ranked near-miss suggestions.
182 pub suggestions: Vec<String>,
183 /// Captured visible+enabled elements at failure time.
184 pub visible_elements: Vec<ElementSummary>,
185 /// Optional one-line hint string.
186 pub hint: Option<String>,
187 /// Optional base64 PNG screenshot at failure time.
188 pub screenshot: Option<String>,
189 /// Optional captured device log tail.
190 pub device_log: Vec<String>,
191 /// See [`ExpectationFailure::visible_total`].
192 pub visible_total: Option<usize>,
193 /// See [`ExpectationFailure::windows`].
194 pub windows: Vec<WindowInfo>,
195 /// See [`ExpectationFailure::unreadable_windows`].
196 pub unreadable_windows: Option<u32>,
197}
198
199impl FailureInit {
200 /// The screen the failure happened on: its elements (already in the
201 /// order a reader should meet them), how many there were, whose
202 /// windows they sat in, and how many windows could not be read.
203 ///
204 /// One entry point, so a call site cannot hand over the list and
205 /// forget the count that says it is a sample.
206 #[must_use]
207 pub fn with_screen(mut self, facts: ScreenFacts) -> Self {
208 self.visible_elements = facts.elements;
209 self.visible_total = Some(facts.total);
210 self.windows = facts.windows;
211 self.unreadable_windows = (facts.unreadable > 0).then_some(facts.unreadable);
212 self
213 }
214
215 /// The screen an earlier failure described, carried into a new one.
216 ///
217 /// For a failure that re-states another (a timeout reported as "not
218 /// visible"): copying `visible_elements` by hand left the count and
219 /// the windows behind, and the next field added would be left behind
220 /// the same way.
221 #[must_use]
222 pub fn with_screen_from(mut self, earlier: &ExpectationFailure) -> Self {
223 self.visible_elements = earlier.visible_elements.clone();
224 self.visible_total = earlier.visible_total;
225 self.windows = earlier.windows.clone();
226 self.unreadable_windows = earlier.unreadable_windows;
227 self
228 }
229}
230
231impl ExpectationFailure {
232 /// Construct from an init struct. `code` defaults to `DriverError`
233 /// if `init.code` was None (defensive; SDK callers should always
234 /// set it).
235 pub fn new(init: FailureInit) -> Self {
236 ExpectationFailure {
237 ok: False(false),
238 code: init.code.unwrap_or(FailureCode::DriverError),
239 message: init.message,
240 selector: init.selector,
241 suggestions: init.suggestions,
242 visible_elements: init.visible_elements,
243 hint: init.hint,
244 smix_version: env!("CARGO_PKG_VERSION").to_string(),
245 screenshot: init.screenshot,
246 device_log: init.device_log,
247 visible_total: init.visible_total,
248 windows: init.windows,
249 unreadable_windows: init.unreadable_windows,
250 }
251 }
252
253 /// AI-facing rendering. Designed so the output can be pasted back
254 /// as a user message into a coding agent.
255 #[must_use]
256 pub fn to_prompt(&self) -> String {
257 let mut lines: Vec<String> = Vec::new();
258 lines.push(format!(
259 "FAIL [{}]: {}",
260 format_code(self.code),
261 self.message
262 ));
263 // The version, on every failure, because the reader's next
264 // question after "what went wrong" is often "is my smix old".
265 // A consumer once wrote up two defects that had been fixed
266 // hours earlier, quoting a message this build no longer emits.
267 if !self.smix_version.is_empty() {
268 lines.push(format!(" smix: {}", self.smix_version));
269 }
270 if let Some(sel) = &self.selector {
271 lines.push(format!(" selector: {}", describe_selector(sel)));
272 }
273 if !self.suggestions.is_empty() {
274 lines.push(" suggestions:".into());
275 for s in &self.suggestions {
276 lines.push(format!(" - {}", s));
277 }
278 }
279 if let Some(line) = on_screen_line(&self.windows, self.unreadable_windows) {
280 lines.push(line);
281 }
282 if !self.visible_elements.is_empty() {
283 let n = self.visible_elements.len().min(10);
284 lines.push(match self.visible_total {
285 Some(total) if self.windows.iter().any(|w| w.focused) => {
286 format!(" visible elements ({n} of {total}, the focused app's first):")
287 }
288 Some(total) => format!(" visible elements ({n} of {total}):"),
289 None => format!(" visible elements (top {n}):"),
290 });
291 for el in self.visible_elements.iter().take(10) {
292 lines.push(format!(" - {}", render_element(el)));
293 }
294 }
295 if let Some(h) = &self.hint {
296 lines.push(format!(" hint: {}", h));
297 }
298 if !self.device_log.is_empty() {
299 // LOG_PROMPT_CAP — defensive ceiling for AI prompt size.
300 const LOG_PROMPT_CAP: usize = 200;
301 let n = self.device_log.len();
302 lines.push(format!(" device log (last {} lines):", n));
303 let start = n.saturating_sub(LOG_PROMPT_CAP);
304 for dl in &self.device_log[start..] {
305 lines.push(format!(" - {}", dl));
306 }
307 }
308 lines.join("\n")
309 }
310}
311
312impl fmt::Display for ExpectationFailure {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 write!(f, "{}", self.to_prompt())
315 }
316}
317
318impl std::error::Error for ExpectationFailure {}
319
320/// One line naming the windows a failure's screen held.
321///
322/// Repeats collapse (`×2`) because a status bar and a navigation bar are
323/// both `com.android.systemui`, and listing it twice says nothing the
324/// count does not. `None` when there is nothing to say — a tree that
325/// carries no window information gets no line rather than an empty one.
326fn on_screen_line(windows: &[WindowInfo], unreadable: Option<u32>) -> Option<String> {
327 let mut parts: Vec<(String, usize)> = Vec::new();
328 for w in windows {
329 let who = w.package.as_deref().unwrap_or("(package not reported)");
330 let kind = match w.kind {
331 WindowKind::Application => "application",
332 WindowKind::InputMethod => "keyboard",
333 WindowKind::System => "system",
334 WindowKind::Other => "other",
335 };
336 let label = if w.focused {
337 format!("{who} ({kind}, focused)")
338 } else {
339 format!("{who} ({kind})")
340 };
341 match parts.last_mut() {
342 Some((last, n)) if *last == label => *n += 1,
343 _ => parts.push((label, 1)),
344 }
345 }
346 let mut bits: Vec<String> = parts
347 .into_iter()
348 .map(|(l, n)| if n > 1 { format!("{l} ×{n}") } else { l })
349 .collect();
350 match unreadable {
351 Some(1) => bits.push("1 window unreadable".into()),
352 Some(n) if n > 1 => bits.push(format!("{n} windows unreadable")),
353 _ => {}
354 }
355 (!bits.is_empty()).then(|| format!(" on screen: {}", bits.join(" · ")))
356}
357
358fn format_code(c: FailureCode) -> &'static str {
359 match c {
360 FailureCode::ElementNotFound => "ELEMENT_NOT_FOUND",
361 FailureCode::NotVisible => "NOT_VISIBLE",
362 FailureCode::NotEnabled => "NOT_ENABLED",
363 FailureCode::Ambiguous => "AMBIGUOUS",
364 FailureCode::Timeout => "TIMEOUT",
365 FailureCode::AssertionFailed => "ASSERTION_FAILED",
366 FailureCode::AppNotRunning => "APP_NOT_RUNNING",
367 FailureCode::SimulatorNotBooted => "SIMULATOR_NOT_BOOTED",
368 FailureCode::TapMissed => "TAP_MISSED",
369 FailureCode::CoordinateSpaceMismatch => "COORDINATE_SPACE_MISMATCH",
370 FailureCode::DriverError => "DRIVER_ERROR",
371 FailureCode::CaptureBackpressure => "CAPTURE_BACKPRESSURE",
372 }
373}
374
375fn render_element(el: &ElementSummary) -> String {
376 let role_str = el.role.map(|r| r.as_str()).unwrap_or("unknown");
377 let mut bits: Vec<String> = vec![role_str.to_string()];
378 if let Some(n) = &el.name {
379 bits.push(format!("name={:?}", n));
380 }
381 if let Some(i) = &el.id {
382 bits.push(format!("id=\"{}\"", i));
383 }
384 if let Some(t) = &el.text
385 && Some(t) != el.name.as_ref()
386 {
387 bits.push(format!("text={:?}", t));
388 }
389 if !el.enabled {
390 bits.push("disabled".into());
391 }
392 bits.join(" ")
393}
394
395// -------------------- buildSuggestions ----------------------------------
396
397const SUGGESTION_THRESHOLD: f64 = 0.5;
398const SUGGESTION_TOP_N: usize = 3;
399
400/// Generate "Did you mean ...?" suggestions from the current visible
401/// element list. Contract: threshold > 0.5, top 3, score desc →
402/// field (name > text) → DFS index asc.
403///
404/// `target = None` → empty vec.
405#[must_use]
406pub fn build_suggestions(target: Option<&str>, visible: &[ElementSummary]) -> Vec<String> {
407 let Some(target) = target else {
408 return Vec::new();
409 };
410 let lower_target = target.to_lowercase();
411 let mut candidates: Vec<(f64, &'static str, String, usize)> = Vec::new();
412 for (i, el) in visible.iter().enumerate() {
413 let mut best: Option<(f64, &'static str, String)> = None;
414 if let Some(name) = &el.name
415 && !name.is_empty()
416 {
417 let s = similarity(&name.to_lowercase(), &lower_target);
418 if s > SUGGESTION_THRESHOLD {
419 best = Some((s, "name", name.clone()));
420 }
421 }
422 if let Some(id) = &el.id
423 && !id.is_empty()
424 {
425 let s = similarity(&id.to_lowercase(), &lower_target);
426 if s > SUGGESTION_THRESHOLD && best.as_ref().map(|(bs, _, _)| s > *bs).unwrap_or(true) {
427 best = Some((s, "id", id.clone()));
428 }
429 }
430 if let Some(text) = &el.text
431 && !text.is_empty()
432 {
433 let s = similarity(&text.to_lowercase(), &lower_target);
434 if s > SUGGESTION_THRESHOLD && best.as_ref().map(|(bs, _, _)| s > *bs).unwrap_or(true) {
435 best = Some((s, "text", text.clone()));
436 }
437 }
438 if let Some((score, field, value)) = best {
439 candidates.push((score, field, value, i));
440 }
441 }
442 // Sort: score desc → field name>text → index asc.
443 let field_rank = |f: &str| match f {
444 "name" => 0,
445 "id" => 1,
446 _ => 2, // text
447 };
448 candidates.sort_by(|a, b| {
449 b.0.partial_cmp(&a.0)
450 .unwrap_or(std::cmp::Ordering::Equal)
451 .then_with(|| field_rank(a.1).cmp(&field_rank(b.1)))
452 .then_with(|| a.3.cmp(&b.3))
453 });
454 candidates
455 .into_iter()
456 .take(SUGGESTION_TOP_N)
457 .map(|(score, field, value, _)| {
458 format!(
459 "Did you mean {:?}? (similarity {:.2}, field {})",
460 value, score, field
461 )
462 })
463 .collect()
464}
465
466/// Normalized `[0, 1]` string similarity. 1 = identical,
467/// 0 = completely different.
468#[must_use]
469pub fn similarity(a: &str, b: &str) -> f64 {
470 if a == b {
471 return 1.0;
472 }
473 let (longer, shorter) = if a.chars().count() >= b.chars().count() {
474 (a, b)
475 } else {
476 (b, a)
477 };
478 let llen = longer.chars().count();
479 if llen == 0 {
480 return 1.0;
481 }
482 let dist = edit_distance(longer, shorter);
483 (llen as f64 - dist as f64) / llen as f64
484}
485
486/// Levenshtein edit distance (Wagner-Fischer).
487#[must_use]
488pub fn edit_distance(a: &str, b: &str) -> usize {
489 let a_chars: Vec<char> = a.chars().collect();
490 let b_chars: Vec<char> = b.chars().collect();
491 let a_len = a_chars.len();
492 let b_len = b_chars.len();
493 let mut dp: Vec<usize> = (0..=b_len).collect();
494 for i in 1..=a_len {
495 let mut prev = dp[0];
496 dp[0] = i;
497 for j in 1..=b_len {
498 let tmp = dp[j];
499 dp[j] = if a_chars[i - 1] == b_chars[j - 1] {
500 prev
501 } else {
502 1 + dp[j].min(dp[j - 1]).min(prev)
503 };
504 prev = tmp;
505 }
506 }
507 dp[b_len]
508}
509
510impl FailureCode {
511 /// Every code, once.
512 ///
513 /// Exported because the alternative is a second hand-written copy,
514 /// and there was one: `tests/it/sdk_failure_code_parity.rs` kept its
515 /// own `all_variants()` list, and every assertion in that file —
516 /// the three SDK declarations and the errors guide — is derived
517 /// from it. Its docstring said adding a variant would stop the file
518 /// compiling. That stopped being true when the exhaustiveness guard
519 /// moved in here, because `#[non_exhaustive]` makes an outside
520 /// `match` accept anything; adding `CaptureBackpressure` in 7.1 left
521 /// all seven of those assertions green while no SDK declared it.
522 ///
523 /// [`Self::ALL`] is the one list. The exhaustive match in
524 /// `the_vocabulary_is_pinned` guards it from in here, where
525 /// `non_exhaustive` cannot weaken the match, and everything else
526 /// reads it rather than repeating it.
527 pub const ALL: &'static [FailureCode] = &[
528 FailureCode::ElementNotFound,
529 FailureCode::NotVisible,
530 FailureCode::NotEnabled,
531 FailureCode::Ambiguous,
532 FailureCode::Timeout,
533 FailureCode::AssertionFailed,
534 FailureCode::AppNotRunning,
535 FailureCode::SimulatorNotBooted,
536 FailureCode::TapMissed,
537 FailureCode::CoordinateSpaceMismatch,
538 FailureCode::DriverError,
539 FailureCode::CaptureBackpressure,
540 ];
541
542 /// Whether this code is a verdict about the screen, as opposed to
543 /// smix being unable to look at it.
544 ///
545 /// Two readers ask this and must get the same answer. A flow step
546 /// marked `optional` turns a verdict into a skip and lets a driver
547 /// failure fail the step; an end-to-end check judges a flow by its
548 /// own rule only when the flow failed on a verdict, and names the
549 /// driver's code otherwise. A runner that answered garbage once read,
550 /// in one of those checks, as "the scroll stopped in the wrong place"
551 /// — the red carried a verdict that was not its own.
552 ///
553 /// Exhaustive on purpose: a code added later does not compile until
554 /// somebody has said which side it is on.
555 pub fn judges_the_screen(self) -> bool {
556 match self {
557 FailureCode::ElementNotFound
558 | FailureCode::NotVisible
559 | FailureCode::NotEnabled
560 | FailureCode::Ambiguous
561 | FailureCode::Timeout
562 | FailureCode::AssertionFailed
563 | FailureCode::TapMissed
564 | FailureCode::CoordinateSpaceMismatch => true,
565 FailureCode::AppNotRunning
566 | FailureCode::SimulatorNotBooted
567 | FailureCode::DriverError
568 | FailureCode::CaptureBackpressure => false,
569 }
570 }
571}
572
573#[cfg(test)]
574mod what_a_code_is_about {
575 use super::*;
576
577 /// The eight codes that say something about the screen, and the four
578 /// that say smix could not look. Named rather than counted: a code
579 /// moved from one side to the other has to be moved here too.
580 #[test]
581 fn a_verdict_is_about_the_screen_and_a_driver_failure_is_not() {
582 let about_the_screen = [
583 FailureCode::ElementNotFound,
584 FailureCode::NotVisible,
585 FailureCode::NotEnabled,
586 FailureCode::Ambiguous,
587 FailureCode::Timeout,
588 FailureCode::AssertionFailed,
589 FailureCode::TapMissed,
590 FailureCode::CoordinateSpaceMismatch,
591 ];
592 let could_not_look = [
593 FailureCode::AppNotRunning,
594 FailureCode::SimulatorNotBooted,
595 FailureCode::DriverError,
596 FailureCode::CaptureBackpressure,
597 ];
598 for c in about_the_screen {
599 assert!(c.judges_the_screen(), "{c:?} is a verdict about the screen");
600 }
601 for c in could_not_look {
602 assert!(
603 !c.judges_the_screen(),
604 "{c:?} says smix could not look, not what it saw"
605 );
606 }
607 assert_eq!(
608 about_the_screen.len() + could_not_look.len(),
609 FailureCode::ALL.len(),
610 "every code is on one side or the other"
611 );
612 }
613}
614
615#[cfg(test)]
616mod vocabulary {
617 use super::*;
618
619 /// A new `FailureCode` must not compile until it has been thought
620 /// about everywhere.
621 ///
622 /// This match is exhaustive on purpose and lives inside the crate on
623 /// purpose: `#[non_exhaustive]` makes an outside `match` accept
624 /// anything, so the guard that used to sit in
625 /// `tests/it/sdk_failure_code_parity.rs` would have started passing for
626 /// every future variant. Adding a code should break this line, then
627 /// the wire-string arm below it, then the three SDK declarations the
628 /// parity test reads, then the errors guide.
629 #[test]
630 fn the_vocabulary_is_pinned() {
631 let every = FailureCode::ALL;
632 for &code in every {
633 match code {
634 FailureCode::ElementNotFound
635 | FailureCode::NotVisible
636 | FailureCode::NotEnabled
637 | FailureCode::Ambiguous
638 | FailureCode::Timeout
639 | FailureCode::AssertionFailed
640 | FailureCode::AppNotRunning
641 | FailureCode::SimulatorNotBooted
642 | FailureCode::TapMissed
643 | FailureCode::CoordinateSpaceMismatch
644 | FailureCode::DriverError
645 | FailureCode::CaptureBackpressure => {}
646 }
647 // Every code renders as a wire string, and a code whose
648 // string nobody wrote would reach a consumer as whatever
649 // the formatter defaults to.
650 assert!(!format_code(code).is_empty(), "{code:?} has no wire string");
651 }
652 assert_eq!(
653 every.len(),
654 12,
655 "counted off the list above, not remembered"
656 );
657 }
658}