mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 21:33:58 +01:00
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
198 lines
7.8 KiB
Python
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"
|