# 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.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()