Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions src/frontend/e2e-tests/fmtk/fmtkharness.py
Original file line number Diff line number Diff line change
Expand Up @@ -2434,6 +2434,26 @@ def client(self) -> FmtkClient:
raise FmtkError("flutter run not launched (call boot() first)")
return FmtkClient(self.flutter)

def drain_app_errors(self) -> list:
"""The pre-restart error drain, tolerant of an already-dead app.

The drain exists so the outgoing instance cannot launder its
errors through the restart — but an app with no isolate (the
dead-instance state ``restart_app`` is the designated recovery
for; the flows nightly's network module hit it ten runs running
after the auth module's dying deep-link-booted instance) has no
error window left to launder: that state drains empty instead
of failing the restart it exists to perform. Same trade as
:meth:`FmtkClient.recover_if_wedged`-driven restarts: the
alternative is every subsequent test failing on a dead debug
connection (#3231)."""
try:
return self.client.app_errors()
except FmtkError as exc:
if ISOLATE_GONE_MARK in f"{exc}\n{exc.stderr}":
return []
raise

def restart_app(self, at_path: str | None = None) -> None:
"""Stop and relaunch the debug app (fresh main() -> config
re-fetch). The deterministic substitute for dwds hot restart,
Expand All @@ -2446,14 +2466,22 @@ def restart_app(self, at_path: str | None = None) -> None:

The app-error monitor is a rolling window that a restart would
silently clear — drain it first so errors from the outgoing app
instance fail the test instead of being laundered away."""
errors = self.client.app_errors()
instance fail the test instead of being laundered away. An app
whose isolate is already gone drains empty (see
:meth:`drain_app_errors`): callers reach for this method to
recover exactly that state."""
errors = self.drain_app_errors()
if errors:
raise FmtkError(f"app errors before restart_app: {errors}")
url_suffix = ""
if at_path is not None:
self.flutter.launch_serial += 1
url_suffix = f"/?e2e_boot={self.flutter.launch_serial}#{at_path}"
# The wedge marker describes the outgoing instance — a restart
# supersedes it, and a stale armed marker would fire a spurious
# recovery against the fresh app (same reset recover_from_wedge
# performs).
self.flutter.isolate_gone_since = None
self.flutter.stop()
self.flutter.launch(url_suffix)

Expand Down
33 changes: 31 additions & 2 deletions src/frontend/e2e-tests/fmtk/test_harness_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@
"No Flutter isolate found"). ``FmtkClient.exec`` carries the recovery:
a gone isolate with the tab on the app origin arms a marker, a state
that outlives the window restarts the flutter run transparently, and a
parked-away tab never arms. This scenario pins all three legs against
the live stack.
parked-away tab never arms. The first scenario pins all three legs
against the live stack.

``Harness.restart_app`` is the manual counterpart of that recovery —
callers reach for it when they find the app already dead (the flows
nightly lost its whole network module ten runs running to this: the
auth module's dying deep-link-booted instance, then a drain that died
on the dead app before the restart could run). The second scenario
pins that the pre-restart drain treats the gone-isolate state as
"no window left to launder" and the restart proceeds.
"""

from __future__ import annotations
Expand All @@ -17,6 +25,7 @@

from fmtkharness import (
BACKEND_PORT,
FmtkClient,
FmtkError,
ISOLATE_GONE_MARK,
WEDGE_RECOVERY_SECONDS,
Expand Down Expand Up @@ -83,3 +92,23 @@ def test_isolate_wedge_recovery(harness, app):
app.flutter.isolate_gone_since = time.monotonic() - (WEDGE_RECOVERY_SECONDS + 5)
assert app.recover_if_wedged(gone_error()) is True
app.wait_for_login_page()


def test_restart_app_survives_dead_app(harness, app, monkeypatch):
# the dead-instance state restart_app exists to recover: the drain
# must treat a gone isolate as "no window left to launder", not
# fail the restart it is part of. The gone failure is fabricated
# with the same synthetic error the wedge legs use — killing
# Chrome for real on CI takes the VM service down with it
# (connection refused), a different dead state than the nightly's
# (service answers, no isolate behind it).
at_login(harness, app)

def raise_gone(self):
raise gone_error()

monkeypatch.setattr(FmtkClient, "app_errors", raise_gone)
harness.restart_app() # must not raise; stop + relaunch run
monkeypatch.undo()
assert app.flutter.isolate_gone_since is None
app.wait_for_login_page()
Loading