mirror of
https://github.com/kevinveenbirkenbach/cli-gnome-extension-manager.git
synced 2026-08-16 19:52:49 +00:00
Replaces main.sh with the 'cli-gnome-extension-manager' distribution in a src layout, shipping both the cli-gnome-extension-manager and the older goexma command. sync_repository keeps the three states of the shell original apart: clone when the folder is missing, pull when it is a checkout, leave it alone otherwise, so a manually installed extension is never overwritten. build_extension still moves the checkout away before make install, but into a TemporaryDirectory instead of a fixed /tmp/<name>, which removes the collision between parallel runs and cleans up on abort. Every step passes through the exit code of the failing tool, replacing the shell chain of || exit 1; a missing gnome-extensions aborts with exit 127. The e2e suite builds a real git repository, clones it with real git and runs real make install. Adds --extensions-dir, unit, integration and container-based e2e tests, ruff and markdown/mermaid linting, CodeQL and Dependabot, and pins the core metadata to 2.4 so twine accepts the artifacts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
223 lines
7.3 KiB
Python
223 lines
7.3 KiB
Python
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()
|