mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 12:47:41 +01:00
Cover storage_service.dart to 100%
Migration (v1->v3), reps-increment at weight cap, and restore-from-backup paths were untested. Add targeted unit tests plus a file-db test hook (resetForTesting dbPath) so onUpgrade actually runs. coverage:ignore the prod DB path and the defensive null-state branch (unreachable — seeding guarantees a state row). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
This commit is contained in:
parent
a5ea2ff206
commit
b76d156ae3
@ -73,17 +73,20 @@ class StorageService {
|
||||
|
||||
/// Resets the singleton so [init] can be called again in tests.
|
||||
///
|
||||
/// Also switches to an in-memory database so each test starts with a clean
|
||||
/// slate and file-based data from other tests does not leak in.
|
||||
/// Defaults to an in-memory database so each test starts with a clean
|
||||
/// slate and file-based data from other tests does not leak in. Pass a
|
||||
/// file [dbPath] to exercise on-disk paths (e.g. schema migrations, which
|
||||
/// need a persisted DB opened twice at different versions).
|
||||
@visibleForTesting
|
||||
static void resetForTesting() {
|
||||
static void resetForTesting({String dbPath = ':memory:'}) {
|
||||
_instance = null;
|
||||
_testDbPath = ':memory:';
|
||||
_testDbPath = dbPath;
|
||||
}
|
||||
|
||||
Future<void> _open() async {
|
||||
final dbPath =
|
||||
_testDbPath ?? p.join(await getDatabasesPath(), 'workout_app.db');
|
||||
_testDbPath ??
|
||||
p.join(await getDatabasesPath(), 'workout_app.db'); // coverage:ignore-line
|
||||
_db = await openDatabase(
|
||||
dbPath,
|
||||
version: 3,
|
||||
@ -307,7 +310,11 @@ class StorageService {
|
||||
for (final ex in template) {
|
||||
final state = await getExerciseState(ex.name);
|
||||
if (state == null) {
|
||||
// Defensive: _seedDefaultsIfNeeded guarantees every template exercise
|
||||
// has a state row, so this branch is unreachable in practice.
|
||||
// coverage:ignore-start
|
||||
result.add(ex);
|
||||
// coverage:ignore-end
|
||||
} else {
|
||||
result.add(ex.copyWith(weight: state.weight, reps: state.reps));
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:workout_app/models/workout_plan.dart';
|
||||
import 'package:workout_app/services/backup_service.dart';
|
||||
import 'package:workout_app/services/storage_service.dart';
|
||||
|
||||
StorageService get _svc => StorageService.instance;
|
||||
@ -316,4 +320,159 @@ void main() {
|
||||
final b = await StorageService.init();
|
||||
expect(identical(a, b), isTrue);
|
||||
});
|
||||
|
||||
// ── Schema migration (v1 → v3) ──────────────────────────────────────────────
|
||||
|
||||
test('migrates a v1 database up to v3 on open', () async {
|
||||
// A v1 DB predates the threshold columns and the settings/active_session
|
||||
// tables — build one on disk, then open it through StorageService (v3) so
|
||||
// _migrateSchema runs both the <2 and <3 upgrade blocks.
|
||||
final dir = await Directory.systemTemp.createTemp('mw_migrate');
|
||||
final dbFile = p.join(dir.path, 'old.db');
|
||||
final oldDb = await databaseFactory.openDatabase(
|
||||
dbFile,
|
||||
options: OpenDatabaseOptions(
|
||||
version: 1,
|
||||
onCreate: (db, _) async {
|
||||
await db.execute(
|
||||
'CREATE TABLE exercise_state ('
|
||||
'name TEXT PRIMARY KEY, weight REAL NOT NULL, reps INTEGER NOT NULL, '
|
||||
'success_streak INTEGER NOT NULL DEFAULT 0, '
|
||||
'fail_streak INTEGER NOT NULL DEFAULT 0, max_weight REAL NOT NULL)',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await oldDb.close();
|
||||
|
||||
StorageService.resetForTesting(dbPath: dbFile);
|
||||
await StorageService.init();
|
||||
|
||||
// <3 block created the settings table (getNextWorkoutType reads it) …
|
||||
expect(await _svc.getNextWorkoutType(), 'A');
|
||||
// … and the <2 block added the threshold columns (defaulted to 3/2).
|
||||
final st = await _svc.getExerciseState(workoutA.first.name);
|
||||
expect(st, isNotNull);
|
||||
expect(st!.successThreshold, 3);
|
||||
expect(st.failThreshold, 2);
|
||||
|
||||
await dir.delete(recursive: true);
|
||||
});
|
||||
|
||||
// ── Progression at the weight cap ───────────────────────────────────────────
|
||||
|
||||
test('applyProgression bumps reps (not weight) once at max weight', () async {
|
||||
final name = workoutA.first.name;
|
||||
final maxW = (await _svc.getExerciseState(name))!.maxWeight;
|
||||
// Pin the working weight at the cap; streaks reset to 0.
|
||||
await _svc.setExerciseWeight(name, maxW);
|
||||
final startReps = (await _svc.getExerciseState(name))!.reps;
|
||||
|
||||
// Default success threshold is 3 — three straight successes trigger a
|
||||
// progression, which at the cap increments reps instead of weight.
|
||||
final today = DateTime.now();
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await _svc.applyProgression(
|
||||
succeededExercises: {name: true},
|
||||
lastWorkoutDate: today,
|
||||
);
|
||||
}
|
||||
|
||||
final st = (await _svc.getExerciseState(name))!;
|
||||
expect(st.weight, maxW); // weight stayed capped
|
||||
expect(st.reps, startReps + 1); // reps incremented instead
|
||||
});
|
||||
|
||||
// ── Restore from backup ─────────────────────────────────────────────────────
|
||||
|
||||
group('restoreFromBackupIfNeeded', () {
|
||||
late Directory tmp;
|
||||
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('mw_restore');
|
||||
BackupService.baseDirForTesting = tmp.path;
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
BackupService.baseDirForTesting = kBackupDir;
|
||||
tmp.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
test('returns early and does not restore when the DB has data', () async {
|
||||
await _svc.saveSession(
|
||||
date: '2026-07-10',
|
||||
workoutType: 'A',
|
||||
durationSeconds: 60,
|
||||
succeeded: true,
|
||||
json: '{}',
|
||||
);
|
||||
// A backup that would add a second history row if (wrongly) applied.
|
||||
await BackupService.instance.export({
|
||||
'workout_history': [
|
||||
{
|
||||
'date': '2000-01-01',
|
||||
'workout_type': 'B',
|
||||
'duration_seconds': 1,
|
||||
'succeeded': 0,
|
||||
'json': '{}',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await _svc.restoreFromBackupIfNeeded();
|
||||
|
||||
// Early-return path: existing history is untouched, backup ignored.
|
||||
final hist = await _svc.getWorkoutHistory();
|
||||
expect(hist.length, 1);
|
||||
expect(hist.first['date'], '2026-07-10');
|
||||
});
|
||||
|
||||
test('restores exercise_state, history and settings when empty', () async {
|
||||
await BackupService.instance.export({
|
||||
'exercise_state': [
|
||||
{
|
||||
'name': 'Dumbbell Lunge',
|
||||
'weight': 42.5,
|
||||
'reps': 9,
|
||||
'success_streak': 0,
|
||||
'fail_streak': 0,
|
||||
'max_weight': 100.0,
|
||||
'success_threshold': 3,
|
||||
'fail_threshold': 2,
|
||||
},
|
||||
],
|
||||
'workout_history': [
|
||||
{
|
||||
'date': '2026-07-01',
|
||||
'workout_type': 'A',
|
||||
'duration_seconds': 120,
|
||||
'succeeded': 1,
|
||||
'json': '{}',
|
||||
},
|
||||
],
|
||||
'settings': [
|
||||
{'key': 'last_workout_type', 'value': 'A'},
|
||||
],
|
||||
});
|
||||
|
||||
await _svc.restoreFromBackupIfNeeded();
|
||||
|
||||
// Settings restored: last was A → next is B.
|
||||
expect(await _svc.getNextWorkoutType(), 'B');
|
||||
// History restored.
|
||||
final hist = await _svc.getWorkoutHistory();
|
||||
expect(hist.length, 1);
|
||||
expect(hist.first['date'], '2026-07-01');
|
||||
// Exercise state overwritten from the backup.
|
||||
final st = await _svc.getExerciseState('Dumbbell Lunge');
|
||||
expect(st!.weight, 42.5);
|
||||
});
|
||||
|
||||
test('does nothing when the DB is empty and no backup exists', () async {
|
||||
// No export() call → readBackup returns null → early return after the
|
||||
// has-data check.
|
||||
await _svc.restoreFromBackupIfNeeded();
|
||||
expect(await _svc.getWorkoutHistory(), isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user