feat(m6): PowerMizer + Wine/Steam versions + non-Steam launchers — 0.22.0
M6 leftovers (the watcher defers to M9's trigger-mode work): - gameenv: check_gpu_powermizer (NVIDIA, X; degrades when the gpu target won't resolve), check_wine (wine --version), check_steam_client (dpkg package version); steam.client_version() helper. - core/launchers.py: detect Lutris (read-only SQLite pga.db) and Heroic (Epic legendary + GOG JSON) installed games; Game gained a `launcher` field. - Games page + `rigdoctor games` list non-Steam games alongside Steam, tagged by launcher; Run Diagnostic works on them (auto-launch stays Steam-only). - Tests for launchers (synthetic Lutris db + Heroic json). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,19 @@ All notable changes to RigDoctor are recorded here. Format follows
|
||||
(`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git
|
||||
release tag (so the auto-updater, D18, can compare versions).
|
||||
|
||||
## [0.22.0] - 2026-05-22
|
||||
### Added
|
||||
- **M6 breadth.** Environment checks now also report **GPU PowerMizer** mode (NVIDIA, X — flags
|
||||
Adaptive/Auto and suggests Prefer-Max-Performance), the **Wine** version, and the **Steam
|
||||
client** version.
|
||||
- **Non-Steam launchers.** Lutris (its SQLite library) and Heroic (Epic + GOG JSON stores) are
|
||||
detected (`core/launchers.py`) and listed on the Games page and `rigdoctor games`, tagged by
|
||||
launcher. You can Run Diagnostic on them too (records while you play; auto-launch stays
|
||||
Steam-only).
|
||||
### Notes
|
||||
- The zero-config game watcher (D12 fallback) is deferred to the M9 trigger-mode work, where the
|
||||
service integration lives.
|
||||
|
||||
## [0.21.0] - 2026-05-22
|
||||
### Added
|
||||
- **Live monitor TUI (M2).** `rigdoctor monitor` is now a proper **curses** dashboard:
|
||||
|
||||
+4
-2
@@ -67,8 +67,10 @@ Status: ⬜ not started · 🟦 designing · 🟨 in progress · ✅ done
|
||||
banner → results dialog). **Auto-capture** via the D12 wrapper (`rigdoctor wrap %command%`,
|
||||
`core/wrap.py`; GUI "Auto-capture…" helper). **Hard crashes are detected** (capture left
|
||||
without a clean stop) and flagged on next launch with a crash-boot kernel-log analysis
|
||||
(`pending_crash`/`analyze_crash` + `health.check_previous_boot`). *Pending:* non-Steam
|
||||
launchers (Lutris/Heroic), GPU power-profile (PowerMizer) checks, and the zero-config watcher.
|
||||
(`pending_crash`/`analyze_crash` + `health.check_previous_boot`). **Non-Steam launchers**
|
||||
(Lutris SQLite + Heroic JSON, `core/launchers.py`) are detected and listed alongside Steam
|
||||
games; env checks also cover **GPU PowerMizer** (X), **Wine** and **Steam-client** versions.
|
||||
*Pending:* the zero-config watcher (D12 fallback) — landing with M9's trigger-mode work.
|
||||
- **M8 Alerting** — threshold/event notifications; integrates with the tray applet (M11).
|
||||
- **M10 Desktop GUI** — PySide6 graphical front-end over the core engine. Optional; adds the
|
||||
Qt dependency. Dark-themed window with a **grouped sidebar** (Monitor / Diagnose / System /
|
||||
|
||||
+3
-1
@@ -34,7 +34,9 @@ Ubuntu + NVIDIA first; `.deb` distribution (see `DECISIONS.md`).
|
||||
This is also the D12 "pick a game" foundation. *Env-check engine done* (`rigdoctor gameenv`
|
||||
+ GUI Environment page): PCIe ASPM, NVIDIA persistence, CPU governor, GameMode, MangoHud,
|
||||
swappiness, shader cache, THP, mitigations, Proton versions — read-only with fix commands.
|
||||
*Pending:* non-Steam launchers (Lutris/Heroic) + GPU power-profile (PowerMizer) checks.
|
||||
Also: GPU PowerMizer (X), Wine + Steam-client versions, and non-Steam launchers
|
||||
(Lutris/Heroic, `core/launchers.py`). *Pending:* the zero-config watcher (D12 fallback,
|
||||
lands with M9's trigger-mode work).
|
||||
- [ ] SMART integration (smartmontools if present)
|
||||
|
||||
## Phase 4 — Desktop UI & installer
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rigdoctor"
|
||||
version = "0.21.0"
|
||||
version = "0.22.0"
|
||||
description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
||||
|
||||
__version__ = "0.21.0"
|
||||
__version__ = "0.22.0"
|
||||
|
||||
+29
-22
@@ -431,34 +431,41 @@ def cmd_gameenv(args) -> int:
|
||||
|
||||
|
||||
def cmd_games(args) -> int:
|
||||
from .core import steam
|
||||
|
||||
selected = steam.selected_library_paths()
|
||||
if not selected:
|
||||
print("No Steam libraries selected to scan.")
|
||||
print(" See them with: rigdoctor games libraries")
|
||||
print(" Then enable one: rigdoctor games libraries --enable <path> (or --all)")
|
||||
return 1
|
||||
result = steam.rescan()
|
||||
if args.json:
|
||||
from dataclasses import asdict
|
||||
|
||||
from .core import launchers, steam
|
||||
|
||||
selected = steam.selected_library_paths()
|
||||
result = steam.rescan() if selected else None
|
||||
steam_games = result.games if result else []
|
||||
extra = launchers.scan() # non-Steam (Lutris/Heroic)
|
||||
all_games = list(steam_games) + list(extra)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps({
|
||||
"scanned_at": result.scanned_at,
|
||||
"new_appids": result.new_appids,
|
||||
"games": [asdict(g) for g in result.games],
|
||||
"scanned_at": result.scanned_at if result else None,
|
||||
"new_appids": result.new_appids if result else [],
|
||||
"games": [asdict(g) for g in all_games],
|
||||
}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
if not result.games:
|
||||
print("No games found in the selected Steam libraries.")
|
||||
|
||||
if not all_games:
|
||||
if not selected:
|
||||
print("No Steam libraries selected and no non-Steam games found.")
|
||||
print(" Pick a Steam library: rigdoctor games libraries --enable <path> (or --all)")
|
||||
return 1
|
||||
print("No games found.")
|
||||
return 0
|
||||
new = set(result.new_appids)
|
||||
print(f"{len(result.games)} game(s) across {len(selected)} librar(y/ies):\n")
|
||||
for g in result.games:
|
||||
flag = " NEW" if g.appid in new else ""
|
||||
print(f" {g.name:<48} {steam.human_size(g.size_bytes):>9}{flag}")
|
||||
if new:
|
||||
print(f"\n{len(new)} newly-installed since the last scan.")
|
||||
|
||||
new = set(result.new_appids) if result else set()
|
||||
print(f"{len(all_games)} game(s):\n")
|
||||
for g in all_games:
|
||||
tag = " NEW" if g.appid in new else ""
|
||||
src = "" if g.launcher == "steam" else f" [{g.launcher}]"
|
||||
size = steam.human_size(g.size_bytes) if g.size_bytes else ""
|
||||
print(f" {g.name:<46}{src:<10} {size:>9}{tag}")
|
||||
if not selected:
|
||||
print("\n(no Steam libraries selected — `rigdoctor games libraries --all` to add them)")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,32 @@ def check_pcie_aspm() -> list[Finding]:
|
||||
|
||||
# --- NVIDIA persistence mode (seed-case relevant) -------------------------------------
|
||||
|
||||
def check_gpu_powermizer() -> list[Finding]:
|
||||
"""NVIDIA PowerMizer preferred-performance mode (X only, via nvidia-settings)."""
|
||||
if shutil.which("nvidia-settings") is None or not os.environ.get("DISPLAY"):
|
||||
return []
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["nvidia-settings", "-q", "[gpu:0]/GPUPowerMizerMode", "-t"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
except (subprocess.SubprocessError, OSError):
|
||||
return []
|
||||
raw = proc.stdout.strip().splitlines()[0].strip() if proc.stdout.strip() else ""
|
||||
if not raw.isdigit(): # no X target / Wayland / query failed — skip quietly
|
||||
return []
|
||||
names = {0: "Adaptive", 1: "Prefer Maximum Performance", 2: "Auto"}
|
||||
name = names.get(int(raw), f"mode {raw}")
|
||||
if int(raw) == 1:
|
||||
return [Finding(OK, "GPU", f"GPU PowerMizer: {name}", "The GPU prefers maximum performance.")]
|
||||
return [Finding(
|
||||
INFO, "GPU", f"GPU PowerMizer: {name}",
|
||||
"Adaptive/Auto can downclock the GPU between load spikes, hurting frame consistency.",
|
||||
"Prefer max performance (X only, resets on reboot): "
|
||||
"`nvidia-settings -a '[gpu:0]/GPUPowerMizerMode=1'`.",
|
||||
)]
|
||||
|
||||
|
||||
def check_gpu_persistence() -> list[Finding]:
|
||||
if shutil.which("nvidia-smi") is None:
|
||||
return []
|
||||
@@ -235,6 +261,34 @@ def check_mitigations() -> list[Finding]:
|
||||
|
||||
# --- Proton versions (informational) --------------------------------------------------
|
||||
|
||||
def check_wine() -> list[Finding]:
|
||||
"""System Wine version (used by Lutris / non-Proton games)."""
|
||||
if shutil.which("wine") is None:
|
||||
return []
|
||||
try:
|
||||
proc = subprocess.run(["wine", "--version"], capture_output=True, text=True, timeout=10)
|
||||
except (subprocess.SubprocessError, OSError):
|
||||
return []
|
||||
ver = proc.stdout.strip().split()[0] if proc.stdout.strip() else ""
|
||||
if not ver:
|
||||
return []
|
||||
return [Finding(
|
||||
INFO, "Tools", f"Wine: {ver}",
|
||||
"System Wine — used by Lutris and non-Proton titles.",
|
||||
"Steam games generally run best on Proton; keep Wine current for native/Lutris use.",
|
||||
)]
|
||||
|
||||
|
||||
def check_steam_client() -> list[Finding]:
|
||||
"""Installed Steam client package version."""
|
||||
from . import steam
|
||||
|
||||
ver = steam.client_version()
|
||||
if not ver:
|
||||
return []
|
||||
return [Finding(INFO, "Tools", f"Steam client: {ver}", "The installed Steam package version.")]
|
||||
|
||||
|
||||
def check_proton() -> list[Finding]:
|
||||
from . import steam
|
||||
|
||||
@@ -259,6 +313,7 @@ def run_gameenv_checks() -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
findings += check_pcie_aspm()
|
||||
findings += check_gpu_persistence()
|
||||
findings += check_gpu_powermizer()
|
||||
findings += check_cpu_governor()
|
||||
findings += check_gamemode()
|
||||
findings += check_mangohud()
|
||||
@@ -267,5 +322,7 @@ def run_gameenv_checks() -> list[Finding]:
|
||||
findings += check_thp()
|
||||
findings += check_mitigations()
|
||||
findings += check_proton()
|
||||
findings += check_wine()
|
||||
findings += check_steam_client()
|
||||
findings.sort(key=lambda f: _ORDER.get(f.severity, 9))
|
||||
return findings
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Non-Steam game detection (M6): Lutris + Heroic installed games.
|
||||
|
||||
Reads each launcher's own install records (Lutris' SQLite library, Heroic's JSON stores),
|
||||
returning the same `steam.Game` shape tagged with the launcher. Stdlib only; every reader
|
||||
degrades to [] if the launcher isn't installed or its files can't be parsed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
from .steam import Game
|
||||
|
||||
LUTRIS_DB = Path(os.path.expanduser("~/.local/share/lutris/pga.db"))
|
||||
HEROIC_DIR = Path(os.path.expanduser("~/.config/heroic"))
|
||||
|
||||
|
||||
def _lutris_games() -> list[Game]:
|
||||
db = LUTRIS_DB
|
||||
if not db.exists():
|
||||
return []
|
||||
games: list[Game] = []
|
||||
try:
|
||||
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True) # read-only
|
||||
try:
|
||||
rows = con.execute(
|
||||
"SELECT name, slug FROM games WHERE installed = 1 AND name IS NOT NULL"
|
||||
).fetchall()
|
||||
finally:
|
||||
con.close()
|
||||
except (sqlite3.Error, OSError):
|
||||
return []
|
||||
for name, slug in rows:
|
||||
if name:
|
||||
games.append(Game(appid=slug or "", name=str(name), library="", installdir="",
|
||||
launcher="lutris"))
|
||||
return games
|
||||
|
||||
|
||||
def _read_json(path: Path):
|
||||
try:
|
||||
return json.loads(path.read_text())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _heroic_games() -> list[Game]:
|
||||
base = HEROIC_DIR
|
||||
if not base.is_dir():
|
||||
return []
|
||||
games: list[Game] = []
|
||||
|
||||
# Epic / Legendary: {app_name: {"title": ..., ...}}
|
||||
epic = _read_json(base / "legendaryConfig" / "legendary" / "installed.json")
|
||||
if isinstance(epic, dict):
|
||||
for app_name, info in epic.items():
|
||||
if isinstance(info, dict):
|
||||
games.append(Game(appid=str(app_name), name=info.get("title") or str(app_name),
|
||||
library="", installdir="", launcher="heroic"))
|
||||
|
||||
# GOG: {"installed": [{"appName", "install_path", "title"?}]}
|
||||
gog = _read_json(base / "gog_store" / "installed.json")
|
||||
entries = gog.get("installed") if isinstance(gog, dict) else None
|
||||
if isinstance(entries, list):
|
||||
for e in entries:
|
||||
if not isinstance(e, dict):
|
||||
continue
|
||||
install_path = e.get("install_path") or ""
|
||||
title = e.get("title") or os.path.basename(install_path.rstrip("/")) or str(e.get("appName", ""))
|
||||
if title:
|
||||
games.append(Game(appid=str(e.get("appName", "")), name=title, library="",
|
||||
installdir="", launcher="heroic"))
|
||||
return games
|
||||
|
||||
|
||||
def scan() -> list[Game]:
|
||||
"""Installed non-Steam games (Lutris + Heroic), de-duplicated, sorted by name."""
|
||||
seen: set[tuple[str, str]] = set()
|
||||
out: list[Game] = []
|
||||
for game in _lutris_games() + _heroic_games():
|
||||
key = (game.launcher, game.name)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(game)
|
||||
return sorted(out, key=lambda g: g.name.lower())
|
||||
@@ -58,10 +58,11 @@ class SteamLibrary:
|
||||
class Game:
|
||||
appid: str
|
||||
name: str
|
||||
library: str # library path the game lives in
|
||||
library: str # library path the game lives in (Steam)
|
||||
installdir: str # folder name under <library>/steamapps/common
|
||||
size_bytes: int = 0
|
||||
last_updated: int = 0 # epoch seconds (acf LastUpdated), 0 if unknown
|
||||
launcher: str = "steam" # "steam" | "lutris" | "heroic"
|
||||
|
||||
|
||||
# --- VDF (Valve Data Format) parsing --------------------------------------------------
|
||||
@@ -313,7 +314,8 @@ def cached_games() -> list[Game]:
|
||||
cache = load_cache()
|
||||
if not cache:
|
||||
return []
|
||||
return [Game(**{k: g.get(k) for k in Game.__dataclass_fields__}) for g in cache.get("games", [])]
|
||||
# Only pass keys present in the record so dataclass defaults fill any new fields.
|
||||
return [Game(**{k: g[k] for k in Game.__dataclass_fields__ if k in g}) for g in cache.get("games", [])]
|
||||
|
||||
|
||||
def rescan(cfg: dict | None = None) -> ScanResult:
|
||||
@@ -353,6 +355,21 @@ def acknowledge_new() -> None:
|
||||
|
||||
# --- formatting -----------------------------------------------------------------------
|
||||
|
||||
def client_version() -> str | None:
|
||||
"""The installed Steam package version (apt), or None — best-effort, offline."""
|
||||
if shutil.which("dpkg-query") is None:
|
||||
return None
|
||||
for pkg in ("steam-installer", "steam-launcher", "steam"):
|
||||
try:
|
||||
proc = subprocess.run(["dpkg-query", "-W", "-f=${Version}", pkg],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
except (subprocess.SubprocessError, OSError):
|
||||
continue
|
||||
if proc.returncode == 0 and proc.stdout.strip():
|
||||
return proc.stdout.strip()
|
||||
return None
|
||||
|
||||
|
||||
def launch_game(appid: str) -> bool:
|
||||
"""Best-effort: ask Steam to launch a game by appid (steam:// URL). Non-blocking."""
|
||||
if not appid:
|
||||
|
||||
@@ -88,6 +88,7 @@ class GamesPage(QWidget):
|
||||
self._diag_done.connect(self._on_diag_done)
|
||||
self._busy = False
|
||||
self._new_appids: set[str] = set()
|
||||
self._extra_games: list = [] # non-Steam (Lutris/Heroic), appended after a scan
|
||||
self._diag_game: str | None = None
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
@@ -213,7 +214,7 @@ class GamesPage(QWidget):
|
||||
threading.Thread(target=self._work, daemon=True).start()
|
||||
|
||||
def _work(self) -> None:
|
||||
from ..core import steam
|
||||
from ..core import launchers, steam
|
||||
|
||||
try:
|
||||
selected = {os.path.realpath(p) for p in steam.selected_library_paths()}
|
||||
@@ -223,6 +224,10 @@ class GamesPage(QWidget):
|
||||
for lib in steam.discover_libraries()
|
||||
]
|
||||
self._libraries_ready.emit(libs)
|
||||
try:
|
||||
self._extra_games = launchers.scan() # Lutris / Heroic (non-Steam)
|
||||
except Exception:
|
||||
self._extra_games = []
|
||||
self._scanned.emit(steam.rescan())
|
||||
except Exception:
|
||||
self._scanned.emit(None)
|
||||
@@ -265,11 +270,13 @@ class GamesPage(QWidget):
|
||||
self._status.setText("scan failed")
|
||||
return
|
||||
self._new_appids = set(result.new_appids)
|
||||
self._populate_games(result.games, self._new_appids)
|
||||
games = list(result.games) + list(self._extra_games)
|
||||
self._populate_games(games, self._new_appids)
|
||||
new = len(self._new_appids)
|
||||
suffix = f" · {new} new" if new else ""
|
||||
non_steam = f" · {len(self._extra_games)} non-Steam" if self._extra_games else ""
|
||||
self._status.setText(
|
||||
f"{len(result.games)} games · {time.strftime('%H:%M:%S')}{suffix}"
|
||||
f"{len(games)} games · {time.strftime('%H:%M:%S')}{suffix}{non_steam}"
|
||||
)
|
||||
self.new_count_changed.emit(new)
|
||||
|
||||
@@ -293,12 +300,17 @@ class GamesPage(QWidget):
|
||||
return
|
||||
|
||||
for g in games:
|
||||
launcher = getattr(g, "launcher", "steam")
|
||||
if launcher != "steam":
|
||||
sublabel, appid = launcher.title(), "" # non-Steam: can't steam:// launch it
|
||||
else:
|
||||
sublabel, appid = (os.path.basename(g.library.rstrip("/")) or g.library), g.appid
|
||||
self._list.addWidget(_game_row(
|
||||
g.name,
|
||||
os.path.basename(g.library.rstrip("/")) or g.library,
|
||||
sublabel,
|
||||
steam.human_size(g.size_bytes),
|
||||
g.appid in new_appids,
|
||||
appid=g.appid,
|
||||
appid=appid,
|
||||
on_diagnose=self._start_diagnostic,
|
||||
))
|
||||
self._list.addStretch(1)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Tests for M6 non-Steam game detection (Lutris SQLite + Heroic JSON)."""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from rigdoctor.core import launchers
|
||||
|
||||
|
||||
class LutrisTests(unittest.TestCase):
|
||||
def test_reads_installed_games_only(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
db = Path(d) / "pga.db"
|
||||
con = sqlite3.connect(db)
|
||||
con.execute("CREATE TABLE games (id INTEGER, name TEXT, slug TEXT, installed INTEGER)")
|
||||
con.executemany(
|
||||
"INSERT INTO games VALUES (?, ?, ?, ?)",
|
||||
[(1, "Hades", "hades", 1), (2, "Hollow Knight", "hollow-knight", 1), (3, "Old Game", "old", 0)],
|
||||
)
|
||||
con.commit()
|
||||
con.close()
|
||||
with mock.patch.object(launchers, "LUTRIS_DB", db), \
|
||||
mock.patch.object(launchers, "HEROIC_DIR", Path(d) / "nope"):
|
||||
games = launchers.scan()
|
||||
names = {g.name for g in games}
|
||||
self.assertEqual(names, {"Hades", "Hollow Knight"})
|
||||
self.assertTrue(all(g.launcher == "lutris" for g in games))
|
||||
|
||||
def test_missing_db_is_empty(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
with mock.patch.object(launchers, "LUTRIS_DB", Path(d) / "absent.db"), \
|
||||
mock.patch.object(launchers, "HEROIC_DIR", Path(d) / "nope"):
|
||||
self.assertEqual(launchers.scan(), [])
|
||||
|
||||
|
||||
class HeroicTests(unittest.TestCase):
|
||||
def test_epic_and_gog(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
base = Path(d) / "heroic"
|
||||
(base / "legendaryConfig" / "legendary").mkdir(parents=True)
|
||||
(base / "gog_store").mkdir(parents=True)
|
||||
(base / "legendaryConfig" / "legendary" / "installed.json").write_text(
|
||||
json.dumps({"abc123": {"title": "Control"}}))
|
||||
(base / "gog_store" / "installed.json").write_text(
|
||||
json.dumps({"installed": [{"appName": "777", "title": "The Witcher 3"}]}))
|
||||
with mock.patch.object(launchers, "LUTRIS_DB", Path(d) / "nope.db"), \
|
||||
mock.patch.object(launchers, "HEROIC_DIR", base):
|
||||
names = {g.name for g in launchers.scan()}
|
||||
self.assertEqual(names, {"Control", "The Witcher 3"})
|
||||
|
||||
def test_gog_title_falls_back_to_install_path(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
base = Path(d) / "heroic"
|
||||
(base / "gog_store").mkdir(parents=True)
|
||||
(base / "gog_store" / "installed.json").write_text(
|
||||
json.dumps({"installed": [{"appName": "9", "install_path": "/games/Stardew Valley"}]}))
|
||||
with mock.patch.object(launchers, "LUTRIS_DB", Path(d) / "nope.db"), \
|
||||
mock.patch.object(launchers, "HEROIC_DIR", base):
|
||||
names = {g.name for g in launchers.scan()}
|
||||
self.assertEqual(names, {"Stardew Valley"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user