diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md new file mode 100644 index 000000000..57f092a39 --- /dev/null +++ b/.gitlab/merge_request_templates/Default.md @@ -0,0 +1,24 @@ +## What does this MR do? + + + +## Author's checklist + +- [ ] The MR fully addresses the requirements of the associated task. +- [ ] I did a self-review of the changes and did not spot any issues. Among others, this includes: + * I added unit tests for new/changed behavior; all test pass. + * My code conforms to our coding standards and guidelines. + * My changes are prepared in a way that makes the review straightforward for the reviewer. +- [ ] I amended [`CHANGELOG.md`](objectbox/CHANGELOG.md) if this affects users in any way. + +## Review checklist + +- [ ] I reviewed all changes line-by-line and addressed relevant issues +- [ ] The requirements of the associated task are fully met +- [ ] I can confirm that: + * CI passes + * Coverage percentages do not decrease + * New code conforms to standards and guidelines + * If applicable, additional checks were done for special code changes (e.g. core performance, binary size, OSS licenses) + +/assign me diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 7c0b9d1b7..7f3565f66 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,4 +1,12 @@ ## latest +* Add `Store.runInTransactionAsync` to run database operations asynchronously in the background + (requires Flutter 2.8.0/Dart 2.15.0 or newer). +* Rename `Store.runIsolated` to `runAsync`, drop unused `mode` parameter, propagate errors and + handle premature isolate exit. + +* The native ObjectBox library is also searched for in the `lib` subfolder on desktop OS (macOS, + Linux, Windows). This is where the [`install.sh`](/install.sh) script downloads it by default. + E.g. it is no longer necessary to install the library globally to run `dart test` or `flutter test`. ## 1.4.1 (2022-03-01) diff --git a/objectbox/example/flutter/objectbox_demo/lib/main.dart b/objectbox/example/flutter/objectbox_demo/lib/main.dart index 4dd18dbea..b95ccb402 100644 --- a/objectbox/example/flutter/objectbox_demo/lib/main.dart +++ b/objectbox/example/flutter/objectbox_demo/lib/main.dart @@ -44,9 +44,9 @@ class _MyHomePageState extends State { final _noteInputController = TextEditingController(); final _listController = StreamController>(sync: true); - void _addNote() { + Future _addNote() async { if (_noteInputController.text.isEmpty) return; - objectbox.noteBox.put(Note(_noteInputController.text)); + await objectbox.addNote(_noteInputController.text); _noteInputController.text = ''; } diff --git a/objectbox/example/flutter/objectbox_demo/lib/objectbox.dart b/objectbox/example/flutter/objectbox_demo/lib/objectbox.dart index 0eb25a480..053ee201a 100644 --- a/objectbox/example/flutter/objectbox_demo/lib/objectbox.dart +++ b/objectbox/example/flutter/objectbox_demo/lib/objectbox.dart @@ -40,6 +40,28 @@ class ObjectBox { Note('Delete notes by tapping on one'), Note('Write a demo app for ObjectBox') ]; - noteBox.putMany(demoNotes); + store.runInTransactionAsync(TxMode.write, _putNotesInTx, demoNotes); + } + + static void _putNotesInTx(Store store, List notes) => + store.box().putMany(notes); + + /// Add a note within a transaction. + /// + /// To avoid frame drops, run ObjectBox operations that take longer than a + /// few milliseconds, e.g. putting many objects, in an isolate with its + /// own Store instance. + /// For this example only a single object is put which would also be fine if + /// done here directly. + Future addNote(String text) => + store.runInTransactionAsync(TxMode.write, _addNoteInTx, text); + + /// Note: due to [dart-lang/sdk#36983](https://github.com/dart-lang/sdk/issues/36983) + /// not using a closure as it may capture more objects than expected. + /// These might not be send-able to an isolate. See Store.runAsync for details. + static void _addNoteInTx(Store store, String text) { + // Perform ObjectBox operations that take longer than a few milliseconds + // here. To keep it simple, this example just puts a single object. + store.box().put(Note(text)); } } diff --git a/objectbox/example/flutter/objectbox_demo_sync/lib/main.dart b/objectbox/example/flutter/objectbox_demo_sync/lib/main.dart index 894347a8c..03ec20564 100644 --- a/objectbox/example/flutter/objectbox_demo_sync/lib/main.dart +++ b/objectbox/example/flutter/objectbox_demo_sync/lib/main.dart @@ -44,9 +44,9 @@ class _MyHomePageState extends State { final _noteInputController = TextEditingController(); final _listController = StreamController>(sync: true); - void _addNote() { + Future _addNote() async { if (_noteInputController.text.isEmpty) return; - objectbox.noteBox.put(Note(_noteInputController.text)); + await objectbox.addNote(_noteInputController.text); _noteInputController.text = ''; } diff --git a/objectbox/example/flutter/objectbox_demo_sync/lib/objectbox.dart b/objectbox/example/flutter/objectbox_demo_sync/lib/objectbox.dart index 0a40fc9f7..545daa935 100644 --- a/objectbox/example/flutter/objectbox_demo_sync/lib/objectbox.dart +++ b/objectbox/example/flutter/objectbox_demo_sync/lib/objectbox.dart @@ -42,4 +42,23 @@ class ObjectBox { ); return ObjectBox._create(store); } + + /// Add a note within a transaction. + /// + /// To avoid frame drops, run ObjectBox operations that take longer than a + /// few milliseconds, e.g. putting many objects, in an isolate with its + /// own Store instance. + /// For this example only a single object is put which would also be fine if + /// done here directly. + Future addNote(String text) => + store.runInTransactionAsync(TxMode.write, _addNoteInTx, text); + + /// Note: due to [dart-lang/sdk#36983](https://github.com/dart-lang/sdk/issues/36983) + /// not using a closure as it may capture more objects than expected. + /// These might not be send-able to an isolate. See Store.runAsync for details. + static void _addNoteInTx(Store store, String text) { + // Perform ObjectBox operations that take longer than a few milliseconds + // here. To keep it simple, this example just puts a single object. + store.box().put(Note(text)); + } } diff --git a/objectbox/lib/src/native/bindings/bindings.dart b/objectbox/lib/src/native/bindings/bindings.dart index b9e4694c8..1e03ed0c6 100644 --- a/objectbox/lib/src/native/bindings/bindings.dart +++ b/objectbox/lib/src/native/bindings/bindings.dart @@ -1,5 +1,7 @@ import 'dart:ffi'; -import 'dart:io' show Platform; +import 'dart:io' show Directory, Platform; + +import 'package:path/path.dart'; import 'helpers.dart'; import 'objectbox_c.dart'; @@ -31,29 +33,55 @@ ObjectBoxC? _tryObjectBoxLibProcess() { ObjectBoxC? _tryObjectBoxLibFile() { _lib = null; - var libName = 'objectbox'; + final String libName; if (Platform.isWindows) { - libName += '.dll'; - try { - _lib = DynamicLibrary.open(libName); - } on ArgumentError { - libName = 'lib/' + libName; - } + libName = 'objectbox.dll'; } else if (Platform.isMacOS) { - libName = 'lib' + libName + '.dylib'; - try { - _lib = DynamicLibrary.open(libName); - } on ArgumentError { - libName = '/usr/local/lib/' + libName; - } + libName = 'libobjectbox.dylib'; } else if (Platform.isAndroid) { - libName = 'lib' + libName + '-jni.so'; + libName = 'libobjectbox-jni.so'; } else if (Platform.isLinux) { - libName = 'lib' + libName + '.so'; + libName = 'libobjectbox.so'; } else { + // Other platforms not supported (for iOS see _tryObjectBoxLibProcess). return null; } - _lib ??= DynamicLibrary.open(libName); + // For desktop OS prefer version in 'lib' subfolder as this is where + // install.sh (which calls objectbox-c download.sh) puts the library. + if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { + // Must use absolute directory as relative directory fails on macOS + // due to security restrictions ("file system relative paths not allowed in + // hardened programs"). + String libPath = join(Directory.current.path, "lib", libName); + try { + _lib = DynamicLibrary.open(libPath); + } on ArgumentError { + // On macOS also try /usr/local/lib, this is where the objectbox-c + // download script installs to as well. + if (Platform.isMacOS) { + try { + _lib ??= DynamicLibrary.open('/usr/local/lib/' + libName); + } on ArgumentError { + // Ignore. + } + } + // Try default path, see below. + } + } + try { + // This will look in some standard places for shared libraries: + // - on Android in the JNI lib folder for the architecture + // - on Linux in /lib and /usr/lib + // - on macOS? + // - on Windows in the working directory and System32 + _lib ??= DynamicLibrary.open(libName); + } catch (e) { + print("Failed to load ObjectBox library. For Flutter apps, check if " + "objectbox_flutter_libs is added to dependencies. " + "For unit tests and Dart apps, check if the ObjectBox library was " + "downloaded (https://docs.objectbox.io/getting-started)."); + rethrow; + } return ObjectBoxC(_lib!); } diff --git a/objectbox/lib/src/native/store.dart b/objectbox/lib/src/native/store.dart index bbf41b8a2..954e45109 100644 --- a/objectbox/lib/src/native/store.dart +++ b/objectbox/lib/src/native/store.dart @@ -412,45 +412,165 @@ class Store { return _runInTransaction(mode, (tx) => fn()); } - // Isolate entry point must be static or top-level. + /// Like [runAsync], but executes [callback] within a read or write + /// transaction depending on [mode]. + /// + /// See the documentation on [runAsync] for important usage details. + /// + /// The following example gets the name of a User object, deletes the object + /// and returns the name within a write transaction: + /// ```dart + /// String? readNameAndRemove(Store store, int objectId) { + /// var box = store.box(); + /// final nameOrNull = box.get(objectId)?.name; + /// box.remove(objectId); + /// return nameOrNull; + /// } + /// await store.runInTransactionAsync(TxMode.write, readNameAndRemove, objectId); + /// ``` + Future runInTransactionAsync( + TxMode mode, TxAsyncCallback callback, P param) => + runAsync( + (Store store, P p) => + store.runInTransaction(mode, () => callback(store, p)), + param); + + // Isolate entry point must be able to be sent via SendPort.send. + // Must guarantee only a single result event is sent. + // runAsync only handles a single event, any sent afterwards are ignored. E.g. + // in case [Error] or [Exception] are thrown after the result is sent. static Future _callFunctionWithStoreInIsolate( - _IsoPass isoPass) async { + _RunAsyncIsolateConfig isoPass) async { final store = Store.attach(isoPass.model, isoPass.dbDirectoryPath, queriesCaseSensitiveDefault: isoPass.queriesCaseSensitiveDefault); - final result = await isoPass.runFn(store); - store.close(); - // Note: maybe replace with Isolate.exit (and remove kill call in - // runIsolated) once min Dart SDK 2.15. - isoPass.resultPort?.send(result); + dynamic result; + try { + final callbackResult = await isoPass.runCallback(store); + result = _RunAsyncResult(callbackResult); + } catch (error, stack) { + result = _RunAsyncError(error, stack); + } finally { + store.close(); + } + + // Note: maybe replace with Isolate.exit (and remove kill() call in caller) + // once min Dart SDK 2.15. + isoPass.resultPort.send(result); } /// Spawns an isolate, runs [callback] in that isolate passing it [param] with /// its own Store and returns the result of callback. /// - /// Instances of [callback] must be top-level functions or static methods - /// of classes, not closures or instance methods of objects. + /// This is useful for ObjectBox operations that take longer than a few + /// milliseconds, e.g. putting many objects, which would cause frame drops. + /// If all operations can execute within a single transaction, prefer to use + /// [runInTransactionAsync]. + /// + /// The following example gets the name of a User object, deletes the object + /// and returns the name: + /// ```dart + /// String? readNameAndRemove(Store store, int objectId) { + /// var box = store.box(); + /// final nameOrNull = box.get(objectId)?.name; + /// box.remove(objectId); + /// return nameOrNull; + /// } + /// await store.runAsync(readNameAndRemove, objectId); + /// ``` + /// + /// The [callback] must be a function that can be sent to an isolate: either a + /// top-level function, static method or a closure that only captures objects + /// that can be sent to an isolate. + /// + /// Warning: Due to + /// [dart-lang/sdk#36983](https://github.com/dart-lang/sdk/issues/36983) a + /// closure may capture more objects than expected, even if they are not + /// directly used in the closure itself. + /// + /// The types `P` (type of the parameter to be passed to the callback) and + /// `R` (type of the result returned by the callback) must be able to be sent + /// to or received from an isolate. The same applies to errors originating + /// from the callback. + /// + /// See [SendPort.send] for a discussion on which values can be sent to and + /// received from isolates. /// /// Note: this requires Dart 2.15.0 or newer /// (shipped with Flutter 2.8.0 or newer). - Future runIsolated( - TxMode mode, FutureOr Function(Store, P) callback, P param) async { - final resultPort = ReceivePort(); - // Await isolate spawn to avoid waiting forever if it fails to spawn. - final isolate = await Isolate.spawn( - _callFunctionWithStoreInIsolate, - _IsoPass(_defs, directoryPath, _queriesCaseSensitiveDefault, - resultPort.sendPort, callback, param)); - // Use Completer to return result so type is not lost. - final result = Completer(); - resultPort.listen((dynamic message) { - result.complete(message as R); - }); - await result.future; - resultPort.close(); + Future runAsync(RunAsyncCallback callback, P param) async { + final port = RawReceivePort(); + final completer = Completer(); + + void _cleanup() { + port.close(); + } + + port.handler = (dynamic message) { + _cleanup(); + completer.complete(message); + }; + + final Isolate isolate; + try { + // Await isolate spawn to avoid waiting forever if it fails to spawn. + isolate = await Isolate.spawn( + _callFunctionWithStoreInIsolate, + _RunAsyncIsolateConfig(_defs, directoryPath, + _queriesCaseSensitiveDefault, port.sendPort, callback, param), + errorsAreFatal: true, + onError: port.sendPort, + onExit: port.sendPort); + } on Object { + _cleanup(); + rethrow; + } + + final dynamic response = await completer.future; + // Replace with Isolate.exit in _callFunctionWithStoreInIsolate + // once min SDK 2.15. isolate.kill(); - return result.future; + + if (response == null) { + throw RemoteError('Isolate exited without result or error.', ''); + } + + if (response is _RunAsyncResult) { + // Success, return result. + return response.result as R; + } else if (response is List) { + // See isolate.addErrorListener docs for message structure. + assert(response.length == 2); + await Future.error(RemoteError( + response[0] as String, + response[1] as String, + )); + } else { + // Error thrown by callback. + assert(response is _RunAsyncError); + response as _RunAsyncError; + + await Future.error( + response.error, + response.stack, + ); + } } + /// Deprecated. Use [runAsync] instead. Will be removed in a future release. + /// + /// Spawns an isolate, runs [callback] in that isolate passing it [param] with + /// its own Store and returns the result of callback. + /// + /// Instances of [callback] must be top-level functions or static methods + /// of classes, not closures or instance methods of objects. + /// + /// Note: this requires Dart 2.15.0 or newer + /// (shipped with Flutter 2.8.0 or newer). + @Deprecated('Use `runAsync` instead. Will be removed in a future release.') + Future runIsolated(TxMode mode, + FutureOr Function(Store, P) callback, P param) async => + runAsync(callback, param); + /// Internal only - bypasses the main checks for async functions, you may /// only pass synchronous callbacks! R _runInTransaction(TxMode mode, R Function(Transaction) fn) { @@ -571,10 +691,16 @@ final _openStoreDirectories = HashSet(); final _nullSafetyEnabled = _nullReturningFn is! Future Function(); final _nullReturningFn = () => null; +// Define type so IDE generates named parameters. +/// Signature for the callback passed to [Store.runAsync]. +/// +/// Instances must be functions that can be sent to an isolate. +typedef RunAsyncCallback = FutureOr Function(Store store, P parameter); + /// Captures everything required to create a "copy" of a store in an isolate /// and run user code. @immutable -class _IsoPass { +class _RunAsyncIsolateConfig { final ModelDefinition model; /// Used to attach to store in separate isolate @@ -584,15 +710,15 @@ class _IsoPass { final bool queriesCaseSensitiveDefault; /// Non-void functions can use this port to receive the result. - final SendPort? resultPort; + final SendPort resultPort; /// Parameter passed to [callback]. final P param; /// To be called in isolate. - final FutureOr Function(Store, P) callback; + final RunAsyncCallback callback; - const _IsoPass( + const _RunAsyncIsolateConfig( this.model, this.dbDirectoryPath, // ignore: avoid_positional_boolean_parameters @@ -603,5 +729,26 @@ class _IsoPass { /// Calls [callback] inside this class so types are not lost /// (if called in isolate types would be dynamic instead of P and R). - FutureOr runFn(Store store) => callback(store, param); + FutureOr runCallback(Store store) => callback(store, param); +} + +@immutable +class _RunAsyncResult { + final R result; + + const _RunAsyncResult(this.result); +} + +@immutable +class _RunAsyncError { + final Object error; + final StackTrace stack; + + const _RunAsyncError(this.error, this.stack); } + +// Specify so IDE generates named parameters. +/// Signature for callback passed to [Store.runInTransactionAsync]. +/// +/// Instances must be functions that can be sent to an isolate. +typedef TxAsyncCallback = R Function(Store store, P parameter); diff --git a/objectbox/test/box_test.dart b/objectbox/test/box_test.dart index ed1297a8d..94b7012f3 100644 --- a/objectbox/test/box_test.dart +++ b/objectbox/test/box_test.dart @@ -553,6 +553,17 @@ void main() { expect(count, equals(6)); }); + test('simple write in txn works - async', () async { + int count; + void callback(Store store, List param) { + store.box().putMany(param); + } + + await store.runInTransactionAsync(TxMode.write, callback, simpleItems()); + count = box.count(); + expect(count, equals(6)); + }, skip: notAtLeastDart2_15_0()); + test('failing transactions', () { expect( () => store.runInTransaction(TxMode.write, () { @@ -568,6 +579,22 @@ void main() { expect(box.count(), equals(0)); }); + test('failing transactions - async', () async { + expect( + () async => await store.runInTransactionAsync(TxMode.write, + (Store store, List param) { + store.box().putMany(param); + // note: we're throwing conditionally (but always true) so that + // the return type is not [Never]. See [Transaction.execute()] + // testing for the return type to be a [Future]. [Never] is a + // base class to everything, so a [Future] is also a [Never]. + if (1 + 1 == 2) throw 'test-exception'; + return 1; + }, simpleItems()), + throwsA('test-exception')); + expect(box.count(), equals(0)); + }, skip: notAtLeastDart2_15_0()); + test('recursive write in write transaction', () { store.runInTransaction(TxMode.write, () { box.putMany(simpleItems()); @@ -578,6 +605,22 @@ void main() { expect(box.count(), equals(12)); }); + test('recursive write in write transaction - async', () async { + await store.runInTransactionAsync(TxMode.write, + (Store store, List param) { + final box = store.box(); + box.putMany(param); + store.runInTransaction(TxMode.write, () { + // Re-set IDs to re-insert. + for (var element in param) { + element.id = 0; + } + box.putMany(param); + }); + }, simpleItems()); + expect(box.count(), equals(12)); + }, skip: notAtLeastDart2_15_0()); + test('recursive read in write transaction', () { int count = store.runInTransaction(TxMode.write, () { box.putMany(simpleItems()); @@ -586,6 +629,16 @@ void main() { expect(count, equals(6)); }); + test('recursive read in write transaction - async', () async { + int count = await store.runInTransactionAsync(TxMode.write, + (Store store, List param) { + final box = store.box(); + box.putMany(param); + return store.runInTransaction(TxMode.read, box.count); + }, simpleItems()); + expect(count, equals(6)); + }, skip: notAtLeastDart2_15_0()); + test('recursive write in read -> fails during creation', () { expect( () => store.runInTransaction(TxMode.read, () { @@ -597,6 +650,19 @@ void main() { e.toString().contains('failed to create transaction')))); }); + test('recursive write in async read -> fails during creation', () { + expect( + () => store.runInTransactionAsync(TxMode.read, + (Store store, List param) { + final box = store.box(); + box.count(); + return store.runInTransaction( + TxMode.write, () => box.putMany(param)); + }, simpleItems()), + throwsA(predicate((StateError e) => e.toString().contains( + 'Bad state: failed to create transaction: 10001 Cannot start a write transaction inside a read only transaction')))); + }, skip: notAtLeastDart2_15_0()); + test('failing in recursive txn', () { store.runInTransaction(TxMode.write, () { //should throw code10001 -> valid until fix diff --git a/objectbox/test/store_test.dart b/objectbox/test/store_test.dart index 09ab2802e..1122e56a1 100644 --- a/objectbox/test/store_test.dart +++ b/objectbox/test/store_test.dart @@ -111,6 +111,45 @@ void main() { env.closeAndDelete(); }); + test('async transactions', () async { + final env = TestEnv('store'); + expect(TxMode.values.length, 2); + for (var mode in TxMode.values) { + // Returned value falls through. + expect( + await env.store + .runInTransactionAsync(mode, (store, param) => 1, null), + 1); + + // Async callbacks are forbidden. + final asyncCallbacks = [ + (Store s, Object? p) async => null, + (Store s, Object? p) => + Future.delayed(const Duration(milliseconds: 1)), + (Store s, Object? p) => Future.value(), + ]; + for (var callback in asyncCallbacks) { + try { + await env.store.runInTransactionAsync(mode, callback, null); + fail("Should throw UnsupportedError"); + } on UnsupportedError catch (e) { + expect(e.message, + 'Executing an "async" function in a transaction is not allowed.'); + } + } + + // Functions that [Never] finish won't be executed at all. + try { + await env.store.runInTransactionAsync(mode, (store, param) { + throw 'Should never execute'; + }, null); + } on UnsupportedError catch (e) { + expect(e.message, 'Given transaction callback always fails.'); + } + } + env.closeAndDelete(); + }, skip: notAtLeastDart2_15_0()); + test('store multi-open', () { final stores = []; @@ -191,34 +230,42 @@ void main() { Directory('store').deleteSync(recursive: true); }); - test('store_runInIsolatedTx', () async { + test('store run in isolate', () async { final env = TestEnv('store'); final id = env.box.put(TestEntity(tString: 'foo')); - final futureResult = - env.store.runIsolated(TxMode.write, readStringAndRemove, id); + final futureResult = env.store.runAsync(_readStringAndRemove, id); print('Count in main isolate: ${env.box.count()}'); - final String x; + final String x = await futureResult; + expect(x, 'foo!'); + expect(env.box.count(), 0); // Must be removed once awaited + env.closeAndDelete(); + }, skip: notAtLeastDart2_15_0()); + + test('store runAsync returns isolate error', () async { + final env = TestEnv('store'); + try { + await env.store.runAsync(_producesIsolateError, 'nothing'); + fail("Should throw RemoteError"); + } on RemoteError { + // expected + } + env.closeAndDelete(); + }, skip: notAtLeastDart2_15_0()); + + test('store runAsync returns callback error', () async { + final env = TestEnv('store'); try { - x = await futureResult; + await env.store.runAsync(_producesCallbackError, 'nothing'); + fail("Should throw error produced by callback"); } catch (e) { - final dartVersion = RegExp('([0-9]+).([0-9]+).([0-9]+)') - .firstMatch(Platform.version) - ?.group(0); - if (dartVersion != null && dartVersion.compareTo('2.15.0') < 0) { - print('runIsolated requires Dart 2.15, ignoring error.'); - env.closeAndDelete(); - return; - } else { - rethrow; - } + expect(e, isA()); + expect(e, predicate((ArgumentError e) => e.message == 'Return me')); } - expect(x, 'foo!'); - expect(env.box.count(), 0); // Must be removed once awaited env.closeAndDelete(); - }); + }, skip: notAtLeastDart2_15_0()); } -Future readStringAndRemove(Store store, int id) async { +Future _readStringAndRemove(Store store, int id) async { var box = store.box(); var testEntity = box.get(id); final result = testEntity!.tString! + '!'; @@ -230,6 +277,23 @@ Future readStringAndRemove(Store store, int id) async { return await Future.delayed(const Duration(milliseconds: 10), () => result); } +// Produce an error within the isolate that triggers the onError handler case. +// Errors because ReceivePort can not be sent via SendPort. +int _producesIsolateError(Store store, String param) { + final port = ReceivePort(); + try { + throw port; + } finally { + port.close(); + } +} + +// Produce an error that is caught and sent, triggering the error thrown +// by callable case. +int _producesCallbackError(Store store, String param) { + throw ArgumentError('Return me'); +} + class StoreAttachIsolateInit { SendPort sendPort; String path; diff --git a/objectbox/test/test_env.dart b/objectbox/test/test_env.dart index ebea8fc48..354614314 100644 --- a/objectbox/test/test_env.dart +++ b/objectbox/test/test_env.dart @@ -64,3 +64,14 @@ Matcher sameAsList(List list) => unorderedEquals(list); // We need to do this to receive an event in the stream before processing // the remainder of the test case. final yieldExecution = () async => await Future.delayed(Duration.zero); + +dynamic notAtLeastDart2_15_0() { + final dartVersion = RegExp('([0-9]+).([0-9]+).([0-9]+)') + .firstMatch(Platform.version) + ?.group(0); + if (dartVersion != null && dartVersion.compareTo('2.15.0') < 0) { + return 'Test requires Dart 2.15.0, skipping.'; + } else { + return false; + } +}