# sb-builder - self-checks for the GUI # Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Run: python3 -m unittest discover -s tests (headless, no VM needed)""" import os os.environ["QT_QPA_PLATFORM"] = "offscreen" # never the real display, even if the session sets one import importlib.machinery # noqa: E402 import importlib.util # noqa: E402 import sys # noqa: E402 import tempfile # noqa: E402 import time # noqa: E402 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, pyqtSignal # noqa: E402 from PyQt6.QtGui import QColor # noqa: E402 from PyQt6.QtWidgets import QApplication # noqa: E402 from sb_builder import VERSION, config # noqa: E402 from sb_builder import hints as hintlib # noqa: E402 from sb_builder import packages as pk # noqa: E402 from sb_builder.mainwindow import MainWindow # noqa: E402 from sb_builder.remote import DOWN, MISSING, parse # noqa: E402 app = QApplication.instance() or QApplication([]) class ConfigTest(unittest.TestCase): def load(self, text): with tempfile.NamedTemporaryFile("w", suffix=".toml", delete=False) as f: f.write(text) self.addCleanup(os.unlink, f.name) return config.load(f.name) def test_missing_file(self): self.assertEqual(config.load("/nonexistent/config.toml"), (config.DEFAULTS, None)) def test_example_matches_defaults(self): self.assertEqual(config.load(os.path.join(ROOT, "config.example.toml")), (config.DEFAULTS, None)) def test_override(self): cfg, err = self.load("[refresh]\njobs = 2\n") self.assertIsNone(err) self.assertEqual(cfg["refresh"], {"dashboard": 30, "jobs": 2}) def test_unknown_key(self): cfg, err = self.load("[remote]\nbogus = 1\n") self.assertIn("remote.bogus", err) self.assertEqual(cfg, config.DEFAULTS) def test_wrong_type(self): cfg, err = self.load('[vm]\nautostart = "yes"\n') self.assertIn("vm.autostart: expected bool", err) self.assertEqual(cfg, config.DEFAULTS) class FmtTest(unittest.TestCase): def test_italian_date(self): from sb_builder.fmt import fmt_time ts = time.mktime((2026, 3, 7, 9, 5, 0, 0, 0, -1)) self.assertEqual(fmt_time(ts), "07/03/2026 09:05") class ParseTest(unittest.TestCase): def test_cases(self): self.assertEqual(parse(255, b"", b""), (None, DOWN)) self.assertEqual(parse(127, b"", b""), (None, MISSING)) self.assertEqual(parse(0, b'{"a": 1}', b""), ({"a": 1}, None)) self.assertEqual(parse(1, b'{"error": "boom"}', b""), (None, "boom")) self.assertEqual(parse(1, b"", b"bash: x\n"), (None, "bash: x")) class VersionTest(unittest.TestCase): def test_gui_matches_helper(self): script = os.path.join(ROOT, "remote", "sbb-remote") loader = importlib.machinery.SourceFileLoader("sbb_remote", script) sbb = importlib.util.module_from_spec(importlib.util.spec_from_loader("sbb_remote", loader)) loader.exec_module(sbb) self.assertEqual(sbb.VERSION, VERSION) class FakeRemote: def __init__(self, answers): self.answers = answers self.calls = [] self.virsh_answer = (1, "") def call(self, cb, *args, stdin=None): self.calls.append(args if stdin is None else (*args, stdin)) answer = self.answers.get(args[0], (None, DOWN)) cb(*(answer(args, stdin) if callable(answer) else answer)) def virsh(self, cb, *args): self.calls.append(("virsh", *args)) cb(*self.virsh_answer) def follow(self, jid, offset): p = QProcess(app) # parented, like Remote's, so it outlives this call p.start("true", []) return p class FakeNotifier(QObject): clicked = pyqtSignal(str) def __init__(self): super().__init__() self.sent = [] def send(self, title, body, jid=""): self.sent.append((title, body, jid)) STATS = {"load": [0.5, 0.4, 0.3], "uptime": 3600.0, "cpu": 85.0, "mem": {"total": 16 * 2**30, "available": 12 * 2**30}, "disk": {"total": 100 * 2**30, "free": 10 * 2**30}, "slackpkg": {"update": 1791760212, "upgrade": None}, "packages": {"total": 10, "personal": 2, "pentesting": 1, "shadowed": 1}, "sync": {"personal": None, "pentesting": 1792351223}, "slackrepo": None} JOB = {"id": "20260101-000000-sbshadow", "kind": "sbshadow", "args": ["--no"], "argv": [], "status": "running", "created": 0, "start": 0, "pid": 5} def inv_row(path, version="1.0", built=None, result=None, hints=None, requires=(), summary=None, maintainer="Some One", info_version=None): return [path, version, info_version or version, None, built, 1788978222 if built else None, result, maintainer, list(requires), summary, "/h/x.hint" if hints else None, hints] COLS = ["path", "version", "info_version", "hint_version", "built_version", "built_time", "result", "maintainer", "requires", "summary", "hint_path", "hints"] INVENTORY = {"columns": COLS, "rows": [ inv_row("misc/nuclei", "3.11.1", built="3.11.0", result="ok", summary="Fast scanner", requires=["go"]), inv_row("personal/foo", built="1.0", result="ok", hints={"DELREQUIRES": "x"}, requires=["x", "y"]), inv_row("pentesting/nmap", built="7.9", result="bad"), inv_row("network/curl"), ]} RESULTS = {"rows": [["games/pcsx2", "failed", 1789992366, None, True], # newest first, as the helper ["development/whisper.cpp", "aborted", 1789753147, None, False], ["misc/nuclei", "ok", 1788978222, "3.11.0", True]]} PREVIEW = {"id": "20260101-000000-slackrepo", "kind": "slackrepo", "status": "ok", "args": ["update", "--preview", "nuclei"], "argv": [], "created": 0, "start": 0, "end": 1} class HintsTest(unittest.TestCase): def view(self, hints, requires=(), info_version="1.0"): return hintlib.interpret(hints, "pkg", info_version, requires) def section(self, view, title): return dict(view["sections"]).get(title) def test_no_hint(self): v = self.view(None, requires=["a"]) self.assertEqual((v["skip"], v["sections"], v["deps"], v["ignored"]), (None, [], [("a", "")], [])) def test_version_arch_and_forced_checksum_skip(self): v = self.view({"VERSION": "2.0", "ARCH": "x86_64", "DOWNLOAD": "http://a/src.tgz", "DOWNLOAD_x86_64": "http://a/bin-x64.tgz http://a/extra.tgz"}) self.assertEqual(self.section(v, "Version override"), [ ("version", "1.0 \u2192 2.0"), ("arch", "x86_64"), ("download", "http://a/bin-x64.tgz"), ("download", "http://a/extra.tgz"), ("md5sum", "not checked")]) def test_arch_variant_empty_falls_back(self): v = self.view({"ARCH": "x86_64", "DOWNLOAD": "http://a/s", "MD5SUM": "abc", "MD5SUM_x86_64": ""}) self.assertEqual(self.section(v, "Version override")[1:], [("download", "http://a/s"), ("md5sum", "abc")]) def test_no_download_forms(self): for hints in ({"DOWNLOAD": "no"}, {"NODOWNLOAD": "yes"}): with self.subTest(hints=hints): self.assertEqual(self.section(self.view(hints), "Version override"), [("download", "no download, the source must already be in the cache")]) self.assertEqual(self.view(hints)["ignored"], []) def test_dependencies(self): v = self.view({"DELREQUIRES": "b %README%", "ADDREQUIRES": "z", "BUILDTIME": "cmake", "PRAGMA": "python3 need_net", "CONFLICTS": "old"}, requires=["a", "b", "%README%"]) self.assertEqual(v["deps"], [("a", ""), ("b", "del"), ("%README%", "del"), ("python3", "pragma"), ("z", "add"), ("cmake", "build")]) self.assertEqual(v["conflicts"], ["old"]) self.assertEqual(self.section(v, "Pragmas")[1], ("need_net", "network not blocked during the build")) def test_skip_forms(self): self.assertIsNone(self.view({"SKIP": "no"})["skip"]) self.assertEqual(self.view({"SKIP": "yes"})["skip"], "always skipped") self.assertEqual(self.view({"SKIP": "remove nc first"})["skip"], "remove nc first") def test_build_settings(self): v = self.view({"OPTIONS": "A=yes B=no", "NUMJOBS": "-j1", "INSTALL": "N", "ANSWER": "ok\\nyes", "NOWARNINGS": "Nonstandard"}) self.assertEqual(self.section(v, "Build settings"), [ ("option", "A=yes"), ("option", "B=no"), ("make jobs", "-j1"), ("install after build", "no"), ("answers to the build", "ok \u23ce yes"), ("hide warnings matching", "Nonstandard")]) self.assertEqual(dict(self.section(self.view({"INSTALL": "1"}), "Build settings")), {"install after build": "yes"}) def test_users_and_groups(self): v = self.view({"GROUPADD": "209:postgres 300", "USERADD": "209:postgres:-d/var/lib/pgsql:-r"}) self.assertEqual(self.section(v, "Host changes"), [ ("group", "postgres (gid 209)"), ("group", "pkg (gid 300)"), ("user", "postgres (uid 209), group postgres, home /var/lib/pgsql, shell /bin/false, -r")]) def test_pragma_forms(self): self.assertEqual(hintlib.pragma_meaning("x86arch=i686"), "on 32 bit x86, build with ARCH=i686") self.assertEqual(hintlib.pragma_meaning("unset_DISTCC_HOSTS"), "unset DISTCC_HOSTS for the build") self.assertEqual(hintlib.pragma_meaning("bogus"), "unknown pragma") def test_ignored_and_shell(self): v = self.view({"ANSWERS": "x", "FOO": "1", "DOWNLOAD_arm": "u", "OPTIONS": {"value": '"X=$(uname -m)"', "shell": True}}) self.assertEqual(v["ignored"], ["ANSWERS", "FOO"]) self.assertEqual(v["shell"], ["OPTIONS"]) def test_cleanup(self): self.assertEqual(self.section(self.view({"CLEANUP": "depmod -a"}), "On uninstall"), [("run", "depmod -a")]) class FilterTest(unittest.TestCase): def setUp(self): self.rows = {r["name"]: r for r in pk.load_rows(INVENTORY)} def test_status(self): self.assertEqual({n: r["status"] for n, r in self.rows.items()}, {"nuclei": "outdated", "foo": "ok", "nmap": "failed", "curl": "never"}) def test_query(self): self.assertEqual(pk.parse_query("Nuc cat:misc bogus:x status:"), [(None, "nuc"), ("category", "misc"), (None, "bogus:x"), (None, "status:")]) def names(self, text="", overlays=(), statuses=()): terms = pk.parse_query(text) return sorted(n for n, r in self.rows.items() if pk.matches(r, terms, overlays, statuses)) def test_matching(self): self.assertEqual(self.names("scanner"), ["nuclei"]) self.assertEqual(self.names("req:go"), ["nuclei"]) self.assertEqual(self.names("maint:some status:o"), ["foo", "nuclei"]) self.assertEqual(self.names(overlays={"personal", "pentesting"}), ["foo", "nmap"]) self.assertEqual(self.names(overlays={"upstream"}, statuses={"never", "failed"}), ["curl"]) self.assertEqual(self.names("cat:misc", statuses={"ok"}), []) class MeterTest(unittest.TestCase): def test_color_thresholds(self): from sb_builder.dashboard import STATUS, meter_color accent = QColor("#123456") self.assertEqual(meter_color(79.9, accent), accent) self.assertEqual(meter_color(80, accent), QColor(STATUS["aborted"])) self.assertEqual(meter_color(95, accent), QColor(STATUS["failed"])) def test_sparkline_cap_and_nulls(self): from sb_builder.dashboard import Meter m = Meter("CPU", spark=True) for i in range(100): m.set(float(i), "") self.assertEqual(len(m.spark.points), m.spark.points.maxlen) self.assertEqual(m.spark.points[-1], 99.0) m.set(None, "") self.assertEqual(m.value.text(), "n/a") class UpgradeDotTest(unittest.TestCase): def test_thresholds(self): from sb_builder.dashboard import STATUS, upgrade_color now = 1_800_000_000 day = 86400 self.assertEqual(upgrade_color(now - day + 60, now), STATUS["ok"]) self.assertEqual(upgrade_color(now - day - 60, now), STATUS["aborted"]) self.assertEqual(upgrade_color(now - 2 * day - 60, now), STATUS["failed"]) self.assertIsNone(upgrade_color(None, now)) class SmokeTest(unittest.TestCase): def window(self, **answers): remote = FakeRemote({"version": ({"version": VERSION, "overlays": ["personal"]}, None), "stats": (STATS, None), "jobs": ({"jobs": []}, None), "gpg-status": ({"unlocked": True, "key": "K"}, None), "inventory": (INVENTORY, None), "results": (RESULTS, None), "build-log": ({"name": "update.log", "build": "\x1b[1mERROR\x1b[0m: make failed\n", "truncated": True, "mtime": 0, "resource": "peak memory 3308M\n"}, None), **answers}) w = MainWindow(config.load("/nonexistent")[0], remote, FakeNotifier()) self.addCleanup(w.deleteLater) self.addCleanup(lambda: w.jobs_tab and w.jobs_tab.stop()) return w, remote def test_tabs_and_dashboard(self): w, _ = self.window() w.poll_jobs() self.assertEqual(w.conn.text(), "● connected") self.assertEqual(w.tabs.tabText(0), "Dashboard") tiles = w.dashboard.tiles self.assertEqual(tiles["Packages"].value.text(), "10") self.assertEqual(tiles["Packages"].detail.text(), "2\u00a0personal · 1\u00a0pentesting · 1\u00a0shadowed") self.assertEqual(tiles["Built"].value.text(), "n/a") d = w.dashboard self.assertEqual((d.cpu.value.text(), d.ram.value.text(), d.disk.value.text()), ("85%", "25%", "90%")) self.assertIn("load 0.50", d.cpu.detail.text()) self.assertEqual(d.disk.detail.text(), "10.0 GiB free of 100.0 GiB") self.assertEqual(tiles["slackpkg"].value.text(), "n/a") # no upgrade time: no dot self.assertIn("lists updated", tiles["slackpkg"].detail.text()) self.assertEqual(len(d.cpu.spark.points), 1) 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)") def test_end_notification_once(self): w, remote = self.window(jobs=({"jobs": [JOB]}, None)) w.poll_jobs() remote.answers["jobs"] = ({"jobs": [dict(JOB, status="failed", end=5, exit=1)]}, None) w.poll_jobs() w.poll_jobs() self.assertEqual(w.notifier.sent, [("sb-builder: failed", "sbshadow --no", JOB["id"])]) def test_down_offers_vm_start(self): w, remote = self.window(jobs=(None, DOWN)) remote.virsh_answer = (0, "shut off") w.poll_jobs() self.assertIn(("virsh", "domstate", "buildsystem"), remote.calls) self.assertEqual(w.conn.text(), "● VM off") self.assertFalse(w.vm_btn.isHidden()) def test_slackpkg_dot(self): old = dict(STATS, slackpkg={"update": None, "upgrade": 1}) w, _ = self.window(stats=(old, None)) w.poll_jobs() text = w.dashboard.tiles["slackpkg"].value.text() self.assertIn("#d03b3b", text) self.assertTrue(text.endswith(" ago")) def test_results_bar(self): w, _ = self.window(stats=(dict(STATS, slackrepo={"results": {"failed": 1, "ok": 3, "removed": 1}, "built": 4}), None)) w.poll_jobs() self.assertEqual([(label, n) for label, n, _ in w.dashboard.bar.parts], [("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() tab = w.open_packages() self.assertEqual(tab.proxy.rowCount(), 4) tab.chips["failed"].setChecked(True) self.assertEqual(tab.proxy.rowCount(), 1) tab.chips["failed"].setChecked(False) started = [] tab.start_requested.connect(started.append) tab.act("update") self.assertEqual(started, []) # nothing checked, no current row tab.table.setCurrentIndex(tab.proxy.index(1, 1)) self.assertIn("x", tab.detail.text()) tab.act("update") tab.model.checked.update({"nuclei", "curl"}) tab.act("build") tab.update_all() self.assertEqual(started, [["slackrepo", "update", "--preview", "foo"], ["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)}) w.poll_jobs() jobs = w.open_jobs() jobs.select(PREVIEW["id"]) self.assertTrue(jobs.run_btn.isVisibleTo(jobs)) answers = iter(["wrong", "right"]) jobs.ask_passphrase = lambda retry: next(answers) jobs.publish_box.setChecked(False) jobs.run_preview() 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")) 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() jobs = w.open_jobs() jobs.select(PREVIEW["id"]) jobs.ask_passphrase = lambda retry: None jobs.run_preview() self.assertFalse(any(c[0] == "job-start" for c in remote.calls)) def test_key_indicator_and_tile_links(self): w, _ = self.window(**{"gpg-status": ({"unlocked": False, "key": "K"}, None)}) w.poll_jobs() self.assertEqual(w.key.text(), "🔒 key locked") w.dashboard.tiles["Packages"].clicked.emit() self.assertIs(w.tabs.currentWidget(), w.packages_tab) w.dashboard.jobs.clicked.emit() self.assertIs(w.tabs.currentWidget(), w.jobs_tab) def test_ribbon_opens_tabs(self): w, _ = self.window() w.poll_jobs() self.assertEqual([a.text() for a in w.ribbon.actions()], ["Dashboard", "Packages", "Hints", "Results", "Jobs"]) 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_hints_tab(self): w, remote = self.window(**{"job-start": ({"id": "20260101-000001-hint-update"}, None), "hint-delete": ({"ok": True}, None)}) w.poll_jobs() tab = w.open_hints() with tempfile.NamedTemporaryFile("w", suffix=".toml", delete=False) as f: f.write('[nuclei]\nsource = "manual"\n[curl]\nsource = "manual"\n') self.addCleanup(os.unlink, f.name) tab.stanzas = f.name tab.reload() self.assertEqual(sorted(r["name"] for r in tab.rows), ["curl", "foo", "nuclei"]) # foo: hint only states = {tab.table.item(i, 1).text(): tab.table.item(i, 5).text() for i in range(3)} self.assertEqual(states, {"curl": "not checked", "foo": "no stanza", "nuclei": "not checked"}) tab._checked({"nuclei": "3.12.0", "curl": "1.0"}, {}, "") self.assertEqual(tab.checked(), ["nuclei"]) # outdated rows come checked tab.chips["current"].setChecked(True) visible = [tab.table.item(i, 1).text() for i in range(3) if not tab.table.isRowHidden(i)] self.assertEqual(visible, ["curl"]) tab.no_dl.setChecked(True) tab.update_selected() self.assertIn(("job-start", "hint-update", "nuclei=3.12.0", "--no-dl"), remote.calls) tab._checked({}, {"curl": "HTTPError(404)"}, "") row = next(i for i in range(3) if tab.table.item(i, 1).text() == "curl") self.assertEqual(tab.table.item(row, 5).toolTip(), "HTTPError(404)") self.assertIs(w.open_hints("foo"), tab) self.assertEqual(tab.current(), "foo") def test_hint_job_end_opens_diff(self): job = dict(JOB, id="20260101-000000-hint-update", kind="hint-update", args=["nuclei=3.12.0"]) w, remote = self.window(jobs=({"jobs": [job]}, None), **{"job-result": ({"ok": ["nuclei"], "failed": [["curl", "HTTP 404"]]}, None), "hint-diff": ({"bak": 'VERSION="3.11.1"\n', "current": 'VERSION="3.12.0"\n'}, None), "hint-revert": ({"ok": True}, None), "job-start": ({"id": "20260101-000001-slackrepo"}, None)}) w.poll_jobs() remote.answers["jobs"] = ({"jobs": [dict(job, status="failed", end=5, exit=1)]}, None) w.poll_jobs() self.assertEqual(len(w.hint_dialogs), 1) d = w.hint_dialogs[0] self.assertIn('-VERSION="3.11.1"\n+VERSION="3.12.0"', d.diff.toPlainText()) d.revert() d.revert() # a second revert must not reach the helper: it would delete the hint self.assertEqual([c for c in remote.calls if c[0] == "hint-revert"], [("hint-revert", "nuclei")]) self.assertEqual(d.list.item(0).text(), "nuclei (reverted)") d.build() # nothing left to build self.assertFalse(any(c[0] == "job-start" for c in remote.calls)) self.assertEqual(w.hint_dialogs, []) w.show_hint_result({"ok": ["nuclei"], "failed": []}) w.hint_dialogs[0].build() self.assertIn(("job-start", "slackrepo", "update", "--preview", "nuclei"), remote.calls) def test_packages_new_hint(self): w, remote = self.window(**{"job-start": ({"id": "20260101-000001-hint-new"}, None)}) 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.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) def test_tab_shortcuts_wrap(self): w, _ = self.window() w.poll_jobs() w.open_packages() w.open_jobs() nxt, prev = w.tab_next, w.tab_prev self.assertEqual((nxt.key().toString(), prev.key().toString()), ("Ctrl+PgDown", "Ctrl+PgUp")) nxt.activated.emit() self.assertEqual(w.tabs.currentIndex(), 0) # wrapped from Jobs to Dashboard prev.activated.emit() self.assertEqual(w.tabs.currentIndex(), 2) def test_results_tab(self): w, remote = self.window() w.poll_jobs() tab = w.open_results() tab.table.selectRow(2) # nuclei, ok tab.filter_to("failed") self.assertEqual(tab.detail.text(), "") # hidden by the filter: no stale detail self.assertIs(w.tabs.currentWidget(), tab) self.assertEqual(tab.visible_paths(), ["games/pcsx2"]) 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()) tab.table.selectRow(0) self.assertTrue(tab.log_btn.isEnabled()) tab.show_log() self.assertIn(("build-log", "games/pcsx2"), remote.calls) viewer = tab.viewers[-1] self.assertIn("ERROR: make failed", viewer.text.toPlainText()) # ANSI stripped self.assertIn("peak memory", viewer.text.toPlainText()) self.assertIn("truncated", viewer.text.toPlainText()) self.assertTrue(viewer.text.toPlainText().endswith("make failed\n")) # failure stays last self.assertIn("update.log", viewer.windowTitle()) viewer.close() def test_dashboard_results_clicks(self): w, _ = self.window(stats=(dict(STATS, slackrepo={"results": {"ok": 3, "failed": 1}, "built": 3}), None)) w.poll_jobs() w.dashboard.legend.linkActivated.emit("failed") self.assertIs(w.tabs.currentWidget(), w.results_tab) self.assertEqual(w.results_tab.visible_paths(), ["games/pcsx2"]) bar = w.dashboard.bar bar.resize(400, 14) hit = bar.label_at(QPointF(5, 5)) self.assertEqual(hit, "ok") def test_log_carriage_return(self): w, _ = self.window() jobs = w.open_jobs() jobs.append_log("10%\r50%\r100%\ndone\r\n") self.assertEqual(jobs.log.toPlainText(), "100%\ndone\n") if __name__ == "__main__": unittest.main()