From aa92ed30b33a2ad1a0fea99e883dd1ff61823a1a Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sun, 4 Oct 2026 19:28:37 +0200 Subject: [PATCH] refactor(installer): extract run_command helper to cut cognitive complexity Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + guided_installer.py | 27 ++++++++++++++++----------- tests/test_guided_installer.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 tests/test_guided_installer.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a022ae6..ebaf821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ยท versioned wi - **`npm run qa`**: central QA command (`scripts/qa.mjs`) with timed stages (lint, python/node unit tests, Playwright, security scans, bundle freshness) and an opt-in SonarQube stage (`--sonar`, token from the environment only). See `architecture/qa.md`. ### Changed +- `guided_installer.py`: command execution extracted into a testable `run_command` helper (lowers cognitive complexity, python:S3776); no behaviour change. - **Voice list rows no longer show the "Source unknown" badge** (it crowded the play buttons). The badge stays in the voice inspector, next to the Source & consent panel. ### Fixed diff --git a/guided_installer.py b/guided_installer.py index b50734e..c3982df 100755 --- a/guided_installer.py +++ b/guided_installer.py @@ -46,6 +46,21 @@ MODEL_EXPLANATIONS = { } +def run_command(cmd: list[str], log) -> bool: + """Run one command, log its output; return False if it failed (non-zero exit or exception).""" + try: + proc = subprocess.run(cmd, cwd=ROOT, text=True, capture_output=True) + except Exception as exc: + log(f"Failed: {exc}") + return False + for out in (proc.stdout, proc.stderr): + if out: + log(out) + if proc.returncode: + log(f"Command exited with {proc.returncode}") + return not proc.returncode + + class GuidedInstaller(tk.Tk): def __init__(self) -> None: super().__init__() @@ -192,17 +207,7 @@ class GuidedInstaller(tk.Tk): self.log_line(f"\n== {title} ==") for cmd in commands: self.log_line("$ " + " ".join(cmd)) - try: - proc = subprocess.run(cmd, cwd=ROOT, text=True, capture_output=True) - if proc.stdout: - self.log_line(proc.stdout) - if proc.stderr: - self.log_line(proc.stderr) - if proc.returncode: - self.log_line(f"Command exited with {proc.returncode}") - break - except Exception as exc: - self.log_line(f"Failed: {exc}") + if not run_command(cmd, self.log_line): break if cleanup_dir: shutil.rmtree(cleanup_dir, ignore_errors=True) diff --git a/tests/test_guided_installer.py b/tests/test_guided_installer.py new file mode 100644 index 0000000..23c2b92 --- /dev/null +++ b/tests/test_guided_installer.py @@ -0,0 +1,31 @@ +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +try: + from guided_installer import run_command +except ImportError: # no tkinter on this host + run_command = None + + +@unittest.skipIf(run_command is None, "tkinter unavailable") +class RunCommandTest(unittest.TestCase): + def test_success_logs_output(self): + out = [] + self.assertTrue(run_command([sys.executable, "-c", "print('hi')"], out.append)) + self.assertIn("hi", "".join(out)) + + def test_nonzero_exit(self): + out = [] + self.assertFalse(run_command([sys.executable, "-c", "raise SystemExit(3)"], out.append)) + self.assertIn("exited with 3", out[-1]) + + def test_missing_binary(self): + out = [] + self.assertFalse(run_command(["/nonexistent-binary-xyz"], out.append)) + self.assertTrue(out[0].startswith("Failed:")) + + +if __name__ == "__main__": + unittest.main()