diff --git a/docs/superpowers/contracts/vscode-optimizer-split-2026-08-17.json b/docs/superpowers/contracts/vscode-optimizer-split-2026-08-17.json new file mode 100644 index 00000000..d8cebd9c --- /dev/null +++ b/docs/superpowers/contracts/vscode-optimizer-split-2026-08-17.json @@ -0,0 +1,18 @@ +{ + "title": "Split _optimize.py 498 -> six modules under the cap", + "objective": "python_pkg/vscode_optimizer/_optimize.py was the last python file over the 250-line cap at 498 lines. Now that it sits at 100% line and branch coverage, split it along its natural seams into shared types, hardware probes, the pure planning layer, filesystem I/O, terminal rendering, and the CLI entry. Code moves verbatim; the tests predate the seam and are re-pointed, not rewritten.", + "acceptance_criteria": [ + "Every resulting module is under 250 lines; the over-cap total falls 36 -> 35", + "All 26 top-level definitions are AST-identical to the pre-split file, proved by a throwaway ast.dump comparison against HEAD", + "The same 98 tests pass, with every module at 100% line and branch coverage", + "python3 -m python_pkg.vscode_optimizer --dry-run reproduces the same 18-change plan and writes nothing", + "Import sites name the owning module - no re-export barrels, no sys.path manipulation", + "No suppression of any kind: no noqa, no type: ignore, no lowered threshold" + ], + "out_of_scope": [ + "Fixing the _VENDOR_KW 'ati' substring bug, still pinned by a strict xfail and reported to the user", + "Any behaviour change: this commit is a pure relocation of definitions", + "The three transcribe_* files and the 29 over-cap shell scripts" + ], + "verifier": "A throwaway ast.dump identity check against HEAD for the verbatim claim; pytest python_pkg/vscode_optimizer/ --cov --cov-branch for tests and coverage; python3 -m python_pkg.vscode_optimizer --dry-run for the behaviour claim; bash ~/utils/scripts/check_file_length.sh --all for the cap; pre-commit run --files for lint." +} diff --git a/docs/superpowers/evidence/vscode-optimizer-split-2026-08-17.json b/docs/superpowers/evidence/vscode-optimizer-split-2026-08-17.json new file mode 100644 index 00000000..aa79a332 --- /dev/null +++ b/docs/superpowers/evidence/vscode-optimizer-split-2026-08-17.json @@ -0,0 +1,61 @@ +{ + "intent": "Split the 498-line _optimize.py into six modules, each well under the 250-line cap, moving every definition verbatim and leaving behaviour unchanged.", + "scope": [ + "python_pkg/vscode_optimizer/_optimize.py 498 -> 100 (CLI entry: _apply_variant, main)", + "New: _types.py 48, _render.py 69, _config_io.py 104, _hardware.py 106, _plan.py 156", + "Re-pointed imports and patch targets in the six existing test modules", + "Non-goal: any behaviour change, including the known _VENDOR_KW bug" + ], + "changes": [ + "Extracted the three dataclasses into _types.py so no module imports a sibling merely to name a type, which is what would have created a cycle.", + "Extracted _run and the four probes into _hardware.py, the pure planning functions into _plan.py, variant discovery plus JSONC parsing and all file reads/writes into _config_io.py, and the ANSI palette plus the printers into _render.py.", + "Definitions were moved with sed over line ranges rather than retyped, so no character could drift; the ast identity check below is what proves it.", + "Test imports now name the owning module (from ... import _plan as opt, and so on). No re-export barrels were added.", + "One patch target moved: test_main.py's guard-rail test patches _config_io.Path and calls _config_io._discover_variants, because that is the namespace resolving the name once it is called directly rather than through main(). The three patches inside main() correctly still target _optimize, whose namespace resolves them." + ], + "verification": [ + { + "command": "python3 verify_split.py HEAD python_pkg/vscode_optimizer/_optimize.py ", + "result": "pass", + "evidence": "IDENTICAL: all 26 top-level definitions moved verbatim. Each def/class was normalised through ast.dump(ast.parse(ast.unparse(node))) and compared to the pre-split file at HEAD, so the check is formatting-blind but logic-sensitive." + }, + { + "command": "AST scan for names used but not bound in each of the six modules", + "result": "pass", + "evidence": "Zero unbound names. Run after the first split attempt raised NameError: name '_BO' is not defined at _optimize.py:87 - _BO was used by main() but missing from the _render import list. Scanning all six modules at once was what confirmed nothing else was missing, rather than fixing one failure at a time." + }, + { + "command": "pytest python_pkg/vscode_optimizer/ --cov=python_pkg/vscode_optimizer --cov-branch --cov-report=term-missing", + "result": "pass", + "evidence": "98 passed, 1 xfailed - the same count as before the split, which is the discriminating check that no test file fell outside the runner's glob. Per-module: _types 26/0, _render 45/0, _plan 56/0, _optimize 58/0, _hardware 65/0, _config_io 66/0 statements missing, all 100% line and branch." + }, + { + "command": "python3 -m python_pkg.vscode_optimizer --dry-run", + "result": "pass", + "evidence": "Same output as the pre-split run: Ryzen 9 7900X3D / RTX 3090 / NVME detected, 18 change(s) proposed, '(dry-run) No files modified.' Every unit test stubs subprocess, so only this real run proves the six-module import graph resolves. Re-run after ruff moved _Hw/_Variant into TYPE_CHECKING, since that rewrite is a known source of runtime-only failures." + }, + { + "command": "bash ~/utils/scripts/check_file_length.sh --all", + "result": "pass", + "evidence": "35 files over cap, down from 36. Largest new module is _plan.py at 156; largest test file is test_main.py at 241. No file in this package is now over the cap." + }, + { + "command": "pre-commit run --files ", + "result": "pass", + "evidence": "All hooks pass, including ruff, ruff format, mypy, pylint, bandit and the pytest coverage gate. Ruff moved two annotation-only imports into TYPE_CHECKING; the suite and the real run were both repeated afterwards." + }, + { + "command": "pytest (whole repo, memory-capped)", + "result": "pass", + "evidence": "1846 passed at 100% total coverage, confirming the split did not disturb the repo-wide gate." + } + ], + "risks": [ + "A six-way split multiplies import edges. Mitigated by keeping _types.py dependency-free and _plan.py/_render.py importing nothing from siblings except types, so the graph is a tree rather than a web; the real run confirms it resolves.", + "Patch targets that moved could silently stop patching, letting a test call the real function. Mitigated: the suite was run after re-pointing and the count is unchanged at 98, and the one target that genuinely moved failed loudly with AttributeError rather than passing vacuously." + ], + "rollback": [ + "git revert the commit; _optimize.py returns to its 498-line form and the five new modules disappear.", + "After rollback, confirm python3 -m python_pkg.vscode_optimizer --dry-run still prints the 18-change plan and that pytest python_pkg/vscode_optimizer/ is still 98 passed at 100%." + ] +} diff --git a/python_pkg/vscode_optimizer/_config_io.py b/python_pkg/vscode_optimizer/_config_io.py new file mode 100644 index 00000000..cb420a52 --- /dev/null +++ b/python_pkg/vscode_optimizer/_config_io.py @@ -0,0 +1,104 @@ +"""Find VS Code installations and read and write their configuration. + +Settings are JSON with Comments, which the stdlib cannot parse, so this +module carries a small JSONC reader. Every write is preceded by a backup. +""" + +from __future__ import annotations + +from datetime import datetime, timezone +import json +from pathlib import Path +import re +import shutil + +from python_pkg.vscode_optimizer._types import _Opt, _Variant + + +def _discover_variants() -> list[_Variant]: + """Find all installed VS Code variants.""" + cfg = Path.home() / ".config" + cands = [ + ("VS Code (stable)", "Code", "code-flags.conf", "code"), + ( + "VS Code Insiders", + "Code - Insiders", + "code-insiders-flags.conf", + "code-insiders", + ), + ("VSCodium", "VSCodium", "vscodium-flags.conf", "codium"), + ] + found: list[_Variant] = [] + for name, dir_name, flags_name, binary in cands: + sp = cfg / dir_name / "User" / "settings.json" + fp = cfg / flags_name + if sp.exists() or shutil.which(binary): + found.append(_Variant(name, sp, fp, binary)) + return found + + +def _parse_jsonc(text: str) -> dict[str, object]: + """Parse JSON with Comments (JSONC) used by VS Code.""" + out: list[str] = [] + i, n = 0, len(text) + while i < n: + ch = text[i] + if ch == '"': + j = i + 1 + while j < n: + if text[j] == "\\": + j += 2 + continue + if text[j] == '"': + j += 1 + break + j += 1 + out.append(text[i:j]) + i = j + elif ch == "/" and i + 1 < n and text[i + 1] == "/": + while i < n and text[i] != "\n": + i += 1 + elif ch == "/" and i + 1 < n and text[i + 1] == "*": + end = text.find("*/", i + 2) + i = end + 2 if end != -1 else n + else: + out.append(ch) + i += 1 + cleaned = re.sub(r",(\s*[}\]])", r"\1", "".join(out)) + if not cleaned.strip(): + return {} + parsed: dict[str, object] = json.loads(cleaned) + return parsed + + +def _backup(path: Path) -> Path | None: + if not path.exists(): + return None + ts = datetime.now(tz=timezone.utc).strftime("%Y%m%dT%H%M%SZ") + dst = path.with_suffix(f".{ts}.bak") + shutil.copy2(path, dst) + return dst + + +def _read_settings(path: Path) -> dict[str, object]: + return _parse_jsonc(path.read_text()) if path.exists() else {} + + +def _write_settings(path: Path, current: dict[str, object], opts: list[_Opt]) -> None: + merged = {**current, **{o.key: o.value for o in opts}} + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(merged, indent=4, ensure_ascii=False) + "\n") + + +def _read_flags(path: Path) -> list[str]: + if not path.exists(): + return [] + return [ + ln.strip() + for ln in path.read_text().splitlines() + if ln.strip() and not ln.strip().startswith("#") + ] + + +def _write_flags(path: Path, flags: list[str]) -> None: + path.write_text("\n".join(flags) + "\n") diff --git a/python_pkg/vscode_optimizer/_hardware.py b/python_pkg/vscode_optimizer/_hardware.py new file mode 100644 index 00000000..31aea000 --- /dev/null +++ b/python_pkg/vscode_optimizer/_hardware.py @@ -0,0 +1,106 @@ +"""Probe the machine's CPU, RAM, GPU, and root disk. + +Every probe shells out and degrades to a default rather than raising, so a +missing tool narrows the report instead of failing the run. +""" + +from __future__ import annotations + +from pathlib import Path +import re +import subprocess + +from python_pkg.vscode_optimizer._types import _Hw + +_MIB_1024 = 1024 +_LSCPU = { + "Model name": "cpu_model", + "CPU(s)": "cpu_logical_cores", + "Core(s) per socket": "cpu_physical_cores", + "CPU max MHz": "cpu_max_mhz", +} +_VENDOR_KW = {"nvidia": "NVIDIA", "amd": "AMD", "ati": "AMD", "intel": "Intel"} + + +def _run(args: list[str]) -> str: + """Run *args* and return stdout, or ``""`` on failure.""" + try: + proc = subprocess.run( + args, + capture_output=True, + text=True, + timeout=10, + check=False, + ) + except (subprocess.TimeoutExpired, FileNotFoundError): + return "" + return proc.stdout.strip() + + +def _detect_cpu(hw: _Hw) -> None: + for line in _run(["lscpu"]).splitlines(): + key, _, val = line.partition(":") + attr = _LSCPU.get(key.strip()) + if attr == "cpu_model": + hw.cpu_model = val.strip() + elif attr == "cpu_max_mhz": + hw.cpu_max_mhz = float(val) + elif attr is not None: + setattr(hw, attr, int(val)) + + +def _detect_ram(hw: _Hw) -> None: + try: + meminfo = Path("/proc/meminfo").read_text(encoding="utf-8") + except OSError: + return + m = re.search(r"MemTotal:\s+(\d+)\s+kB", meminfo) + if m: + hw.ram_total_mb = int(m.group(1)) // _MIB_1024 + + +def _detect_gpu(hw: _Hw) -> None: + for line in _run(["lspci"]).splitlines(): + low = line.lower() + if "vga" not in low and "3d" not in low: + continue + hw.gpu_model = line.rsplit(":", maxsplit=1)[-1].strip() + for kw, vendor in _VENDOR_KW.items(): + if kw in low: + hw.gpu_vendor = vendor + break + if hw.gpu_vendor == "NVIDIA": + vram = _run( + [ + "nvidia-smi", + "--query-gpu=memory.total", + "--format=csv,noheader,nounits", + ] + ) + if vram: + hw.gpu_vram_mb = int(vram.split("\n")[0].strip()) + break + + +def _detect_disk(hw: _Hw) -> None: + root_dev = _run(["findmnt", "-n", "-o", "SOURCE", "/"]) + if not root_dev: + return + base = re.sub(r"p?\d+$", "", Path(root_dev).name) + rotational = Path(f"/sys/block/{base}/queue/rotational") + if not rotational.exists(): + return + if rotational.read_text(encoding="utf-8").strip() == "1": + hw.disk_type = "hdd" + elif "nvme" in base: + hw.disk_type = "nvme" + else: + hw.disk_type = "ssd" + + +def _detect_hardware() -> _Hw: + """Probe CPU, RAM, GPU, and root disk type.""" + hw = _Hw() + for fn in (_detect_cpu, _detect_ram, _detect_gpu, _detect_disk): + fn(hw) + return hw diff --git a/python_pkg/vscode_optimizer/_optimize.py b/python_pkg/vscode_optimizer/_optimize.py old mode 100755 new mode 100644 index 80969d1f..2cacdab9 --- a/python_pkg/vscode_optimizer/_optimize.py +++ b/python_pkg/vscode_optimizer/_optimize.py @@ -1,432 +1,37 @@ -#!/usr/bin/env python3 -"""Auto-optimize VS Code settings based on detected hardware.""" +"""Auto-optimize VS Code settings based on detected hardware. + +Run as ``python3 -m python_pkg.vscode_optimizer [--dry-run] [--yes]``. +""" from __future__ import annotations import argparse -from dataclasses import dataclass -from datetime import datetime, timezone -import json -from pathlib import Path -import re -import shutil -import subprocess import sys +from typing import TYPE_CHECKING -_RAM_THRESHOLDS = ((28000, 4096), (14000, 2048), (7000, 1024)) -_DEFAULT_MEM = 512 -_MIB_1024 = 1024 -_MIN_THREADS = 4 -_SUBMOD_LIMIT = 30 -_LSCPU = { - "Model name": "cpu_model", - "CPU(s)": "cpu_logical_cores", - "Core(s) per socket": "cpu_physical_cores", - "CPU max MHz": "cpu_max_mhz", -} -_VENDOR_KW = {"nvidia": "NVIDIA", "amd": "AMD", "ati": "AMD", "intel": "Intel"} -_WATCHER_EX: dict[str, bool] = dict.fromkeys( - [ - "**/.git/objects/**", - "**/.git/subtree-cache/**", - "**/node_modules/**", - "**/.venv/**", - "**/venv/**", - "**/__pycache__/**", - "**/build/**", - "**/.mypy_cache/**", - "**/.ruff_cache/**", - "**/.pytest_cache/**", - "**/dist/**", - "**/*.egg-info/**", - ], - True, +from python_pkg.vscode_optimizer._config_io import ( + _backup, + _discover_variants, + _read_flags, + _read_settings, + _write_flags, + _write_settings, ) -_SEARCH_EX: dict[str, bool] = dict.fromkeys( - [ - "**/node_modules", - "**/build", - "**/.venv", - "**/venv", - "**/__pycache__", - "**/.mypy_cache", - "**/.ruff_cache", - "**/.pytest_cache", - "**/dist", - ], - True, +from python_pkg.vscode_optimizer._hardware import _detect_hardware +from python_pkg.vscode_optimizer._plan import _compute_opts, _gpu_flags +from python_pkg.vscode_optimizer._render import ( + _BO, + _G, + _R, + _Y, + _hdr, + _out, + _show_hw, + _show_plan, ) -_B = "\033[94m" -_G = "\033[92m" -_Y = "\033[93m" -_C = "\033[96m" -_BO = "\033[1m" -_R = "\033[0m" - -@dataclass -class _Hw: - """Detected system hardware.""" - - cpu_model: str = "Unknown" - cpu_physical_cores: int = 1 - cpu_logical_cores: int = 1 - cpu_max_mhz: float = 0.0 - ram_total_mb: int = 0 - gpu_vendor: str = "Unknown" - gpu_model: str = "Unknown" - gpu_vram_mb: int = 0 - disk_type: str = "unknown" - - -@dataclass -class _Opt: - """Single proposed change.""" - - key: str - value: object - reason: str - current: object = None - - -@dataclass -class _Variant: - """A VS Code installation.""" - - name: str - settings: Path - flags: Path - binary: str - - -def _run(args: list[str]) -> str: - """Run *args* and return stdout, or ``""`` on failure.""" - try: - proc = subprocess.run( - args, - capture_output=True, - text=True, - timeout=10, - check=False, - ) - except (subprocess.TimeoutExpired, FileNotFoundError): - return "" - return proc.stdout.strip() - - -def _detect_cpu(hw: _Hw) -> None: - for line in _run(["lscpu"]).splitlines(): - key, _, val = line.partition(":") - attr = _LSCPU.get(key.strip()) - if attr == "cpu_model": - hw.cpu_model = val.strip() - elif attr == "cpu_max_mhz": - hw.cpu_max_mhz = float(val) - elif attr is not None: - setattr(hw, attr, int(val)) - - -def _detect_ram(hw: _Hw) -> None: - try: - meminfo = Path("/proc/meminfo").read_text(encoding="utf-8") - except OSError: - return - m = re.search(r"MemTotal:\s+(\d+)\s+kB", meminfo) - if m: - hw.ram_total_mb = int(m.group(1)) // _MIB_1024 - - -def _detect_gpu(hw: _Hw) -> None: - for line in _run(["lspci"]).splitlines(): - low = line.lower() - if "vga" not in low and "3d" not in low: - continue - hw.gpu_model = line.rsplit(":", maxsplit=1)[-1].strip() - for kw, vendor in _VENDOR_KW.items(): - if kw in low: - hw.gpu_vendor = vendor - break - if hw.gpu_vendor == "NVIDIA": - vram = _run( - [ - "nvidia-smi", - "--query-gpu=memory.total", - "--format=csv,noheader,nounits", - ] - ) - if vram: - hw.gpu_vram_mb = int(vram.split("\n")[0].strip()) - break - - -def _detect_disk(hw: _Hw) -> None: - root_dev = _run(["findmnt", "-n", "-o", "SOURCE", "/"]) - if not root_dev: - return - base = re.sub(r"p?\d+$", "", Path(root_dev).name) - rotational = Path(f"/sys/block/{base}/queue/rotational") - if not rotational.exists(): - return - if rotational.read_text(encoding="utf-8").strip() == "1": - hw.disk_type = "hdd" - elif "nvme" in base: - hw.disk_type = "nvme" - else: - hw.disk_type = "ssd" - - -def _detect_hardware() -> _Hw: - """Probe CPU, RAM, GPU, and root disk type.""" - hw = _Hw() - for fn in (_detect_cpu, _detect_ram, _detect_gpu, _detect_disk): - fn(hw) - return hw - - -def _discover_variants() -> list[_Variant]: - """Find all installed VS Code variants.""" - cfg = Path.home() / ".config" - cands = [ - ("VS Code (stable)", "Code", "code-flags.conf", "code"), - ( - "VS Code Insiders", - "Code - Insiders", - "code-insiders-flags.conf", - "code-insiders", - ), - ("VSCodium", "VSCodium", "vscodium-flags.conf", "codium"), - ] - found: list[_Variant] = [] - for name, dir_name, flags_name, binary in cands: - sp = cfg / dir_name / "User" / "settings.json" - fp = cfg / flags_name - if sp.exists() or shutil.which(binary): - found.append(_Variant(name, sp, fp, binary)) - return found - - -def _parse_jsonc(text: str) -> dict[str, object]: - """Parse JSON with Comments (JSONC) used by VS Code.""" - out: list[str] = [] - i, n = 0, len(text) - while i < n: - ch = text[i] - if ch == '"': - j = i + 1 - while j < n: - if text[j] == "\\": - j += 2 - continue - if text[j] == '"': - j += 1 - break - j += 1 - out.append(text[i:j]) - i = j - elif ch == "/" and i + 1 < n and text[i + 1] == "/": - while i < n and text[i] != "\n": - i += 1 - elif ch == "/" and i + 1 < n and text[i + 1] == "*": - end = text.find("*/", i + 2) - i = end + 2 if end != -1 else n - else: - out.append(ch) - i += 1 - cleaned = re.sub(r",(\s*[}\]])", r"\1", "".join(out)) - if not cleaned.strip(): - return {} - parsed: dict[str, object] = json.loads(cleaned) - return parsed - - -def _ideal_mem(ram_mb: int) -> int: - for threshold, value in _RAM_THRESHOLDS: - if ram_mb >= threshold: - return value - return _DEFAULT_MEM - - -def _dict_merge_opt( - cur_settings: dict[str, object], - key: str, - ideal: dict[str, bool], - reason: str, -) -> _Opt | None: - cur = cur_settings.get(key, {}) - if not isinstance(cur, dict): - cur = {} - if all(k in cur for k in ideal): - return None - return _Opt(key, {**cur, **ideal}, reason, cur or None) - - -def _compute_opts(hw: _Hw, cur: dict[str, object]) -> list[_Opt]: - """Return optimizations based on hardware and current settings.""" - opts: list[_Opt] = [] - - def _p(key: str, val: object, reason: str) -> None: - if cur.get(key) != val: - opts.append(_Opt(key, val, reason, cur.get(key))) - - threads = max(_MIN_THREADS, hw.cpu_physical_cores) - _p( - "search.maxThreads", - threads, - f"{hw.cpu_physical_cores} physical cores - use them for workspace search", - ) - mem = _ideal_mem(hw.ram_total_mb) - _p( - "files.maxMemoryForLargeFilesMB", - mem, - f"{hw.ram_total_mb // _MIB_1024} GB RAM - allow up to {mem} MB for large files", - ) - if hw.gpu_vendor in ("NVIDIA", "AMD"): - _p( - "terminal.integrated.gpuAcceleration", - "on", - f"{hw.gpu_vendor} GPU - enable GPU-rendered terminal", - ) - smooth = True - for key in ( - "editor.smoothScrolling", - "workbench.list.smoothScrolling", - "terminal.integrated.smoothScrolling", - ): - _p(key, smooth, "Smooth scrolling is free with a dedicated GPU") - no = False - _p("search.followSymlinks", no, "Avoid duplicate results and wasted I/O") - for result in ( - _dict_merge_opt( - cur, - "files.watcherExclude", - _WATCHER_EX, - "Exclude build/cache dirs from file watcher", - ), - _dict_merge_opt( - cur, "search.exclude", _SEARCH_EX, "Exclude build/cache dirs from search" - ), - ): - if result: - opts.extend([result]) - _p("editor.guides.bracketPairs", "active", "Lightweight visual aid") - _p( - "diffEditor.maxComputationTime", - 0, - f"Fast CPU ({hw.cpu_model}) - compute diffs fully", - ) - _p("editor.minimap.enabled", no, "Minimap consumes GPU/CPU for little benefit") - cur_sub = cur.get("git.detectSubmodulesLimit") - if cur_sub is None or (isinstance(cur_sub, int) and cur_sub < _SUBMOD_LIMIT): - opts.append( - _Opt( - "git.detectSubmodulesLimit", - _SUBMOD_LIMIT, - "Higher limit is fine with fast CPU", - cur_sub, - ) - ) - return opts - - -def _gpu_flags(hw: _Hw) -> list[str]: - """Return Electron flags appropriate for the detected GPU.""" - if hw.gpu_vendor in ("NVIDIA", "AMD"): - base = [ - "--enable-gpu-rasterization", - "--enable-zero-copy", - "--ignore-gpu-blocklist", - "--enable-features=CanvasOopRasterization", - ] - if hw.gpu_vendor == "NVIDIA": - base.append("--enable-features=VaapiVideoDecodeLinuxGL,VaapiVideoEncoder") - return base - if hw.gpu_vendor == "Intel": - return [ - "--enable-gpu-rasterization", - "--ignore-gpu-blocklist", - "--enable-features=VaapiVideoDecodeLinuxGL", - ] - return [] - - -def _backup(path: Path) -> Path | None: - if not path.exists(): - return None - ts = datetime.now(tz=timezone.utc).strftime("%Y%m%dT%H%M%SZ") - dst = path.with_suffix(f".{ts}.bak") - shutil.copy2(path, dst) - return dst - - -def _read_settings(path: Path) -> dict[str, object]: - return _parse_jsonc(path.read_text()) if path.exists() else {} - - -def _write_settings(path: Path, current: dict[str, object], opts: list[_Opt]) -> None: - merged = {**current, **{o.key: o.value for o in opts}} - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(merged, indent=4, ensure_ascii=False) + "\n") - - -def _read_flags(path: Path) -> list[str]: - if not path.exists(): - return [] - return [ - ln.strip() - for ln in path.read_text().splitlines() - if ln.strip() and not ln.strip().startswith("#") - ] - - -def _write_flags(path: Path, flags: list[str]) -> None: - path.write_text("\n".join(flags) + "\n") - - -def _out(text: str = "") -> None: - """Write a line to stdout.""" - sys.stdout.write(text + "\n") - - -def _hdr(text: str) -> None: - _out(f"\n{_BO}{_B}{'─' * 60}{_R}\n{_BO}{_B} {text}{_R}\n{_BO}{_B}{'─' * 60}{_R}") - - -def _show_hw(hw: _Hw) -> None: - _hdr("Detected Hardware") - _out(f" {_C}CPU{_R} {hw.cpu_model}") - _out( - f" {hw.cpu_physical_cores} cores / {hw.cpu_logical_cores} threads" - f" @ {hw.cpu_max_mhz:.0f} MHz" - ) - _out(f" {_C}RAM{_R} {hw.ram_total_mb // _MIB_1024} GB") - gpu = f" {_C}GPU{_R} {hw.gpu_vendor} - {hw.gpu_model}" - if hw.gpu_vram_mb: - gpu += f" ({hw.gpu_vram_mb} MB VRAM)" - _out(gpu) - _out(f" {_C}Disk{_R} {hw.disk_type.upper()}") - - -def _show_plan(opts: list[_Opt], new_fl: list[str], old_fl: list[str]) -> None: - _hdr("Optimization Plan") - added = [f for f in new_fl if f not in old_fl] - if added: - _out(f"\n {_BO}Electron flags to add:{_R}") - for fl in added: - _out(f" {_G}+ {fl}{_R}") - elif new_fl: - _out(f"\n {_Y}Electron GPU flags already present{_R}") - if opts: - _out(f"\n {_BO}Settings to change:{_R}") - for i, o in enumerate(opts, 1): - c = json.dumps(o.current)[:55] if o.current is not None else "-" - v = json.dumps(o.value)[:55] - _out(f"\n {_BO}{i}. {o.key}{_R}\n {o.reason}") - _out(f" {_Y}{c}{_R} -> {_G}{v}{_R}") - else: - _out(f"\n {_G}All settings already optimized{_R}") - total = len(opts) + len(added) - if total: - _out(f"\n {_BO}{total} change(s) proposed.{_R}") +if TYPE_CHECKING: + from python_pkg.vscode_optimizer._types import _Hw, _Variant def _apply_variant( diff --git a/python_pkg/vscode_optimizer/_plan.py b/python_pkg/vscode_optimizer/_plan.py new file mode 100644 index 00000000..158535d4 --- /dev/null +++ b/python_pkg/vscode_optimizer/_plan.py @@ -0,0 +1,156 @@ +"""Turn detected hardware plus current settings into a list of changes. + +Pure functions: nothing here reads the filesystem or shells out. Each +proposal carries the value it replaces, so the plan can be shown as a diff. +""" + +from __future__ import annotations + +from python_pkg.vscode_optimizer._types import _Hw, _Opt + +_RAM_THRESHOLDS = ((28000, 4096), (14000, 2048), (7000, 1024)) +_DEFAULT_MEM = 512 +_MIB_1024 = 1024 +_MIN_THREADS = 4 +_SUBMOD_LIMIT = 30 +_WATCHER_EX: dict[str, bool] = dict.fromkeys( + [ + "**/.git/objects/**", + "**/.git/subtree-cache/**", + "**/node_modules/**", + "**/.venv/**", + "**/venv/**", + "**/__pycache__/**", + "**/build/**", + "**/.mypy_cache/**", + "**/.ruff_cache/**", + "**/.pytest_cache/**", + "**/dist/**", + "**/*.egg-info/**", + ], + True, +) +_SEARCH_EX: dict[str, bool] = dict.fromkeys( + [ + "**/node_modules", + "**/build", + "**/.venv", + "**/venv", + "**/__pycache__", + "**/.mypy_cache", + "**/.ruff_cache", + "**/.pytest_cache", + "**/dist", + ], + True, +) + + +def _ideal_mem(ram_mb: int) -> int: + for threshold, value in _RAM_THRESHOLDS: + if ram_mb >= threshold: + return value + return _DEFAULT_MEM + + +def _dict_merge_opt( + cur_settings: dict[str, object], + key: str, + ideal: dict[str, bool], + reason: str, +) -> _Opt | None: + cur = cur_settings.get(key, {}) + if not isinstance(cur, dict): + cur = {} + if all(k in cur for k in ideal): + return None + return _Opt(key, {**cur, **ideal}, reason, cur or None) + + +def _compute_opts(hw: _Hw, cur: dict[str, object]) -> list[_Opt]: + """Return optimizations based on hardware and current settings.""" + opts: list[_Opt] = [] + + def _p(key: str, val: object, reason: str) -> None: + if cur.get(key) != val: + opts.append(_Opt(key, val, reason, cur.get(key))) + + threads = max(_MIN_THREADS, hw.cpu_physical_cores) + _p( + "search.maxThreads", + threads, + f"{hw.cpu_physical_cores} physical cores - use them for workspace search", + ) + mem = _ideal_mem(hw.ram_total_mb) + _p( + "files.maxMemoryForLargeFilesMB", + mem, + f"{hw.ram_total_mb // _MIB_1024} GB RAM - allow up to {mem} MB for large files", + ) + if hw.gpu_vendor in ("NVIDIA", "AMD"): + _p( + "terminal.integrated.gpuAcceleration", + "on", + f"{hw.gpu_vendor} GPU - enable GPU-rendered terminal", + ) + smooth = True + for key in ( + "editor.smoothScrolling", + "workbench.list.smoothScrolling", + "terminal.integrated.smoothScrolling", + ): + _p(key, smooth, "Smooth scrolling is free with a dedicated GPU") + no = False + _p("search.followSymlinks", no, "Avoid duplicate results and wasted I/O") + for result in ( + _dict_merge_opt( + cur, + "files.watcherExclude", + _WATCHER_EX, + "Exclude build/cache dirs from file watcher", + ), + _dict_merge_opt( + cur, "search.exclude", _SEARCH_EX, "Exclude build/cache dirs from search" + ), + ): + if result: + opts.extend([result]) + _p("editor.guides.bracketPairs", "active", "Lightweight visual aid") + _p( + "diffEditor.maxComputationTime", + 0, + f"Fast CPU ({hw.cpu_model}) - compute diffs fully", + ) + _p("editor.minimap.enabled", no, "Minimap consumes GPU/CPU for little benefit") + cur_sub = cur.get("git.detectSubmodulesLimit") + if cur_sub is None or (isinstance(cur_sub, int) and cur_sub < _SUBMOD_LIMIT): + opts.append( + _Opt( + "git.detectSubmodulesLimit", + _SUBMOD_LIMIT, + "Higher limit is fine with fast CPU", + cur_sub, + ) + ) + return opts + + +def _gpu_flags(hw: _Hw) -> list[str]: + """Return Electron flags appropriate for the detected GPU.""" + if hw.gpu_vendor in ("NVIDIA", "AMD"): + base = [ + "--enable-gpu-rasterization", + "--enable-zero-copy", + "--ignore-gpu-blocklist", + "--enable-features=CanvasOopRasterization", + ] + if hw.gpu_vendor == "NVIDIA": + base.append("--enable-features=VaapiVideoDecodeLinuxGL,VaapiVideoEncoder") + return base + if hw.gpu_vendor == "Intel": + return [ + "--enable-gpu-rasterization", + "--ignore-gpu-blocklist", + "--enable-features=VaapiVideoDecodeLinuxGL", + ] + return [] diff --git a/python_pkg/vscode_optimizer/_render.py b/python_pkg/vscode_optimizer/_render.py new file mode 100644 index 00000000..870a2725 --- /dev/null +++ b/python_pkg/vscode_optimizer/_render.py @@ -0,0 +1,69 @@ +"""Render the hardware summary and the optimization plan to the terminal. + +The only module that owns the ANSI colour codes, so changing the palette +touches one file. +""" + +from __future__ import annotations + +import json +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from python_pkg.vscode_optimizer._types import _Hw, _Opt + +_MIB_1024 = 1024 +_B = "\033[94m" +_G = "\033[92m" +_Y = "\033[93m" +_C = "\033[96m" +_BO = "\033[1m" +_R = "\033[0m" + + +def _out(text: str = "") -> None: + """Write a line to stdout.""" + sys.stdout.write(text + "\n") + + +def _hdr(text: str) -> None: + _out(f"\n{_BO}{_B}{'─' * 60}{_R}\n{_BO}{_B} {text}{_R}\n{_BO}{_B}{'─' * 60}{_R}") + + +def _show_hw(hw: _Hw) -> None: + _hdr("Detected Hardware") + _out(f" {_C}CPU{_R} {hw.cpu_model}") + _out( + f" {hw.cpu_physical_cores} cores / {hw.cpu_logical_cores} threads" + f" @ {hw.cpu_max_mhz:.0f} MHz" + ) + _out(f" {_C}RAM{_R} {hw.ram_total_mb // _MIB_1024} GB") + gpu = f" {_C}GPU{_R} {hw.gpu_vendor} - {hw.gpu_model}" + if hw.gpu_vram_mb: + gpu += f" ({hw.gpu_vram_mb} MB VRAM)" + _out(gpu) + _out(f" {_C}Disk{_R} {hw.disk_type.upper()}") + + +def _show_plan(opts: list[_Opt], new_fl: list[str], old_fl: list[str]) -> None: + _hdr("Optimization Plan") + added = [f for f in new_fl if f not in old_fl] + if added: + _out(f"\n {_BO}Electron flags to add:{_R}") + for fl in added: + _out(f" {_G}+ {fl}{_R}") + elif new_fl: + _out(f"\n {_Y}Electron GPU flags already present{_R}") + if opts: + _out(f"\n {_BO}Settings to change:{_R}") + for i, o in enumerate(opts, 1): + c = json.dumps(o.current)[:55] if o.current is not None else "-" + v = json.dumps(o.value)[:55] + _out(f"\n {_BO}{i}. {o.key}{_R}\n {o.reason}") + _out(f" {_Y}{c}{_R} -> {_G}{v}{_R}") + else: + _out(f"\n {_G}All settings already optimized{_R}") + total = len(opts) + len(added) + if total: + _out(f"\n {_BO}{total} change(s) proposed.{_R}") diff --git a/python_pkg/vscode_optimizer/_types.py b/python_pkg/vscode_optimizer/_types.py new file mode 100644 index 00000000..11d3c946 --- /dev/null +++ b/python_pkg/vscode_optimizer/_types.py @@ -0,0 +1,48 @@ +"""Detected hardware, proposed changes, and the VS Code installations found. + +These three dataclasses are the vocabulary the other modules share, kept +apart from them so nothing has to import a sibling just to name a type. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + + +@dataclass +class _Hw: + """Detected system hardware.""" + + cpu_model: str = "Unknown" + cpu_physical_cores: int = 1 + cpu_logical_cores: int = 1 + cpu_max_mhz: float = 0.0 + ram_total_mb: int = 0 + gpu_vendor: str = "Unknown" + gpu_model: str = "Unknown" + gpu_vram_mb: int = 0 + disk_type: str = "unknown" + + +@dataclass +class _Opt: + """Single proposed change.""" + + key: str + value: object + reason: str + current: object = None + + +@dataclass +class _Variant: + """A VS Code installation.""" + + name: str + settings: Path + flags: Path + binary: str diff --git a/python_pkg/vscode_optimizer/tests/test_gpu_disk.py b/python_pkg/vscode_optimizer/tests/test_gpu_disk.py index cfbf68e4..9ea13d4a 100644 --- a/python_pkg/vscode_optimizer/tests/test_gpu_disk.py +++ b/python_pkg/vscode_optimizer/tests/test_gpu_disk.py @@ -12,7 +12,8 @@ from typing import TYPE_CHECKING import pytest -from python_pkg.vscode_optimizer import _optimize as opt +from python_pkg.vscode_optimizer import _hardware as opt +from python_pkg.vscode_optimizer._types import _Hw from python_pkg.vscode_optimizer.tests.conftest import fake_run as _fake_run if TYPE_CHECKING: @@ -35,7 +36,7 @@ def test_detect_gpu_reads_nvidia_vram(monkeypatch: pytest.MonkeyPatch) -> None: "run", _fake_run({"lspci": _NVIDIA_LINE, "nvidia-smi": "24576\n"}), ) - hw = opt._Hw() + hw = _Hw() opt._detect_gpu(hw) @@ -49,7 +50,7 @@ def test_detect_gpu_tolerates_nvidia_smi_being_absent( ) -> None: """Without nvidia-smi the vendor is still known; VRAM stays unset.""" monkeypatch.setattr(subprocess, "run", _fake_run({"lspci": _NVIDIA_LINE})) - hw = opt._Hw() + hw = _Hw() opt._detect_gpu(hw) @@ -76,7 +77,7 @@ def test_detect_gpu_maps_each_vendor_keyword( ) -> None: """Vendor keywords map to display names; 3D controllers count as GPUs.""" monkeypatch.setattr(subprocess, "run", _fake_run({"lspci": line})) - hw = opt._Hw() + hw = _Hw() opt._detect_gpu(hw) @@ -100,7 +101,7 @@ def test_detect_gpu_should_recognise_an_intel_igpu( """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() + hw = _Hw() opt._detect_gpu(hw) @@ -114,7 +115,7 @@ def test_detect_gpu_leaves_defaults_when_lspci_lists_no_display_device( monkeypatch.setattr( subprocess, "run", _fake_run({"lspci": "00:1f.3 Audio device: Intel\n"}) ) - hw = opt._Hw() + hw = _Hw() opt._detect_gpu(hw) @@ -130,7 +131,7 @@ def test_detect_gpu_skips_non_display_devices( monkeypatch.setattr( subprocess, "run", _fake_run({"lspci": listing, "nvidia-smi": "24576"}) ) - hw = opt._Hw() + hw = _Hw() opt._detect_gpu(hw) @@ -179,7 +180,7 @@ def test_detect_disk_classifies_the_root_device( """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() + hw = _Hw() opt._detect_disk(hw) @@ -191,7 +192,7 @@ def test_detect_disk_gives_up_when_findmnt_says_nothing( ) -> None: """No root device means the type stays unknown.""" monkeypatch.setattr(subprocess, "run", _fake_run({})) - hw = opt._Hw() + hw = _Hw() opt._detect_disk(hw) @@ -204,7 +205,7 @@ def test_detect_disk_gives_up_without_a_rotational_flag( """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() + hw = _Hw() opt._detect_disk(hw) @@ -218,10 +219,10 @@ def test_detect_hardware_runs_every_probe(monkeypatch: pytest.MonkeyPatch) -> No """The aggregate calls all four detectors against one _Hw.""" called: list[str] = [] - def _recorder(name: str) -> Callable[[opt._Hw], None]: + def _recorder(name: str) -> Callable[[_Hw], None]: """Build a probe double that records the name it stands in for.""" - def _probe(_hw: opt._Hw) -> None: + def _probe(_hw: _Hw) -> None: called.append(name) return _probe @@ -231,5 +232,5 @@ def test_detect_hardware_runs_every_probe(monkeypatch: pytest.MonkeyPatch) -> No result = opt._detect_hardware() - assert isinstance(result, opt._Hw) + assert isinstance(result, _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 index 9e0be1f0..e8ecfbd9 100644 --- a/python_pkg/vscode_optimizer/tests/test_hw.py +++ b/python_pkg/vscode_optimizer/tests/test_hw.py @@ -12,7 +12,8 @@ from typing import TYPE_CHECKING import pytest -from python_pkg.vscode_optimizer import _optimize as opt +from python_pkg.vscode_optimizer import _hardware as opt +from python_pkg.vscode_optimizer._types import _Hw from python_pkg.vscode_optimizer.tests.conftest import FakeProc as _Proc from python_pkg.vscode_optimizer.tests.conftest import fake_run as _fake_run @@ -64,7 +65,7 @@ def test_detect_cpu_reads_model_cores_and_clock( ) -> None: """Each recognised lscpu key lands on its typed _Hw attribute.""" monkeypatch.setattr(subprocess, "run", _fake_run({"lscpu": _LSCPU_OUT})) - hw = opt._Hw() + hw = _Hw() opt._detect_cpu(hw) @@ -81,7 +82,7 @@ def test_detect_cpu_ignores_unknown_keys(monkeypatch: pytest.MonkeyPatch) -> Non "run", _fake_run({"lscpu": "Architecture: x86_64\nBogoMIPS: 9000\n"}), ) - hw = opt._Hw() + hw = _Hw() opt._detect_cpu(hw) @@ -99,7 +100,7 @@ def test_detect_ram_converts_kb_to_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() + hw = _Hw() opt._detect_ram(hw) @@ -113,7 +114,7 @@ def test_detect_ram_leaves_zero_when_memtotal_is_absent( meminfo = tmp_path / "meminfo" meminfo.write_text("SwapTotal: 4096 kB\n") monkeypatch.setattr(opt, "Path", lambda _p: meminfo) - hw = opt._Hw() + hw = _Hw() opt._detect_ram(hw) @@ -125,7 +126,7 @@ def test_detect_ram_survives_an_unreadable_procfs( ) -> 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() + hw = _Hw() opt._detect_ram(hw) diff --git a/python_pkg/vscode_optimizer/tests/test_io.py b/python_pkg/vscode_optimizer/tests/test_io.py index bbe35974..f5e1a289 100644 --- a/python_pkg/vscode_optimizer/tests/test_io.py +++ b/python_pkg/vscode_optimizer/tests/test_io.py @@ -13,7 +13,8 @@ from typing import TYPE_CHECKING import pytest -from python_pkg.vscode_optimizer import _optimize as opt +from python_pkg.vscode_optimizer import _config_io as opt +from python_pkg.vscode_optimizer._types import _Opt if TYPE_CHECKING: from pathlib import Path @@ -183,8 +184,8 @@ def test_write_settings_merges_the_plan_over_the_current_values( 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("a", 2, "reason"), + _Opt(key="b", value=True, reason="reason"), ] opt._write_settings(path, current, opts) diff --git a/python_pkg/vscode_optimizer/tests/test_main.py b/python_pkg/vscode_optimizer/tests/test_main.py index 9a82bf20..c8378157 100644 --- a/python_pkg/vscode_optimizer/tests/test_main.py +++ b/python_pkg/vscode_optimizer/tests/test_main.py @@ -14,15 +14,17 @@ from typing import TYPE_CHECKING import pytest +from python_pkg.vscode_optimizer import _config_io as config_io from python_pkg.vscode_optimizer import _optimize as opt +from python_pkg.vscode_optimizer._types import _Hw, _Variant if TYPE_CHECKING: from pathlib import Path -def _variant(tmp_path: Path) -> opt._Variant: +def _variant(tmp_path: Path) -> _Variant: """A VS Code variant rooted entirely inside tmp_path.""" - return opt._Variant( + return _Variant( name="Test Code", settings=tmp_path / "User" / "settings.json", flags=tmp_path / "code-flags.conf", @@ -30,7 +32,7 @@ def _variant(tmp_path: Path) -> opt._Variant: ) -_HW = opt._Hw(cpu_physical_cores=8, ram_total_mb=32000, gpu_vendor="NVIDIA") +_HW = _Hw(cpu_physical_cores=8, ram_total_mb=32000, gpu_vendor="NVIDIA") # --------------------------------------------------------------------------- # @@ -232,8 +234,8 @@ def test_the_suite_never_touches_the_real_user_settings( 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)) + monkeypatch.setattr(config_io.Path, "home", classmethod(lambda _cls: tmp_path)) - for variant in opt._discover_variants(): + for variant in config_io._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 index 76707a10..7673c5fc 100644 --- a/python_pkg/vscode_optimizer/tests/test_plan.py +++ b/python_pkg/vscode_optimizer/tests/test_plan.py @@ -9,10 +9,11 @@ from __future__ import annotations import pytest -from python_pkg.vscode_optimizer import _optimize as opt +from python_pkg.vscode_optimizer import _plan as opt +from python_pkg.vscode_optimizer._types import _Hw, _Opt -def _opts_by_key(opts: list[opt._Opt]) -> dict[str, object]: +def _opts_by_key(opts: list[_Opt]) -> dict[str, object]: """Index a plan by setting key, for assertions that ignore ordering.""" return {o.key: o.value for o in opts} @@ -91,7 +92,7 @@ def test_dict_merge_opt_reports_no_current_value_when_the_key_is_absent() -> Non # --------------------------------------------------------------------------- # 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) + hw = _Hw(cpu_physical_cores=12, ram_total_mb=32000) plan = _opts_by_key(opt._compute_opts(hw, {})) @@ -101,7 +102,7 @@ def test_compute_opts_scales_threads_and_memory_to_the_hardware() -> None: 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) + hw = _Hw(cpu_physical_cores=2) plan = _opts_by_key(opt._compute_opts(hw, {})) @@ -110,7 +111,7 @@ def test_compute_opts_keeps_a_floor_under_search_threads() -> None: 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") + hw = _Hw(gpu_vendor="NVIDIA") plan = _opts_by_key(opt._compute_opts(hw, {})) @@ -122,7 +123,7 @@ def test_compute_opts_enables_gpu_extras_for_a_dedicated_card() -> None: 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"), {})) + plan = _opts_by_key(opt._compute_opts(_Hw(gpu_vendor="Intel"), {})) assert "terminal.integrated.gpuAcceleration" not in plan assert "editor.smoothScrolling" not in plan @@ -130,7 +131,7 @@ def test_compute_opts_leaves_gpu_extras_alone_without_a_dedicated_card() -> None 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) + hw = _Hw(cpu_physical_cores=8, ram_total_mb=32000) current: dict[str, object] = { "search.maxThreads": 8, "files.maxMemoryForLargeFilesMB": 4096, @@ -164,7 +165,7 @@ def test_compute_opts_raises_only_a_low_submodule_limit( {} if current_limit is None else {"git.detectSubmodulesLimit": current_limit} ) - plan = _opts_by_key(opt._compute_opts(opt._Hw(), current)) + plan = _opts_by_key(opt._compute_opts(_Hw(), current)) assert ("git.detectSubmodulesLimit" in plan) is proposed @@ -175,7 +176,7 @@ def test_compute_opts_records_the_previous_value_for_display() -> None: minimap = next( o - for o in opt._compute_opts(opt._Hw(), current) + for o in opt._compute_opts(_Hw(), current) if o.key == "editor.minimap.enabled" ) @@ -188,8 +189,8 @@ def test_compute_opts_records_the_previous_value_for_display() -> None: # --------------------------------------------------------------------------- # 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")) + nvidia = opt._gpu_flags(_Hw(gpu_vendor="NVIDIA")) + amd = opt._gpu_flags(_Hw(gpu_vendor="AMD")) assert "--enable-gpu-rasterization" in nvidia assert "--enable-zero-copy" in nvidia @@ -199,7 +200,7 @@ def test_gpu_flags_adds_a_video_decode_flag_only_for_nvidia() -> None: 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")) + flags = opt._gpu_flags(_Hw(gpu_vendor="Intel")) assert "--enable-gpu-rasterization" in flags assert "--enable-zero-copy" not in flags @@ -207,4 +208,4 @@ def test_gpu_flags_gives_intel_a_reduced_set() -> None: 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()) == [] + assert opt._gpu_flags(_Hw()) == [] diff --git a/python_pkg/vscode_optimizer/tests/test_render.py b/python_pkg/vscode_optimizer/tests/test_render.py index 03d2a224..e33283ea 100644 --- a/python_pkg/vscode_optimizer/tests/test_render.py +++ b/python_pkg/vscode_optimizer/tests/test_render.py @@ -8,7 +8,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -from python_pkg.vscode_optimizer import _optimize as opt +from python_pkg.vscode_optimizer import _render as opt +from python_pkg.vscode_optimizer._types import _Hw, _Opt if TYPE_CHECKING: import pytest @@ -18,7 +19,7 @@ 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( + hw = _Hw( cpu_model="AMD Ryzen 9 7900X3D", cpu_physical_cores=12, cpu_logical_cores=24, @@ -46,7 +47,7 @@ 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")) + opt._show_hw(_Hw(gpu_vendor="AMD", gpu_model="Radeon")) assert "VRAM" not in capsys.readouterr().out @@ -56,7 +57,7 @@ def test_show_plan_lists_new_flags_and_settings( ) -> None: """Each proposed change is numbered and shows before -> after.""" opts = [ - opt._Opt( + _Opt( key="editor.minimap.enabled", value=False, reason="Minimap costs GPU", @@ -101,7 +102,7 @@ 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")], [], []) + opt._show_plan([_Opt("search.maxThreads", 12, "cores")], [], []) assert "-" in capsys.readouterr().out @@ -112,7 +113,7 @@ def test_show_plan_truncates_very_long_values( """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)], [], []) + opt._show_plan([_Opt("files.watcherExclude", big, "exclude", big)], [], []) for line in capsys.readouterr().out.splitlines(): assert len(line) < 200