aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_gui.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_gui.py')
-rw-r--r--tests/test_gui.py219
1 files changed, 151 insertions, 68 deletions
diff --git a/tests/test_gui.py b/tests/test_gui.py
index 1f3f7b0..ee568c1 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -29,7 +29,7 @@ import unittest # noqa: E402
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
sys.path.insert(0, ROOT)
-from PyQt6.QtCore import QCoreApplication, QEvent, QObject, QPointF, QProcess, Qt, pyqtSignal # noqa: E402
+from PyQt6.QtCore import QObject, QPointF, QProcess, pyqtSignal # noqa: E402
from PyQt6.QtGui import QColor # noqa: E402
from PyQt6.QtWidgets import QApplication # noqa: E402
@@ -309,7 +309,7 @@ class SmokeTest(unittest.TestCase):
return w, remote
def test_tabs_and_dashboard(self):
- w, _ = self.window()
+ w, remote = self.window(**{"job-start": ({"id": "20260101-000001-sbpull"}, None)})
w.poll_jobs()
self.assertEqual(w.conn.text(), "● connected")
self.assertEqual(w.tabs.tabText(0), "Dashboard")
@@ -328,8 +328,10 @@ class SmokeTest(unittest.TestCase):
self.assertEqual(w.dashboard.legend.text(), "n/a")
jobs = w.open_jobs()
self.assertIs(w.open_jobs(), jobs)
- self.assertEqual(w.tabs.count(), 2)
- self.assertEqual(jobs.start_menu.actions()[0].text(), "sbpull personal (preview)")
+ self.assertEqual(w.tabs.count(), 5)
+ self.assertEqual([a.text() for a in jobs.pull_menu.actions()], ["personal (preview)"])
+ jobs.pull_menu.actions()[0].trigger()
+ self.assertIn(("job-start", "sbpull", "personal", "--no"), remote.calls)
def test_end_notification_once(self):
w, remote = self.window(jobs=({"jobs": [JOB]}, None))
@@ -363,30 +365,6 @@ class SmokeTest(unittest.TestCase):
[("ok", 3), ("failed", 1), ("removed", 1)])
self.assertEqual(w.dashboard.tiles["Built"].detail.text(), "of 10 · 6 never built")
- def test_close_jobs_tab_while_following(self):
- # regression: the follow process ended after the tab was deleted, its
- # finished slot touched dead widgets and PyQt aborted the app
- w, remote = self.window(jobs=({"jobs": [JOB]}, None))
- procs = QObject()
- self.addCleanup(procs.deleteLater)
-
- def follow(jid, offset):
- p = QProcess(procs)
- p.start("sleep", ["5"])
- return p
-
- remote.follow = follow
- w.poll_jobs()
- jobs = w.open_jobs()
- jobs.select(JOB["id"])
- self.assertIsNotNone(jobs.proc)
- w._close_tab(w.tabs.indexOf(jobs))
- QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete.value)
- end = time.monotonic() + 1
- while time.monotonic() < end: # let the killed process report in
- app.processEvents()
- self.assertIsNone(w.jobs_tab)
-
def test_packages_tab(self):
w, remote = self.window(**{"job-start": ({"id": "20260101-000001-slackrepo"}, None)})
w.poll_jobs()
@@ -422,34 +400,107 @@ class SmokeTest(unittest.TestCase):
["slackrepo", "build", "--preview", "curl", "nuclei"],
["slackrepo", "update", "--preview"]])
- def test_run_preview_with_signing(self):
- w, remote = self.window(jobs=({"jobs": [PREVIEW]}, None),
- **{"gpg-status": ({"unlocked": False, "key": "K"}, None),
- "gpg-unlock": lambda args, stdin: (
- (None, "bad passphrase") if stdin == b"wrong\n" else ({}, None)),
- "job-start": ({"id": "20260101-000001-slackrepo"}, None)})
+ def end_preview(self, job, **answers):
+ """A window where job, started by this session, goes from running to its status."""
+ w, remote = self.window(jobs=({"jobs": [dict(job, status="running")]}, None), **answers)
+ w.mine.add(job["id"])
w.poll_jobs()
- jobs = w.open_jobs()
- jobs.select(PREVIEW["id"])
- self.assertTrue(jobs.run_btn.isVisibleTo(jobs))
+ remote.answers["jobs"] = ({"jobs": [job]}, None)
+ w.poll_jobs()
+ return w, remote
+
+ def test_preview_next_step_with_signing(self):
+ w, remote = self.end_preview(
+ PREVIEW, **{"gpg-status": ({"unlocked": False, "key": "K"}, None),
+ "gpg-unlock": lambda args, stdin: (
+ (None, "bad passphrase") if stdin == b"wrong\n" else ({}, None)),
+ "job-start": ({"id": "20260101-000001-slackrepo"}, None)})
+ self.assertIs(w.tabs.currentWidget(), w.dashboard) # nobody was sent to Jobs
+ d = w.next_steps[PREVIEW["id"]]
+ self.assertEqual(d.windowTitle(), "Preview: slackrepo update nuclei")
+ self.assertTrue(d.publish.isChecked())
answers = iter(["wrong", "right"])
- jobs.ask_passphrase = lambda retry: next(answers)
- jobs.publish_box.setChecked(False)
- jobs.run_preview()
+ w.jobs_tab.ask_passphrase = lambda retry: next(answers)
+ d.publish.setChecked(False)
+ d.run_btn.click()
self.assertIn(("gpg-unlock", b"wrong\n"), remote.calls)
self.assertIn(("gpg-unlock", b"right\n"), remote.calls)
- self.assertEqual(remote.calls[-2], ("job-start", "slackrepo", "update", "--no-publish", "nuclei"))
+ self.assertIn(("job-start", "slackrepo", "update", "--no-publish", "nuclei"), remote.calls)
+ self.assertEqual(w.next_steps, {})
- def test_signing_cancel_starts_nothing(self):
- w, remote = self.window(jobs=({"jobs": [PREVIEW]}, None),
- **{"gpg-status": ({"unlocked": False, "key": "K"}, None)})
- w.poll_jobs()
+ def test_preview_not_now_then_run_it(self):
+ w, remote = self.end_preview(PREVIEW, **{"job-start": ({"id": "20260101-000001-slackrepo"}, None)})
+ w.next_steps[PREVIEW["id"]].reject()
+ self.assertEqual(w.next_steps, {})
jobs = w.open_jobs()
jobs.select(PREVIEW["id"])
- jobs.ask_passphrase = lambda retry: None
- jobs.run_preview()
+ self.assertTrue(jobs.act_run.isEnabled())
+ jobs.act_run.trigger() # Not now loses nothing
+ w.next_steps[PREVIEW["id"]].run_btn.click()
+ self.assertIn(("job-start", "slackrepo", "update", "nuclei"), remote.calls)
+
+ def test_signing_cancel_starts_nothing(self):
+ w, remote = self.end_preview(PREVIEW, **{"gpg-status": ({"unlocked": False, "key": "K"}, None)})
+ w.jobs_tab.ask_passphrase = lambda retry: None
+ w.next_steps[PREVIEW["id"]].run_btn.click()
self.assertFalse(any(c[0] == "job-start" for c in remote.calls))
+ def test_preview_from_elsewhere_has_no_dialog(self):
+ w, remote = self.window(jobs=({"jobs": [dict(PREVIEW, status="running")]}, None))
+ w.poll_jobs()
+ remote.answers["jobs"] = ({"jobs": [PREVIEW]}, None)
+ w.poll_jobs()
+ self.assertEqual(w.next_steps, {})
+ self.assertEqual(w.notifier.sent[0][0], "sb-builder: finished")
+
+ def test_shadow_preview_dialog_only_with_a_plan(self):
+ shadow = dict(PREVIEW, id="20260101-000000-sbshadow", kind="sbshadow", args=["--no"])
+ for log, shown in (("shadow: misc/foo\n", True),
+ ("sbshadow: nothing to shadow or unshadow.\n", False)):
+ with self.subTest(log=log):
+ procs = QObject()
+ self.addCleanup(procs.deleteLater)
+
+ def follow(jid, offset, log=log):
+ p = QProcess(procs)
+ p.start("printf", ["%s", log])
+ return p
+ w, remote = self.window(jobs=({"jobs": [dict(shadow, status="running")]}, None),
+ **{"job-start": ({"id": "20260101-000001-sbshadow"}, None)})
+ remote.follow = follow
+ w.mine.add(shadow["id"])
+ w.poll_jobs()
+ remote.answers["jobs"] = ({"jobs": [shadow]}, None)
+ w.poll_jobs()
+ d = w.next_steps.get(shadow["id"])
+ end = time.monotonic() + 3
+ while d and not d.isVisible() and shadow["id"] in w.next_steps and time.monotonic() < end:
+ app.processEvents()
+ self.assertEqual(shadow["id"] in w.next_steps and d.isVisible(), shown)
+ if shown:
+ self.assertEqual(d.run_btn.text(), "Apply shadowing")
+ self.assertFalse(d.publish.isVisibleTo(d))
+ d.confirm = lambda q: True
+ d.run_btn.click()
+ self.assertIn(("job-start", "sbshadow", "--yes"), remote.calls)
+
+ def test_jobs_tab_badge(self):
+ w, remote = self.window(jobs=({"jobs": [JOB]}, None))
+ w.poll_jobs()
+ jobs_i = w.tabs.indexOf(w.jobs_tab)
+ self.assertEqual(w.badge, "running")
+ self.assertFalse(w.tabs.tabIcon(jobs_i).isNull())
+ self.assertEqual(w.tabs.tabText(jobs_i), "Jobs (running)")
+ remote.answers["jobs"] = ({"jobs": [dict(JOB, status="failed", end=5, exit=1)]}, None)
+ w.poll_jobs()
+ self.assertEqual(w.badge, "bad") # stays until Jobs is shown
+ self.assertEqual(w.tabs.tabText(jobs_i), "Jobs")
+ w.poll_jobs()
+ self.assertEqual(w.badge, "bad")
+ w.open_jobs()
+ self.assertIsNone(w.badge)
+ self.assertTrue(w.tabs.tabIcon(jobs_i).isNull())
+
def test_key_indicator_and_tile_links(self):
w, _ = self.window(**{"gpg-status": ({"unlocked": False, "key": "K"}, None)})
w.poll_jobs()
@@ -459,16 +510,46 @@ class SmokeTest(unittest.TestCase):
w.dashboard.jobs.clicked.emit()
self.assertIs(w.tabs.currentWidget(), w.jobs_tab)
- def test_ribbon_opens_tabs(self):
- w, _ = self.window()
+ def test_ribbons_per_tab(self):
+ w, remote = self.window()
w.poll_jobs()
- self.assertEqual([a.text() for a in w.ribbon.actions()],
+ self.assertEqual([w.tabs.tabText(i) for i in range(w.tabs.count())],
["Dashboard", "Packages", "Hints", "Results", "Jobs"])
+ self.assertFalse(w.tabs.tabsClosable())
+ self.assertNotIn(("inventory",), remote.calls) # each tab loads on first show
+ expect = {
+ "Dashboard": (["Refresh"], []),
+ "Packages": (["Build", "Update", "Rebuild", "Remove", "Update all", "New hint", "Publish"],
+ ["Select visible", "Clear", "Refresh"]),
+ "Hints": (["Check upstream", "Update selected", "Delete hint"],
+ ["Select visible", "Clear", "No download", "Refresh"]),
+ "Results": (["Show build log"], ["Refresh"]),
+ "Jobs": (["Pull overlay", "Check shadowing", "Run it", "Kill"], []),
+ }
+ for i in range(w.tabs.count()):
+ w.tabs.setCurrentIndex(i)
+ main, view = expect[w.tabs.tabText(i)]
+ self.assertEqual([a.text() for a in w.ribbon.actions()], main)
+ self.assertEqual([a.text() for a in w.view_bar.actions()], view)
+ self.assertEqual(w.view_bar.isVisibleTo(w), bool(view))
+ self.assertEqual(remote.calls.count(("inventory",)), 2) # Packages and Hints, once each
+ w.open_packages()
+ self.assertEqual(remote.calls.count(("inventory",)), 2)
self.assertEqual([m.text() for m in w.menuBar().actions()], ["&File"])
- w.ribbon.actions()[1].trigger()
- self.assertIs(w.tabs.currentWidget(), w.packages_tab)
- w.ribbon.actions()[0].trigger()
- self.assertIs(w.tabs.currentWidget(), w.dashboard)
+
+ def test_disabled_actions_say_why(self):
+ w, _ = self.window()
+ w.poll_jobs()
+ p, h, j = w.open_packages(), w.open_hints(), w.open_jobs()
+ for a, why in ((p.act_build["build"], "tick packages first"),
+ (p.act_new_hint, "none of the ticked packages is without a hint"),
+ (h.act_update, "tick outdated packages after Check upstream"),
+ (h.act_delete, "none of the ticked packages has a hint"),
+ (j.act_run, "select a finished preview"), (j.act_kill, "select a running job")):
+ with self.subTest(action=a.text()):
+ self.assertFalse(a.isEnabled())
+ self.assertTrue(a.toolTip().endswith(why), a.toolTip())
+ self.assertEqual(h.view_actions[0].toolTip(), "Select visible (Ctrl+A)")
def test_hints_tab(self):
w, remote = self.window(**{"job-start": ({"id": "20260101-000001-hint-update"}, None),
@@ -534,6 +615,7 @@ class SmokeTest(unittest.TestCase):
None),
"hint-revert": ({"ok": True}, None),
"job-start": ({"id": "20260101-000001-slackrepo"}, None)})
+ w.mine.add(job["id"])
w.poll_jobs()
remote.answers["jobs"] = ({"jobs": [dict(job, status="failed", end=5, exit=1)]}, None)
w.poll_jobs()
@@ -561,17 +643,17 @@ class SmokeTest(unittest.TestCase):
w.poll_jobs()
tab = w.open_packages()
tab.table.setCurrentIndex(tab.proxy.index(1, 1)) # foo, has a hint
- self.assertFalse(tab.new_hint_btn.isVisibleTo(tab))
- tab.open_hint_btn.click()
+ self.assertTrue(tab.open_hint_btn.isVisibleTo(tab))
+ tab.open_hint_btn.linkActivated.emit("#")
self.assertIs(w.tabs.currentWidget(), w.hints_tab)
- tab.table.setCurrentIndex(tab.proxy.index(3, 1)) # curl, no hint
- self.assertTrue(tab.new_hint_btn.isVisibleTo(tab))
- tab.ask_new_hint = lambda name: None
- tab.new_hint()
- self.assertFalse(any(c[0] == "job-start" for c in remote.calls))
- tab.ask_new_hint = lambda name: ("8.0", True)
- tab.new_hint()
- self.assertIn(("job-start", "hint-new", "curl", "8.0", "--no-dl"), remote.calls)
+ tab.model.checked.update({"foo"})
+ tab.update_count()
+ self.assertFalse(tab.act_new_hint.isEnabled()) # foo has one
+ tab.model.checked.update({"curl", "nuclei"})
+ tab.update_count()
+ tab.act_new_hint.trigger()
+ self.assertIn(("job-start", "hint-new", "curl", "nuclei"), remote.calls)
+ self.assertIs(w.tabs.currentWidget(), w.hints_tab) # starting a job stays put
def test_tab_shortcuts_wrap(self):
w, _ = self.window()
@@ -583,7 +665,7 @@ class SmokeTest(unittest.TestCase):
nxt.activated.emit()
self.assertEqual(w.tabs.currentIndex(), 0) # wrapped from Jobs to Dashboard
prev.activated.emit()
- self.assertEqual(w.tabs.currentIndex(), 2)
+ self.assertEqual(w.tabs.currentIndex(), 4)
def test_results_tab(self):
w, remote = self.window()
@@ -597,10 +679,11 @@ class SmokeTest(unittest.TestCase):
tab.chips["failed"].setChecked(False)
self.assertEqual(tab.visible_paths(), ["games/pcsx2", "development/whisper.cpp", "misc/nuclei"])
tab.table.selectRow(1) # whisper.cpp: no log dir
- self.assertFalse(tab.log_btn.isEnabled())
+ self.assertFalse(tab.act_log.isEnabled())
+ self.assertTrue(tab.act_log.toolTip().endswith("select a result with a log"))
tab.table.selectRow(0)
- self.assertTrue(tab.log_btn.isEnabled())
- tab.show_log()
+ self.assertTrue(tab.act_log.isEnabled())
+ tab.act_log.trigger()
self.assertIn(("build-log", "games/pcsx2"), remote.calls)
viewer = tab.viewers[-1]
self.assertIn("ERROR: make failed", viewer.text.toPlainText()) # ANSI stripped