From 9d12c03ccc0cc0b6c9f4b0cb09ccb686f91601ad Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 17 Aug 2026 13:10:10 +0200 Subject: [PATCH] vscode_optimizer: move under python_pkg and cover 0% -> 100% meta/scripts/optimize_vscode.py was 498 lines against a 250-line cap and completely untested, and at that path the repo-wide gate (--cov=python_pkg, fail_under = 100) could not see it. Moved verbatim to python_pkg/vscode_optimizer/_optimize.py - git records a pure rename, and the content is byte-identical to HEAD. The move and the tests are one commit by necessity: any intermediate state with the module under python_pkg/ but uncovered turns every pytest run in the repo red, including ci-mirror on pre-push for unrelated work. Running it by path no longer works after the move (sys.path[0] would be the module's own directory), so __main__.py replaces it, following the existing focus_policy convention rather than a sys.path insert. Nothing invoked the old path: grep over .sh/.desktop/.service/.yml/.py found zero callers. 98 tests reach 100% line and branch coverage. Patching is confined to real boundaries - subprocess.run, the module-level Path, builtins.input, sys.argv - so the forthcoming split has few monkeypatch targets to re-point. Writing the tests surfaced a pre-existing bug, left unfixed here and pinned by a strict xfail: _VENDOR_KW's "ati" key is a substring of "VGA compatible controller", which appears in every lspci display line, so it matches before "intel" is ever tried and Intel iGPUs are misreported as AMD. Verified: python3 -m python_pkg.vscode_optimizer --dry-run reproduces the pre-move 18-change plan and writes nothing; the real settings.json is untouched; repo-wide pytest is 1846 passed at 100%; over-cap files 37 -> 36. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY --- ...scode-optimizer-move-tests-2026-08-17.json | 17 ++ ...scode-optimizer-move-tests-2026-08-17.json | 62 +++++ python_pkg/vscode_optimizer/__init__.py | 3 + python_pkg/vscode_optimizer/__main__.py | 15 ++ .../vscode_optimizer/_optimize.py | 0 python_pkg/vscode_optimizer/tests/__init__.py | 3 + python_pkg/vscode_optimizer/tests/conftest.py | 33 +++ .../vscode_optimizer/tests/test_gpu_disk.py | 235 +++++++++++++++++ python_pkg/vscode_optimizer/tests/test_hw.py | 132 ++++++++++ python_pkg/vscode_optimizer/tests/test_io.py | 230 +++++++++++++++++ .../vscode_optimizer/tests/test_main.py | 239 ++++++++++++++++++ .../vscode_optimizer/tests/test_plan.py | 210 +++++++++++++++ .../vscode_optimizer/tests/test_render.py | 118 +++++++++ 13 files changed, 1297 insertions(+) create mode 100644 docs/superpowers/contracts/vscode-optimizer-move-tests-2026-08-17.json create mode 100644 docs/superpowers/evidence/vscode-optimizer-move-tests-2026-08-17.json create mode 100644 python_pkg/vscode_optimizer/__init__.py create mode 100644 python_pkg/vscode_optimizer/__main__.py rename meta/scripts/optimize_vscode.py => python_pkg/vscode_optimizer/_optimize.py (100%) create mode 100644 python_pkg/vscode_optimizer/tests/__init__.py create mode 100644 python_pkg/vscode_optimizer/tests/conftest.py create mode 100644 python_pkg/vscode_optimizer/tests/test_gpu_disk.py create mode 100644 python_pkg/vscode_optimizer/tests/test_hw.py create mode 100644 python_pkg/vscode_optimizer/tests/test_io.py create mode 100644 python_pkg/vscode_optimizer/tests/test_main.py create mode 100644 python_pkg/vscode_optimizer/tests/test_plan.py create mode 100644 python_pkg/vscode_optimizer/tests/test_render.py diff --git a/docs/superpowers/contracts/vscode-optimizer-move-tests-2026-08-17.json b/docs/superpowers/contracts/vscode-optimizer-move-tests-2026-08-17.json new file mode 100644 index 00000000..7aa9797b --- /dev/null +++ b/docs/superpowers/contracts/vscode-optimizer-move-tests-2026-08-17.json @@ -0,0 +1,17 @@ +{ + "title": "Move optimize_vscode.py under python_pkg/ and cover it to 100%", + "objective": "meta/scripts/optimize_vscode.py was 498 lines against a 250-line cap and completely untested, and at that path the repo-wide gate (--cov=python_pkg, fail_under = 100) could not see it. This commit moves it verbatim to python_pkg/vscode_optimizer/_optimize.py and lands a full test suite, so the module arrives already measured by the standing gate. The move and the tests are one commit by necessity: any intermediate state with the module under python_pkg/ but uncovered turns every pytest run in the repo red, including ci-mirror on pre-push for unrelated work. No production logic changes here; the split follows in later commits.", + "acceptance_criteria": [ + "_optimize.py reaches 100% line and branch coverage, from 0%", + "_optimize.py is byte-identical to the file at HEAD - git records it as a pure rename", + "python3 -m python_pkg.vscode_optimizer --dry-run reproduces the pre-move plan (18 changes) and writes nothing", + "No suppression of any kind: no noqa, no type: ignore, no lowered threshold", + "Every new test file is under the 250-line cap, so the over-cap total falls to 36 rather than rising" + ], + "out_of_scope": [ + "The 3+ way split of _optimize.py itself, which is the next commit", + "Fixing the _VENDOR_KW 'ati' substring bug found while writing these tests - a behaviour change that does not belong in a move commit; it is pinned by a strict xfail and reported to the user", + "The three transcribe_* files and the 29 over-cap shell scripts" + ], + "verifier": "pytest python_pkg/vscode_optimizer/ --cov=python_pkg/vscode_optimizer --cov-branch --cov-report=term-missing for the coverage claim; git diff --cached -M --stat to confirm the rename is content-identical; python3 -m python_pkg.vscode_optimizer --dry-run for the behaviour claim; pre-commit run --files for lint; bash ~/utils/scripts/check_file_length.sh --all for the cap." +} diff --git a/docs/superpowers/evidence/vscode-optimizer-move-tests-2026-08-17.json b/docs/superpowers/evidence/vscode-optimizer-move-tests-2026-08-17.json new file mode 100644 index 00000000..d2911622 --- /dev/null +++ b/docs/superpowers/evidence/vscode-optimizer-move-tests-2026-08-17.json @@ -0,0 +1,62 @@ +{ + "intent": "Move the untested 498-line optimize_vscode.py under python_pkg/ so the repo-wide 100% coverage gate measures it, and land a full test suite in the same commit, as the groundwork for splitting it under the 250-line cap.", + "scope": [ + "meta/scripts/optimize_vscode.py -> python_pkg/vscode_optimizer/_optimize.py (pure rename)", + "New: python_pkg/vscode_optimizer/{__init__,__main__}.py and seven test modules", + "Non-goal: the split itself, which follows in its own commits", + "Non-goal: fixing the _VENDOR_KW bug these tests uncovered - pinned by a strict xfail, reported to the user" + ], + "changes": [ + "git mv of the module with zero content change, verified by diffing against HEAD.", + "Added __main__.py so the script is run as `python3 -m python_pkg.vscode_optimizer`. Running it by path no longer works after the move, because sys.path[0] would be the module's own directory rather than the repo root; this follows the existing focus_policy convention rather than a sys.path insert.", + "Added 98 tests across five test modules plus a conftest for the two shared subprocess doubles, reaching 100% line and branch coverage.", + "Patched only at real boundaries - subprocess.run, the module-level Path name, builtins.input and sys.argv - so the forthcoming split has as few monkeypatch targets to re-point as possible. Settings and flags I/O uses real files under tmp_path.", + "Split the hardware tests into test_hw.py (runner, CPU, RAM) and test_gpu_disk.py (GPU, disk, aggregate) because the single module landed at 362 lines, over the cap it exists to help clear." + ], + "verification": [ + { + "command": "git show HEAD:meta/scripts/optimize_vscode.py | diff - python_pkg/vscode_optimizer/_optimize.py", + "result": "pass", + "evidence": "No output: the moved file is byte-identical to HEAD. git diff --cached -M records it as a rename." + }, + { + "command": "pytest python_pkg/vscode_optimizer/ --cov=python_pkg/vscode_optimizer --cov-branch --cov-report=term-missing", + "result": "pass", + "evidence": "98 passed, 1 xfailed. _optimize.py: 295 statements, 0 missing, 120 branches, 0 partial, 100%. __main__.py and __init__.py both 100%. Coverage was driven from 0%; the last three gaps closed were __main__'s import, _detect_gpu's no-display-device loop exit, and _apply_variant's flags-only path." + }, + { + "command": "python3 -m python_pkg.vscode_optimizer --dry-run", + "result": "pass", + "evidence": "Detects Ryzen 9 7900X3D / 30 GB / RTX 3090 / NVME and proposes the same 18 changes as the pre-move run of the old path, ending '(dry-run) No files modified.' Confirms the import graph resolves through the new entry point - the unit tests stub subprocess, so only a real run proves this." + }, + { + "command": "ls -l ~/.config/VSCodium/User/settings.json", + "result": "pass", + "evidence": "No such file or directory, both before and after the dry run - the real user settings were never created or modified. A dedicated test also asserts that with Path.home redirected, every discovered variant path stays inside tmp_path." + }, + { + "command": "pytest python_pkg/vscode_optimizer/ (test count across the test-file split)", + "result": "pass", + "evidence": "98 passed both before and after splitting test_hw.py, which is the discriminating check for a test-file split: a file outside the runner's glob would be silently uncollected." + }, + { + "command": "bash ~/utils/scripts/check_file_length.sh --all", + "result": "pass", + "evidence": "36 over-cap files, down from 37 at the moment test_hw.py stood at 362 lines. Every new test file is under 250: 235/133/227/239/210/111. _optimize.py is still 498 and is the next commit's target." + }, + { + "command": "pre-commit run --files <11 staged files>", + "result": "pass", + "evidence": "All hooks pass, including ruff, ruff format, mypy, pylint, bandit, name-tests-test, check-python-location and the pytest coverage gate." + } + ], + "risks": [ + "The module is no longer runnable as a path script. Mitigated: grep over .sh/.desktop/.service/.yml/.py found zero callers, so the only invocation was by hand, and __main__.py replaces it.", + "Running `python3 -m python_pkg.vscode_optimizer` from outside the repo root fails with ModuleNotFoundError. This is true of every python_pkg module here (focus_policy included) and is the established convention, not a regression from this move.", + "The strict xfail pinning the _VENDOR_KW bug will fail the suite if someone fixes the bug without removing the marker. That is intended: it forces the fix and its test to land together." + ], + "rollback": [ + "git revert the commit; the module returns to meta/scripts/optimize_vscode.py and the tests disappear with it.", + "After rollback, confirm `python3 meta/scripts/optimize_vscode.py --dry-run` prints the 18-change plan again, and that the repo-wide `pytest` still passes with python_pkg back at its previous coverage." + ] +} diff --git a/python_pkg/vscode_optimizer/__init__.py b/python_pkg/vscode_optimizer/__init__.py new file mode 100644 index 00000000..08b19d1e --- /dev/null +++ b/python_pkg/vscode_optimizer/__init__.py @@ -0,0 +1,3 @@ +"""Auto-optimize VS Code settings based on detected hardware.""" + +from __future__ import annotations diff --git a/python_pkg/vscode_optimizer/__main__.py b/python_pkg/vscode_optimizer/__main__.py new file mode 100644 index 00000000..808b3b56 --- /dev/null +++ b/python_pkg/vscode_optimizer/__main__.py @@ -0,0 +1,15 @@ +"""Run the VS Code optimizer as ``python3 -m python_pkg.vscode_optimizer``. + +The optimizer used to live at ``meta/scripts/optimize_vscode.py`` and was run +by path. It moved under ``python_pkg/`` so the repo-wide coverage gate +(``--cov=python_pkg``, ``fail_under = 100``) measures it; running it by path +no longer works, because ``sys.path[0]`` would be the script's own directory +rather than the repo root. +""" + +from __future__ import annotations + +from python_pkg.vscode_optimizer._optimize import main + +if __name__ == "__main__": + main() diff --git a/meta/scripts/optimize_vscode.py b/python_pkg/vscode_optimizer/_optimize.py similarity index 100% rename from meta/scripts/optimize_vscode.py rename to python_pkg/vscode_optimizer/_optimize.py diff --git a/python_pkg/vscode_optimizer/tests/__init__.py b/python_pkg/vscode_optimizer/tests/__init__.py new file mode 100644 index 00000000..b1d07d7c --- /dev/null +++ b/python_pkg/vscode_optimizer/tests/__init__.py @@ -0,0 +1,3 @@ +"""Tests for the VS Code settings optimizer.""" + +from __future__ import annotations diff --git a/python_pkg/vscode_optimizer/tests/conftest.py b/python_pkg/vscode_optimizer/tests/conftest.py new file mode 100644 index 00000000..c3017401 --- /dev/null +++ b/python_pkg/vscode_optimizer/tests/conftest.py @@ -0,0 +1,33 @@ +"""Shared doubles for the hardware-probe tests. + +``name-tests-test`` requires every module under a tests directory to be named +``test_*.py``, so helpers shared between test modules live here rather than in +a helper module. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + + +class FakeProc: + """Stand-in for the CompletedProcess ``_run`` inspects.""" + + def __init__(self, stdout: str) -> None: + self.stdout = stdout + + +def fake_run(mapping: dict[str, str]) -> Callable[..., FakeProc]: + """Return a ``subprocess.run`` double keyed on the executable name. + + Commands absent from *mapping* return empty stdout, which is how the + probes see a tool that is installed but reports nothing. + """ + + def _run(args: list[str], **_kw: object) -> FakeProc: + return FakeProc(mapping.get(args[0], "")) + + return _run diff --git a/python_pkg/vscode_optimizer/tests/test_gpu_disk.py b/python_pkg/vscode_optimizer/tests/test_gpu_disk.py new file mode 100644 index 00000000..cfbf68e4 --- /dev/null +++ b/python_pkg/vscode_optimizer/tests/test_gpu_disk.py @@ -0,0 +1,235 @@ +"""Tests for GPU and root-disk detection, and the aggregate probe. + +Both probes shell out, so ``subprocess.run`` is patched at the boundary; the +sysfs reads in ``_detect_disk`` are redirected by patching the module-level +``Path`` name. Nothing here touches the machine's real hardware. +""" + +from __future__ import annotations + +import subprocess +from typing import TYPE_CHECKING + +import pytest + +from python_pkg.vscode_optimizer import _optimize as opt +from python_pkg.vscode_optimizer.tests.conftest import fake_run as _fake_run + +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + + +# --------------------------------------------------------------------------- # +# _detect_gpu +# --------------------------------------------------------------------------- # +_NVIDIA_LINE = ( + "01:00.0 VGA compatible controller: NVIDIA Corporation GA102 [GeForce RTX 3090]" +) + + +def test_detect_gpu_reads_nvidia_vram(monkeypatch: pytest.MonkeyPatch) -> None: + """An NVIDIA card triggers a second probe for its VRAM.""" + monkeypatch.setattr( + subprocess, + "run", + _fake_run({"lspci": _NVIDIA_LINE, "nvidia-smi": "24576\n"}), + ) + hw = opt._Hw() + + opt._detect_gpu(hw) + + assert hw.gpu_vendor == "NVIDIA" + assert hw.gpu_model == "NVIDIA Corporation GA102 [GeForce RTX 3090]" + assert hw.gpu_vram_mb == 24576 + + +def test_detect_gpu_tolerates_nvidia_smi_being_absent( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Without nvidia-smi the vendor is still known; VRAM stays unset.""" + monkeypatch.setattr(subprocess, "run", _fake_run({"lspci": _NVIDIA_LINE})) + hw = opt._Hw() + + opt._detect_gpu(hw) + + assert hw.gpu_vendor == "NVIDIA" + assert hw.gpu_vram_mb == 0 + + +@pytest.mark.parametrize( + ("line", "vendor"), + [ + ("03:00.0 VGA compatible controller: AMD Radeon RX 7900", "AMD"), + ("03:00.0 VGA compatible controller: ATI Technologies Inc", "AMD"), + # Characterisation, not endorsement: see the xfail below. Every real + # lspci display line contains "VGA compATIble", so "ati" matches first + # and both of these are reported as AMD. + ("00:02.0 VGA compatible controller: Intel UHD Graphics 630", "AMD"), + ("04:00.0 VGA compatible controller: Some Unlisted Vendor", "AMD"), + # A 3D controller line has no "compatible" in it, so it escapes the trap. + ("04:00.0 3D controller: Some Unlisted Vendor", "Unknown"), + ], +) +def test_detect_gpu_maps_each_vendor_keyword( + monkeypatch: pytest.MonkeyPatch, line: str, vendor: str +) -> None: + """Vendor keywords map to display names; 3D controllers count as GPUs.""" + monkeypatch.setattr(subprocess, "run", _fake_run({"lspci": line})) + hw = opt._Hw() + + opt._detect_gpu(hw) + + assert hw.gpu_vendor == vendor + + +@pytest.mark.xfail( + reason=( + "Pre-existing bug carried verbatim through the move: _VENDOR_KW's 'ati' " + "key is a substring of 'VGA compatible controller', which appears in " + "every lspci display line, so it matches before 'intel' is ever tried. " + "Intel iGPUs are misreported as AMD, which wrongly enables the " + "GPU-accelerated terminal and AMD-only Electron flags. Fixing it is a " + "behaviour change and does not belong in a move commit." + ), + strict=True, +) +def test_detect_gpu_should_recognise_an_intel_igpu( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An Intel iGPU ought to be detected as Intel.""" + line = "00:02.0 VGA compatible controller: Intel UHD Graphics 630" + monkeypatch.setattr(subprocess, "run", _fake_run({"lspci": line})) + hw = opt._Hw() + + opt._detect_gpu(hw) + + assert hw.gpu_vendor == "Intel" + + +def test_detect_gpu_leaves_defaults_when_lspci_lists_no_display_device( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A listing with no VGA/3D line exhausts the loop without a match.""" + monkeypatch.setattr( + subprocess, "run", _fake_run({"lspci": "00:1f.3 Audio device: Intel\n"}) + ) + hw = opt._Hw() + + opt._detect_gpu(hw) + + assert hw.gpu_vendor == "Unknown" + assert hw.gpu_model == "Unknown" + + +def test_detect_gpu_skips_non_display_devices( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Only VGA/3D lines are considered; the first match wins.""" + listing = "00:1f.3 Audio device: Intel\n" + _NVIDIA_LINE + "\n05:00.0 VGA: AMD\n" + monkeypatch.setattr( + subprocess, "run", _fake_run({"lspci": listing, "nvidia-smi": "24576"}) + ) + hw = opt._Hw() + + opt._detect_gpu(hw) + + assert hw.gpu_vendor == "NVIDIA" + + +# --------------------------------------------------------------------------- # +# _detect_disk +# --------------------------------------------------------------------------- # +def _disk_paths( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, base: str, rotational: str | None +) -> None: + """Redirect /sys/block lookups into tmp_path, keeping /dev names real.""" + if rotational is not None: + queue = tmp_path / "sys" / "block" / base / "queue" + queue.mkdir(parents=True) + (queue / "rotational").write_text(rotational) + real_path = opt.Path + monkeypatch.setattr( + opt, + "Path", + lambda p: ( + tmp_path / str(p).lstrip("/") + if str(p).startswith("/sys/") + else real_path(p) + ), + ) + + +@pytest.mark.parametrize( + ("device", "base", "rotational", "expected"), + [ + ("/dev/nvme0n1p2", "nvme0n1", "0", "nvme"), + ("/dev/sda1", "sda", "0", "ssd"), + ("/dev/sdb2", "sdb", "1", "hdd"), + ], +) +def test_detect_disk_classifies_the_root_device( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + device: str, + base: str, + rotational: str, + expected: str, +) -> None: + """Partition suffixes are stripped, then rotational/nvme decide the type.""" + monkeypatch.setattr(subprocess, "run", _fake_run({"findmnt": device})) + _disk_paths(monkeypatch, tmp_path, base, rotational) + hw = opt._Hw() + + opt._detect_disk(hw) + + assert hw.disk_type == expected + + +def test_detect_disk_gives_up_when_findmnt_says_nothing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """No root device means the type stays unknown.""" + monkeypatch.setattr(subprocess, "run", _fake_run({})) + hw = opt._Hw() + + opt._detect_disk(hw) + + assert hw.disk_type == "unknown" + + +def test_detect_disk_gives_up_without_a_rotational_flag( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A device with no sysfs queue entry (e.g. dm-crypt) stays unknown.""" + monkeypatch.setattr(subprocess, "run", _fake_run({"findmnt": "/dev/mapper/root"})) + _disk_paths(monkeypatch, tmp_path, "mapper/root", None) + hw = opt._Hw() + + opt._detect_disk(hw) + + assert hw.disk_type == "unknown" + + +# --------------------------------------------------------------------------- # +# _detect_hardware +# --------------------------------------------------------------------------- # +def test_detect_hardware_runs_every_probe(monkeypatch: pytest.MonkeyPatch) -> None: + """The aggregate calls all four detectors against one _Hw.""" + called: list[str] = [] + + def _recorder(name: str) -> Callable[[opt._Hw], None]: + """Build a probe double that records the name it stands in for.""" + + def _probe(_hw: opt._Hw) -> None: + called.append(name) + + return _probe + + for probe_name in ("_detect_cpu", "_detect_ram", "_detect_gpu", "_detect_disk"): + monkeypatch.setattr(opt, probe_name, _recorder(probe_name)) + + result = opt._detect_hardware() + + assert isinstance(result, opt._Hw) + assert called == ["_detect_cpu", "_detect_ram", "_detect_gpu", "_detect_disk"] diff --git a/python_pkg/vscode_optimizer/tests/test_hw.py b/python_pkg/vscode_optimizer/tests/test_hw.py new file mode 100644 index 00000000..9e0be1f0 --- /dev/null +++ b/python_pkg/vscode_optimizer/tests/test_hw.py @@ -0,0 +1,132 @@ +"""Tests for the command runner and for CPU and RAM detection. + +``_run`` shells out, so ``subprocess.run`` is patched at the boundary, and +``_detect_ram``'s hardcoded ``/proc/meminfo`` is redirected by patching the +module-level ``Path`` name. Nothing here reads the machine's real hardware. +""" + +from __future__ import annotations + +import subprocess +from typing import TYPE_CHECKING + +import pytest + +from python_pkg.vscode_optimizer import _optimize as opt +from python_pkg.vscode_optimizer.tests.conftest import FakeProc as _Proc +from python_pkg.vscode_optimizer.tests.conftest import fake_run as _fake_run + +if TYPE_CHECKING: + from pathlib import Path + +_LSCPU_OUT = """\ +Architecture: x86_64 +Model name: AMD Ryzen 9 7900X3D 12-Core Processor +CPU(s): 24 +Core(s) per socket: 12 +CPU max MHz: 5662.5000 +""" + + +# --------------------------------------------------------------------------- # +# _run +# --------------------------------------------------------------------------- # +def test_run_returns_stripped_stdout(monkeypatch: pytest.MonkeyPatch) -> None: + """A successful command hands back its stdout without surrounding space.""" + monkeypatch.setattr(subprocess, "run", lambda *_a, **_k: _Proc(" out \n")) + + assert opt._run(["anything"]) == "out" + + +@pytest.mark.parametrize( + "error", + [subprocess.TimeoutExpired(cmd="x", timeout=10), FileNotFoundError()], +) +def test_run_returns_empty_when_the_command_fails( + monkeypatch: pytest.MonkeyPatch, + error: Exception, +) -> None: + """A missing binary or a timeout degrades to an empty string, not a crash.""" + + def _boom(*_a: object, **_k: object) -> None: + raise error + + monkeypatch.setattr(subprocess, "run", _boom) + + assert opt._run(["anything"]) == "" + + +# --------------------------------------------------------------------------- # +# _detect_cpu +# --------------------------------------------------------------------------- # +def test_detect_cpu_reads_model_cores_and_clock( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Each recognised lscpu key lands on its typed _Hw attribute.""" + monkeypatch.setattr(subprocess, "run", _fake_run({"lscpu": _LSCPU_OUT})) + hw = opt._Hw() + + opt._detect_cpu(hw) + + assert hw.cpu_model == "AMD Ryzen 9 7900X3D 12-Core Processor" + assert hw.cpu_logical_cores == 24 + assert hw.cpu_physical_cores == 12 + assert hw.cpu_max_mhz == pytest.approx(5662.5) + + +def test_detect_cpu_ignores_unknown_keys(monkeypatch: pytest.MonkeyPatch) -> None: + """Lines outside the _LSCPU map leave the defaults alone.""" + monkeypatch.setattr( + subprocess, + "run", + _fake_run({"lscpu": "Architecture: x86_64\nBogoMIPS: 9000\n"}), + ) + hw = opt._Hw() + + opt._detect_cpu(hw) + + assert hw.cpu_model == "Unknown" + assert hw.cpu_physical_cores == 1 + + +# --------------------------------------------------------------------------- # +# _detect_ram +# --------------------------------------------------------------------------- # +def test_detect_ram_converts_kb_to_mb( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """MemTotal in kB becomes whole MB.""" + meminfo = tmp_path / "meminfo" + meminfo.write_text("MemTotal: 32768000 kB\nMemFree: 100 kB\n") + monkeypatch.setattr(opt, "Path", lambda _p: meminfo) + hw = opt._Hw() + + opt._detect_ram(hw) + + assert hw.ram_total_mb == 32000 + + +def test_detect_ram_leaves_zero_when_memtotal_is_absent( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A procfs file without MemTotal is not an error, just no reading.""" + meminfo = tmp_path / "meminfo" + meminfo.write_text("SwapTotal: 4096 kB\n") + monkeypatch.setattr(opt, "Path", lambda _p: meminfo) + hw = opt._Hw() + + opt._detect_ram(hw) + + assert hw.ram_total_mb == 0 + + +def test_detect_ram_survives_an_unreadable_procfs( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """An OSError reading /proc/meminfo leaves the default in place.""" + monkeypatch.setattr(opt, "Path", lambda _p: tmp_path / "does-not-exist") + hw = opt._Hw() + + opt._detect_ram(hw) + + assert hw.ram_total_mb == 0 diff --git a/python_pkg/vscode_optimizer/tests/test_io.py b/python_pkg/vscode_optimizer/tests/test_io.py new file mode 100644 index 00000000..bbe35974 --- /dev/null +++ b/python_pkg/vscode_optimizer/tests/test_io.py @@ -0,0 +1,230 @@ +"""Tests for the filesystem layer: variant discovery, the JSONC parser, +backups, and the settings/flags readers and writers. + +These use real files under ``tmp_path``. Nothing in this module may resolve a +path inside the running user's real ``~/.config``. +""" + +from __future__ import annotations + +import json +import shutil +from typing import TYPE_CHECKING + +import pytest + +from python_pkg.vscode_optimizer import _optimize as opt + +if TYPE_CHECKING: + from pathlib import Path + + +# --------------------------------------------------------------------------- # +# _parse_jsonc +# --------------------------------------------------------------------------- # +def test_parse_jsonc_reads_plain_json() -> None: + """The parser is a superset of JSON.""" + assert opt._parse_jsonc('{"a": 1}') == {"a": 1} + + +def test_parse_jsonc_strips_line_and_block_comments() -> None: + """Both VS Code comment styles are removed before parsing.""" + text = """ + { + // a line comment + "a": 1, /* an inline block */ + /* a + multi-line block */ + "b": 2 + } + """ + + assert opt._parse_jsonc(text) == {"a": 1, "b": 2} + + +def test_parse_jsonc_keeps_comment_markers_inside_strings() -> None: + """A // or /* inside a string literal is data, not a comment.""" + text = '{"url": "https://example.com/*x*/", "path": "// not a comment"}' + + assert opt._parse_jsonc(text) == { + "url": "https://example.com/*x*/", + "path": "// not a comment", + } + + +def test_parse_jsonc_handles_escaped_quotes_in_strings() -> None: + """A backslash-escaped quote does not end the string.""" + assert opt._parse_jsonc(r'{"a": "he said \"hi\""}') == {"a": 'he said "hi"'} + + +def test_parse_jsonc_drops_trailing_commas() -> None: + """Trailing commas are legal in JSONC, before both } and ].""" + assert opt._parse_jsonc('{"a": [1, 2,], "b": 3,}') == {"a": [1, 2], "b": 3} + + +def test_parse_jsonc_treats_an_empty_document_as_no_settings() -> None: + """A blank or comment-only file parses to an empty mapping, not an error.""" + assert opt._parse_jsonc("") == {} + assert opt._parse_jsonc(" \n\t ") == {} + assert opt._parse_jsonc("// nothing but a comment") == {} + + +def test_parse_jsonc_tolerates_a_comment_without_a_trailing_newline() -> None: + """A line comment running to end-of-file terminates cleanly.""" + assert opt._parse_jsonc('{"a": 1}\n// trailing') == {"a": 1} + + +def test_parse_jsonc_tolerates_an_unterminated_block_comment() -> None: + """An unclosed /* swallows the rest of the file rather than looping.""" + assert opt._parse_jsonc('{"a": 1}\n/* never closed') == {"a": 1} + + +def test_parse_jsonc_tolerates_an_unterminated_string() -> None: + """An unclosed quote consumes to end-of-input and then fails to parse.""" + with pytest.raises(json.JSONDecodeError): + opt._parse_jsonc('{"a": "unterminated') + + +# --------------------------------------------------------------------------- # +# _discover_variants +# --------------------------------------------------------------------------- # +def test_discover_variants_finds_an_installation_by_its_settings_file( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A settings.json on disk is enough, even with no binary on PATH.""" + settings = tmp_path / ".config" / "VSCodium" / "User" / "settings.json" + settings.parent.mkdir(parents=True) + settings.write_text("{}") + monkeypatch.setattr(opt.Path, "home", classmethod(lambda _cls: tmp_path)) + monkeypatch.setattr(shutil, "which", lambda _b: None) + + found = opt._discover_variants() + + assert [v.name for v in found] == ["VSCodium"] + assert found[0].settings == settings + + +def test_discover_variants_finds_an_installation_by_its_binary( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A binary on PATH counts even before the settings file exists.""" + monkeypatch.setattr(opt.Path, "home", classmethod(lambda _cls: tmp_path)) + monkeypatch.setattr( + shutil, "which", lambda b: "/usr/bin/code" if b == "code" else None + ) + + found = opt._discover_variants() + + assert [v.name for v in found] == ["VS Code (stable)"] + assert found[0].flags == tmp_path / ".config" / "code-flags.conf" + + +def test_discover_variants_returns_nothing_when_no_editor_is_installed( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A machine with no VS Code variant yields an empty list.""" + monkeypatch.setattr(opt.Path, "home", classmethod(lambda _cls: tmp_path)) + monkeypatch.setattr(shutil, "which", lambda _b: None) + + assert opt._discover_variants() == [] + + +def test_discover_variants_finds_every_installed_variant( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """All three known variants can coexist.""" + monkeypatch.setattr(opt.Path, "home", classmethod(lambda _cls: tmp_path)) + monkeypatch.setattr(shutil, "which", lambda _b: "/usr/bin/x") + + assert len(opt._discover_variants()) == 3 + + +# --------------------------------------------------------------------------- # +# _backup +# --------------------------------------------------------------------------- # +def test_backup_copies_the_file_and_returns_the_destination(tmp_path: Path) -> None: + """The backup keeps the original content and leaves the source in place.""" + src = tmp_path / "settings.json" + src.write_text('{"a": 1}') + + dst = opt._backup(src) + + assert dst is not None + assert dst.read_text() == '{"a": 1}' + assert src.exists() + assert dst.name.endswith(".bak") + + +def test_backup_of_a_missing_file_is_a_no_op(tmp_path: Path) -> None: + """Nothing to back up is not an error.""" + assert opt._backup(tmp_path / "absent.json") is None + + +# --------------------------------------------------------------------------- # +# settings and flags round-trips +# --------------------------------------------------------------------------- # +def test_read_settings_parses_jsonc_from_disk(tmp_path: Path) -> None: + """Comments in a real settings.json are tolerated.""" + path = tmp_path / "settings.json" + path.write_text('{\n // comment\n "editor.fontSize": 14\n}') + + assert opt._read_settings(path) == {"editor.fontSize": 14} + + +def test_read_settings_of_a_missing_file_is_empty(tmp_path: Path) -> None: + """A variant with no settings yet reads as no settings.""" + assert opt._read_settings(tmp_path / "absent.json") == {} + + +def test_write_settings_merges_the_plan_over_the_current_values( + tmp_path: Path, +) -> None: + """Proposed values win; untouched user settings survive.""" + path = tmp_path / "nested" / "settings.json" + current: dict[str, object] = {"editor.fontSize": 14, "a": 1} + opts = [ + opt._Opt("a", 2, "reason"), + opt._Opt(key="b", value=True, reason="reason"), + ] + + opt._write_settings(path, current, opts) + + assert json.loads(path.read_text()) == {"editor.fontSize": 14, "a": 2, "b": True} + + +def test_write_settings_creates_the_parent_directory(tmp_path: Path) -> None: + """A brand-new variant directory is created rather than failing.""" + path = tmp_path / "deep" / "nested" / "settings.json" + + opt._write_settings(path, {}, []) + + assert path.exists() + + +def test_read_flags_ignores_comments_and_blank_lines(tmp_path: Path) -> None: + """The flags file is a plain list, with # comments.""" + path = tmp_path / "code-flags.conf" + path.write_text( + "# a comment\n\n --enable-gpu-rasterization \n--enable-zero-copy\n" + ) + + assert opt._read_flags(path) == [ + "--enable-gpu-rasterization", + "--enable-zero-copy", + ] + + +def test_read_flags_of_a_missing_file_is_empty(tmp_path: Path) -> None: + """No flags file means no existing flags.""" + assert opt._read_flags(tmp_path / "absent.conf") == [] + + +def test_write_flags_round_trips_through_read_flags(tmp_path: Path) -> None: + """What is written is what is read back.""" + path = tmp_path / "code-flags.conf" + flags = ["--enable-gpu-rasterization", "--ignore-gpu-blocklist"] + + opt._write_flags(path, flags) + + assert opt._read_flags(path) == flags + assert path.read_text().endswith("\n") diff --git a/python_pkg/vscode_optimizer/tests/test_main.py b/python_pkg/vscode_optimizer/tests/test_main.py new file mode 100644 index 00000000..9a82bf20 --- /dev/null +++ b/python_pkg/vscode_optimizer/tests/test_main.py @@ -0,0 +1,239 @@ +"""Tests for applying a plan to one variant, and for the CLI entry point. + +Every variant used here points at ``tmp_path``. The suite must never be able +to resolve a real ``~/.config/*/User/settings.json``; the last test in this +module asserts that directly. +""" + +from __future__ import annotations + +import builtins +import json +import sys +from typing import TYPE_CHECKING + +import pytest + +from python_pkg.vscode_optimizer import _optimize as opt + +if TYPE_CHECKING: + from pathlib import Path + + +def _variant(tmp_path: Path) -> opt._Variant: + """A VS Code variant rooted entirely inside tmp_path.""" + return opt._Variant( + name="Test Code", + settings=tmp_path / "User" / "settings.json", + flags=tmp_path / "code-flags.conf", + binary="code", + ) + + +_HW = opt._Hw(cpu_physical_cores=8, ram_total_mb=32000, gpu_vendor="NVIDIA") + + +# --------------------------------------------------------------------------- # +# _apply_variant +# --------------------------------------------------------------------------- # +def test_apply_variant_writes_settings_and_flags(tmp_path: Path) -> None: + """With --yes, both files are written and the plan is applied.""" + v = _variant(tmp_path) + + opt._apply_variant(v, _HW, ["--enable-zero-copy"], dry_run=False, auto_yes=True) + + written = json.loads(v.settings.read_text()) + assert written["search.maxThreads"] == 8 + assert written["files.maxMemoryForLargeFilesMB"] == 4096 + assert v.flags.read_text().strip() == "--enable-zero-copy" + + +def test_apply_variant_writes_nothing_in_dry_run( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """--dry-run reports the plan and leaves the disk untouched.""" + v = _variant(tmp_path) + + opt._apply_variant(v, _HW, ["--enable-zero-copy"], dry_run=True, auto_yes=True) + + assert not v.settings.exists() + assert not v.flags.exists() + assert "dry-run" in capsys.readouterr().out + + +def test_apply_variant_backs_up_an_existing_settings_file(tmp_path: Path) -> None: + """An existing file is copied aside before being overwritten.""" + v = _variant(tmp_path) + v.settings.parent.mkdir(parents=True) + v.settings.write_text('{"editor.fontSize": 14}') + + opt._apply_variant(v, _HW, [], dry_run=False, auto_yes=True) + + backups = list(v.settings.parent.glob("*.bak")) + assert len(backups) == 1 + assert json.loads(backups[0].read_text()) == {"editor.fontSize": 14} + assert json.loads(v.settings.read_text())["editor.fontSize"] == 14 + + +def test_apply_variant_backs_up_an_existing_flags_file(tmp_path: Path) -> None: + """The flags file gets the same backup treatment as settings.""" + v = _variant(tmp_path) + v.flags.write_text("--existing-flag\n") + + opt._apply_variant(v, _HW, ["--enable-zero-copy"], dry_run=False, auto_yes=True) + + assert list(tmp_path.glob("*.bak")) + assert "--existing-flag" in v.flags.read_text() + assert "--enable-zero-copy" in v.flags.read_text() + + +def test_apply_variant_does_nothing_when_already_optimized( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """A variant needing no changes is reported and skipped.""" + v = _variant(tmp_path) + opt._apply_variant(v, _HW, [], dry_run=False, auto_yes=True) + before = v.settings.read_text() + capsys.readouterr() + + opt._apply_variant(v, _HW, [], dry_run=False, auto_yes=True) + + assert v.settings.read_text() == before + assert "Nothing to do" in capsys.readouterr().out + + +def test_apply_variant_writes_only_flags_when_settings_are_current( + tmp_path: Path, +) -> None: + """A flags-only change leaves settings.json untouched, with no backup.""" + v = _variant(tmp_path) + opt._apply_variant(v, _HW, [], dry_run=False, auto_yes=True) + settings_before = v.settings.read_text() + + opt._apply_variant(v, _HW, ["--ignore-gpu-blocklist"], dry_run=False, auto_yes=True) + + assert v.settings.read_text() == settings_before + assert not list(v.settings.parent.glob("*.bak")) + assert "--ignore-gpu-blocklist" in v.flags.read_text() + + +@pytest.mark.parametrize("answer", ["y", "yes", "Y", "YES"]) +def test_apply_variant_applies_on_confirmation( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, answer: str +) -> None: + """Any spelling of yes at the prompt applies the change.""" + v = _variant(tmp_path) + monkeypatch.setattr(builtins, "input", lambda _p: answer) + + opt._apply_variant(v, _HW, [], dry_run=False, auto_yes=False) + + assert v.settings.exists() + + +@pytest.mark.parametrize("answer", ["n", "", "no", "maybe"]) +def test_apply_variant_skips_when_declined( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + answer: str, +) -> None: + """Anything other than yes leaves the disk untouched.""" + v = _variant(tmp_path) + monkeypatch.setattr(builtins, "input", lambda _p: answer) + + opt._apply_variant(v, _HW, [], dry_run=False, auto_yes=False) + + assert not v.settings.exists() + assert "Skipped" in capsys.readouterr().out + + +def test_apply_variant_adds_flags_without_duplicating_existing_ones( + tmp_path: Path, +) -> None: + """Merging flags preserves order and drops duplicates.""" + v = _variant(tmp_path) + v.flags.write_text("--enable-zero-copy\n--custom\n") + + opt._apply_variant( + v, + _HW, + ["--enable-zero-copy", "--ignore-gpu-blocklist"], + dry_run=False, + auto_yes=True, + ) + + assert v.flags.read_text().split() == [ + "--enable-zero-copy", + "--custom", + "--ignore-gpu-blocklist", + ] + + +# --------------------------------------------------------------------------- # +# main +# --------------------------------------------------------------------------- # +def test_main_optimizes_every_discovered_variant( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """The entry point detects hardware, then applies to each variant.""" + v = _variant(tmp_path) + monkeypatch.setattr(sys, "argv", ["optimize_vscode", "--yes"]) + monkeypatch.setattr(opt, "_detect_hardware", lambda: _HW) + monkeypatch.setattr(opt, "_discover_variants", lambda: [v]) + + opt.main() + + assert json.loads(v.settings.read_text())["search.maxThreads"] == 8 + + +def test_main_honours_dry_run(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """--dry-run reaches _apply_variant and prevents all writes.""" + v = _variant(tmp_path) + monkeypatch.setattr(sys, "argv", ["optimize_vscode", "--dry-run"]) + monkeypatch.setattr(opt, "_detect_hardware", lambda: _HW) + monkeypatch.setattr(opt, "_discover_variants", lambda: [v]) + + opt.main() + + assert not v.settings.exists() + + +def test_main_exits_nonzero_without_an_installation( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """No VS Code variant is a usage error, not a silent success.""" + monkeypatch.setattr(sys, "argv", ["optimize_vscode", "--dry-run"]) + monkeypatch.setattr(opt, "_detect_hardware", lambda: _HW) + monkeypatch.setattr(opt, "_discover_variants", list) + + with pytest.raises(SystemExit) as excinfo: + opt.main() + + assert excinfo.value.code == 1 + assert "No VS Code installation found" in capsys.readouterr().out + + +def test_the_module_entry_point_exposes_main() -> None: + """`python3 -m python_pkg.vscode_optimizer` resolves to the same main(). + + The script used to be run by path from meta/scripts/; this import is what + proves the replacement entry point works after the move. + """ + from python_pkg.vscode_optimizer import __main__ as entry + + assert entry.main is opt.main + + +def test_the_suite_never_touches_the_real_user_settings( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Guard rail: with Path.home redirected, discovery stays inside tmp_path. + + A regression here would mean some test could rewrite the developer's own + VS Code configuration, which is exactly what --dry-run exists to prevent. + """ + monkeypatch.setattr(opt.Path, "home", classmethod(lambda _cls: tmp_path)) + + for variant in opt._discover_variants(): + assert tmp_path in variant.settings.parents + assert tmp_path in variant.flags.parents diff --git a/python_pkg/vscode_optimizer/tests/test_plan.py b/python_pkg/vscode_optimizer/tests/test_plan.py new file mode 100644 index 00000000..76707a10 --- /dev/null +++ b/python_pkg/vscode_optimizer/tests/test_plan.py @@ -0,0 +1,210 @@ +"""Tests for the pure planning layer: memory sizing, dict merges, the +settings plan, and the Electron GPU flags. + +Nothing here touches the filesystem or a subprocess: every function under +test maps hardware plus current settings to a proposed change. +""" + +from __future__ import annotations + +import pytest + +from python_pkg.vscode_optimizer import _optimize as opt + + +def _opts_by_key(opts: list[opt._Opt]) -> dict[str, object]: + """Index a plan by setting key, for assertions that ignore ordering.""" + return {o.key: o.value for o in opts} + + +# --------------------------------------------------------------------------- # +# _ideal_mem +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize( + ("ram_mb", "expected"), + [ + (32000, 4096), + (28000, 4096), + (27999, 2048), + (14000, 2048), + (13999, 1024), + (7000, 1024), + (6999, 512), + (0, 512), + ], +) +def test_ideal_mem_picks_the_band_for_the_installed_ram( + ram_mb: int, expected: int +) -> None: + """Each threshold is inclusive, and anything below the last one defaults.""" + assert opt._ideal_mem(ram_mb) == expected + + +# --------------------------------------------------------------------------- # +# _dict_merge_opt +# --------------------------------------------------------------------------- # +def test_dict_merge_opt_returns_none_when_every_key_is_present() -> None: + """A superset of the ideal keys needs no change, whatever the values.""" + current = {"search.exclude": {"**/node_modules": True, "**/extra": False}} + + result = opt._dict_merge_opt( + current, "search.exclude", {"**/node_modules": True}, "reason" + ) + + assert result is None + + +def test_dict_merge_opt_preserves_user_entries_while_adding_missing_ones() -> None: + """The merge is additive: existing keys survive alongside the ideal set.""" + current = {"search.exclude": {"**/mine": True}} + + result = opt._dict_merge_opt( + current, "search.exclude", {"**/node_modules": True}, "reason" + ) + + assert result is not None + assert result.value == {"**/mine": True, "**/node_modules": True} + assert result.current == {"**/mine": True} + + +def test_dict_merge_opt_replaces_a_non_dict_current_value() -> None: + """A malformed setting (not an object) is treated as empty, not merged.""" + result = opt._dict_merge_opt( + {"search.exclude": "oops"}, "search.exclude", {"**/x": True}, "reason" + ) + + assert result is not None + assert result.value == {"**/x": True} + assert result.current is None + + +def test_dict_merge_opt_reports_no_current_value_when_the_key_is_absent() -> None: + """A missing key yields a plain add with nothing to show as 'before'.""" + result = opt._dict_merge_opt({}, "search.exclude", {"**/x": True}, "reason") + + assert result is not None + assert result.current is None + + +# --------------------------------------------------------------------------- # +# _compute_opts +# --------------------------------------------------------------------------- # +def test_compute_opts_scales_threads_and_memory_to_the_hardware() -> None: + """Physical cores drive search threads; RAM drives the large-file budget.""" + hw = opt._Hw(cpu_physical_cores=12, ram_total_mb=32000) + + plan = _opts_by_key(opt._compute_opts(hw, {})) + + assert plan["search.maxThreads"] == 12 + assert plan["files.maxMemoryForLargeFilesMB"] == 4096 + + +def test_compute_opts_keeps_a_floor_under_search_threads() -> None: + """A dual-core machine still gets the four-thread minimum.""" + hw = opt._Hw(cpu_physical_cores=2) + + plan = _opts_by_key(opt._compute_opts(hw, {})) + + assert plan["search.maxThreads"] == opt._MIN_THREADS + + +def test_compute_opts_enables_gpu_extras_for_a_dedicated_card() -> None: + """A discrete GPU turns on terminal acceleration and smooth scrolling.""" + hw = opt._Hw(gpu_vendor="NVIDIA") + + plan = _opts_by_key(opt._compute_opts(hw, {})) + + assert plan["terminal.integrated.gpuAcceleration"] == "on" + assert plan["editor.smoothScrolling"] is True + assert plan["workbench.list.smoothScrolling"] is True + assert plan["terminal.integrated.smoothScrolling"] is True + + +def test_compute_opts_leaves_gpu_extras_alone_without_a_dedicated_card() -> None: + """Integrated or unknown graphics get no GPU-dependent settings.""" + plan = _opts_by_key(opt._compute_opts(opt._Hw(gpu_vendor="Intel"), {})) + + assert "terminal.integrated.gpuAcceleration" not in plan + assert "editor.smoothScrolling" not in plan + + +def test_compute_opts_proposes_nothing_already_set() -> None: + """Settings that already hold the ideal value are not re-proposed.""" + hw = opt._Hw(cpu_physical_cores=8, ram_total_mb=32000) + current: dict[str, object] = { + "search.maxThreads": 8, + "files.maxMemoryForLargeFilesMB": 4096, + "search.followSymlinks": False, + "editor.guides.bracketPairs": "active", + "diffEditor.maxComputationTime": 0, + "editor.minimap.enabled": False, + "files.watcherExclude": dict(opt._WATCHER_EX), + "search.exclude": dict(opt._SEARCH_EX), + "git.detectSubmodulesLimit": opt._SUBMOD_LIMIT, + } + + assert opt._compute_opts(hw, current) == [] + + +@pytest.mark.parametrize( + ("current_limit", "proposed"), + [ + (None, True), + (10, True), + (opt._SUBMOD_LIMIT, False), + (99, False), + ("many", False), + ], +) +def test_compute_opts_raises_only_a_low_submodule_limit( + current_limit: object, *, proposed: bool +) -> None: + """The limit is raised when unset or too low, and left alone otherwise.""" + current: dict[str, object] = ( + {} if current_limit is None else {"git.detectSubmodulesLimit": current_limit} + ) + + plan = _opts_by_key(opt._compute_opts(opt._Hw(), current)) + + assert ("git.detectSubmodulesLimit" in plan) is proposed + + +def test_compute_opts_records_the_previous_value_for_display() -> None: + """Each proposed change carries the value it replaces, for the diff.""" + current: dict[str, object] = {"editor.minimap.enabled": True} + + minimap = next( + o + for o in opt._compute_opts(opt._Hw(), current) + if o.key == "editor.minimap.enabled" + ) + + assert minimap.current is True + assert minimap.value is False + + +# --------------------------------------------------------------------------- # +# _gpu_flags +# --------------------------------------------------------------------------- # +def test_gpu_flags_adds_a_video_decode_flag_only_for_nvidia() -> None: + """NVIDIA gets the VAAPI extras on top of the shared rasterization flags.""" + nvidia = opt._gpu_flags(opt._Hw(gpu_vendor="NVIDIA")) + amd = opt._gpu_flags(opt._Hw(gpu_vendor="AMD")) + + assert "--enable-gpu-rasterization" in nvidia + assert "--enable-zero-copy" in nvidia + assert any("VaapiVideoEncoder" in f for f in nvidia) + assert not any("VaapiVideoEncoder" in f for f in amd) + + +def test_gpu_flags_gives_intel_a_reduced_set() -> None: + """Intel gets rasterization and decode, but not zero-copy.""" + flags = opt._gpu_flags(opt._Hw(gpu_vendor="Intel")) + + assert "--enable-gpu-rasterization" in flags + assert "--enable-zero-copy" not in flags + + +def test_gpu_flags_are_empty_for_an_unknown_gpu() -> None: + """No flags are forced on hardware the script could not identify.""" + assert opt._gpu_flags(opt._Hw()) == [] diff --git a/python_pkg/vscode_optimizer/tests/test_render.py b/python_pkg/vscode_optimizer/tests/test_render.py new file mode 100644 index 00000000..03d2a224 --- /dev/null +++ b/python_pkg/vscode_optimizer/tests/test_render.py @@ -0,0 +1,118 @@ +"""Tests for the terminal output: the hardware summary and the plan listing. + +These assert on the informational content of each line rather than its exact +decoration, so the ANSI escapes stay free to change. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from python_pkg.vscode_optimizer import _optimize as opt + +if TYPE_CHECKING: + import pytest + + +def test_show_hw_reports_every_detected_component( + capsys: pytest.CaptureFixture[str], +) -> None: + """CPU, RAM, GPU and disk each appear, with RAM converted to GB.""" + hw = opt._Hw( + cpu_model="AMD Ryzen 9 7900X3D", + cpu_physical_cores=12, + cpu_logical_cores=24, + cpu_max_mhz=5662.6, + ram_total_mb=30000, + gpu_vendor="NVIDIA", + gpu_model="GeForce RTX 3090", + gpu_vram_mb=24576, + disk_type="nvme", + ) + + opt._show_hw(hw) + + out = capsys.readouterr().out + assert "AMD Ryzen 9 7900X3D" in out + assert "12 cores / 24 threads" in out + assert "5663 MHz" in out + assert "29 GB" in out + assert "NVIDIA - GeForce RTX 3090" in out + assert "24576 MB VRAM" in out + assert "NVME" in out + + +def test_show_hw_omits_vram_when_it_is_unknown( + capsys: pytest.CaptureFixture[str], +) -> None: + """A card whose VRAM could not be read shows no empty parentheses.""" + opt._show_hw(opt._Hw(gpu_vendor="AMD", gpu_model="Radeon")) + + assert "VRAM" not in capsys.readouterr().out + + +def test_show_plan_lists_new_flags_and_settings( + capsys: pytest.CaptureFixture[str], +) -> None: + """Each proposed change is numbered and shows before -> after.""" + opts = [ + opt._Opt( + key="editor.minimap.enabled", + value=False, + reason="Minimap costs GPU", + current=True, + ) + ] + + opt._show_plan(opts, ["--enable-zero-copy"], []) + + out = capsys.readouterr().out + assert "--enable-zero-copy" in out + assert "1. editor.minimap.enabled" in out + assert "Minimap costs GPU" in out + assert "true" in out + assert "false" in out + assert "2 change(s) proposed" in out + + +def test_show_plan_says_flags_are_already_present( + capsys: pytest.CaptureFixture[str], +) -> None: + """Flags that are all already set are reported, not listed as additions.""" + opt._show_plan([], ["--enable-zero-copy"], ["--enable-zero-copy"]) + + out = capsys.readouterr().out + assert "already present" in out + assert "+ --enable-zero-copy" not in out + + +def test_show_plan_reports_a_fully_optimized_configuration( + capsys: pytest.CaptureFixture[str], +) -> None: + """With nothing to change, the plan says so and proposes no count.""" + opt._show_plan([], [], []) + + out = capsys.readouterr().out + assert "All settings already optimized" in out + assert "change(s) proposed" not in out + + +def test_show_plan_renders_an_addition_with_no_previous_value( + capsys: pytest.CaptureFixture[str], +) -> None: + """A setting that was never set shows a dash as its 'before'.""" + opt._show_plan([opt._Opt("search.maxThreads", 12, "cores")], [], []) + + assert "-" in capsys.readouterr().out + + +def test_show_plan_truncates_very_long_values( + capsys: pytest.CaptureFixture[str], +) -> None: + """A large exclude dict is clipped so one setting cannot flood the screen.""" + big = {f"**/dir{i}": True for i in range(50)} + + opt._show_plan([opt._Opt("files.watcherExclude", big, "exclude", big)], [], []) + + for line in capsys.readouterr().out.splitlines(): + assert len(line) < 200