fix stop button disabled during connection
This commit is contained in:
@@ -15,6 +15,8 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -45,6 +47,55 @@ def section(name: str) -> None:
|
||||
print(f"\n== {name} ==")
|
||||
|
||||
|
||||
class _StuckWorker:
|
||||
"""A worker whose device open never returns.
|
||||
|
||||
Mirrors the real worker's non-blocking ``stop()`` contract so the window can
|
||||
be exercised against the failure mode the Stop button exists to survive.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._thread: threading.Thread | None = None
|
||||
self._end_reason: str | None = None
|
||||
self.thread_still_owned = False
|
||||
self.stop_blocked_for = 0.0
|
||||
|
||||
def start(self, _source, *, config_ref=None) -> None:
|
||||
self._thread = threading.Thread(target=self._hang, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _hang(self) -> None:
|
||||
time.sleep(600.0)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._thread is not None and self._thread.is_alive()
|
||||
|
||||
@property
|
||||
def phase(self) -> str:
|
||||
return "connecting"
|
||||
|
||||
def latest(self):
|
||||
return None
|
||||
|
||||
def consume_end_reason(self):
|
||||
reason, self._end_reason = self._end_reason, None
|
||||
return reason
|
||||
|
||||
def reset(self) -> None:
|
||||
return None
|
||||
|
||||
def stop(self, *, wait: bool = False) -> None:
|
||||
self._end_reason = self._end_reason or "stopped"
|
||||
started = time.perf_counter()
|
||||
if wait and self._thread is not None and self._thread.is_alive():
|
||||
self._thread.join(timeout=5.0)
|
||||
if self._thread is None or not self._thread.is_alive():
|
||||
self._thread = None
|
||||
self.thread_still_owned = self._thread is not None
|
||||
self.stop_blocked_for = time.perf_counter() - started
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Palette rules
|
||||
#
|
||||
@@ -440,6 +491,74 @@ def main() -> int:
|
||||
window.close()
|
||||
app.processEvents()
|
||||
|
||||
section("stopping a hung connect")
|
||||
stuck = _StuckWorker()
|
||||
window = StudioMainWindow(
|
||||
prefs={
|
||||
"language": "zh_CN",
|
||||
"theme": "dark",
|
||||
"sensor_type": "box",
|
||||
"source_mode": "camera",
|
||||
}
|
||||
)
|
||||
window.resize(1440, 880)
|
||||
window.show()
|
||||
window._ensure_workers = lambda: None
|
||||
window._box_worker = stuck
|
||||
window._finger_worker = stuck
|
||||
app.processEvents()
|
||||
|
||||
check(not window.act_stop.isEnabled(), "Stop shortcut is live before a session")
|
||||
window.toolbar.btn_run.setChecked(True)
|
||||
app.processEvents()
|
||||
|
||||
check(window._session_phase == "connecting", "run did not enter the connecting phase")
|
||||
check(
|
||||
window.toolbar.btn_run.isEnabled(),
|
||||
"Stop is disabled while connecting — a hung connect could not be cancelled",
|
||||
)
|
||||
check(window.toolbar.btn_run.isChecked(), "run button did not latch into the stop state")
|
||||
check(
|
||||
window.toolbar.btn_run.text() == t("toolbar.cancel_label"),
|
||||
f"connecting label is not the cancel label: {window.toolbar.btn_run.text()!r}",
|
||||
)
|
||||
check(
|
||||
window.toolbar.btn_run.toolTip() == t("toolbar.cancel_tip"),
|
||||
"connecting tooltip does not explain how to cancel",
|
||||
)
|
||||
check(window.act_stop.isEnabled(), "Stop shortcut is disabled while connecting")
|
||||
|
||||
started = time.perf_counter()
|
||||
window.toolbar.btn_run.setChecked(False)
|
||||
elapsed = time.perf_counter() - started
|
||||
check(elapsed < 0.5, f"Stop blocked the GUI for {elapsed:.2f}s on a hung connect")
|
||||
check(not window._running, "session stayed active after Stop")
|
||||
check(window._session_phase == "idle", "phase did not return to idle after Stop")
|
||||
check(
|
||||
stuck.thread_still_owned,
|
||||
"Stop abandoned the still-running worker thread",
|
||||
)
|
||||
|
||||
# The slow-connect hint must point at the escape hatch.
|
||||
window.toolbar.btn_run.setChecked(True)
|
||||
app.processEvents()
|
||||
window._connect_started_at = time.perf_counter() - 3600.0
|
||||
window._on_run_tick()
|
||||
check(
|
||||
window.statusBar().currentMessage() == t("status.connect_slow"),
|
||||
f"no slow-connect hint: {window.statusBar().currentMessage()!r}",
|
||||
)
|
||||
|
||||
escaped = time.perf_counter()
|
||||
window._on_stop_shortcut()
|
||||
check(
|
||||
time.perf_counter() - escaped < 0.5,
|
||||
"Esc did not cancel the hung connect immediately",
|
||||
)
|
||||
check(not window._running, "Esc left the session running")
|
||||
window.close()
|
||||
app.processEvents()
|
||||
|
||||
print(f"\nscenes: {scene_count} checks: {CHECKS} failures: {len(FAILURES)}")
|
||||
for failure in FAILURES:
|
||||
print(f" FAIL: {failure}")
|
||||
|
||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = APP_ROOT.parents[1]
|
||||
for _path in (APP_ROOT / "src", REPO_ROOT / "shared" / "src"):
|
||||
for _path in (APP_ROOT / "src", REPO_ROOT / "shared" / "src", APP_ROOT / "tools"):
|
||||
if _path.is_dir() and str(_path) not in sys.path:
|
||||
sys.path.insert(0, str(_path))
|
||||
|
||||
@@ -96,6 +96,12 @@ def main() -> int:
|
||||
parser.add_argument("--size", default="1440x880", help="WxH window size")
|
||||
parser.add_argument("--view", default="arrows_pure")
|
||||
parser.add_argument("--demo", action="store_true", help="inject demo plot/table data")
|
||||
parser.add_argument(
|
||||
"--scene",
|
||||
choices=("idle", "connecting"),
|
||||
default="idle",
|
||||
help="connecting renders the Stop/cancel state of a hung device open",
|
||||
)
|
||||
parser.add_argument("--out", required=True, help="output PNG path")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -135,6 +141,19 @@ def main() -> int:
|
||||
for _ in range(4):
|
||||
app.processEvents()
|
||||
|
||||
if args.scene == "connecting":
|
||||
# A device open that never returns: the state the Stop button must
|
||||
# survive. ``ui_check._StuckWorker`` drives the same window state.
|
||||
from ui_check import _StuckWorker
|
||||
|
||||
stuck = _StuckWorker()
|
||||
window._ensure_workers = lambda: None
|
||||
window._box_worker = stuck
|
||||
window._finger_worker = stuck
|
||||
window.toolbar.btn_run.setChecked(True)
|
||||
for _ in range(4):
|
||||
app.processEvents()
|
||||
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
ok = window.grab().save(str(out))
|
||||
|
||||
Reference in New Issue
Block a user