import os import subprocess import sys import tempfile import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] SRC_DIR = REPO_ROOT / "src" GNOME_STUB = '#!/bin/sh\necho "$@" >> "$GNOME_LOG"\nexit 0\n' GNOME_STUB_FAILING = "#!/bin/sh\nexit 3\n" EXTENSION_MAKEFILE = "install:\n\ttouch $$MARKER\n" def git(*args, cwd): """Run git with a deterministic identity.""" subprocess.run( [ "git", "-c", "user.email=test@example.com", "-c", "user.name=Test", *args, ], cwd=cwd, check=True, capture_output=True, ) def make_source_repo(path, with_makefile): """Create a git repository holding a minimal GNOME extension. Args: path: Directory the repository is created in. with_makefile: Add an install target writing the $MARKER file. """ path.mkdir(parents=True) (path / "metadata.json").write_text('{"uuid": "ext@example"}') if with_makefile: (path / "Makefile").write_text(EXTENSION_MAKEFILE) git("init", "-b", "main", cwd=path) git("add", "-A", cwd=path) git("commit", "-m", "initial", cwd=path) return path def write_stub(directory, body): """Write an executable gnome-extensions stub into directory.""" path = directory / "gnome-extensions" path.write_text(body) path.chmod(0o755) return path def run_cli(*args, stub_dir=None, gnome_log=None, marker=None, isolated_path=False): """Run 'python -m gnome_extension_manager' with a stubbed gnome-extensions. Args: args: Command line arguments for the CLI. stub_dir: Directory holding the gnome-extensions stub. gnome_log: File the stub appends its arguments to. marker: File the extension Makefile touches on install. isolated_path: Use only stub_dir as PATH, hiding the real binaries. """ env = dict(os.environ) env["PYTHONPATH"] = str(SRC_DIR) if isolated_path: env["PATH"] = str(stub_dir) elif stub_dir: env["PATH"] = f"{stub_dir}{os.pathsep}{env['PATH']}" if gnome_log: env["GNOME_LOG"] = str(gnome_log) if marker: env["MARKER"] = str(marker) return subprocess.run( [sys.executable, "-m", "gnome_extension_manager", *args], capture_output=True, text=True, env=env, check=False, ) class TestCli(unittest.TestCase): def test_help_exits_zero(self): result = run_cli("--help") self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("GNOME shell extensions", result.stdout) def test_unknown_action_is_rejected(self): result = run_cli("restart", "ext@example") self.assertEqual(result.returncode, 2) self.assertIn("invalid choice", result.stderr) def test_disable_calls_gnome_extensions(self): with tempfile.TemporaryDirectory() as base: base_path = Path(base) stub_dir = base_path / "bin" stub_dir.mkdir() write_stub(stub_dir, GNOME_STUB) gnome_log = base_path / "calls.log" result = run_cli( "disable", "ext@example", stub_dir=stub_dir, gnome_log=gnome_log, ) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(gnome_log.read_text().strip(), "disable ext@example") def test_enable_clones_builds_and_enables(self): with tempfile.TemporaryDirectory() as base: base_path = Path(base) stub_dir = base_path / "bin" stub_dir.mkdir() write_stub(stub_dir, GNOME_STUB) gnome_log = base_path / "calls.log" marker = base_path / "built.marker" source = make_source_repo(base_path / "source", with_makefile=True) extensions_dir = base_path / "extensions" result = run_cli( "enable", "ext@example", str(source), "--extensions-dir", str(extensions_dir), stub_dir=stub_dir, gnome_log=gnome_log, marker=marker, ) self.assertEqual(result.returncode, 0, result.stderr) self.assertTrue(marker.exists(), "make install did not run") self.assertEqual(gnome_log.read_text().strip(), "enable ext@example") self.assertIn("Installation complete.", result.stdout) def test_extension_without_makefile_stays_installed(self): with tempfile.TemporaryDirectory() as base: base_path = Path(base) stub_dir = base_path / "bin" stub_dir.mkdir() write_stub(stub_dir, GNOME_STUB) gnome_log = base_path / "calls.log" source = make_source_repo(base_path / "source", with_makefile=False) extensions_dir = base_path / "extensions" result = run_cli( "enable", "ext@example", str(source), "--extensions-dir", str(extensions_dir), stub_dir=stub_dir, gnome_log=gnome_log, ) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("No Makefile found", result.stdout) self.assertTrue((extensions_dir / "ext@example" / "metadata.json").exists()) def test_second_run_pulls_instead_of_cloning(self): with tempfile.TemporaryDirectory() as base: base_path = Path(base) stub_dir = base_path / "bin" stub_dir.mkdir() write_stub(stub_dir, GNOME_STUB) gnome_log = base_path / "calls.log" source = make_source_repo(base_path / "source", with_makefile=False) extensions_dir = base_path / "extensions" arguments = ( "enable", "ext@example", str(source), "--extensions-dir", str(extensions_dir), ) run_cli(*arguments, stub_dir=stub_dir, gnome_log=gnome_log) result = run_cli(*arguments, stub_dir=stub_dir, gnome_log=gnome_log) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("Pulling changes from git...", result.stdout) def test_missing_gnome_extensions_fails_cleanly(self): with tempfile.TemporaryDirectory() as base: empty = Path(base) / "empty" empty.mkdir() result = run_cli( "disable", "ext@example", stub_dir=empty, isolated_path=True ) self.assertEqual(result.returncode, 127) self.assertIn( "required command 'gnome-extensions' is not installed", result.stderr ) self.assertNotIn("Traceback", result.stderr) def test_failing_gnome_extensions_propagates_the_exit_code(self): with tempfile.TemporaryDirectory() as base: base_path = Path(base) stub_dir = base_path / "bin" stub_dir.mkdir() write_stub(stub_dir, GNOME_STUB_FAILING) result = run_cli("disable", "ext@example", stub_dir=stub_dir) self.assertEqual(result.returncode, 3) if __name__ == "__main__": unittest.main()