mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:47:02 +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
139 lines
4.0 KiB
Python
139 lines
4.0 KiB
Python
"""Pytest bootstrap: make non-package script dirs importable for these tests.
|
|
|
|
Several helper modules live in standalone script directories (outside
|
|
``python_pkg/``) and are invoked as ``python <file>.py`` rather than imported as
|
|
packages. To unit-test them they must be importable by bare module name, so each
|
|
directory is placed on ``sys.path`` before the tests import them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
# Repo root is two levels up from this file (linux_configuration/tests/conftest.py).
|
|
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
# Each standalone script directory whose Python modules these tests import.
|
|
_SCRIPT_DIRS = (
|
|
_REPO_ROOT / "meta" / "scripts", # validate_evidence, validate_contract
|
|
_REPO_ROOT / "linux_configuration" / "utils", # fast_count
|
|
_REPO_ROOT / "linux_configuration" / "fixes", # g29_shifter_capture
|
|
_REPO_ROOT
|
|
/ "linux_configuration"
|
|
/ "misc"
|
|
/ "tools", # transcribe_fw and its helpers
|
|
)
|
|
|
|
for _script_dir in _SCRIPT_DIRS:
|
|
if str(_script_dir) not in sys.path:
|
|
sys.path.insert(0, str(_script_dir))
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_whisper() -> type[FakeWhisper]:
|
|
"""The faster_whisper double, as a fixture (conftest is not importable)."""
|
|
return FakeWhisper
|
|
|
|
|
|
@pytest.fixture
|
|
def importer() -> Callable[[dict[str, object]], object]:
|
|
"""The ``_try_import`` double factory, as a fixture."""
|
|
return fake_importer
|
|
|
|
|
|
class FakeWhisper:
|
|
"""Stand-in for faster_whisper, recording how a model was constructed.
|
|
|
|
``WhisperModel`` is upstream's CamelCase name, so it is exposed through
|
|
``__getattr__`` rather than defined as a method: defining it would mean
|
|
naming a non-PEP8 identifier here just to satisfy a third-party API.
|
|
"""
|
|
|
|
def __init__(self, error: Exception | None = None) -> None:
|
|
self.calls: list[dict[str, object]] = []
|
|
self._error = error
|
|
|
|
def _construct(self, name: str, **kwargs: object) -> object:
|
|
if self._error is not None:
|
|
raise self._error
|
|
self.calls.append({"name": name, **kwargs})
|
|
return object()
|
|
|
|
def __getattr__(self, attr: str) -> object:
|
|
if attr == "WhisperModel":
|
|
return self._construct
|
|
raise AttributeError(attr)
|
|
|
|
|
|
def fake_importer(available: dict[str, object]) -> object:
|
|
"""Return a ``_try_import`` double resolving only the named modules."""
|
|
|
|
def _try(name: str) -> object | None:
|
|
return available.get(name)
|
|
|
|
return _try
|
|
|
|
|
|
@pytest.fixture
|
|
def g29() -> type[G29Reports]:
|
|
"""Raw/xxd G29 report builders for the shifter-grader tests."""
|
|
return G29Reports
|
|
|
|
|
|
class G29Reports:
|
|
"""Builders for 12-byte G29 input reports with chosen gear and hall values."""
|
|
|
|
NEUTRAL_Y = 105
|
|
TOP_Y = 200
|
|
BOTTOM_Y = 45
|
|
X_LEFT = 60
|
|
X_CENTRE = 120
|
|
X_RIGHT = 180
|
|
|
|
@classmethod
|
|
def raw_report(
|
|
cls,
|
|
gears: int = 0,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
*,
|
|
down: bool = False,
|
|
) -> bytearray:
|
|
"""One raw input report; X/Y default to the centred, neutral stick."""
|
|
import g29_shifter_capture as gsc
|
|
|
|
raw = bytearray(gsc.REPORT_LEN)
|
|
raw[2] = gears
|
|
raw[9] = cls.X_CENTRE if x is None else x
|
|
raw[10] = cls.NEUTRAL_Y if y is None else y
|
|
raw[11] = 0x40 if down else 0x00
|
|
return raw
|
|
|
|
@classmethod
|
|
def xxd_line(
|
|
cls,
|
|
gears: int = 0,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
*,
|
|
down: bool = False,
|
|
) -> str:
|
|
"""The same report as one ``xxd -c 12`` line."""
|
|
raw = cls.raw_report(gears, x, y, down=down)
|
|
words = " ".join(raw[i : i + 2].hex() for i in range(0, len(raw), 2))
|
|
return f"00000000: {words}\n"
|
|
|
|
@staticmethod
|
|
def parse_lines(lines: list[str]) -> list[object]:
|
|
"""Parse xxd lines into Report objects."""
|
|
import g29_shifter_capture as gsc
|
|
|
|
return list(gsc.parse(lines))
|