From 71cb5eecdba1d763a951bef32044a7d9630eb44e Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Wed, 11 Nov 2026 12:00:58 +0100 Subject: [PATCH] Drop GPS dropout bridging Revert 6777656; the app again writes zero distance across a gap over 30 s. Also drop the importer-test skip for its fixture. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WzxdWYwyP67ppvkeie3X1F --- .../RunnerUp_2026-11-09-18-00-00_Running.tcx | 811 ------------------ screen_locker/tests/test_run_tcx_golden.py | 30 - .../workout_app/lib/services/gps_filter.dart | 14 +- .../lib/services/position_source.dart | 3 +- .../workout_app/lib/services/run_math.dart | 38 +- .../test/services/_runnerup_fake_dufs.dart | 8 +- .../test/services/gps_filter_test.dart | 60 +- .../test/services/run_dropout_tcx_test.dart | 79 -- .../test/services/run_math_test.dart | 19 +- .../test/services/run_recorder_test.dart | 37 - 10 files changed, 29 insertions(+), 1070 deletions(-) delete mode 100644 screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-11-09-18-00-00_Running.tcx delete mode 100644 stronglift_replacement/workout_app/test/services/run_dropout_tcx_test.dart diff --git a/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-11-09-18-00-00_Running.tcx b/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-11-09-18-00-00_Running.tcx deleted file mode 100644 index 5beaca8..0000000 --- a/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-11-09-18-00-00_Running.tcx +++ /dev/null @@ -1,811 +0,0 @@ - - - - - 2026-11-09T17:00:00Z - - 1800 - 5400.009 - 0 - Active - Manual - - - - - 48.8584000 - 2.2945000 - - 30.0 - 0.000 - - - - - 48.8589396 - 2.2945000 - - 30.0 - 60.000 - - - - - 48.8594792 - 2.2945000 - - 30.0 - 120.000 - - - - - 48.8600188 - 2.2945000 - - 30.0 - 180.000 - - - - - 48.8605584 - 2.2945000 - - 30.0 - 240.000 - - - - - 48.8610980 - 2.2945000 - - 30.0 - 300.000 - - - - - 48.8616376 - 2.2945000 - - 30.0 - 360.001 - - - - - 48.8621772 - 2.2945000 - - 30.0 - 420.001 - - - - - 48.8627167 - 2.2945000 - - 30.0 - 480.001 - - - - - 48.8632563 - 2.2945000 - - 30.0 - 540.001 - - - - - 48.8637959 - 2.2945000 - - 30.0 - 600.001 - - - - - 48.8643355 - 2.2945000 - - 30.0 - 660.001 - - - - - 48.8648751 - 2.2945000 - - 30.0 - 720.001 - - - - - 48.8654147 - 2.2945000 - - 30.0 - 780.001 - - - - - 48.8659543 - 2.2945000 - - 30.0 - 840.001 - - - - - 48.8664939 - 2.2945000 - - 30.0 - 900.001 - - - - - 48.8670335 - 2.2945000 - - 30.0 - 960.002 - - - - - 48.8675731 - 2.2945000 - - 30.0 - 1020.002 - - - - - 48.8681127 - 2.2945000 - - 30.0 - 1080.002 - - - - - 48.8686523 - 2.2945000 - - 30.0 - 1140.002 - - - - - 48.8691919 - 2.2945000 - - 30.0 - 1200.002 - - - - - 48.8697315 - 2.2945000 - - 30.0 - 1260.002 - - - - - 48.8702710 - 2.2945000 - - 30.0 - 1320.002 - - - - - 48.8708106 - 2.2945000 - - 30.0 - 1380.002 - - - - - 48.8713502 - 2.2945000 - - 30.0 - 1440.002 - - - - - 48.8718898 - 2.2945000 - - 30.0 - 1500.002 - - - - - 48.8724294 - 2.2945000 - - 30.0 - 1560.003 - - - - - 48.8729690 - 2.2945000 - - 30.0 - 1620.003 - - - - - 48.8735086 - 2.2945000 - - 30.0 - 1680.003 - - - - - 48.8740482 - 2.2945000 - - 30.0 - 1740.003 - - - - - 48.8745878 - 2.2945000 - - 30.0 - 1800.003 - - - - - 48.8778254 - 2.2945000 - - 30.0 - 2160.004 - - - - - 48.8783649 - 2.2945000 - - 30.0 - 2220.004 - - - - - 48.8789045 - 2.2945000 - - 30.0 - 2280.004 - - - - - 48.8794441 - 2.2945000 - - 30.0 - 2340.004 - - - - - 48.8799837 - 2.2945000 - - 30.0 - 2400.004 - - - - - 48.8805233 - 2.2945000 - - 30.0 - 2460.004 - - - - - 48.8810629 - 2.2945000 - - 30.0 - 2520.004 - - - - - 48.8816025 - 2.2945000 - - 30.0 - 2580.004 - - - - - 48.8821421 - 2.2945000 - - 30.0 - 2640.004 - - - - - 48.8826817 - 2.2945000 - - 30.0 - 2700.004 - - - - - 48.8832213 - 2.2945000 - - 30.0 - 2760.004 - - - - - 48.8837609 - 2.2945000 - - 30.0 - 2820.005 - - - - - 48.8843005 - 2.2945000 - - 30.0 - 2880.005 - - - - - 48.8848401 - 2.2945000 - - 30.0 - 2940.005 - - - - - 48.8853797 - 2.2945000 - - 30.0 - 3000.005 - - - - - 48.8859192 - 2.2945000 - - 30.0 - 3060.005 - - - - - 48.8864588 - 2.2945000 - - 30.0 - 3120.005 - - - - - 48.8869984 - 2.2945000 - - 30.0 - 3180.005 - - - - - 48.8875380 - 2.2945000 - - 30.0 - 3240.005 - - - - - 48.8880776 - 2.2945000 - - 30.0 - 3300.005 - - - - - 48.8886172 - 2.2945000 - - 30.0 - 3360.005 - - - - - 48.8891568 - 2.2945000 - - 30.0 - 3420.006 - - - - - 48.8896964 - 2.2945000 - - 30.0 - 3480.006 - - - - - 48.8902360 - 2.2945000 - - 30.0 - 3540.006 - - - - - 48.8907756 - 2.2945000 - - 30.0 - 3600.006 - - - - - 48.8913152 - 2.2945000 - - 30.0 - 3660.006 - - - - - 48.8918548 - 2.2945000 - - 30.0 - 3720.006 - - - - - 48.8923944 - 2.2945000 - - 30.0 - 3780.006 - - - - - 48.8929340 - 2.2945000 - - 30.0 - 3840.006 - - - - - 48.8934736 - 2.2945000 - - 30.0 - 3900.006 - - - - - 48.8940131 - 2.2945000 - - 30.0 - 3960.006 - - - - - 48.8945527 - 2.2945000 - - 30.0 - 4020.007 - - - - - 48.8950923 - 2.2945000 - - 30.0 - 4080.007 - - - - - 48.8956319 - 2.2945000 - - 30.0 - 4140.007 - - - - - 48.8961715 - 2.2945000 - - 30.0 - 4200.007 - - - - - 48.8967111 - 2.2945000 - - 30.0 - 4260.007 - - - - - 48.8972507 - 2.2945000 - - 30.0 - 4320.007 - - - - - 48.8977903 - 2.2945000 - - 30.0 - 4380.007 - - - - - 48.8983299 - 2.2945000 - - 30.0 - 4440.007 - - - - - 48.8988695 - 2.2945000 - - 30.0 - 4500.007 - - - - - 48.8994091 - 2.2945000 - - 30.0 - 4560.007 - - - - - 48.8999487 - 2.2945000 - - 30.0 - 4620.007 - - - - - 48.9004883 - 2.2945000 - - 30.0 - 4680.008 - - - - - 48.9010279 - 2.2945000 - - 30.0 - 4740.008 - - - - - 48.9015674 - 2.2945000 - - 30.0 - 4800.008 - - - - - 48.9021070 - 2.2945000 - - 30.0 - 4860.008 - - - - - 48.9026466 - 2.2945000 - - 30.0 - 4920.008 - - - - - 48.9031862 - 2.2945000 - - 30.0 - 4980.008 - - - - - 48.9037258 - 2.2945000 - - 30.0 - 5040.008 - - - - - 48.9042654 - 2.2945000 - - 30.0 - 5100.008 - - - - - 48.9048050 - 2.2945000 - - 30.0 - 5160.008 - - - - - 48.9053446 - 2.2945000 - - 30.0 - 5220.008 - - - - - 48.9058842 - 2.2945000 - - 30.0 - 5280.009 - - - - - 48.9064238 - 2.2945000 - - 30.0 - 5340.009 - - - - - 48.9069634 - 2.2945000 - - 30.0 - 5400.009 - - - - - kuhy workout_app - 0 - 0 - - 1 - 0 - - - - - - kuhy workout_app - - - 1 - 0 - - - en - 000-00000-00 - - \ No newline at end of file diff --git a/screen_locker/tests/test_run_tcx_golden.py b/screen_locker/tests/test_run_tcx_golden.py index 55a9d3d..d0366f8 100644 --- a/screen_locker/tests/test_run_tcx_golden.py +++ b/screen_locker/tests/test_run_tcx_golden.py @@ -152,36 +152,6 @@ class TestGoldenWalk: 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.""" diff --git a/stronglift_replacement/workout_app/lib/services/gps_filter.dart b/stronglift_replacement/workout_app/lib/services/gps_filter.dart index d307c45..f6fe82f 100644 --- a/stronglift_replacement/workout_app/lib/services/gps_filter.dart +++ b/stronglift_replacement/workout_app/lib/services/gps_filter.dart @@ -64,12 +64,11 @@ enum FixVerdict { /// Accumulates accepted points and their cumulative credited distance. /// -/// A step between two accepted points adds its straight-line distance only -/// when it is a moving step (see [isMovingStep]). A signal dropout (a gap over -/// [kMaxCreditGapSeconds] with no pause) is bridged the way RunnerUp's export -/// bridges it, provided it implies [kMinDropoutSpeedMps] or faster; anything -/// above the jump cap is rejected, and standing-still jitter credits nothing. -/// A user pause ([breakSegment]) never bridges: pauses earn nothing. +/// A step between two accepted points adds distance only when it is a moving +/// step (see [isMovingStep]): a dropout longer than [kMaxCreditGapSeconds] +/// and standing-still jitter credit nothing. The PC judges windows of 10 s or +/// more and keeps a dropout moving at 1 m/s or faster, so it can credit more +/// than this does (see `run_math.dart`). class GpsFilter { /// Creates a filter; every recording uses the running jump cap. GpsFilter(); @@ -128,7 +127,8 @@ class GpsFilter { _accept(fix, time, 0); return FixVerdict.accepted; } - final moving = step / gap >= minStepSpeed(gap); + final moving = + gap <= kMaxCreditGapSeconds && step / gap >= kMinMovingSpeedMps; _accept(fix, time, moving ? step : 0); return FixVerdict.accepted; } diff --git a/stronglift_replacement/workout_app/lib/services/position_source.dart b/stronglift_replacement/workout_app/lib/services/position_source.dart index 815b802..94dd8e9 100644 --- a/stronglift_replacement/workout_app/lib/services/position_source.dart +++ b/stronglift_replacement/workout_app/lib/services/position_source.dart @@ -39,7 +39,6 @@ abstract class PositionSource { /// Fixes roughly every [interval], best accuracy, until cancelled. /// /// [interval] must stay well under the PC's 30 s credit gap - /// (`kMaxCreditGapSeconds`): a quieter stream is treated as a dropout and - /// only counts at 1 m/s or faster. + /// (`kMaxCreditGapSeconds`): a quieter stream earns nothing. Stream fixes({Duration interval = const Duration(seconds: 2)}); } diff --git a/stronglift_replacement/workout_app/lib/services/run_math.dart b/stronglift_replacement/workout_app/lib/services/run_math.dart index ce8fff7..d8938b9 100644 --- a/stronglift_replacement/workout_app/lib/services/run_math.dart +++ b/stronglift_replacement/workout_app/lib/services/run_math.dart @@ -10,14 +10,11 @@ /// 30 s or less earns moving time at 0.5 m/s or faster; a longer one (a GPS /// dropout) at 1 m/s or faster; paused time earns nothing. /// -/// The app mirrors those bounds per pair of its 2 s fixes, and bridges a -/// dropout the way a RunnerUp export does: the straight-line distance across a -/// gap over 30 s that the runner did not pause is credited when it implies -/// 1 m/s to 25 km/h (`GpsFilter`); a user pause credits nothing. The -/// on-screen distance, the saved run and the TCX `DistanceMeters` all come from -/// that one cumulative figure. The only remaining difference is that the PC -/// judges 10 s windows rather than single pairs, so its moving time can differ -/// slightly on a jittery or stop-and-go stretch. +/// The app mirrors only the bounds, per pair of its 2 s fixes, and is stricter: +/// a pair over 30 s credits nothing, where the PC keeps a dropout at 1 m/s or +/// faster. The app writes zero distance across such a gap, so the PC does not +/// credit a dropout in an app-recorded run. Its on-screen moving time is +/// therefore an estimate, not the number the PC will credit. library; import 'dart:math' as math; @@ -27,8 +24,8 @@ import 'package:workout_app/models/run_track.dart'; /// Mean Earth radius in metres (IUGG), the haversine constant. const double kEarthRadiusM = 6371008.8; -/// A pair of fixes further apart in time than this is a GPS dropout whose -/// motion is unknown: it counts only at [kMinDropoutSpeedMps] or faster. +/// A pair of fixes further apart in time than this earns neither distance +/// nor moving time: a GPS dropout whose motion is unknown. /// /// Mirrors `WALK_MAX_GAP_SECONDS` in the PC's `screen_locker/_constants.py`. const int kMaxCreditGapSeconds = 30; @@ -38,12 +35,6 @@ const int kMaxCreditGapSeconds = 30; /// Mirrors `MIN_MOVING_SPEED_MPS` on the PC. const double kMinMovingSpeedMps = 0.5; -/// A dropout (a gap over [kMaxCreditGapSeconds]) counts only when the straight -/// line across it implies at least this speed; slower is standing or a pause. -/// -/// Mirrors `MIN_DROPOUT_SPEED_MPS` on the PC. -const double kMinDropoutSpeedMps = 1; - /// Speed cap above which a fix is a GPS jump, 25 km/h. /// /// Mirrors `MAX_MOVING_SPEED_MPS` on the PC, which applies it to each window @@ -65,23 +56,18 @@ double haversineM(double lat1, double lon1, double lat2, double lon2) { return 2 * kEarthRadiusM * math.asin(math.min(1, math.sqrt(a))); } -/// The slowest speed that counts as moving across a [gapSeconds] step. -double minStepSpeed(num gapSeconds) => gapSeconds > kMaxCreditGapSeconds - ? kMinDropoutSpeedMps - : kMinMovingSpeedMps; - -/// Whether the step from [a] to [b] counts as moving: travelled at a speed in -/// `[kMinMovingSpeedMps, maxSpeed]`, or in `[kMinDropoutSpeedMps, maxSpeed]` -/// when the fixes are further apart than [kMaxCreditGapSeconds] (a dropout). +/// Whether the step from [a] to [b] counts as moving: seen within +/// [kMaxCreditGapSeconds] and travelled at a speed in +/// `[kMinMovingSpeedMps, maxSpeed]`. /// /// Speed comes from the cumulative `distanceM` difference, as the PC reads a /// TCX, but over one pair of fixes rather than a window of 10 s or more, so the /// two can disagree on a jittery or stop-and-go stretch. bool isMovingStep(RunPoint a, RunPoint b, double maxSpeed) { final gap = b.time.difference(a.time).inMilliseconds / 1000; - if (gap <= 0) return false; + if (gap <= 0 || gap > kMaxCreditGapSeconds) return false; final speed = (b.distanceM - a.distanceM) / gap; - return speed >= minStepSpeed(gap) && speed <= maxSpeed; + return speed >= kMinMovingSpeedMps && speed <= maxSpeed; } /// Moving seconds over [points]: the summed gaps of every moving step. diff --git a/stronglift_replacement/workout_app/test/services/_runnerup_fake_dufs.dart b/stronglift_replacement/workout_app/test/services/_runnerup_fake_dufs.dart index 8725e11..f3b7d6a 100644 --- a/stronglift_replacement/workout_app/test/services/_runnerup_fake_dufs.dart +++ b/stronglift_replacement/workout_app/test/services/_runnerup_fake_dufs.dart @@ -13,16 +13,10 @@ import 'package:workout_app/services/tcx_codec.dart'; const String kFixtureDir = '../../screen_locker/tests/fixtures/run_tcx'; -/// The GPS-dropout export: shares the folder for the golden twin, but is not -/// one of the three exports these importer and backup tests count on. -const String kDropoutFixture = 'RunnerUp_2026-11-09-18-00-00_Running.tcx'; - /// The three golden exports, by file name. Map fixtureTcx() => { for (final f in Directory(kFixtureDir).listSync().whereType()) - if (f.path.endsWith('.tcx') && - f.uri.pathSegments.last != kDropoutFixture) - f.uri.pathSegments.last: f.readAsBytesSync(), + if (f.path.endsWith('.tcx')) f.uri.pathSegments.last: f.readAsBytesSync(), }; final DufsCredentials kLogin = DufsCredentials( diff --git a/stronglift_replacement/workout_app/test/services/gps_filter_test.dart b/stronglift_replacement/workout_app/test/services/gps_filter_test.dart index 3216231..770eb3d 100644 --- a/stronglift_replacement/workout_app/test/services/gps_filter_test.dart +++ b/stronglift_replacement/workout_app/test/services/gps_filter_test.dart @@ -90,14 +90,14 @@ void main() { expect(f.points.first.altitudeM, 30); }); - test('standing-still jitter and standing through a gap earn nothing', () { + test('standing-still jitter and a long dropout earn no distance', () { final f = GpsFilter() ..add(_fix(0, 0)) ..add(_fix(10, 2)); // 0.2 m/s: standing expect(f.distanceM, 0); - f.add(_fix(60, 2)); // 50 s later, same spot: nothing to bridge + f.add(_fix(60, 100)); // 50 s later: unseen motion expect(f.distanceM, 0); - f.add(_fix(70, 29)); // seen again, moving + f.add(_fix(70, 127)); // seen again, moving expect(f.distanceM, closeTo(27, 0.2)); expect(f.points, hasLength(4)); }); @@ -129,60 +129,6 @@ void main() { }); }); - group('signal dropout (a gap over 30 s while running)', () { - // Mirrors the PC: WALK_MAX_GAP_SECONDS 30, MIN_DROPOUT_SPEED_MPS 1.0, - // MAX_MOVING_SPEED_MPS 25 km/h. - GpsFilter runningTo30m() => GpsFilter() - ..add(_fix(0, 0)) - ..add(_fix(10, 30)); - - test('at 3 m/s the straight-line distance is bridged', () { - final f = runningTo30m(); - expect(f.add(_fix(70, 210)), FixVerdict.accepted); // 60 s, 180 m - expect(f.distanceM, closeTo(210, 0.5)); - expect(f.points.last.distanceM, closeTo(210, 0.5)); - }); - - test('just over the dropout floor (1 m/s) is bridged', () { - final f = runningTo30m()..add(_fix(70, 91)); // 60 s, 61 m: 1.02 m/s - expect(f.distanceM, closeTo(91, 0.5)); - }); - - test('0.5 m/s is standing about, not a run: nothing is bridged', () { - final f = runningTo30m(); - expect(f.add(_fix(70, 60)), FixVerdict.accepted); // 60 s, 30 m - expect(f.distanceM, closeTo(30, 0.5)); - }); - - test('40 km/h is a GPS jump: rejected, nothing is bridged', () { - final f = runningTo30m(); - expect(f.add(_fix(70, 30 + 666)), FixVerdict.tooFast); // 11.1 m/s - expect(f.distanceM, closeTo(30, 0.5)); - }); - - test('31 s is a dropout needing 1 m/s, 30 s still needs only 0.5', () { - final slow = runningTo30m()..add(_fix(41, 50)); // 31 s, 20 m: 0.65 m/s - expect(slow.distanceM, closeTo(30, 0.5)); - final ok = runningTo30m()..add(_fix(40, 50)); // 30 s, 20 m: 0.67 m/s - expect(ok.distanceM, closeTo(50, 0.5)); - }); - - test('a user pause with the same geometry bridges nothing', () { - final f = runningTo30m()..breakSegment(); - expect(f.add(_fix(70, 210)), FixVerdict.accepted); - expect(f.distanceM, closeTo(30, 0.5)); - }); - - test('moving time and distance agree across the bridged dropout', () { - final f = runningTo30m() - ..add(_fix(70, 210)) - ..add(_fix(80, 240)); - expect(f.distanceM, closeTo(240, 0.5)); - // 10 s + 60 s dropout + 10 s, as the PC counts it. - expect(movingSeconds(f.points), 80); - }); - }); - group('breakSegment', () { test('the fix after a pause earns nothing, then crediting resumes', () { final f = GpsFilter() diff --git a/stronglift_replacement/workout_app/test/services/run_dropout_tcx_test.dart b/stronglift_replacement/workout_app/test/services/run_dropout_tcx_test.dart deleted file mode 100644 index aa9a93d..0000000 --- a/stronglift_replacement/workout_app/test/services/run_dropout_tcx_test.dart +++ /dev/null @@ -1,79 +0,0 @@ -@TestOn('vm') -library; - -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:workout_app/models/run_track.dart'; -import 'package:workout_app/services/gps_filter.dart'; -import 'package:workout_app/services/run_math.dart'; -import 'package:workout_app/services/tcx_codec.dart'; - -import '../run_test_fixtures.dart'; - -/// A run with a GPS dropout, written by the app's own filter and TCX writer. -/// -/// 30 min at 3 m/s, a fix every 20 s, except that the signal is gone between -/// second 600 and 720. The PC credits a dropout that moved at 1 m/s or more -/// (`MIN_DROPOUT_SPEED_MPS`), so the cumulative `DistanceMeters` must jump -/// 360 m across it. The Python twin, `screen_locker/tests/test_run_tcx_golden.py`, -/// feeds the SAME checked-in file to the real verifier. Regenerate it with -/// `UPDATE_GOLDEN=1 flutter test test/services/run_dropout_tcx_test.dart`. -const String _dir = '../../screen_locker/tests/fixtures/run_tcx'; -const String _file = 'RunnerUp_2026-11-09-18-00-00_Running.tcx'; - -RunTrack _recordedRun() { - final filter = GpsFilter(); - for (var s = 0; s <= 1800; s += 20) { - if (s > 600 && s < 720) continue; // the dropout - final fix = fixAt(s, s * 3.0); - filter.add( - GpsFix( - time: DateTime.utc(2026, 11, 9, 17).add(Duration(seconds: s)), - latitude: fix.latitude, - longitude: fix.longitude, - altitudeM: 30, - accuracyM: 5, - ), - ); - } - final points = filter.points; - return RunTrack.started( - sport: RunSport.running, - startedAt: points.first.time, - points: points, - totalSeconds: movingSeconds(points), - distanceM: filter.distanceM, - ); -} - -void main() { - final track = _recordedRun(); - - test('cumulative DistanceMeters jumps across the dropout', () { - final back = decodeTcx(encodeTcx(track), fileName: _file); - final before = back.points.firstWhere( - (p) => p.time.minute == 10 && p.time.second == 0, - ); - final after = back.points.firstWhere( - (p) => p.time.minute == 12 && p.time.second == 0, - ); - expect(before.distanceM, closeTo(1800, 0.5)); - expect(after.distanceM - before.distanceM, closeTo(360, 0.5)); - }); - - test('lap distance and moving time count the dropout', () { - expect(track.distanceM, closeTo(5400, 1)); - expect(track.totalSeconds, 1800); - }); - - test('encodes to exactly the checked-in file the PC test reads', () { - final encoded = encodeTcx(track); - final file = File('$_dir/$_file'); - if (Platform.environment['UPDATE_GOLDEN'] == '1') { - file.writeAsStringSync(encoded); - } - expect(file.readAsStringSync(), encoded); - expect(tcxFilename(track, utcOffset: const Duration(minutes: 60)), _file); - }); -} diff --git a/stronglift_replacement/workout_app/test/services/run_math_test.dart b/stronglift_replacement/workout_app/test/services/run_math_test.dart index 1704744..96ff005 100644 --- a/stronglift_replacement/workout_app/test/services/run_math_test.dart +++ b/stronglift_replacement/workout_app/test/services/run_math_test.dart @@ -50,17 +50,9 @@ void main() { expect(isMovingStep(_p(10, 0), _p(5, 10), cap), isFalse); }); - test('30 s exactly needs 0.5 m/s, a longer dropout needs 1 m/s', () { + test('a gap over 30 s earns nothing, 30 s exactly does', () { expect(isMovingStep(_p(0, 0), _p(30, 40), cap), isTrue); - expect(isMovingStep(_p(0, 0), _p(30, 16), cap), isTrue); - // 31 s at 0.65 m/s: a pause-like stand, not a bridged dropout. - expect(isMovingStep(_p(0, 0), _p(31, 20), cap), isFalse); - // 60 s at 1 m/s and at 3 m/s: bridged dropouts. - expect(isMovingStep(_p(0, 0), _p(60, 60), cap), isTrue); - expect(isMovingStep(_p(0, 0), _p(60, 180), cap), isTrue); - // A dropout faster than the cap or with no distance (pause) is not. - expect(isMovingStep(_p(0, 0), _p(60, 400), cap), isFalse); - expect(isMovingStep(_p(0, 0), _p(60, 0), cap), isFalse); + expect(isMovingStep(_p(0, 0), _p(31, 40), cap), isFalse); }); test('movingSeconds sums only the moving steps', () { @@ -68,11 +60,10 @@ void main() { _p(0, 0), _p(10, 13), // moving, 10 s _p(20, 13), // standing, 0 distance - _p(55, 23), // 35 s gap at 0.29 m/s, not credited - _p(65, 36), // moving, 10 s - _p(125, 216), // 60 s dropout at 3 m/s, bridged + _p(55, 60), // 35 s gap, not credited + _p(65, 73), // moving, 10 s ]; - expect(movingSeconds(points), 80); + expect(movingSeconds(points), 20); expect(movingSeconds(const []), 0); }); }); diff --git a/stronglift_replacement/workout_app/test/services/run_recorder_test.dart b/stronglift_replacement/workout_app/test/services/run_recorder_test.dart index e2ab8e0..d574cb9 100644 --- a/stronglift_replacement/workout_app/test/services/run_recorder_test.dart +++ b/stronglift_replacement/workout_app/test/services/run_recorder_test.dart @@ -184,43 +184,6 @@ void main() { recorder.dispose(); }); - test( - 'a signal dropout is bridged, a pause of the same length is not', - () async { - Future run({required bool paused}) async { - final source = ManualPositionSource(); - final recorder = _recorder(source); - await recorder.start(); - source.controller - ..add(fixAt(0, 0)) - ..add(fixAt(2, 6)); - await pumpEventQueue(); - if (paused) recorder.pause(); - // 58 s later and 174 m further: 3 m/s, as if the signal dropped out. - if (paused) recorder.resume(); - source.controller - ..add(fixAt(60, 180)) - ..add(fixAt(62, 186)); - await pumpEventQueue(); - return recorder; - } - - final dropout = await run(paused: false); - expect(dropout.distanceM, closeTo(186, 0.5)); - expect(dropout.seconds, 62); - final track = (await dropout.finish())!; - expect(track.distanceM, closeTo(186, 0.5)); - expect(track.totalSeconds, 62); - expect(track.points.last.distanceM, closeTo(186, 0.5)); - - final pause = await run(paused: true); - expect(pause.distanceM, closeTo(12, 0.5)); - expect(pause.seconds, 4); - dropout.dispose(); - pause.dispose(); - }, - ); - test( 'finish works from paused; pause and resume elsewhere are no-ops', () async {