feat(m6): one-click install + apply controls on Environment page — 0.10.0
Make the environment report actionable, not just advisory. Install (reuses M9 installer): - Add GameMode, MangoHud, cpupower to the component catalog (so they also show on the Setup page); catalog.by_id() lookup. - "tool not installed" findings (GameMode/MangoHud) get an Install button. Apply runtime-reversible tunables (D22, realizing the D9 consent-gated milestone): - core/fixes.py: dropdown of live options + Apply for CPU governor, NVIDIA persistence, PCIe ASPM policy, vm.swappiness, THP. One pkexec command each, no reboot, reverts on reboot; chosen value validated against live options; writes go to sysfs/procfs/nvidia-smi, never GRUB. GRUB/mitigations stay suggestion-only. - Finding gained optional action (install) + fix (apply) ids; shared finding_card renders the matching control; Environment page wires both and re-checks after a change. Tests for fixes (parse, command builders, value validation, gameenv wiring). Docs: D22 added (amends D9); SPEC/MODULES/ROADMAP updated. 0.9.0 -> 0.10.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""Tests for M6 runtime tunables (parse, command builders, value validation)."""
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from rigdoctor.core import fixes
|
||||
from rigdoctor.core.fixes import Tunable
|
||||
|
||||
|
||||
class ParseTests(unittest.TestCase):
|
||||
def test_bracketed(self):
|
||||
self.assertEqual(fixes._bracketed("always [madvise] never"), (["always", "madvise", "never"], "madvise"))
|
||||
|
||||
def test_bracketed_none_active(self):
|
||||
self.assertEqual(fixes._bracketed("a b c"), (["a", "b", "c"], None))
|
||||
|
||||
|
||||
class CommandBuilderTests(unittest.TestCase):
|
||||
def test_governor_cmd_writes_value_to_sysfs(self):
|
||||
cmd = fixes._cpu_governor_cmd("performance")
|
||||
self.assertEqual(cmd[:2], ["/bin/sh", "-c"])
|
||||
self.assertIn("performance", cmd[2])
|
||||
self.assertIn("scaling_governor", cmd[2])
|
||||
|
||||
def test_persistence_cmd(self):
|
||||
self.assertEqual(fixes._nvidia_persistence_cmd("Enabled"), ["nvidia-smi", "-pm", "1"])
|
||||
self.assertEqual(fixes._nvidia_persistence_cmd("Disabled"), ["nvidia-smi", "-pm", "0"])
|
||||
|
||||
def test_swappiness_cmd_targets_procfs(self):
|
||||
self.assertIn("/proc/sys/vm/swappiness", fixes._swappiness_cmd("10")[2])
|
||||
|
||||
def test_quoting_is_safe(self):
|
||||
# A value that would be dangerous unquoted stays a single quoted token.
|
||||
cmd = fixes._pcie_aspm_cmd("performance; rm -rf /")
|
||||
self.assertIn("'performance; rm -rf /'", cmd[2])
|
||||
|
||||
|
||||
class ApplyValidationTests(unittest.TestCase):
|
||||
def test_unknown_fix_returns_none(self):
|
||||
self.assertIsNone(fixes.apply_command("does_not_exist", "x"))
|
||||
|
||||
def test_value_validated_against_live_options(self):
|
||||
fake = Tunable("x", "X", ["a", "b"], "a")
|
||||
with mock.patch.dict(fixes._TUNABLES, {"x": (lambda: fake, lambda v: ["echo", v])}, clear=False):
|
||||
self.assertEqual(fixes.apply_command("x", "a"), ["echo", "a"])
|
||||
self.assertIsNone(fixes.apply_command("x", "not-an-option"))
|
||||
|
||||
def test_apply_unknown_is_error(self):
|
||||
rc, _ = fixes.apply("nope", "x")
|
||||
self.assertEqual(rc, 1)
|
||||
|
||||
|
||||
class GameenvWiringTests(unittest.TestCase):
|
||||
def test_findings_reference_known_fix_ids(self):
|
||||
from rigdoctor.core import gameenv
|
||||
|
||||
fix_ids = {f.fix for f in gameenv.run_gameenv_checks() if f.fix}
|
||||
# Whatever fixes the live system surfaces, each must be a real tunable id.
|
||||
self.assertTrue(fix_ids.issubset(set(fixes._TUNABLES)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -30,7 +30,7 @@ class GovernorTests(unittest.TestCase):
|
||||
def test_powersave_is_warning(self):
|
||||
f = gameenv.evaluate_governor({"powersave"})
|
||||
self.assertEqual(f.severity, "warning")
|
||||
self.assertIn("cpupower", f.suggestion)
|
||||
self.assertEqual(f.fix, "cpu_governor") # offers the live Apply dropdown
|
||||
|
||||
def test_dynamic_is_info(self):
|
||||
self.assertEqual(gameenv.evaluate_governor({"schedutil"}).severity, "info")
|
||||
@@ -43,7 +43,7 @@ class SwappinessTests(unittest.TestCase):
|
||||
def test_high_is_info_with_suggestion(self):
|
||||
f = gameenv.evaluate_swappiness(60)
|
||||
self.assertEqual(f.severity, "info")
|
||||
self.assertIn("swappiness", f.suggestion)
|
||||
self.assertEqual(f.fix, "swappiness") # offers the live Apply dropdown
|
||||
|
||||
def test_low_is_ok(self):
|
||||
self.assertEqual(gameenv.evaluate_swappiness(10).severity, "ok")
|
||||
|
||||
Reference in New Issue
Block a user