mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 11:56:12 +00:00
* fix(hopper): distinguish an absent demo dialog from publisher rejection The Linux demo watcher collapsed an admitted launcher with no first-run dialog into unsupported_demo_dialog. Report that post-authentication timeout as demo_dialog_absent, keep the unsupported dialog code for an observed but unrecognized Hopper window, and allow REA_HOPPER_DEMO_DIALOG_TIMEOUT_MS to widen the fixed wait. Closes #8008. * fix(hopper): tolerate callers without the dialog timeout argument * fix(hopper): report an invalid demo dialog timeout as its own failure An invalid REA_HOPPER_DEMO_DIALOG_TIMEOUT_MS exited 73 and was shown as invalid_launch_command. Exit 82 is invalid_dialog_timeout. The public error names the setting, shows the rejected value and source, lists the allowed range, and says to restart REA. Drive the demo watcher with controlled windows and time. No window exits 81, a Hopper window without a supported dialog exits 76, and a dialog that arrives after the old 10 second limit is still accepted. * fix(hopper): keep startup stderr capture on AdapterFailure DemoWatchStop was inserted above with_stderr, so launch failures called a method they no longer had and became unexpected_failure. Restore with_stderr on AdapterFailure and reject a watcher fixture that cannot capture stderr. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
174 lines
5.0 KiB
Python
174 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise demo-dialog timeout parsing and the watcher itself."""
|
|
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
spec = importlib.util.spec_from_file_location(
|
|
"hopper_demo_x11", Path(__file__).resolve().parents[2] / "scripts/hopper-demo-x11.py"
|
|
)
|
|
assert spec is not None and spec.loader is not None
|
|
helper = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(helper)
|
|
|
|
class _Stderr:
|
|
def snapshot(self):
|
|
return (4, "")
|
|
|
|
|
|
_failure = helper.AdapterFailure(81, "demo_dialog_absent", "missing dialog")
|
|
if _failure.with_stderr(_Stderr()) is not _failure or _failure.stderr_bytes != 4:
|
|
raise SystemExit("AdapterFailure.with_stderr did not keep the startup failure")
|
|
|
|
DIALOG = 10
|
|
HOPPER = 11
|
|
DIALOG_TITLES = {
|
|
DIALOG: "Registration",
|
|
HOPPER: helper.HOPPER_WINDOW_TITLE,
|
|
}
|
|
DIALOG_TRANSIENTS = {DIALOG: HOPPER}
|
|
DIALOG_WINDOW = (DIALOG, 20, 100, 400, 280)
|
|
HOPPER_WINDOW = (HOPPER, 0, 0, 1280, 1024)
|
|
|
|
|
|
class Clock:
|
|
def __init__(self) -> None:
|
|
self.ms = 0
|
|
|
|
def monotonic(self) -> float:
|
|
return self.ms / 1000
|
|
|
|
def sleep(self, seconds: float) -> None:
|
|
self.ms += int(round(seconds * 1000))
|
|
|
|
|
|
class FakeX11:
|
|
def __init__(self, titles, transients) -> None:
|
|
self.titles = titles
|
|
self.transients = transients
|
|
|
|
def XFetchName(self, _display, window, target):
|
|
value = self.titles.get(window)
|
|
if value is None:
|
|
return 0
|
|
target._obj.value = value.encode("utf-8")
|
|
return 1
|
|
|
|
def XGetTransientForHint(self, _display, window, target):
|
|
value = self.transients.get(window)
|
|
if value is None:
|
|
return 0
|
|
target._obj.value = value
|
|
return 1
|
|
|
|
def XFree(self, _value):
|
|
return 1
|
|
|
|
|
|
def timeout_outcome(configured, requested=None):
|
|
if configured is None:
|
|
os.environ.pop(helper.DIALOG_TIMEOUT_ENV, None)
|
|
else:
|
|
os.environ[helper.DIALOG_TIMEOUT_ENV] = configured
|
|
try:
|
|
return {"ms": helper.configured_dialog_timeout_ms(requested)}
|
|
except helper.AdapterFailure as failure:
|
|
return {
|
|
"code": failure.code,
|
|
"reason": failure.reason,
|
|
"dialog_timeout": failure.dialog_timeout,
|
|
}
|
|
|
|
|
|
def failure_outcome(owned, window_observed):
|
|
failure = helper.classify_watch_timeout(owned, window_observed)
|
|
return {"code": failure.code, "reason": failure.reason}
|
|
|
|
|
|
def watch_outcome(list_windows, timeout_ms, titles=None, transients=None):
|
|
clock = Clock()
|
|
try:
|
|
dialog = helper.watch_for_demo_dialog(
|
|
FakeX11(titles or {}, transients or {}),
|
|
0,
|
|
timeout_ms,
|
|
owner_alive=lambda: True,
|
|
socket_ready=lambda: False,
|
|
process_owned=lambda: True,
|
|
process_exited=lambda: False,
|
|
monotonic=clock.monotonic,
|
|
sleep=clock.sleep,
|
|
list_windows=list_windows,
|
|
)
|
|
except helper.AdapterFailure as failure:
|
|
return {
|
|
"code": failure.code,
|
|
"reason": failure.reason,
|
|
"elapsed_ms": clock.ms,
|
|
}
|
|
return {"dialog": dialog[0], "elapsed_ms": clock.ms}
|
|
|
|
|
|
def late_dialog(timeout_ms, legacy_limit_ms):
|
|
clock = Clock()
|
|
arrival_ms = legacy_limit_ms + 100
|
|
|
|
def list_windows():
|
|
if clock.ms < arrival_ms:
|
|
return []
|
|
return [DIALOG_WINDOW]
|
|
|
|
try:
|
|
dialog = helper.watch_for_demo_dialog(
|
|
FakeX11(DIALOG_TITLES, DIALOG_TRANSIENTS),
|
|
0,
|
|
timeout_ms,
|
|
owner_alive=lambda: True,
|
|
socket_ready=lambda: False,
|
|
process_owned=lambda: True,
|
|
process_exited=lambda: False,
|
|
monotonic=clock.monotonic,
|
|
sleep=clock.sleep,
|
|
list_windows=list_windows,
|
|
)
|
|
except helper.AdapterFailure as failure:
|
|
return {
|
|
"code": failure.code,
|
|
"reason": failure.reason,
|
|
"elapsed_ms": clock.ms,
|
|
"legacy_limit_ms": legacy_limit_ms,
|
|
}
|
|
return {
|
|
"dialog": dialog[0],
|
|
"elapsed_ms": clock.ms,
|
|
"legacy_limit_ms": legacy_limit_ms,
|
|
}
|
|
|
|
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"not_owned": failure_outcome(False, False),
|
|
"admitted_without_window": failure_outcome(True, False),
|
|
"admitted_with_window": failure_outcome(True, True),
|
|
"default_timeout": timeout_outcome(None),
|
|
"configured_timeout": timeout_outcome("25000"),
|
|
"requested_timeout": timeout_outcome("25000", 500),
|
|
"invalid_timeout": timeout_outcome("-5"),
|
|
"watcher_no_window": watch_outcome(lambda: [], 1_000),
|
|
"watcher_hopper_window_without_dialog": watch_outcome(
|
|
lambda: [HOPPER_WINDOW],
|
|
1_000,
|
|
{HOPPER: helper.HOPPER_WINDOW_TITLE},
|
|
),
|
|
"watcher_dialog_after_legacy_limit": late_dialog(
|
|
helper.DEFAULT_DIALOG_TIMEOUT_MS + 10_000,
|
|
helper.DEFAULT_DIALOG_TIMEOUT_MS,
|
|
),
|
|
},
|
|
separators=(",", ":"),
|
|
)
|
|
)
|