From cf2d917b3399187a66edafc31369db1ce2818127 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 27 Sep 2026 19:06:15 +0200 Subject: [PATCH] Fix red CI: coverage 7.16.2, split workout_screen.dart and modes test - coverage 7.16.1 -> 7.16.2 (dependency freshness). - workout_screen.dart (253) -> _allSetsCompleted/_isPaused move into workout_screen_breaks.dart next to _stateOf. - storage_service_modes_test.dart (289) -> setExerciseSettings and per-mode progression groups move to storage_service_settings_test.dart. Claude-Session: https://claude.ai/code/session_01KeVXsWTuwjmZK5uvbw6vqF Co-Authored-By: Claude Opus 5 (1M context) --- requirements.txt | 2 +- .../lib/screens/workout_screen.dart | 16 --- .../lib/screens/workout_screen_breaks.dart | 18 ++- .../services/storage_service_modes_test.dart | 118 +--------------- .../storage_service_settings_test.dart | 128 ++++++++++++++++++ 5 files changed, 150 insertions(+), 132 deletions(-) create mode 100644 stronglift_replacement/workout_app/test/services/storage_service_settings_test.dart diff --git a/requirements.txt b/requirements.txt index 079d821..532df53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # Runtime: tkinter/subprocess/socket/sqlite3 (stdlib) plus gatelock (below). bandit==1.9.4 codespell==2.4.3 -coverage==7.16.1 +coverage==7.16.2 crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync freedays @ git+https://github.com/kuhyx/utils@freedays-v0.2.0#subdirectory=freedays gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.2#subdirectory=gatelock diff --git a/stronglift_replacement/workout_app/lib/screens/workout_screen.dart b/stronglift_replacement/workout_app/lib/screens/workout_screen.dart index 27651b7..c54ed55 100644 --- a/stronglift_replacement/workout_app/lib/screens/workout_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/workout_screen.dart @@ -172,22 +172,6 @@ class _WorkoutScreenState extends State { // ── Helpers ──────────────────────────────────────────────────────────────── - /// Every set recorded, not counting paused exercises: those have no sets - /// to tap and are recorded as failed at Finish. - bool get _allSetsCompleted { - for (var i = 0; i < _tapped.length; i++) { - if (!_isPaused(i) && !_tapped[i].every((t) => t)) return false; - } - return true; - } - - /// Whether exercise [exIdx] is on an injury pause right now. - bool _isPaused(int exIdx) => - _exerciseStates[widget.exercises[exIdx].name]?.isPausedAt( - DateTime.now(), - ) ?? - false; - /// Runs [fn] inside `setState` on behalf of this library's extensions. /// /// `setState` is `@protected`, so an extension cannot call it directly. This diff --git a/stronglift_replacement/workout_app/lib/screens/workout_screen_breaks.dart b/stronglift_replacement/workout_app/lib/screens/workout_screen_breaks.dart index ef7a66b..03e8b47 100644 --- a/stronglift_replacement/workout_app/lib/screens/workout_screen_breaks.dart +++ b/stronglift_replacement/workout_app/lib/screens/workout_screen_breaks.dart @@ -1,4 +1,4 @@ -// Rest-period timers and per-exercise settings edits. +// Rest-period timers, per-exercise state lookups and settings edits. // // See workout_screen_session.dart for why this is a `part`. `setState` is // `@protected` and unreachable from an extension, so these mutate through the @@ -12,6 +12,22 @@ const _expiredBreakGraceSecs = 120; /// Starting, ticking, cancelling and finishing the rest period between sets. extension _WorkoutScreenBreaks on _WorkoutScreenState { + /// Every set recorded, not counting paused exercises: those have no sets + /// to tap and are recorded as failed at Finish. + bool get _allSetsCompleted { + for (var i = 0; i < _tapped.length; i++) { + if (!_isPaused(i) && !_tapped[i].every((t) => t)) return false; + } + return true; + } + + /// Whether exercise [exIdx] is on an injury pause right now. + bool _isPaused(int exIdx) => + _exerciseStates[widget.exercises[exIdx].name]?.isPausedAt( + DateTime.now(), + ) ?? + false; + /// Exercise [exIdx]'s stored state, or its defaults while the states are /// still loading: that is where its rest lengths live. ExerciseState _stateOf(int exIdx) { diff --git a/stronglift_replacement/workout_app/test/services/storage_service_modes_test.dart b/stronglift_replacement/workout_app/test/services/storage_service_modes_test.dart index c4f04f6..c3fdb6c 100644 --- a/stronglift_replacement/workout_app/test/services/storage_service_modes_test.dart +++ b/stronglift_replacement/workout_app/test/services/storage_service_modes_test.dart @@ -11,8 +11,6 @@ import 'package:workout_app/services/storage_service.dart'; StorageService get _svc => StorageService.instance; -final _yesterday = DateTime.now().subtract(const Duration(days: 1)); - /// Builds a database exactly as a v3 install left it: the v3 schema, a /// finished workout from today, and a workout in progress. Future _writeV3Database(String path) async { @@ -122,12 +120,10 @@ void main() { test('keeps progression and gives every row weight mode', () async { for (final ex in [...workoutA, ...workoutB]) { final s = (await _svc.getExerciseState(ex.name))!; - expect((s.weight, s.reps, s.successStreak, s.failStreak), ( - 17.5, - 9, - 2, - 1, - )); + expect( + (s.weight, s.reps, s.successStreak, s.failStreak), + (17.5, 9, 2, 1), + ); expect((s.successThreshold, s.failThreshold), (5, 4)); expect(s.mode, ProgressionMode.weight); expect((s.repsHigh, s.repsLow), (kDefaultRepsHigh, kDefaultRepsLow)); @@ -180,110 +176,4 @@ void main() { expect(situp.pausedUntil, isNull); expect(await _svc.getLastWorkoutDate(), DateTime(2026, 9, 27)); }); - - group('setExerciseSettings', () { - test('persists mode, rep range, warmup and thresholds', () async { - final name = workoutA.first.name; - final s = (await _svc.getExerciseState(name))!; - await _svc.setExerciseSettings( - s.copyWith( - mode: ProgressionMode.doubleProgression, - repsHigh: 15, - repsLow: 8, - hasWarmup: false, - successThreshold: 1, - failThreshold: 4, - ), - ); - final after = (await _svc.getExerciseState(name))!; - expect(after.mode, ProgressionMode.doubleProgression); - expect((after.repsHigh, after.repsLow), (15, 8)); - expect(after.hasWarmup, isFalse); - expect((after.successThreshold, after.failThreshold), (1, 4)); - // The target itself is untouched until the next finished workout. - expect((after.weight, after.reps), (s.weight, s.reps)); - }); - - test('persists a pause and its resume', () async { - final s = (await _svc.getExerciseState('Situp'))!; - await _svc.setExerciseSettings( - s.copyWith(pausedUntil: DateTime(2026, 10, 11)), - ); - expect( - (await _svc.getExerciseState('Situp'))!.pausedUntil, - DateTime(2026, 10, 11), - ); - await _svc.setExerciseSettings(s.copyWith(resume: true)); - expect((await _svc.getExerciseState('Situp'))!.pausedUntil, isNull); - }); - - test('reaches the workout through getCurrentExercises', () async { - final name = workoutA.first.name; - final s = (await _svc.getExerciseState(name))!; - await _svc.setExerciseSettings(s.copyWith(hasWarmup: false)); - final exercises = await _svc.getCurrentExercises('A'); - expect(exercises.first.hasWarmup, isFalse); - expect(exercises[1].hasWarmup, isTrue); - }); - }); - - group('applyProgression per mode', () { - Future run( - ProgressionMode mode, { - required bool success, - required int reps, - DateTime? last, - }) async { - final name = workoutA.first.name; - final s = (await _svc.getExerciseState(name))!; - await _svc.setExerciseSettings( - s.copyWith(mode: mode, successThreshold: 1, failThreshold: 1), - ); - await _svc.setExerciseReps(name, reps); - await _svc.setExerciseWeight(name, 10); - await _svc.applyProgression( - succeededExercises: {name: success}, - lastWorkoutDate: last ?? _yesterday, - ); - return (await _svc.getExerciseState(name))!; - } - - test('reps: success adds a rep', () async { - final s = await run(ProgressionMode.reps, success: true, reps: 10); - expect((s.weight, s.reps, s.successStreak), (10, 11, 0)); - }); - - test('reps: failure removes a rep', () async { - final s = await run(ProgressionMode.reps, success: false, reps: 10); - expect((s.weight, s.reps, s.failStreak), (10, 9, 0)); - }); - - test('double: success at n moves up in weight and back to m', () async { - final s = await run( - ProgressionMode.doubleProgression, - success: true, - reps: 12, - ); - expect((s.weight, s.reps), (12.5, 6)); - }); - - test('double: failure at m moves down in weight and up to n', () async { - final s = await run( - ProgressionMode.doubleProgression, - success: false, - reps: 6, - ); - expect((s.weight, s.reps), (7.5, 12)); - }); - - test('a week off is one regression step in the active mode', () async { - final s = await run( - ProgressionMode.reps, - success: true, - reps: 10, - last: DateTime.now().subtract(const Duration(days: 9)), - ); - expect((s.weight, s.reps, s.successStreak, s.failStreak), (10, 9, 0, 0)); - }); - }); } diff --git a/stronglift_replacement/workout_app/test/services/storage_service_settings_test.dart b/stronglift_replacement/workout_app/test/services/storage_service_settings_test.dart new file mode 100644 index 0000000..21462d3 --- /dev/null +++ b/stronglift_replacement/workout_app/test/services/storage_service_settings_test.dart @@ -0,0 +1,128 @@ +// Per-exercise settings writes and progression per mode, through storage. +// Split from storage_service_modes_test.dart for the 250-line cap. +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:workout_app/models/workout_plan.dart'; +import 'package:workout_app/services/storage_service.dart'; + +StorageService get _svc => StorageService.instance; + +final _yesterday = DateTime.now().subtract(const Duration(days: 1)); + +void main() { + setUpAll(() { + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + }); + + setUp(() async { + StorageService.resetForTesting(); + await StorageService.init(); + }); + + group('setExerciseSettings', () { + test('persists mode, rep range, warmup and thresholds', () async { + final name = workoutA.first.name; + final s = (await _svc.getExerciseState(name))!; + await _svc.setExerciseSettings( + s.copyWith( + mode: ProgressionMode.doubleProgression, + repsHigh: 15, + repsLow: 8, + hasWarmup: false, + successThreshold: 1, + failThreshold: 4, + ), + ); + final after = (await _svc.getExerciseState(name))!; + expect(after.mode, ProgressionMode.doubleProgression); + expect((after.repsHigh, after.repsLow), (15, 8)); + expect(after.hasWarmup, isFalse); + expect((after.successThreshold, after.failThreshold), (1, 4)); + // The target itself is untouched until the next finished workout. + expect((after.weight, after.reps), (s.weight, s.reps)); + }); + + test('persists a pause and its resume', () async { + final s = (await _svc.getExerciseState('Situp'))!; + await _svc.setExerciseSettings( + s.copyWith(pausedUntil: DateTime(2026, 10, 11)), + ); + expect( + (await _svc.getExerciseState('Situp'))!.pausedUntil, + DateTime(2026, 10, 11), + ); + await _svc.setExerciseSettings(s.copyWith(resume: true)); + expect((await _svc.getExerciseState('Situp'))!.pausedUntil, isNull); + }); + + test('reaches the workout through getCurrentExercises', () async { + final name = workoutA.first.name; + final s = (await _svc.getExerciseState(name))!; + await _svc.setExerciseSettings(s.copyWith(hasWarmup: false)); + final exercises = await _svc.getCurrentExercises('A'); + expect(exercises.first.hasWarmup, isFalse); + expect(exercises[1].hasWarmup, isTrue); + }); + }); + + group('applyProgression per mode', () { + Future run( + ProgressionMode mode, { + required bool success, + required int reps, + DateTime? last, + }) async { + final name = workoutA.first.name; + final s = (await _svc.getExerciseState(name))!; + await _svc.setExerciseSettings( + s.copyWith(mode: mode, successThreshold: 1, failThreshold: 1), + ); + await _svc.setExerciseReps(name, reps); + await _svc.setExerciseWeight(name, 10); + await _svc.applyProgression( + succeededExercises: {name: success}, + lastWorkoutDate: last ?? _yesterday, + ); + return (await _svc.getExerciseState(name))!; + } + + test('reps: success adds a rep', () async { + final s = await run(ProgressionMode.reps, success: true, reps: 10); + expect((s.weight, s.reps, s.successStreak), (10, 11, 0)); + }); + + test('reps: failure removes a rep', () async { + final s = await run(ProgressionMode.reps, success: false, reps: 10); + expect((s.weight, s.reps, s.failStreak), (10, 9, 0)); + }); + + test('double: success at n moves up in weight and back to m', () async { + final s = await run( + ProgressionMode.doubleProgression, + success: true, + reps: 12, + ); + expect((s.weight, s.reps), (12.5, 6)); + }); + + test('double: failure at m moves down in weight and up to n', () async { + final s = await run( + ProgressionMode.doubleProgression, + success: false, + reps: 6, + ); + expect((s.weight, s.reps), (7.5, 12)); + }); + + test('a week off is one regression step in the active mode', () async { + final s = await run( + ProgressionMode.reps, + success: true, + reps: 10, + last: DateTime.now().subtract(const Duration(days: 9)), + ); + expect((s.weight, s.reps, s.successStreak, s.failStreak), (10, 9, 0, 0)); + }); + }); +}