Refresh the Flutter snapshot: v3 tickets V3-04 through V3-16, plus a fresh installable APK
V3-04 through V3-07, V3-10, V3-16 shipped complete; V3-11/V3-12/V3-14 shipped code-complete pending device/account verification; V3-08/V3-09 deferred behind a new V3-17 (self-hosted OSRM investigation). 316 tests passing, up from 221. The APK is a fresh release build (debug-signed, no release signing config exists yet) with two build fixes applied: core library desugaring enabled for flutter_local_notifications, and sentry_flutter bumped to 9.27.0 (8.14.2's bundled Kotlin plugin was incompatible with this project's Kotlin 2.4.0 toolchain).
This commit is contained in:
180
rippr-flutter-src/test/migration_test.dart
Normal file
180
rippr-flutter-src/test/migration_test.dart
Normal file
@@ -0,0 +1,180 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rippr/src/data/database.dart';
|
||||
import 'package:rippr/src/domain/models.dart';
|
||||
import 'package:sqlite3/sqlite3.dart' as sqlite3;
|
||||
|
||||
/// V3-01's migration test — the one that matters more than the feature it belongs to.
|
||||
///
|
||||
/// The destructive fallback is gone for good (see the doc comment on [AppDatabase]), so
|
||||
/// this is the port's first real schema migration, and getting the path right here is
|
||||
/// what makes every later one trustworthy.
|
||||
///
|
||||
/// Builds the v1 schema with raw SQL via `package:sqlite3` directly, rather than
|
||||
/// drift_dev's schema-versioning tooling — deliberately: that tooling needs a
|
||||
/// `build.yaml`-declared set of frozen schema snapshots this project has not adopted, and
|
||||
/// a hand-built v1 table is a faithful enough stand-in. It is copied column-for-column
|
||||
/// from `Trips`/`Segments`/`TrackPoints` as they were before the `activity` column was
|
||||
/// added, using the same names — `build.yaml`'s `case_from_dart_to_sql: preserve` means
|
||||
/// there is no snake_case translation to get wrong.
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
late File dbFile;
|
||||
|
||||
setUp(() async {
|
||||
tempDir = await Directory.systemTemp.createTemp('rippr_migration_test_');
|
||||
dbFile = File('${tempDir.path}/v1.sqlite');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
if (tempDir.existsSync()) tempDir.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
/// Writes a database file containing exactly the v1 schema, seeded with one trip, one
|
||||
/// closed segment, and a handful of points -- then closes it, so Drift can open the
|
||||
/// same file fresh.
|
||||
void seedV1Database() {
|
||||
final raw = sqlite3.sqlite3.open(dbFile.path);
|
||||
raw.execute('''
|
||||
CREATE TABLE trips (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
startedAt INTEGER NOT NULL,
|
||||
endedAt INTEGER NULL,
|
||||
name TEXT NULL,
|
||||
state TEXT NOT NULL,
|
||||
distanceM REAL NOT NULL DEFAULT 0,
|
||||
movingMillis INTEGER NOT NULL DEFAULT 0,
|
||||
maxSpeedKmh REAL NOT NULL DEFAULT 0,
|
||||
elevationGainM REAL NOT NULL DEFAULT 0,
|
||||
pointCount INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_trips_ended ON trips (endedAt);
|
||||
|
||||
CREATE TABLE segments (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
tripId INTEGER NOT NULL REFERENCES trips (id) ON DELETE CASCADE,
|
||||
startedAt INTEGER NOT NULL,
|
||||
endedAt INTEGER NULL
|
||||
);
|
||||
CREATE INDEX idx_segments_trip ON segments (tripId);
|
||||
|
||||
CREATE TABLE track_points (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
tripId INTEGER NOT NULL REFERENCES trips (id) ON DELETE CASCADE,
|
||||
segmentId INTEGER NOT NULL REFERENCES segments (id) ON DELETE CASCADE,
|
||||
timestamp INTEGER NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
speedKmh REAL NOT NULL,
|
||||
altitudeM REAL NOT NULL,
|
||||
accuracyM REAL NOT NULL DEFAULT 0,
|
||||
bearingDeg REAL NOT NULL DEFAULT 0,
|
||||
synced INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_points_trip ON track_points (tripId);
|
||||
CREATE INDEX idx_points_segment ON track_points (segmentId);
|
||||
CREATE INDEX idx_points_synced ON track_points (synced);
|
||||
''');
|
||||
|
||||
raw.execute('''
|
||||
INSERT INTO trips (id, startedAt, endedAt, name, state, distanceM, movingMillis,
|
||||
maxSpeedKmh, elevationGainM, pointCount)
|
||||
VALUES (1, 1000, 5000, 'Old ride', 'completed', 111.2, 4000, 40.0, 12.0, 3);
|
||||
''');
|
||||
raw.execute('''
|
||||
INSERT INTO segments (id, tripId, startedAt, endedAt) VALUES (1, 1, 1000, 5000);
|
||||
''');
|
||||
for (var i = 0; i < 3; i++) {
|
||||
raw.execute('''
|
||||
INSERT INTO track_points
|
||||
(tripId, segmentId, timestamp, latitude, longitude, speedKmh, altitudeM)
|
||||
VALUES (1, 1, ${1000 + i * 1000}, ${51.0 + i * 0.0001}, -114.0, 40.0, 1000.0);
|
||||
''');
|
||||
}
|
||||
|
||||
// Drift persists its schema version in SQLite's own user_version pragma. Setting it
|
||||
// to 1 here is what makes AppDatabase (schemaVersion 2) believe an upgrade is due
|
||||
// when it opens this file, rather than treating it as a fresh database.
|
||||
raw.execute('PRAGMA user_version = 1;');
|
||||
raw.close();
|
||||
}
|
||||
|
||||
test('a v1 database migrates and keeps every ride and point', () async {
|
||||
seedV1Database();
|
||||
|
||||
final db = AppDatabase(NativeDatabase(dbFile));
|
||||
addTearDown(db.close);
|
||||
|
||||
final trip = await db.getTrip(1);
|
||||
expect(trip, isNotNull, reason: 'the pre-existing ride must survive the migration');
|
||||
expect(trip!.name, 'Old ride');
|
||||
expect(trip.distanceM, closeTo(111.2, 1e-9));
|
||||
expect(trip.pointCount, 3);
|
||||
expect(
|
||||
trip.activity,
|
||||
Activity.motorcycle,
|
||||
reason: 'a ride recorded before activity existed must default to motorcycle, '
|
||||
'not be left null or reject the row',
|
||||
);
|
||||
|
||||
final points = await db.pointsForTrip(1);
|
||||
expect(points.length, 3, reason: 'no point may be lost in the upgrade');
|
||||
|
||||
final segments = await db.segmentsForTrip(1);
|
||||
expect(segments.length, 1);
|
||||
|
||||
// The column is genuinely usable afterwards, not just present with a default.
|
||||
await db.setTripActivity(1, Activity.bicycle);
|
||||
expect((await db.getTrip(1))!.activity, Activity.bicycle);
|
||||
});
|
||||
|
||||
test('a v1 database with several rides migrates all of them', () async {
|
||||
seedV1Database();
|
||||
final raw = sqlite3.sqlite3.open(dbFile.path);
|
||||
raw.execute('''
|
||||
INSERT INTO trips (startedAt, endedAt, state, distanceM, pointCount)
|
||||
VALUES (10000, 15000, 'completed', 500.0, 10),
|
||||
(20000, 25000, 'completed', 1000.0, 20);
|
||||
''');
|
||||
raw.close();
|
||||
|
||||
final db = AppDatabase(NativeDatabase(dbFile));
|
||||
addTearDown(db.close);
|
||||
|
||||
final all = await db.watchCompletedTrips().first;
|
||||
expect(all.length, 3);
|
||||
expect(all.every((t) => t.activity == Activity.motorcycle), isTrue);
|
||||
});
|
||||
|
||||
test('a v2 database (V3-07) gains route_plans/waypoints and keeps its trips',
|
||||
() async {
|
||||
// v2: identical to the v1 seed above, plus the `activity` column V3-01 added.
|
||||
seedV1Database();
|
||||
final raw = sqlite3.sqlite3.open(dbFile.path);
|
||||
raw.execute('''
|
||||
ALTER TABLE trips ADD COLUMN activity TEXT NOT NULL DEFAULT 'motorcycle';
|
||||
''');
|
||||
raw.execute('PRAGMA user_version = 2;');
|
||||
raw.close();
|
||||
|
||||
final db = AppDatabase(NativeDatabase(dbFile));
|
||||
addTearDown(db.close);
|
||||
|
||||
// The pre-existing ride is untouched by an upgrade that has nothing to do with it.
|
||||
final trip = await db.getTrip(1);
|
||||
expect(trip, isNotNull);
|
||||
expect(trip!.name, 'Old ride');
|
||||
|
||||
// The new tables are not just present but usable.
|
||||
final routeId = await db.insertRoutePlan(
|
||||
const RoutePlan(name: 'Coast loop', createdAt: 9000),
|
||||
);
|
||||
await db.insertWaypoint(
|
||||
Waypoint(routeId: routeId, ordinal: 0, latitude: 51.0, longitude: -114.0),
|
||||
);
|
||||
final waypoints = await db.waypointsForRoute(routeId);
|
||||
expect(waypoints, hasLength(1));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user