drift/moor/test/isolate_test.dart

253 lines
7.5 KiB
Dart

//@dart=2.9
@TestOn('vm')
import 'dart:async';
import 'dart:isolate';
import 'package:moor/isolate.dart';
import 'package:moor/moor.dart';
import 'package: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<MoorIsolate> 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<TodoEntry>()
.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();
});
test('errors propagate across isolates', () async {
final isolate = await MoorIsolate.spawn(_backgroundConnection);
final db = TodoDb.connect(await isolate.connect());
await expectLater(
() => db.customStatement('UPDATE non_existing_table SET foo = bar'),
throwsA(anything),
);
// Check that isolate is still usable
await expectLater(
db.customSelect('SELECT 1').get(),
completion(isNotEmpty),
);
await db.close();
await isolate.shutdownAll();
});
}
void _runTests(
FutureOr<MoorIsolate> Function() spawner, bool terminateIsolate) {
MoorIsolate isolate;
TodoDb database;
setUp(() async {
isolate = await spawner();
database = TodoDb.connect(
DatabaseConnection.delayed(isolate.connect(isolateDebugLog: false)),
);
});
tearDown(() async {
await database.close();
if (terminateIsolate) {
await isolate.shutdownAll();
}
});
test('can open database and send requests', () async {
final result = await database.select(database.todosTable).get();
expect(result, isEmpty);
});
test('can run beforeOpen', () async {
var beforeOpenCalled = false;
database.migration = MigrationStrategy(beforeOpen: (details) async {
await database.customStatement('PRAGMA foreign_keys = ON');
beforeOpenCalled = true;
});
// run a select statement to verify that the database is open
await database.customSelect('SELECT 1').get();
expect(beforeOpenCalled, isTrue);
});
test('stream queries work as expected', () async {
final initialCompanion = TodosTableCompanion.insert(content: 'my content');
final stream = database.select(database.todosTable).watchSingle();
await expectLater(stream, emits(null));
await database.into(database.todosTable).insert(initialCompanion);
await expectLater(stream, emits(TodoEntry(id: 1, content: 'my content')));
});
test('can start transactions', () async {
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 {
await database.transaction(() {
return Future.value(null);
});
});
test('supports transactions in migrations', () async {
database.migration = MigrationStrategy(beforeOpen: (details) async {
await database.transaction(() async {
return await database.customSelect('SELECT 1').get();
});
});
await database.customSelect('SELECT 2').get();
});
test('transactions have an isolated view on data', () async {
// regression test for https://github.com/simolus3/moor/issues/324
await database
.customStatement('create table tbl (id integer primary key not null)');
Future<void> expectRowCount(TodoDb db, int count) async {
final rows = await db.customSelect('select * from tbl').get();
expect(rows, hasLength(count));
}
final rowInserted = Completer<void>();
final runTransaction = database.transaction(() async {
await database.customInsert('insert into tbl default values');
await expectRowCount(database, 1);
rowInserted.complete();
// Hold transaction open for expectRowCount() outside the transaction to
// finish
await Future.delayed(const Duration(seconds: 1));
await database.customStatement('delete from tbl');
await expectRowCount(database, 0);
});
await rowInserted.future;
await expectRowCount(database, 0);
await runTransaction; // wait for the transaction to complete
});
test("can't run queries on a closed database", () async {
await database.customSelect('SELECT 1;').getSingle();
await database.close();
await expectLater(
() => database.customSelect('SELECT 1;').getSingle(), throwsStateError);
});
test('can run deletes, updates and batches', () async {
await database.into(database.users).insert(
UsersCompanion.insert(name: 'simon.', profilePicture: Uint8List(0)));
await database
.update(database.users)
.write(const UsersCompanion(name: Value('changed name')));
var result = await database.select(database.users).getSingle();
expect(result.name, 'changed name');
await database.delete(database.users).go();
await database.batch((batch) {
batch.insert(
database.users,
UsersCompanion.insert(name: 'not simon', profilePicture: Uint8List(0)),
);
});
result = await database.select(database.users).getSingle();
expect(result.name, 'not simon');
});
test('transactions can be rolled back', () async {
await expectLater(database.transaction(() async {
await database.into(database.categories).insert(
CategoriesCompanion.insert(description: 'my fancy description'));
throw Exception('expected');
}), throwsException);
final result = await database.select(database.categories).get();
expect(result, isEmpty);
await database.close();
});
}
DatabaseConnection _backgroundConnection() {
return DatabaseConnection.fromExecutor(VmDatabase.memory());
}
Future<void> _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);
}