chore: format test files
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 19s
Python / lint (pull_request) Successful in 32s
Python / fmt (pull_request) Failing after 37s
Python / test (pull_request) Successful in 54s
C++ / build (pull_request) Successful in 1m57s
Rust / fmt (pull_request) Successful in 1m16s
Rust / build (pull_request) Successful in 1m56s
Python / buildcheck (pull_request) Successful in 2m44s
Rust / clippy (pull_request) Successful in 1m56s
C++ / clang-tidy (pull_request) Successful in 3m46s

This commit is contained in:
2026-07-08 17:01:25 +02:00
parent 9300da258e
commit 0ae6d6fc55
2 changed files with 283 additions and 214 deletions
+149 -121
View File
@@ -1,167 +1,195 @@
from __future__ import annotations from __future__ import annotations
import pytest
from pathlib import Path from pathlib import Path
import pytest
from zshell.utils import schemepalettes as sp from zshell.utils import schemepalettes as sp
@pytest.fixture @pytest.fixture
def tmp_schemes(tmp_path: Path) -> Path: def tmp_schemes(tmp_path: Path) -> Path:
schemes = tmp_path / "schemes" schemes = tmp_path / "schemes"
schemes.mkdir() schemes.mkdir()
gmedium = schemes / "gruvbox" / "medium" gmedium = schemes / "gruvbox" / "medium"
gmedium.mkdir(parents=True) gmedium.mkdir(parents=True)
(gmedium / "dark.txt").write_text("background 101415\nonBackground e0e3e4\nprimary 81d3e0\nsurface 1c2021\n") (gmedium / "dark.txt").write_text(
(gmedium / "light.txt").write_text("background fbf1c7\nonBackground 3c3836\nprimary 6b5f10\nsurface fbf1c7\n") "background 101415\nonBackground e0e3e4\nprimary 81d3e0\nsurface 1c2021\n"
)
(gmedium / "light.txt").write_text(
"background fbf1c7\nonBackground 3c3836\nprimary 6b5f10\nsurface fbf1c7\n"
)
ghard = schemes / "gruvbox" / "hard" ghard = schemes / "gruvbox" / "hard"
ghard.mkdir(parents=True) ghard.mkdir(parents=True)
(ghard / "dark.txt").write_text("background 0b0d0e\nprimary 81d3e0\n") (ghard / "dark.txt").write_text("background 0b0d0e\nprimary 81d3e0\n")
cmocha = schemes / "catppuccin" / "mocha" cmocha = schemes / "catppuccin" / "mocha"
cmocha.mkdir(parents=True) cmocha.mkdir(parents=True)
(cmocha / "dark.txt").write_text("background 1e1e2e\nprimary cba6f7\nsecondary 756294\nsurface 313244\n") (cmocha / "dark.txt").write_text(
(cmocha / "mauve-dark.txt").write_text("background 1e1e2e\nprimary cba6f7\nsecondary 756294\nsurface 313244\n") "background 1e1e2e\nprimary cba6f7\nsecondary 756294\nsurface 313244\n"
(cmocha / "green-dark.txt").write_text("background 1e1e2e\nprimary a6e3a1\nsecondary 5b8964\nsurface 313244\n") )
(cmocha / "mauve-dark.txt").write_text(
"background 1e1e2e\nprimary cba6f7\nsecondary 756294\nsurface 313244\n"
)
(cmocha / "green-dark.txt").write_text(
"background 1e1e2e\nprimary a6e3a1\nsecondary 5b8964\nsurface 313244\n"
)
clatte = schemes / "catppuccin" / "latte" clatte = schemes / "catppuccin" / "latte"
clatte.mkdir(parents=True) clatte.mkdir(parents=True)
(clatte / "light.txt").write_text("background eff1f5\nprimary 8839ef\nsecondary c2b8d0\nsurface ccd0da\n") (clatte / "light.txt").write_text(
(clatte / "mauve-light.txt").write_text("background eff1f5\nprimary 8839ef\nsecondary c2b8d0\nsurface ccd0da\n") "background eff1f5\nprimary 8839ef\nsecondary c2b8d0\nsurface ccd0da\n"
)
(clatte / "mauve-light.txt").write_text(
"background eff1f5\nprimary 8839ef\nsecondary c2b8d0\nsurface ccd0da\n"
)
cextra = schemes / "extra" / "default" cextra = schemes / "extra" / "default"
cextra.mkdir(parents=True) cextra.mkdir(parents=True)
(cextra / "dark.txt").write_text( (cextra / "dark.txt").write_text(
"# this is a comment\n\nbackground 000000\nprimary ffffff\n\n # indented comment \n secondary cccccc\n" "# this is a comment\n\nbackground 000000\nprimary ffffff\n\n # indented comment \n secondary cccccc\n"
) )
return schemes return schemes
class TestParseTxt: class TestParseTxt:
def test_basic(self, tmp_schemes): def test_basic(self, tmp_schemes):
path = tmp_schemes / "gruvbox" / "medium" / "dark.txt" path = tmp_schemes / "gruvbox" / "medium" / "dark.txt"
colors = sp._parse_txt(path) colors = sp._parse_txt(path)
assert colors["background"] == "#101415" assert colors["background"] == "#101415"
assert colors["primary"] == "#81d3e0" assert colors["primary"] == "#81d3e0"
assert colors["surface"] == "#1c2021" assert colors["surface"] == "#1c2021"
def test_adds_hash_prefix(self, tmp_schemes): def test_adds_hash_prefix(self, tmp_schemes):
path = tmp_schemes / "gruvbox" / "medium" / "dark.txt" path = tmp_schemes / "gruvbox" / "medium" / "dark.txt"
colors = sp._parse_txt(path) colors = sp._parse_txt(path)
for v in colors.values(): for v in colors.values():
assert v.startswith("#"), f"value {v!r} missing # prefix" assert v.startswith("#"), f"value {v!r} missing # prefix"
def test_skips_comments_and_empty_lines(self, tmp_schemes): def test_skips_comments_and_empty_lines(self, tmp_schemes):
path = tmp_schemes / "extra" / "default" / "dark.txt" path = tmp_schemes / "extra" / "default" / "dark.txt"
colors = sp._parse_txt(path) colors = sp._parse_txt(path)
assert colors["background"] == "#000000" assert colors["background"] == "#000000"
assert colors["primary"] == "#ffffff" assert colors["primary"] == "#ffffff"
assert colors["secondary"] == "#cccccc" assert colors["secondary"] == "#cccccc"
assert len(colors) == 3 assert len(colors) == 3
class TestDiscoverSchemes: class TestDiscoverSchemes:
def test_discovers_all_schemes(self): def test_discovers_all_schemes(self):
schemes = sp._discover_schemes() schemes = sp._discover_schemes()
assert "gruvbox" in schemes assert "gruvbox" in schemes
assert "catppuccin" in schemes assert "catppuccin" in schemes
assert "everforest" in schemes assert "everforest" in schemes
assert "nord" in schemes assert "nord" in schemes
assert len(schemes) >= 10 assert len(schemes) >= 10
def test_scheme_has_variants(self): def test_scheme_has_variants(self):
schemes = sp._discover_schemes() schemes = sp._discover_schemes()
gruvbox = schemes["gruvbox"] gruvbox = schemes["gruvbox"]
var_ids = {v.id for v in gruvbox.variants} var_ids = {v.id for v in gruvbox.variants}
assert "medium" in var_ids assert "medium" in var_ids
assert "hard" in var_ids assert "hard" in var_ids
assert "soft" in var_ids assert "soft" in var_ids
def test_variant_has_modes(self): def test_variant_has_modes(self):
schemes = sp._discover_schemes() schemes = sp._discover_schemes()
gmedium = next(v for v in schemes["gruvbox"].variants if v.id == "medium") gmedium = next(
assert "dark" in gmedium.modes v for v in schemes["gruvbox"].variants if v.id == "medium"
assert "light" in gmedium.modes )
assert "dark" in gmedium.modes
assert "light" in gmedium.modes
def test_catppuccin_has_accents(self): def test_catppuccin_has_accents(self):
schemes = sp._discover_schemes() schemes = sp._discover_schemes()
mocha = next(v for v in schemes["catppuccin"].variants if v.id == "mocha") mocha = next(
assert "mauve" in mocha.accents v for v in schemes["catppuccin"].variants if v.id == "mocha"
assert "green" in mocha.accents )
assert "rosewater" in mocha.accents assert "mauve" in mocha.accents
assert len(mocha.accents) >= 14 assert "green" in mocha.accents
assert "rosewater" in mocha.accents
assert len(mocha.accents) >= 14
def test_non_accent_scheme_has_no_accents(self): def test_non_accent_scheme_has_no_accents(self):
schemes = sp._discover_schemes() schemes = sp._discover_schemes()
gmedium = next(v for v in schemes["gruvbox"].variants if v.id == "medium") gmedium = next(
assert gmedium.accents == () v for v in schemes["gruvbox"].variants if v.id == "medium"
)
assert gmedium.accents == ()
class TestGetPalette: class TestGetPalette:
def test_loads_basic_palette(self): def test_loads_basic_palette(self):
pal = sp.get_palette("gruvbox", "medium", "dark") pal = sp.get_palette("gruvbox", "medium", "dark")
assert pal.scheme == "gruvbox" assert pal.scheme == "gruvbox"
assert pal.variant == "medium" assert pal.variant == "medium"
assert pal.mode == "dark" assert pal.mode == "dark"
assert pal.colors["background"].startswith("#") assert pal.colors["background"].startswith("#")
assert pal.colors["primary"].startswith("#") assert pal.colors["primary"].startswith("#")
def test_loads_accent_palette(self): def test_loads_accent_palette(self):
pal = sp.get_palette("catppuccin", "mocha", "dark", accent="mauve") pal = sp.get_palette("catppuccin", "mocha", "dark", accent="mauve")
assert pal.accent == "mauve" assert pal.accent == "mauve"
assert pal.colors["primary"] == "#cba6f7" assert pal.colors["primary"] == "#cba6f7"
def test_different_accent_changes_colors(self): def test_different_accent_changes_colors(self):
mauve = sp.get_palette("catppuccin", "mocha", "dark", accent="mauve") mauve = sp.get_palette("catppuccin", "mocha", "dark", accent="mauve")
green = sp.get_palette("catppuccin", "mocha", "dark", accent="green") green = sp.get_palette("catppuccin", "mocha", "dark", accent="green")
assert mauve.colors["primary"] != green.colors["primary"] assert mauve.colors["primary"] != green.colors["primary"]
assert mauve.colors["secondary"] != green.colors["secondary"] assert mauve.colors["secondary"] != green.colors["secondary"]
def test_unknown_scheme_raises(self): def test_unknown_scheme_raises(self):
with pytest.raises(KeyError, match="Unknown scheme 'nope'"): with pytest.raises(KeyError, match="Unknown scheme 'nope'"):
sp.get_palette("nope", "medium", "dark") sp.get_palette("nope", "medium", "dark")
def test_unknown_variant_raises(self): def test_unknown_variant_raises(self):
with pytest.raises(KeyError, match="Unknown variant 'bogus' for scheme 'gruvbox'"): with pytest.raises(
sp.get_palette("gruvbox", "bogus", "dark") KeyError, match="Unknown variant 'bogus' for scheme 'gruvbox'"
):
sp.get_palette("gruvbox", "bogus", "dark")
def test_unknown_accent_falls_back(self): def test_unknown_accent_falls_back(self):
pal = sp.get_palette("catppuccin", "mocha", "dark", accent="nonexistent") pal = sp.get_palette(
assert pal.accent == "nonexistent" "catppuccin", "mocha", "dark", accent="nonexistent"
assert pal.colors["primary"] is not None )
assert pal.accent == "nonexistent"
assert pal.colors["primary"] is not None
def test_accent_on_non_accent_scheme(self): def test_accent_on_non_accent_scheme(self):
pal = sp.get_palette("gruvbox", "medium", "dark", accent="mauve") pal = sp.get_palette("gruvbox", "medium", "dark", accent="mauve")
assert pal.colors is not None assert pal.colors is not None
def test_non_existent_mode_raises(self): def test_non_existent_mode_raises(self):
with pytest.raises(FileNotFoundError): with pytest.raises(FileNotFoundError):
sp.get_palette("catppuccin", "mocha", "light") sp.get_palette("catppuccin", "mocha", "light")
class TestListSchemes: class TestListSchemes:
def test_returns_dict(self): def test_returns_dict(self):
schemes = sp.list_schemes() schemes = sp.list_schemes()
assert isinstance(schemes, dict) assert isinstance(schemes, dict)
def test_includes_known_schemes(self): def test_includes_known_schemes(self):
schemes = sp.list_schemes() schemes = sp.list_schemes()
assert "catppuccin" in schemes assert "catppuccin" in schemes
assert "gruvbox" in schemes assert "gruvbox" in schemes
class TestResolvePreset: class TestResolvePreset:
def test_two_parts(self): def test_two_parts(self):
assert sp.resolve_preset("gruvbox:medium") == ("gruvbox", "medium") assert sp.resolve_preset("gruvbox:medium") == ("gruvbox", "medium")
def test_three_parts(self): def test_three_parts(self):
with pytest.raises(ValueError, match="Invalid preset spec"): with pytest.raises(ValueError, match="Invalid preset spec"):
sp.resolve_preset("catppuccin:mocha:mauve") sp.resolve_preset("catppuccin:mocha:mauve")
def test_one_part(self): def test_one_part(self):
assert sp.resolve_preset("default") == ("default", "default") assert sp.resolve_preset("default") == ("default", "default")
def test_edge_spaces(self): def test_edge_spaces(self):
assert sp.resolve_preset(" catppuccin : mocha ") == (" catppuccin ", " mocha ") assert sp.resolve_preset(" catppuccin : mocha ") == (
" catppuccin ",
" mocha ",
)
+134 -93
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from subprocess import CompletedProcess from subprocess import CompletedProcess
from unittest.mock import patch, call from unittest.mock import call, patch
from typer.testing import CliRunner from typer.testing import CliRunner
from zshell.subcommands.shell import app from zshell.subcommands.shell import app
@@ -10,122 +10,163 @@ runner = CliRunner()
def invoke(*args: str): def invoke(*args: str):
result = runner.invoke(app, args) result = runner.invoke(app, args)
if result.exit_code != 0: if result.exit_code != 0:
raise RuntimeError(result.output) raise RuntimeError(result.output)
return result return result
class TestKill: class TestKill:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_kill_runs_qs_kill_success(self, mock_run): def test_kill_runs_qs_kill_success(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"Killed abc\n") mock_run.return_value = CompletedProcess([], 0, b"", b"Killed abc\n")
invoke("kill") invoke("kill")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "kill"], capture_output=True) mock_run.assert_called_once_with(
["qs", "-c", "zshell", "kill"], capture_output=True
)
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_kill_no_instance_errors(self, mock_run): def test_kill_no_instance_errors(self, mock_run):
mock_run.return_value = CompletedProcess([], 255, b"", b"No running instances\n") mock_run.return_value = CompletedProcess(
result = runner.invoke(app, ["kill"]) [], 255, b"", b"No running instances\n"
assert result.exit_code != 0 )
assert "No running instance to kill" in result.output result = runner.invoke(app, ["kill"])
assert result.exit_code != 0
assert "No running instance to kill" in result.output
class TestStart: class TestStart:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_start_default_daemon(self, mock_run): def test_start_default_daemon(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"Launching config\n") mock_run.return_value = CompletedProcess(
invoke("start") [], 0, b"", b"Launching config\n"
mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n", "-d"], capture_output=True) )
invoke("start")
mock_run.assert_called_once_with(
["qs", "-c", "zshell", "-n", "-d"], capture_output=True
)
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_start_no_daemon(self, mock_run): def test_start_no_daemon(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"Launching config\n") mock_run.return_value = CompletedProcess(
invoke("start", "--no-daemon") [], 0, b"", b"Launching config\n"
mock_run.assert_called_once_with(["qs", "-c", "zshell", "-n"], capture_output=True) )
invoke("start", "--no-daemon")
mock_run.assert_called_once_with(
["qs", "-c", "zshell", "-n"], capture_output=True
)
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_start_already_running_errors(self, mock_run): def test_start_already_running_errors(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"An instance of this configuration is already running.\n", b"") mock_run.return_value = CompletedProcess(
result = runner.invoke(app, ["start"]) [],
assert result.exit_code != 0 0,
assert "already running" in result.output b"An instance of this configuration is already running.\n",
b"",
)
result = runner.invoke(app, ["start"])
assert result.exit_code != 0
assert "already running" in result.output
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_start_other_failure_errors(self, mock_run): def test_start_other_failure_errors(self, mock_run):
mock_run.return_value = CompletedProcess([], 1, b"", b"Config error\n") mock_run.return_value = CompletedProcess([], 1, b"", b"Config error\n")
result = runner.invoke(app, ["start"]) result = runner.invoke(app, ["start"])
assert result.exit_code != 0 assert result.exit_code != 0
assert "Config error" in result.output assert "Config error" in result.output
class TestShow: class TestShow:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_show_runs_ipc_show(self, mock_run): def test_show_runs_ipc_show(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"target visibilities\n", b"") mock_run.return_value = CompletedProcess(
result = invoke("show") [], 0, b"target visibilities\n", b""
assert "target visibilities" in result.output )
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "show"], capture_output=True) result = invoke("show")
assert "target visibilities" in result.output
mock_run.assert_called_once_with(
["qs", "-c", "zshell", "ipc", "show"], capture_output=True
)
class TestLog: class TestLog:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_log_runs_qs_log(self, mock_run): def test_log_runs_qs_log(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"log output\n", b"") mock_run.return_value = CompletedProcess([], 0, b"log output\n", b"")
invoke("log") invoke("log")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "log"], capture_output=True) mock_run.assert_called_once_with(
["qs", "-c", "zshell", "log"], capture_output=True
)
class TestLock: class TestLock:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_lock_runs_ipc_call_lock(self, mock_run): def test_lock_runs_ipc_call_lock(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"") mock_run.return_value = CompletedProcess([], 0, b"", b"")
invoke("lock") invoke("lock")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "lock", "lock"], capture_output=True) mock_run.assert_called_once_with(
["qs", "-c", "zshell", "ipc", "call", "lock", "lock"],
capture_output=True,
)
class TestCall: class TestCall:
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_call_no_args(self, mock_run): def test_call_no_args(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"") mock_run.return_value = CompletedProcess([], 0, b"", b"")
invoke("call", "target", "method") invoke("call", "target", "method")
mock_run.assert_called_once_with(["qs", "-c", "zshell", "ipc", "call", "target", "method"], capture_output=True) mock_run.assert_called_once_with(
["qs", "-c", "zshell", "ipc", "call", "target", "method"],
capture_output=True,
)
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_call_with_args(self, mock_run): def test_call_with_args(self, mock_run):
mock_run.return_value = CompletedProcess([], 0, b"", b"") mock_run.return_value = CompletedProcess([], 0, b"", b"")
invoke("call", "target", "method", "arg1", "arg2") invoke("call", "target", "method", "arg1", "arg2")
mock_run.assert_called_once_with( mock_run.assert_called_once_with(
["qs", "-c", "zshell", "ipc", "call", "target", "method", "arg1", "arg2"], [
capture_output=True, "qs",
) "-c",
"zshell",
"ipc",
"call",
"target",
"method",
"arg1",
"arg2",
],
capture_output=True,
)
class TestRestart: class TestRestart:
@patch("zshell.subcommands.shell.start_instance") @patch("zshell.subcommands.shell.start_instance")
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_restart_kills_then_starts(self, mock_run, mock_start): def test_restart_kills_then_starts(self, mock_run, mock_start):
mock_run.side_effect = [ mock_run.side_effect = [
CompletedProcess([], 0, b"", b"Killed abc\n"), # first kill (captured) CompletedProcess(
CompletedProcess([], 255, b"", b""), # poll → no instance [], 0, b"", b"Killed abc\n"
] ), # first kill (captured)
invoke("restart") CompletedProcess([], 255, b"", b""), # poll → no instance
assert mock_run.call_args_list == [ ]
call(["qs", "-c", "zshell", "kill"], capture_output=True), invoke("restart")
call(["qs", "-c", "zshell", "kill"], capture_output=True), assert mock_run.call_args_list == [
] call(["qs", "-c", "zshell", "kill"], capture_output=True),
mock_start.assert_called_once_with(no_daemon=False) call(["qs", "-c", "zshell", "kill"], capture_output=True),
]
mock_start.assert_called_once_with(no_daemon=False)
@patch("zshell.subcommands.shell.start_instance") @patch("zshell.subcommands.shell.start_instance")
@patch("zshell.subcommands.shell.subprocess.run") @patch("zshell.subcommands.shell.subprocess.run")
def test_restart_no_daemon(self, mock_run, mock_start): def test_restart_no_daemon(self, mock_run, mock_start):
mock_run.side_effect = [ mock_run.side_effect = [
CompletedProcess([], 0, b"", b"Killed abc\n"), CompletedProcess([], 0, b"", b"Killed abc\n"),
CompletedProcess([], 255, b"", b""), CompletedProcess([], 255, b"", b""),
] ]
invoke("restart", "--no-daemon") invoke("restart", "--no-daemon")
assert mock_run.call_args_list == [ assert mock_run.call_args_list == [
call(["qs", "-c", "zshell", "kill"], capture_output=True), call(["qs", "-c", "zshell", "kill"], capture_output=True),
call(["qs", "-c", "zshell", "kill"], capture_output=True), call(["qs", "-c", "zshell", "kill"], capture_output=True),
] ]
mock_start.assert_called_once_with(no_daemon=True) mock_start.assert_called_once_with(no_daemon=True)