|
| 1 | +"""Tests for the pure logic of skillfish-tuner-helper (no root, no SMU, no Qt). |
| 2 | +
|
| 3 | +These guard the GPU-governor config writer: the multi-point voltage curve and |
| 4 | +the Balanced/Performance mode switch. A wrong curve here can hard-freeze a |
| 5 | +BC-250 (see docs/OPTIMIZATIONS.md), so this is the most safety-critical pure |
| 6 | +logic in the repo. |
| 7 | +""" |
| 8 | +import importlib.machinery |
| 9 | +import importlib.util |
| 10 | +import pathlib |
| 11 | +import types |
| 12 | + |
| 13 | +import pytest |
| 14 | + |
| 15 | +HELPER = pathlib.Path(__file__).resolve().parents[1] / "apps" / "tuner" / "skillfish-tuner-helper" |
| 16 | + |
| 17 | + |
| 18 | +@pytest.fixture() |
| 19 | +def helper(tmp_path, monkeypatch): |
| 20 | + loader = importlib.machinery.SourceFileLoader("tuner_helper", str(HELPER)) |
| 21 | + spec = importlib.util.spec_from_loader("tuner_helper", loader) |
| 22 | + mod = importlib.util.module_from_spec(spec) |
| 23 | + loader.exec_module(mod) |
| 24 | + # never touch the real system from tests |
| 25 | + conf = tmp_path / "config.toml" |
| 26 | + monkeypatch.setattr(mod, "GOV_CONF", str(conf)) |
| 27 | + calls = [] |
| 28 | + monkeypatch.setattr(mod, "sh", lambda cmd, timeout=30: (calls.append(cmd), types.SimpleNamespace(returncode=0, stdout=""))[1]) |
| 29 | + mod._test_calls = calls |
| 30 | + mod._test_conf = conf |
| 31 | + return mod |
| 32 | + |
| 33 | + |
| 34 | +# ---------- _gpu_curve: the multi-point voltage ladder ---------- |
| 35 | + |
| 36 | +def test_curve_full_range_inserts_midpoints(helper): |
| 37 | + assert helper._gpu_curve(350, 700, 2200, 1000) == [(350, 700), (1500, 900), (2000, 1000), (2200, 1000)] |
| 38 | + |
| 39 | + |
| 40 | +def test_curve_low_max_has_no_midpoints_above_it(helper): |
| 41 | + assert helper._gpu_curve(350, 700, 1500, 900) == [(350, 700), (1500, 900)] |
| 42 | + |
| 43 | + |
| 44 | +def test_curve_dedup_when_max_is_a_midpoint(helper): |
| 45 | + pts = helper._gpu_curve(350, 700, 2000, 1000) |
| 46 | + assert pts == [(350, 700), (1500, 900), (2000, 1000)] |
| 47 | + freqs = [f for f, _ in pts] |
| 48 | + assert len(freqs) == len(set(freqs)), "no duplicate frequencies" |
| 49 | + |
| 50 | + |
| 51 | +def test_curve_is_ascending(helper): |
| 52 | + pts = helper._gpu_curve(350, 700, 2200, 1000) |
| 53 | + assert pts == sorted(pts) |
| 54 | + |
| 55 | + |
| 56 | +# ---------- apply_gpu: writes the curve, replaces old safe-points ---------- |
| 57 | + |
| 58 | +def test_apply_gpu_writes_multipoint_curve(helper): |
| 59 | + helper._test_conf.write_text("[load-target]\nupper = 0.95\nlower = 0.7\n" |
| 60 | + "[[safe-points]]\nfrequency = 350\nvoltage = 700\n" |
| 61 | + "[[safe-points]]\nfrequency = 2230\nvoltage = 1000\n") |
| 62 | + assert helper.apply_gpu(350, 700, 2200, 1000) |
| 63 | + txt = helper._test_conf.read_text() |
| 64 | + assert "2230" not in txt, "the dangerous 2230 point must be gone" |
| 65 | + assert txt.count("[[safe-points]]") == 4 |
| 66 | + assert "frequency = 1500" in txt and "frequency = 2000" in txt |
| 67 | + |
| 68 | + |
| 69 | +def test_apply_gpu_strips_commented_safepoints(helper): |
| 70 | + helper._test_conf.write_text("[load-target]\nupper = 0.95\nlower = 0.7\n" |
| 71 | + "[[safe-points]]\nfrequency = 350 # MHz\nvoltage = 700 # mV\n") |
| 72 | + assert helper.apply_gpu(350, 700, 2200, 1000) |
| 73 | + txt = helper._test_conf.read_text() |
| 74 | + assert txt.count("[[safe-points]]") == 4 |
| 75 | + |
| 76 | + |
| 77 | +def test_apply_gpu_reloads_governor_gently(helper): |
| 78 | + helper._test_conf.write_text("[[safe-points]]\nfrequency = 350\nvoltage = 700\n" |
| 79 | + "[[safe-points]]\nfrequency = 2000\nvoltage = 1000\n") |
| 80 | + helper.apply_gpu(350, 700, 2000, 1000) |
| 81 | + joined = " | ".join(helper._test_calls) |
| 82 | + assert "stop cyan-skillfish-governor" in joined and "start cyan-skillfish-governor" in joined |
| 83 | + assert "restart" not in joined, "must use stop -> settle -> start, not an abrupt restart" |
| 84 | + |
| 85 | + |
| 86 | +# ---------- gov_mode: Balanced / Performance switch ---------- |
| 87 | + |
| 88 | +def _stock(helper): |
| 89 | + helper._test_conf.write_text("[timing.intervals]\nsample = 2000\nadjust = 20_000\nfinetune = 1_000_000_000\n" |
| 90 | + "[timing.ramp-rates]\nnormal = 1\nburst = 200\n[timing]\nburst-samples = 48\n" |
| 91 | + "[frequency-thresholds]\nadjust = 100\nfinetune = 10\n" |
| 92 | + "[load-target]\nupper = 0.95\nlower = 0.7\n" |
| 93 | + "[[safe-points]]\nfrequency = 350\nvoltage = 700\n" |
| 94 | + "[[safe-points]]\nfrequency = 1500\nvoltage = 900\n" |
| 95 | + "[[safe-points]]\nfrequency = 2000\nvoltage = 1000\n" |
| 96 | + "[[safe-points]]\nfrequency = 2200\nvoltage = 1000\n") |
| 97 | + |
| 98 | + |
| 99 | +def test_mode_roundtrip_preserves_safepoints(helper): |
| 100 | + _stock(helper) |
| 101 | + assert helper.current_gov_mode() == "balanced" |
| 102 | + assert helper.gov_mode("performance") |
| 103 | + txt = helper._test_conf.read_text() |
| 104 | + assert "upper = 0.20" in txt and helper.current_gov_mode() == "performance" |
| 105 | + assert txt.count("[[safe-points]]") == 4, "performance keeps the user's curve" |
| 106 | + assert helper.gov_mode("balanced") |
| 107 | + txt = helper._test_conf.read_text() |
| 108 | + assert "upper = 0.95" in txt and helper.current_gov_mode() == "balanced" |
| 109 | + assert txt.count("[[safe-points]]") == 4 |
| 110 | + |
| 111 | + |
| 112 | +def test_safepoints_fallback_is_the_safe_curve(helper): |
| 113 | + helper._test_conf.write_text("") # unreadable / empty config |
| 114 | + pts = helper._gov_safepoints() |
| 115 | + assert pts == [(350, 700), (1500, 900), (2000, 1000), (2200, 1000)] |
| 116 | + assert max(f for f, _ in pts) <= 2200, "fallback must never exceed the validated 2200 MHz" |
0 commit comments