diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_gui.py | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/tests/test_gui.py b/tests/test_gui.py new file mode 100644 index 0000000..95f9afb --- /dev/null +++ b/tests/test_gui.py @@ -0,0 +1,167 @@ +# sb-builder - self-checks for the GUI +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# +# 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.setdefault("QT_QPA_PLATFORM", "offscreen") + +import importlib.machinery # noqa: E402 +import importlib.util # noqa: E402 +import sys # noqa: E402 +import tempfile # 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 QObject, pyqtSignal # noqa: E402 +from PyQt6.QtWidgets import QApplication # noqa: E402 + +from sb_builder import VERSION, config # 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 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): + self.calls.append(args) + cb(*self.answers.get(args[0], (None, DOWN))) + + def virsh(self, cb, *args): + self.calls.append(("virsh", *args)) + cb(*self.virsh_answer) + + +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, + "disk_free": {"repo": 10 * 2**30, "state": 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} + + +class SmokeTest(unittest.TestCase): + def window(self, **answers): + remote = FakeRemote({"version": ({"version": VERSION, "overlays": ["personal"]}, None), + "stats": (STATS, None), "jobs": ({"jobs": []}, None), **answers}) + w = MainWindow(config.load("/nonexistent")[0], remote, FakeNotifier()) + self.addCleanup(w.deleteLater) + 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") + self.assertIn("total: 10", w.dashboard.labels["Repo"].text()) + self.assertEqual(w.dashboard.labels["slackrepo"].text(), "n/a") + self.assertIn("10.0 GiB", w.dashboard.labels["VM"].text()) + 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_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() |
