screen-locker/screen_locker/tests/test_run_tcx_golden.py
Krzysztof kuhy Rudnicki 6777656418 Bridge GPS dropouts in run distance
The PC keeps a dropout moving at 1 m/s to 25 km/h; the app zeroed
every gap over 30 s. Bridge unpaused dropouts, pauses stay zero.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWXcjiPpgRfxzrRfoky
2026-11-10 19:11:42 +01:00

198 lines
7.8 KiB
Python

"""Cross-language golden test: the app's TCX is accepted by the real verifier.
The workout app (Dart, ``lib/services/tcx_codec.dart``) writes the TCX files
the PC credits. Each language's own round trip passes happily while the other
side's parser disagrees, so the Dart twin
(``stronglift_replacement/workout_app/test/services/tcx_golden_fixture_test.dart``)
encodes ``fixtures/run_tcx/golden_tracks.json`` and compares the result byte
for byte with the ``.tcx`` files checked in next to it. This test feeds those
SAME files to the real ``RunnerUpVerificationMixin`` -- date glob, parser,
run validation and walk summing -- and asserts the km / minutes the JSON
promises. Everything runs on a temporary drop directory: nothing here reads
or writes ``~/data/cloud/RunnerUp``.
"""
from __future__ import annotations
import json
from pathlib import Path
import shutil
from unittest.mock import MagicMock, patch
import pytest
from screen_locker._runnerup_verification import RunnerUpVerificationMixin
FIXTURES = Path(__file__).parent / "fixtures" / "run_tcx"
SAMPLE = "RunnerUp_2026-08-12-17-42-11_Running.tcx"
# ``_TCX_SPORT_TO_INT`` code for "Running" in screen_locker/_runnerup_tcx.py.
SPORT_RUNNING = 0
def _golden() -> dict[str, dict[str, object]]:
return json.loads((FIXTURES / "golden_tracks.json").read_text(encoding="utf-8"))
class Verifier(RunnerUpVerificationMixin):
"""The real mixin with no phone: only the WebDAV drop directory is read."""
def __init__(self) -> None:
self.adb = MagicMock(return_value=(False, ""))
def _has_adb_device(self) -> bool:
return False
def _run_adb(self, args: list[str]) -> tuple[bool, str]:
return self.adb(args)
@pytest.fixture(name="inbox")
def inbox_fixture(tmp_path: Path) -> Path:
"""A temporary WebDAV inbox standing in for ``~/data/cloud/RunnerUp``."""
inbox = tmp_path / "cloud" / "RunnerUp"
inbox.mkdir(parents=True)
return inbox
def _verify(inbox: Path, today: str) -> tuple[str, str] | None:
"""Run the real file-based verification on ``inbox`` as if it were ``today``."""
with (
patch("screen_locker._runnerup_verification.RUNNERUP_WEBDAV_DIRS", (inbox,)),
patch("screen_locker._runnerup_verification.today_str", return_value=today),
):
return Verifier()._verify_runnerup_via_files()
def _drop(inbox: Path, name: str) -> None:
shutil.copyfile(FIXTURES / name, inbox / name)
class TestGoldenRun:
"""The app's run file passes the PC's run validation unchanged."""
def test_run_is_verified_with_the_promised_km_and_minutes(
self, inbox: Path
) -> None:
"""Date glob, parser and validation accept the app's run file."""
spec = _golden()["running"]
_drop(inbox, str(spec["file"]))
expect = spec["expect"]
assert isinstance(expect, dict)
result = _verify(inbox, "2026-11-09")
assert result == (
expect["status"],
f"Running: {expect['distance_km']} km in {expect['minutes']} min",
)
def test_parsed_fields_match_the_json(self, inbox: Path) -> None:
"""The parser reads sport, duration and distance as the app wrote them."""
spec = _golden()["running"]
_drop(inbox, str(spec["file"]))
data = Verifier()._pull_and_parse_tcx(str(inbox / str(spec["file"])))
assert data is not None
assert data["sport"] == SPORT_RUNNING
assert data["walking"] is False
assert data["duration_seconds"] == spec["total_seconds"]
assert data["distance_m"] == pytest.approx(float(str(spec["distance_m"])))
assert data["activity_id"] == str(spec["started_at"])
def test_the_local_date_in_the_name_is_what_is_globbed(self, inbox: Path) -> None:
"""Another day does not see the file: the stamp is local, the glob is too."""
spec = _golden()["running"]
_drop(inbox, str(spec["file"]))
assert _verify(inbox, "2026-11-10") is None
class TestGoldenWalk:
"""The app's walk file is a walk, and its moving time is what the PC sums."""
def test_walk_is_verified_with_the_promised_moving_minutes(
self, inbox: Path
) -> None:
"""A walk is summed per day: legs, km and moving minutes as promised."""
spec = _golden()["walking"]
_drop(inbox, str(spec["file"]))
expect = spec["expect"]
assert isinstance(expect, dict)
result = _verify(inbox, "2026-11-09")
assert result == (
expect["status"],
(
f"Walking: {expect['legs']} walk(s), {expect['distance_km']} km, "
f"{expect['moving_minutes']} min moving"
),
)
def test_pc_moving_time_equals_the_apps_total_seconds(self, inbox: Path) -> None:
"""Dart's moving-time rule and the PC's ``track_summary`` agree exactly."""
spec = _golden()["walking"]
_drop(inbox, str(spec["file"]))
data = Verifier()._pull_and_parse_tcx(str(inbox / str(spec["file"])))
assert data is not None
assert data["walking"] is True
assert data["moving_seconds"] == pytest.approx(
float(str(spec["total_seconds"]))
)
assert data["activity_id"] == str(spec["started_at"])
def test_walk_is_marked_by_file_name_as_with_runnerup(self, inbox: Path) -> None:
"""Like RunnerUp, the app writes ``Sport="Other"``; the name says walk."""
spec = _golden()["walking"]
name = str(spec["file"])
body = (FIXTURES / name).read_text(encoding="utf-8")
assert 'Sport="Other"' in body
renamed = inbox / "activity.tcx"
renamed.write_text(body)
data = Verifier()._pull_and_parse_tcx(str(renamed))
assert data is not None
assert data["walking"] is False
(inbox / name).write_text(body)
by_name = Verifier()._pull_and_parse_tcx(str(inbox / name))
assert by_name is not None
assert by_name["walking"] is True
class TestGoldenDropout:
"""A run whose GPS dropped out for 2 minutes, written by the app's filter.
The app bridges a dropout it did not pause the way RunnerUp's export does
(straight-line metres across the gap), so the PC's dropout rule keeps both
the distance and the moving time. Pinned by the Dart twin
``test/services/run_dropout_tcx_test.dart``.
"""
NAME = "RunnerUp_2026-11-09-18-00-00_Running.tcx"
def test_pc_credits_the_bridged_dropouts_metres_and_moving_time(
self, inbox: Path
) -> None:
"""30 min at 3 m/s with seconds 600-720 unseen: 5400 m and 1800 s."""
_drop(inbox, self.NAME)
data = Verifier()._pull_and_parse_tcx(str(inbox / self.NAME))
assert data is not None
# Zeroed across the gap (the old app) this was 5040 m and 1680 s.
assert data["track_distance_m"] == pytest.approx(5400, abs=1)
assert data["moving_seconds"] == pytest.approx(1800)
def test_the_run_is_verified_on_its_day(self, inbox: Path) -> None:
"""The whole file passes the real file-based verification."""
_drop(inbox, self.NAME)
result = _verify(inbox, "2026-11-09")
assert result is not None
assert result[0] == "verified"
class TestRunnerUpSample:
"""The trimmed real RunnerUp export parses to the numbers Dart's decoder sees."""
def test_parser_sums_both_laps(self, inbox: Path) -> None:
"""Same literals as the Dart decoder test: 624 + 626 s, two laps' metres."""
_drop(inbox, SAMPLE)
data = Verifier()._pull_and_parse_tcx(str(inbox / SAMPLE))
assert data is not None
assert data["sport"] == SPORT_RUNNING
assert data["walking"] is False
assert data["duration_seconds"] == 1250
assert data["distance_m"] == pytest.approx(983.6306 + 980.7304, abs=1e-3)
assert data["activity_id"] == "2026-08-12T15:42:11Z"