mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
Match RunnerUp TCX: Calories, walk as Other
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
This commit is contained in:
parent
c2544c4a9c
commit
63caf9dee9
@ -1,11 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<TrainingCenterDatabase xmlns="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd">
|
||||
<Activities>
|
||||
<Activity Sport="Walking">
|
||||
<Activity Sport="Other">
|
||||
<Id>2026-11-09T08:15:00Z</Id>
|
||||
<Lap StartTime="2026-11-09T08:15:00Z">
|
||||
<TotalTimeSeconds>1665</TotalTimeSeconds>
|
||||
<DistanceMeters>2331.004</DistanceMeters>
|
||||
<Calories>0</Calories>
|
||||
<Intensity>Active</Intensity>
|
||||
<TriggerMethod>Manual</TriggerMethod>
|
||||
<Track>
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
<Lap StartTime="2026-11-09T16:30:00Z">
|
||||
<TotalTimeSeconds>1560</TotalTimeSeconds>
|
||||
<DistanceMeters>4212.014</DistanceMeters>
|
||||
<Calories>0</Calories>
|
||||
<Intensity>Active</Intensity>
|
||||
<TriggerMethod>Manual</TriggerMethod>
|
||||
<Track>
|
||||
|
||||
@ -135,20 +135,18 @@ class TestGoldenWalk:
|
||||
)
|
||||
assert data["activity_id"] == str(spec["started_at"])
|
||||
|
||||
def test_walk_is_flagged_by_name_even_if_sport_is_not_walking(
|
||||
self, inbox: Path
|
||||
) -> None:
|
||||
"""The app also writes ``Sport="Walking"``; either signal alone suffices."""
|
||||
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="Walking"' in body
|
||||
assert 'Sport="Other"' in body
|
||||
renamed = inbox / "activity.tcx"
|
||||
renamed.write_text(body.replace('Sport="Walking"', 'Sport="Other"'))
|
||||
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.replace('Sport="Walking"', 'Sport="Other"'))
|
||||
(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
|
||||
|
||||
@ -4,21 +4,29 @@ library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// The sports the app records. The name doubles as the TCX `Sport` attribute
|
||||
/// and the file-name suffix the PC keys on (`..._Walking.tcx`).
|
||||
/// The sports the app records.
|
||||
enum RunSport {
|
||||
/// A run; credited on distance OR duration by the PC.
|
||||
running('Running'),
|
||||
running('Running', 'Running'),
|
||||
|
||||
/// A walk; the PC sums a day's walks and credits their moving time.
|
||||
walking('Walking');
|
||||
///
|
||||
/// Written as `Sport="Other"` exactly like RunnerUp does (the TCX schema
|
||||
/// only knows Running, Biking and Other); the `_Walking.tcx` file-name
|
||||
/// suffix is what marks it a walk.
|
||||
walking('Walking', 'Other');
|
||||
|
||||
const RunSport(this.tcxName);
|
||||
const RunSport(this.tcxName, this.tcxSport);
|
||||
|
||||
/// `Sport="..."` on the TCX `Activity` and `_<name>.tcx` in the file name.
|
||||
/// The sport's name: the `_<name>.tcx` file-name suffix the PC keys on, and
|
||||
/// the `runs.sport` column.
|
||||
final String tcxName;
|
||||
|
||||
/// The sport named [tcxName], or null for anything the app does not record.
|
||||
/// The schema-valid `Sport="..."` attribute the encoder writes.
|
||||
final String tcxSport;
|
||||
|
||||
/// The sport named [tcxName] (also accepts the Strava-style
|
||||
/// `Sport="Walking"`), or null for anything the app does not record.
|
||||
static RunSport? fromTcxName(String? tcxName) {
|
||||
for (final sport in values) {
|
||||
if (sport.tcxName == tcxName) return sport;
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
///
|
||||
/// * File name `RunnerUp_YYYY-MM-DD-HH-MM-SS_<Sport>.tcx`, stamp in LOCAL
|
||||
/// time (the PC globs today's local date in the name), suffix `Running` or
|
||||
/// `Walking` (a walk is recognised by that suffix or `Sport="Walking"`).
|
||||
/// `Walking` (a walk is recognised by that suffix or `Sport="Walking"`; the
|
||||
/// encoder writes `Sport="Other"` for it, as RunnerUp does).
|
||||
/// * `<Id>` and every `<Time>` are UTC with a `Z`; one Lap whose
|
||||
/// `TotalTimeSeconds` is moving time and `DistanceMeters` the credited
|
||||
/// distance; every Trackpoint carries `Time` and a cumulative
|
||||
@ -62,7 +63,7 @@ String encodeTcx(RunTrack track) {
|
||||
|
||||
void _activity(XmlBuilder b, RunTrack track) => b.element(
|
||||
'Activity',
|
||||
attributes: {'Sport': track.sport.tcxName},
|
||||
attributes: {'Sport': track.sport.tcxSport},
|
||||
nest: () {
|
||||
b.element('Id', nest: track.id);
|
||||
_lap(b, track);
|
||||
@ -96,6 +97,8 @@ void _lap(XmlBuilder b, RunTrack track) => b.element(
|
||||
b
|
||||
..element('TotalTimeSeconds', nest: '${track.totalSeconds}')
|
||||
..element('DistanceMeters', nest: _num(track.distanceM, 3))
|
||||
// Required by the TCX schema, as in RunnerUp's own files.
|
||||
..element('Calories', nest: '0')
|
||||
..element('Intensity', nest: 'Active')
|
||||
..element('TriggerMethod', nest: 'Manual')
|
||||
..element('Track', nest: () => track.points.forEach(_point(b)));
|
||||
|
||||
@ -39,6 +39,7 @@ void main() {
|
||||
expect(xml, contains('<Lap StartTime="2026-11-09T16:30:00Z">'));
|
||||
expect(xml, contains('<TotalTimeSeconds>20</TotalTimeSeconds>'));
|
||||
expect(xml, contains('<DistanceMeters>27.500</DistanceMeters>'));
|
||||
expect(xml, contains('<Calories>0</Calories>'));
|
||||
expect(xml, contains('<Time>2026-11-09T16:30:10Z</Time>'));
|
||||
expect(xml, contains('<Name>$kRunSourceName</Name>'));
|
||||
expect(
|
||||
@ -49,6 +50,21 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('a walk is Sport="Other" like RunnerUp writes it, named by file', () {
|
||||
final walk = _track(sport: RunSport.walking);
|
||||
final xml = encodeTcx(walk);
|
||||
expect(xml, contains('<Activity Sport="Other">'));
|
||||
expect(
|
||||
() => decodeTcx(xml),
|
||||
throwsA(isA<FormatException>()),
|
||||
reason: 'only the _Walking suffix says it is a walk',
|
||||
);
|
||||
expect(
|
||||
decodeTcx(xml, fileName: tcxFilename(walk)).sport,
|
||||
RunSport.walking,
|
||||
);
|
||||
});
|
||||
|
||||
test('omits AltitudeMeters when the point has none', () {
|
||||
expect(encodeTcx(_track(alt: null)), isNot(contains('AltitudeMeters')));
|
||||
expect(encodeTcx(_track()), contains('<AltitudeMeters>31.5<'));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user