Skip to content

Commit daaf71a

Browse files
authored
refactor(ios): safely(tag:default:) wrapper for the catch-log-and-default band-aid (#660)
Consolidate the repeated `var x = default; catchException({ x = expr }); if let m { NSLog("..._IGNORED_EXCEPTION=%@", m); return default }; return x` shape used around exception-prone XCUITest queries into one generic helper (RunnerTests+Exceptions.swift): func safely<T>(_ tag:, _ fallback:, _ block:) -> T func safely<T>(_ tag:, _ block: () -> T?) -> T? // nil-default convenience 11 uniform sites adopt it: SystemModal (4), Snapshot (2), TextEntry (2), Interaction (3). Each drops from ~7-12 lines to 1-3. The NSLog format is preserved byte-for-byte via the tag arg (tag "MODAL_QUERY" -> "AGENT_DEVICE_RUNNER_MODAL_QUERY_IGNORED_EXCEPTION=%@"), so the silently-logged-and-continued path keeps its searchable format and now has a single place to add per-tag exception telemetry. Left inline by design (not the uniform pattern): the silent `_ = catchException` KVC reads (elementHasFocus, snapshotHasFocus), the throw-on-AX snapshot path, executeOnMainSafely's retry-driving catch, and the bespoke pressKeyboardReturn fallback / performElementTap branches whose result depends on the exception message. Behavior-preserving: same defaults, same log output, same returned values. catchException is non-escaping, so the inout capture in safeIsActionableCandidate is preserved. Verified: xcodebuild build-for-testing -> TEST BUILD SUCCEEDED.
1 parent 3f65124 commit daaf71a

5 files changed

Lines changed: 52 additions & 127 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import XCTest
2+
3+
extension RunnerTests {
4+
/// Runs `block` and returns its value. If it raises an Objective-C exception, logs the message
5+
/// under `AGENT_DEVICE_RUNNER_<tag>_IGNORED_EXCEPTION` and returns `fallback`.
6+
///
7+
/// Consolidates the catch-log-and-default band-aid the runner uses around exception-prone
8+
/// XCUITest queries (flaky `allElementsBoundByIndex` snapshots, stale element reads), giving the
9+
/// "silently logged and continued" path one searchable format and one place to add per-tag
10+
/// exception telemetry later. `RunnerObjCExceptionCatcher.catchException` takes a non-escaping
11+
/// block, so `block` may capture `inout` state.
12+
func safely<T>(_ tag: String, _ fallback: T, _ block: () -> T) -> T {
13+
var result = fallback
14+
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
15+
result = block()
16+
})
17+
if let exceptionMessage {
18+
NSLog("AGENT_DEVICE_RUNNER_%@_IGNORED_EXCEPTION=%@", tag, exceptionMessage)
19+
return fallback
20+
}
21+
return result
22+
}
23+
24+
/// Optional-returning convenience: returns `nil` on exception (matching the common
25+
/// `var x: T?` + catch-and-return-nil shape).
26+
func safely<T>(_ tag: String, _ block: () -> T?) -> T? {
27+
safely(tag, nil, block)
28+
}
29+
}

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -285,14 +285,13 @@ extension RunnerTests {
285285
}
286286

287287
private func textInputCandidatesAt(app: XCUIApplication, point: CGPoint) -> [XCUIElement] {
288-
var candidates: [XCUIElement] = []
289-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
288+
safely("TEXT_INPUT_AT_POINT", []) {
290289
// Query the text-input element types directly instead of enumerating the entire tree
291290
// (app.descendants(.any).allElementsBoundByIndex snapshots every element and is ~10x
292291
// slower — it dominated fill latency because resolveTextEntryElement re-runs this on
293292
// each verify/repair poll once the focused field reference goes stale).
294293
// Prefer the smallest matching field so nested editable controls win over large containers.
295-
candidates = [
294+
[
296295
app.textFields,
297296
app.secureTextFields,
298297
app.searchFields,
@@ -318,15 +317,7 @@ extension RunnerTests {
318317
}
319318
return left.elementType.rawValue < right.elementType.rawValue
320319
}
321-
})
322-
if let exceptionMessage {
323-
NSLog(
324-
"AGENT_DEVICE_RUNNER_TEXT_INPUT_AT_POINT_IGNORED_EXCEPTION=%@",
325-
exceptionMessage
326-
)
327-
return []
328320
}
329-
return candidates
330321
}
331322

332323
private func frameContainsPoint(_ frame: CGRect, _ point: CGPoint, tolerance: CGFloat) -> Bool {
@@ -431,9 +422,8 @@ extension RunnerTests {
431422

432423
private func singleTextEntryElement(app: XCUIApplication) -> XCUIElement? {
433424
#if os(iOS)
434-
var matches: [XCUIElement] = []
435-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
436-
matches = app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
425+
let matches = safely("KEYBOARD_RETURN_TEXT_ENTRY_QUERY", []) {
426+
app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
437427
guard element.exists else { return false }
438428
switch element.elementType {
439429
case .textField, .secureTextField, .searchField, .textView:
@@ -442,13 +432,6 @@ extension RunnerTests {
442432
return false
443433
}
444434
}
445-
})
446-
if let exceptionMessage {
447-
NSLog(
448-
"AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TEXT_ENTRY_QUERY_IGNORED_EXCEPTION=%@",
449-
exceptionMessage
450-
)
451-
return nil
452435
}
453436
return matches.count == 1 ? matches[0] : nil
454437
#else
@@ -784,22 +767,13 @@ extension RunnerTests {
784767

785768
private func visibleKeyboardFrame(app: XCUIApplication) -> CGRect? {
786769
#if os(iOS)
787-
var frame: CGRect?
788-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
770+
return safely("KEYBOARD_FRAME") {
789771
let keyboard = app.keyboards.firstMatch
790-
guard keyboard.exists else { return }
772+
guard keyboard.exists else { return nil }
791773
let keyboardFrame = keyboard.frame
792-
guard !keyboardFrame.isEmpty else { return }
793-
frame = keyboardFrame
794-
})
795-
if let exceptionMessage {
796-
NSLog(
797-
"AGENT_DEVICE_RUNNER_KEYBOARD_FRAME_IGNORED_EXCEPTION=%@",
798-
exceptionMessage
799-
)
800-
return nil
774+
guard !keyboardFrame.isEmpty else { return nil }
775+
return keyboardFrame
801776
}
802-
return frame
803777
#else
804778
return nil
805779
#endif

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -354,14 +354,7 @@ extension RunnerTests {
354354
}
355355

356356
private func safeSnapshotViewport(app: XCUIApplication) -> CGRect {
357-
var viewport = CGRect.infinite
358-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
359-
viewport = snapshotViewport(app: app)
360-
})
361-
if let exceptionMessage {
362-
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_VIEWPORT_IGNORED_EXCEPTION=%@", exceptionMessage)
363-
}
364-
return viewport
357+
safely("SNAPSHOT_VIEWPORT", CGRect.infinite) { snapshotViewport(app: app) }
365358
}
366359

367360
private func describeSnapshotError(_ error: Error) -> String {
@@ -718,18 +711,7 @@ extension RunnerTests {
718711
}
719712

720713
private func safeSnapshotElementsQuery(_ fetch: () -> [XCUIElement]) -> [XCUIElement] {
721-
var elements: [XCUIElement] = []
722-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
723-
elements = fetch()
724-
})
725-
if let exceptionMessage {
726-
NSLog(
727-
"AGENT_DEVICE_RUNNER_SNAPSHOT_QUERY_IGNORED_EXCEPTION=%@",
728-
exceptionMessage
729-
)
730-
return []
731-
}
732-
return elements
714+
safely("SNAPSHOT_QUERY", [], fetch)
733715
}
734716

735717
private func isScrollableContainer(_ snapshot: XCUIElementSnapshot, visible: Bool) -> Bool {

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -77,33 +77,11 @@ extension RunnerTests {
7777
}
7878

7979
func safeElementsQuery(_ fetch: () -> [XCUIElement]) -> [XCUIElement] {
80-
var elements: [XCUIElement] = []
81-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
82-
elements = fetch()
83-
})
84-
if let exceptionMessage {
85-
NSLog(
86-
"AGENT_DEVICE_RUNNER_MODAL_QUERY_IGNORED_EXCEPTION=%@",
87-
exceptionMessage
88-
)
89-
return []
90-
}
91-
return elements
80+
safely("MODAL_QUERY", [], fetch)
9281
}
9382

9483
private func safeIsBlockingSystemModal(_ element: XCUIElement, in springboard: XCUIApplication) -> Bool {
95-
var isBlocking = false
96-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
97-
isBlocking = isBlockingSystemModal(element, in: springboard)
98-
})
99-
if let exceptionMessage {
100-
NSLog(
101-
"AGENT_DEVICE_RUNNER_MODAL_CHECK_IGNORED_EXCEPTION=%@",
102-
exceptionMessage
103-
)
104-
return false
105-
}
106-
return isBlocking
84+
safely("MODAL_CHECK", false) { isBlockingSystemModal(element, in: springboard) }
10785
}
10886

10987
private func isBlockingSystemModal(_ element: XCUIElement, in springboard: XCUIApplication) -> Bool {
@@ -134,25 +112,16 @@ extension RunnerTests {
134112
}
135113

136114
private func safeIsActionableCandidate(_ candidate: XCUIElement, seen: inout Set<String>) -> Bool {
137-
var include = false
138-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
139-
if !candidate.exists || !candidate.isHittable { return }
140-
if !actionableTypes.contains(candidate.elementType) { return }
115+
safely("MODAL_ACTION", false) {
116+
if !candidate.exists || !candidate.isHittable { return false }
117+
if !actionableTypes.contains(candidate.elementType) { return false }
141118
let frame = candidate.frame
142-
if frame.isNull || frame.isEmpty { return }
119+
if frame.isNull || frame.isEmpty { return false }
143120
let key = "\(candidate.elementType.rawValue)-\(frame.origin.x)-\(frame.origin.y)-\(frame.size.width)-\(frame.size.height)-\(candidate.label)"
144-
if seen.contains(key) { return }
121+
if seen.contains(key) { return false }
145122
seen.insert(key)
146-
include = true
147-
})
148-
if let exceptionMessage {
149-
NSLog(
150-
"AGENT_DEVICE_RUNNER_MODAL_ACTION_IGNORED_EXCEPTION=%@",
151-
exceptionMessage
152-
)
153-
return false
123+
return true
154124
}
155-
return include
156125
}
157126

158127
private func preferredSystemModalTitle(_ element: XCUIElement) -> String {
@@ -205,9 +174,8 @@ extension RunnerTests {
205174
depth: Int,
206175
hittableOverride: Bool? = nil
207176
) -> SnapshotNode? {
208-
var node: SnapshotNode?
209-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
210-
node = makeSnapshotNode(
177+
safely("MODAL_NODE") {
178+
makeSnapshotNode(
211179
element: element,
212180
index: index,
213181
type: type,
@@ -216,14 +184,6 @@ extension RunnerTests {
216184
depth: depth,
217185
hittableOverride: hittableOverride
218186
)
219-
})
220-
if let exceptionMessage {
221-
NSLog(
222-
"AGENT_DEVICE_RUNNER_MODAL_NODE_IGNORED_EXCEPTION=%@",
223-
exceptionMessage
224-
)
225-
return nil
226187
}
227-
return node
228188
}
229189
}

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -73,30 +73,21 @@ extension RunnerTests {
7373
// under XCUITest, so text entry readiness is driven by tap/keyboard state.
7474
return nil
7575
#else
76-
var focused: XCUIElement?
77-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
76+
return safely("FOCUSED_INPUT_QUERY") {
7877
let candidates = app
7978
.descendants(matching: .any)
8079
.matching(NSPredicate(format: "hasKeyboardFocus == 1"))
8180
.allElementsBoundByIndex
8281
for candidate in candidates where candidate.exists {
8382
switch candidate.elementType {
8483
case .textField, .secureTextField, .searchField, .textView:
85-
focused = candidate
86-
return
84+
return candidate
8785
default:
8886
continue
8987
}
9088
}
91-
})
92-
if let exceptionMessage {
93-
NSLog(
94-
"AGENT_DEVICE_RUNNER_FOCUSED_INPUT_QUERY_IGNORED_EXCEPTION=%@",
95-
exceptionMessage
96-
)
9789
return nil
9890
}
99-
return focused
10091
#endif
10192
}
10293

@@ -650,18 +641,7 @@ extension RunnerTests {
650641

651642
private func keyboardElementExists(app: XCUIApplication) -> Bool {
652643
#if os(iOS)
653-
var exists = false
654-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
655-
exists = app.keyboards.firstMatch.exists
656-
})
657-
if let exceptionMessage {
658-
NSLog(
659-
"AGENT_DEVICE_RUNNER_KEYBOARD_EXISTS_IGNORED_EXCEPTION=%@",
660-
exceptionMessage
661-
)
662-
return false
663-
}
664-
return exists
644+
return safely("KEYBOARD_EXISTS", false) { app.keyboards.firstMatch.exists }
665645
#else
666646
return false
667647
#endif

0 commit comments

Comments
 (0)