@TestOn('vm') import 'dart:async'; import 'dart:isolate'; import 'package:moor/isolate.dart'; import 'package:moor/moor.dart'; import 'package:moor_ffi/moor_ffi.dart'; import 'package:test/test.dart'; import 'data/tables/todos.dart'; void main() { // Using the MoorIsolate apis without actually running on a background isolate // is pointless, but we can't collect coverage for background isolates: // https://github.com/dart-lang/test/issues/1108 group('in same isolate', () { MoorIsolate spawnInSame() { return MoorIsolate.inCurrent(_backgroundConnection); } _runTests(spawnInSame, false); }); group('in background isolate', () { Future spawnBackground() { return MoorIsolate.spawn(_backgroundConnection); } _runTests(spawnBackground, true); }); test('stream queries across isolates', () async { // three isolates: // 1. this one, starting a query stream // 2. another one running an insert // 3. the MoorIsolate executor the other two are connecting to final moorIsolate = await MoorIsolate.spawn(_backgroundConnection); final receiveDone = ReceivePort(); final writer = await Isolate.spawn(_writeTodoEntryInBackground, _BackgroundEntryMessage(moorIsolate, receiveDone.sendPort)); final db = TodoDb.connect(await moorIsolate.connect()); final expectedEntry = const TypeMatcher() .having((e) => e.content, 'content', 'Hello from background'); final expectation = expectLater( db.select(db.todosTable).watch(), // can optionally emit an empty list if this isolate connected before the // other one. emitsInOrder([ mayEmit([]), [expectedEntry] ]), ); await receiveDone.first; writer.kill(); await expectation; await moorIsolate.shutdownAll(); }); } void _runTests( FutureOr Function() spawner, bool terminateIsolate) { MoorIsolate isolate; DatabaseConnection isolateConnection; setUp(() async { isolate = await spawner(); isolateConnection = await isolate.connect(isolateDebugLog: true); }); tearDown(() { isolateConnection.executor.close(); if (terminateIsolate) { return isolate.shutdownAll(); } else { return Future.value(); } }); test('can open database and send requests', () async { final database = TodoDb.connect(isolateConnection); final result = await database.select(database.todosTable).get(); expect(result, isEmpty); }); test('stream queries work as expected', () async { final database = TodoDb.connect(isolateConnection); final initialCompanion = TodosTableCompanion.insert(content: 'my content'); final stream = database.select(database.todosTable).watchSingle(); final expectation = expectLater( stream, emitsInOrder([null, TodoEntry(id: 1, content: 'my content')]), ); await database.into(database.todosTable).insert(initialCompanion); await expectation; }); test('can start transactions', () async { final database = TodoDb.connect(isolateConnection); final initialCompanion = TodosTableCompanion.insert(content: 'my content'); await database.transaction(() async { await database.into(database.todosTable).insert(initialCompanion); }); final result = await database.select(database.todosTable).get(); expect(result, isNotEmpty); }); test('supports no-op transactions', () async { final database = TodoDb.connect(isolateConnection); await database.transaction(() { return Future.value(null); }); await database.close(); }); test('transactions have an isolated view on data', () async { // regression test for https://github.com/simolus3/moor/issues/324 final db = TodoDb.connect(isolateConnection); await db .customStatement('create table tbl (id integer primary key not null)'); Future expectRowCount(TodoDb db, int count) async { final rows = await db.customSelect('select * from tbl').get(); expect(rows, hasLength(count)); } final rowInserted = Completer(); final runTransaction = db.transaction(() async { await db.customInsert('insert into tbl default values'); await expectRowCount(db, 1); rowInserted.complete(); // Hold transaction open for expectRowCount() outside the transaction to // finish await Future.delayed(const Duration(seconds: 1)); await db.customStatement('delete from tbl'); await expectRowCount(db, 0); }); await rowInserted.future; await expectRowCount(db, 0); await runTransaction; // wait for the transaction to complete await db.close(); }); test("can't run queries on a closed database", () async { final db = TodoDb.connect(isolateConnection); await db.customSelect('SELECT 1;').getSingle(); await db.close(); await expectLater( () => db.customSelect('SELECT 1;').getSingle(), throwsStateError); }); test('can run deletes, updates and batches', () async { final db = TodoDb.connect(isolateConnection); await db.into(db.users).insert( UsersCompanion.insert(name: 'simon.', profilePicture: Uint8List(0))); await db .update(db.users) .write(const UsersCompanion(name: Value('changed name'))); var result = await db.select(db.users).getSingle(); expect(result.name, 'changed name'); await db.delete(db.users).go(); await db.batch((batch) { batch.insert( db.users, UsersCompanion.insert(name: 'not simon', profilePicture: Uint8List(0)), ); }); result = await db.select(db.users).getSingle(); expect(result.name, 'not simon'); await db.close(); }); test('transactions can be rolled back', () async { final db = TodoDb.connect(isolateConnection); await expectLater(db.transaction(() async { await db.into(db.categories).insert( CategoriesCompanion.insert(description: 'my fancy description')); throw Exception('expected'); }), throwsException); final result = await db.select(db.categories).get(); expect(result, isEmpty); await db.close(); }); } DatabaseConnection _backgroundConnection() { return DatabaseConnection.fromExecutor(VmDatabase.memory()); } Future _writeTodoEntryInBackground(_BackgroundEntryMessage msg) async { final connection = await msg.isolate.connect(); final database = TodoDb.connect(connection); await database .into(database.todosTable) .insert(TodosTableCompanion.insert(content: 'Hello from background')); msg.sendDone.send(null); } class _BackgroundEntryMessage { final MoorIsolate isolate; final SendPort sendDone; _BackgroundEntryMessage(this.isolate, this.sendDone); }