Skip to content

test: wait for the snackbar instead of assuming one looper round - #344

Merged
almothafar merged 2 commits into
masterfrom
fix/343-flaky-snackbar-assertions
Sep 16, 2026
Merged

almothafar merged 2 commits into
masterfrom
fix/343-flaky-snackbar-assertions

Conversation

@almothafar

@almothafar almothafar commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Addresses #343. Deliberately does not close it — see below.

The three snackbar assertions in GaugeThemeToggleTest looked the bar up immediately after a single idle(). SnackbarManager shows one bar at a time and hands them to BaseTransientBottomBar.showView — the call that actually attaches the view being looked up — through the main looper, so the number of rounds between show() and the view existing is not a constant a test gets to assume. They now wait for the view, bounded to 20 × 25 ms (well inside the bar's 7 s life), and report what they were waiting for if it never arrives.

What I could not do

Reproduce it. CI saw the null once. Since then: 15 runs of the class alone, 6 runs of the whole suite, the release variant as well as debug, and a constructed case with monitoringStopped set so a second bar queues behind the first and forces the manager's hand-off. All green, every time.

So the mechanism is unknown, and the issue text's guess — that it waits on a layout pass — is wrong: Material's bytecode shows showView() attaches the view and only defers visibility until layout, and findViewById finds invisible views. I have corrected that on the issue.

This change cannot be worse than what it replaces, and it turns a bare "expected not null" into a bounded wait that names its subject. But it is a mitigation, not a fix, and calling it one would be claiming a guard I never watched fail.

Also recorded: something that did not work

An @After draining the looper, to clear any bar left current in the manager, seemed like sensible insurance. It ran the activity recreate that the tap tests leave queued on purpose, against an activity the try-with-resources had already destroyed, and failed two tests with "Cannot recreate activity since it's destroyed already". Reverted, and written next to the @After so the idea is not had twice.

Verified

733 tests green on both the debug and release variants (CI runs ./gradlew test, which is both — worth knowing, since running only testDebugUnitTest locally covers half of what CI does). Lint clean.

From review

Mutation-checked, and it changed what this PR claims. Setting AWAIT_ROUNDS to zero kills exactly one assertion: the post-rotation wait in theOfferSurvivesARotationInsideItsWindow. So that is the only site where the bar genuinely needs looper rounds to arrive — at the other three it is already attached when the assertion runs, and the wait is insurance rather than a guard. That is now said in the code instead of implied.

All four keep the wait anyway. The sites differ only by whether an activity was recreated first, and which of those needs waiting is not a property a reader should have to re-derive.

Corrected an overstatement of my own. The helper's JavaDoc said "more than twenty local runs of the class alone". It was fifteen of the class and six of the whole suite. That paragraph is gone rather than fixed — run counts rot, and they belong here, not in the source.

One review finding I did not take. The Spec axis argued the change sits on nondeterministic ground that no bound can fix: that SnackbarManager.cancelSnackbarLocked returns false without clearing currentSnackbar when its WeakReference has been collected, wedging the queue after a GC. The first half is right — I disassembled it. The conclusion is backwards. In show(), that false is what takes the branch to currentSnackbar = null; showNextSnackbarLocked(), so a collected reference is the recovery path, not a wedge. The wedge case would be a live callback, which resolves through the looper — exactly what a bounded wait absorbs.

The three offer assertions asserted immediately after a single idle(). SnackbarManager shows one bar at a time and hands them to BaseTransientBottomBar.showView — the call that attaches the view they look up — through the main looper, so the number of rounds between show() and the view existing is not a constant a test gets to assume. They now wait for the view, bounded, and say what they were waiting for when it never arrives.

This is a mitigation, not a diagnosis. CI saw the lookup come back null once and it has not reproduced since: twenty-plus runs of the class alone and of the whole suite, both variants, plus a constructed case queueing a second bar behind the first. The cause is still unknown and the issue should stay open until CI gives more evidence.

An @after that drained the looper was tried as well, to clear any bar left current in the manager, and reverted: it ran the activity recreate the tap tests leave queued on purpose, against an activity the try-with-resources had already destroyed, failing two of them. That is recorded next to the @after so it is not attempted again.
Review asked for the new guard to be mutation-checked rather than trusted. Setting AWAIT_ROUNDS to zero kills exactly one assertion — the post-rotation wait — so that is the only site where the bar needs looper rounds to arrive. At the other three it is already attached and the wait is insurance. All four keep it, because the sites differ only by whether an activity was recreated first, and that is not something a reader should have to re-derive to know whether waiting is required.

Also from review: the shared failure message becomes a constant with a one-argument overload rather than the same literal at three call sites; the unfound root cause is marked TODO(#343) rather than only described; AWAIT_ROUND_MS gains its own comment; and the paragraph narrating how many times it was run locally is gone — it was going to rot, and it overstated what was run, saying "more than twenty runs of the class alone" when it was fifteen of the class and six of the suite.
@almothafar
almothafar merged commit ae54178 into master Sep 16, 2026
5 checks passed
@almothafar
almothafar deleted the fix/343-flaky-snackbar-assertions branch September 16, 2026 19:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant