mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
Live on this host since 2026-09-11 but never committed. The wheel firmware decodes the shifter's hall sensor itself and this unit's top row rests at Y 180-193, below the ~195 engage threshold, so 1st/3rd often did not register. g29_shifter.bpf.c re-decodes those two gates from the raw X/Y bytes with hysteresis (engage >= 160, release <= 140) and ORs the bit in before hid-logitech parses the report; it never clears a bit, so the firmware's working gates cannot regress. fix_g29_shifter.sh builds, attaches and installs it persistently (udev-hid-bpf rule + a systemd safety-net unit pulled in on every re-enumeration); g29_shifter_capture.py records and grades raw captures per gate. Two files were over the 250-line cap: the test file is split at the record/CLI boundary with the report builders moved into a conftest fixture (the naming hook allows only test_*.py in tests/), and the grader's docstrings are tightened. No behaviour change to the installed fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SS8M5wk88nJTXxDWz61KKk
165 lines
5.9 KiB
Python
165 lines
5.9 KiB
Python
"""Tests for linux_configuration/fixes/g29_shifter_capture.py (G29 shifter grader)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import g29_shifter_capture as gsc
|
|
import pytest
|
|
|
|
if TYPE_CHECKING:
|
|
from conftest import G29Reports
|
|
|
|
NEUTRAL_Y = 105
|
|
TOP_Y = 200
|
|
BOTTOM_Y = 45
|
|
X_LEFT = 60
|
|
X_CENTRE = 120
|
|
X_RIGHT = 180
|
|
|
|
|
|
class TestParse:
|
|
"""``parse`` decodes xxd lines and skips anything malformed."""
|
|
|
|
def test_decodes_fields(self, g29: type[G29Reports]) -> None:
|
|
(report,) = g29.parse_lines(
|
|
[g29.xxd_line(gears=0x04, x=X_CENTRE, y=TOP_Y, down=True)]
|
|
)
|
|
assert (report.gears, report.x, report.y, report.down) == (
|
|
0x04,
|
|
X_CENTRE,
|
|
TOP_Y,
|
|
True,
|
|
)
|
|
|
|
def test_skips_non_xxd_lines(self, g29: type[G29Reports]) -> None:
|
|
assert g29.parse_lines(["not a capture line\n", "\n"]) == []
|
|
|
|
def test_skips_truncated_reports(self, g29: type[G29Reports]) -> None:
|
|
assert g29.parse_lines(["00000000: 0800 0000 b984\n"]) == []
|
|
|
|
|
|
class TestGate:
|
|
"""``Report.gate`` maps raw X/Y onto the physical H-pattern."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("x", "y", "down", "expected"),
|
|
[
|
|
(X_LEFT, TOP_Y, False, "1st"),
|
|
(X_LEFT, BOTTOM_Y, False, "2nd"),
|
|
(X_CENTRE, TOP_Y, False, "3rd"),
|
|
(X_CENTRE, BOTTOM_Y, False, "4th"),
|
|
(X_RIGHT, TOP_Y, False, "5th"),
|
|
(X_RIGHT, BOTTOM_Y, False, "6th"),
|
|
(X_RIGHT, BOTTOM_Y, True, "R"),
|
|
(X_CENTRE, NEUTRAL_Y, False, None),
|
|
(X_LEFT, TOP_Y, True, "1st"),
|
|
],
|
|
)
|
|
def test_gate(
|
|
self, g29: type[G29Reports], x: int, y: int, *, down: bool, expected: str | None
|
|
) -> None:
|
|
(report,) = g29.parse_lines([g29.xxd_line(x=x, y=y, down=down)])
|
|
assert report.gate == expected
|
|
|
|
|
|
class TestRuns:
|
|
"""``runs`` collapses equal gear masks and tracks the X/Y envelope."""
|
|
|
|
def test_collapses_and_tracks_envelope(self, g29: type[G29Reports]) -> None:
|
|
reports = g29.parse_lines(
|
|
[
|
|
g29.xxd_line(gears=0x01, x=X_LEFT, y=TOP_Y),
|
|
g29.xxd_line(gears=0x01, x=X_LEFT + 10, y=TOP_Y - 20),
|
|
g29.xxd_line(gears=0x00, x=X_CENTRE, y=NEUTRAL_Y),
|
|
]
|
|
)
|
|
first, second = gsc.runs(reports)
|
|
assert (first.mask, first.length, first.start) == (0x01, 2, 0)
|
|
assert (first.x_min, first.x_max) == (X_LEFT, X_LEFT + 10)
|
|
assert (first.y_min, first.y_max) == (TOP_Y - 20, TOP_Y)
|
|
assert (second.mask, second.length) == (0x00, 1)
|
|
|
|
|
|
class TestEdges:
|
|
"""``edges`` reports each gear bit transition once, in order."""
|
|
|
|
def test_on_then_off(self, g29: type[G29Reports]) -> None:
|
|
reports = g29.parse_lines(
|
|
[g29.xxd_line(gears=0x01), g29.xxd_line(gears=0x01), g29.xxd_line()]
|
|
)
|
|
assert [(name, on) for _, name, on, _ in gsc.edges(reports)] == [
|
|
("1st", True),
|
|
("1st", False),
|
|
]
|
|
|
|
|
|
class TestMisses:
|
|
"""A dwell is a miss when the gear bit stays clear for MIN_DWELL reports."""
|
|
|
|
def test_registered_dwell_is_not_a_miss(self, g29: type[G29Reports]) -> None:
|
|
lines = [g29.xxd_line(gears=0x01, x=X_LEFT, y=TOP_Y)] * (gsc.MIN_DWELL + 5)
|
|
assert gsc.misses(g29.parse_lines(lines))["1st"] == (1, 0)
|
|
|
|
def test_unregistered_dwell_is_a_miss(self, g29: type[G29Reports]) -> None:
|
|
lines = [g29.xxd_line(x=X_LEFT, y=TOP_Y)] * (gsc.MIN_DWELL + 5)
|
|
assert gsc.misses(g29.parse_lines(lines))["1st"] == (1, 1)
|
|
|
|
def test_short_dwell_is_not_an_attempt(self, g29: type[G29Reports]) -> None:
|
|
lines = [g29.xxd_line(x=X_LEFT, y=TOP_Y)] * (gsc.MIN_DWELL - 1)
|
|
assert gsc.misses(g29.parse_lines(lines))["1st"] == (0, 0)
|
|
|
|
def test_brief_dropout_inside_a_dwell_is_forgiven(
|
|
self, g29: type[G29Reports]
|
|
) -> None:
|
|
lines = (
|
|
[g29.xxd_line(gears=0x01, x=X_LEFT, y=TOP_Y)] * gsc.MIN_DWELL
|
|
+ [g29.xxd_line(x=X_LEFT, y=TOP_Y)] * (gsc.MIN_DWELL - 1)
|
|
+ [g29.xxd_line(gears=0x01, x=X_LEFT, y=TOP_Y)] * gsc.MIN_DWELL
|
|
)
|
|
assert gsc.misses(g29.parse_lines(lines))["1st"] == (1, 0)
|
|
|
|
def test_neutral_reports_are_not_a_gate(self, g29: type[G29Reports]) -> None:
|
|
lines = [g29.xxd_line()] * (gsc.MIN_DWELL + 5)
|
|
assert all(
|
|
stat == (0, 0) for stat in gsc.misses(g29.parse_lines(lines)).values()
|
|
)
|
|
|
|
|
|
class TestReport:
|
|
"""``report`` prints the summary and exits non-zero only on a miss."""
|
|
|
|
def test_clean_capture_returns_zero(
|
|
self, g29: type[G29Reports], capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
reports = g29.parse_lines(
|
|
[g29.xxd_line(gears=0x01, x=X_LEFT, y=TOP_Y)] * (gsc.MIN_DWELL + 1)
|
|
)
|
|
assert gsc.report(reports, show_runs=False, show_edges=False) == 0
|
|
assert "1st: 1 attempts, 0 missed" in capsys.readouterr().out
|
|
|
|
def test_miss_returns_one(
|
|
self, g29: type[G29Reports], capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
reports = g29.parse_lines(
|
|
[g29.xxd_line(x=X_LEFT, y=TOP_Y)] * (gsc.MIN_DWELL + 1)
|
|
)
|
|
assert gsc.report(reports, show_runs=False, show_edges=False) == 1
|
|
assert "1 missed" in capsys.readouterr().out
|
|
|
|
def test_runs_and_edges_are_optional_output(
|
|
self, g29: type[G29Reports], capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
reports = g29.parse_lines([g29.xxd_line(gears=0x01, x=X_LEFT, y=TOP_Y)] * 25)
|
|
gsc.report(reports, show_runs=True, show_edges=True)
|
|
out = capsys.readouterr().out
|
|
assert "1st" in out
|
|
assert "ON " in out
|
|
|
|
def test_short_runs_are_omitted(
|
|
self, g29: type[G29Reports], capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
reports = g29.parse_lines([g29.xxd_line(gears=0x01, x=X_LEFT, y=TOP_Y)] * 5)
|
|
gsc.report(reports, show_runs=True, show_edges=False)
|
|
assert "X=" not in capsys.readouterr().out
|