Skip to content

Commit a09cc7d

Browse files
thymikeeclaude
andcommitted
test(android): pin that a child vanishing mid-read reads as gone
Route AccessibilityNodeReads.child through a generic nullIfGone seam so the JVM suite can reach the catch, and document that a null node is a programmer error the catch would also hide. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
1 parent 5f6dfb3 commit a09cc7d

2 files changed

Lines changed: 53 additions & 4 deletions

File tree

‎android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityNodeReads.java‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,30 @@ static AccessibilityNodeInfo root(AccessibilityWindowInfo window) {
3030
return window.getRoot();
3131
}
3232

33+
interface Read<T> {
34+
T read();
35+
}
36+
3337
/**
3438
* The child, or null when it is gone. When the child's window closes during the read, the
3539
* framework can throw a NullPointerException from its result integrity check instead of answering
3640
* null (seen on API 35 while an alert dialog closed), so that throw means the same as null.
41+
*
42+
* <p>A null {@code node} is a programmer error, but the same catch hides its NullPointerException
43+
* as a gone child.
3744
*/
3845
static AccessibilityNodeInfo child(AccessibilityNodeInfo node, int index) {
46+
return nullIfGone(
47+
() ->
48+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
49+
? node.getChild(index, PREFETCH_STRATEGY)
50+
: node.getChild(index));
51+
}
52+
53+
/** Runs a framework read that answers null for a vanished node and can throw instead. */
54+
static <T> T nullIfGone(Read<T> read) {
3955
try {
40-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
41-
return node.getChild(index, PREFETCH_STRATEGY);
42-
}
43-
return node.getChild(index);
56+
return read.read();
4457
} catch (NullPointerException vanished) {
4558
return null;
4659
}

‎android/snapshot-helper/src/test/java/com/callstack/agentdevice/snapshothelper/AccessibilityReadBatchTest.java‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ static void run() throws Exception {
1414
releasesUnreadResultsAfterFailure();
1515
waitsForReadersBeforeReleasingTheParent();
1616
preservesInterruptionDuringCleanup();
17+
readsAChildThatVanishedMidReadAsGone();
1718
}
1819

1920
private static void queuesConcurrentReadsAndPreservesOrder() throws Exception {
@@ -134,6 +135,41 @@ private static void preservesInterruptionDuringCleanup() {
134135
}
135136
}
136137

138+
private static void readsAChildThatVanishedMidReadAsGone() {
139+
List<Integer> recycled = new ArrayList<>();
140+
try (AccessibilityReadBatch<Integer> batch =
141+
new AccessibilityReadBatch<>(
142+
0, 3,
143+
index ->
144+
AccessibilityNodeReads.nullIfGone(
145+
() -> {
146+
if (index == 1) {
147+
throw new NullPointerException("window closed");
148+
}
149+
if (index == 2) {
150+
throw new IllegalStateException("not a vanished child");
151+
}
152+
return index;
153+
}),
154+
recycled::add)) {
155+
if (batch.get(0) != 0) {
156+
throw new AssertionError("Expected the first child");
157+
}
158+
if (batch.get(1) != null) {
159+
throw new AssertionError("A child that vanished mid-read must read as gone");
160+
}
161+
try {
162+
batch.get(2);
163+
throw new AssertionError("Other read failures must propagate");
164+
} catch (IllegalStateException expected) {
165+
// Only the vanished-window throw means a gone child.
166+
}
167+
}
168+
if (!recycled.equals(Arrays.asList(0))) {
169+
throw new AssertionError("Only the present child must be released: " + recycled);
170+
}
171+
}
172+
137173
private static void await(CountDownLatch latch) {
138174
try {
139175
if (!latch.await(2, TimeUnit.SECONDS)) {

0 commit comments

Comments
 (0)