From 4ceb37dc1ee1ab7f13097db960c0348d6bf4a9f9 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Fri, 14 Jul 2023 15:30:35 -0500 Subject: [PATCH] refactor(shorebird_code_push): ffi + noop implementation and web compat (#55) --- .../action.yaml | 16 +- .github/workflows/main.yaml | 18 +- shorebird_code_push/.gitignore | 3 +- shorebird_code_push/README.md | 2 +- shorebird_code_push/example/lib/main.dart | 60 ++++--- .../lib/shorebird_code_push.dart | 3 +- .../lib/shorebird_code_push_io.dart | 56 +++++++ .../lib/shorebird_code_push_web.dart | 8 + .../lib/src/shorebird_code_push.dart | 123 -------------- .../lib/src/shorebird_code_push_base.dart | 32 ++++ .../lib/src/shorebird_code_push_ffi.dart | 60 +++++++ .../lib/src/shorebird_code_push_noop.dart | 33 ++++ shorebird_code_push/lib/src/updater.dart | 9 +- .../test/shorebird_code_push_io_test.dart | 127 +++++++++++++++ .../src/shorebird_code_push_ffi_test.dart | 154 ++++++++++++++++++ .../src/shorebird_code_push_noop_test.dart | 72 ++++++++ .../test/src/shorebird_code_push_test.dart | 136 ---------------- .../test/src/updater_test.dart | 22 ++- 18 files changed, 615 insertions(+), 319 deletions(-) rename .github/actions/{dart_package => flutter_package}/action.yaml (86%) create mode 100644 shorebird_code_push/lib/shorebird_code_push_io.dart create mode 100644 shorebird_code_push/lib/shorebird_code_push_web.dart delete mode 100644 shorebird_code_push/lib/src/shorebird_code_push.dart create mode 100644 shorebird_code_push/lib/src/shorebird_code_push_base.dart create mode 100644 shorebird_code_push/lib/src/shorebird_code_push_ffi.dart create mode 100644 shorebird_code_push/lib/src/shorebird_code_push_noop.dart create mode 100644 shorebird_code_push/test/shorebird_code_push_io_test.dart create mode 100644 shorebird_code_push/test/src/shorebird_code_push_ffi_test.dart create mode 100644 shorebird_code_push/test/src/shorebird_code_push_noop_test.dart delete mode 100644 shorebird_code_push/test/src/shorebird_code_push_test.dart diff --git a/.github/actions/dart_package/action.yaml b/.github/actions/flutter_package/action.yaml similarity index 86% rename from .github/actions/dart_package/action.yaml rename to .github/actions/flutter_package/action.yaml index 459b1f0..0069b01 100644 --- a/.github/actions/dart_package/action.yaml +++ b/.github/actions/flutter_package/action.yaml @@ -1,5 +1,5 @@ -name: Dart Package Workflow -description: Build and test your Dart packages. +name: Flutter Package Workflow +description: Build and test your Flutter packages. inputs: codecov_token: @@ -13,10 +13,6 @@ inputs: required: false default: "" description: Globs to exclude from coverage - dart_sdk: - required: false - default: "stable" - description: "The dart sdk version to use" working_directory: required: false default: "." @@ -41,14 +37,12 @@ inputs: runs: using: "composite" steps: - - uses: dart-lang/setup-dart@v1 - with: - sdk: ${{inputs.dart_sdk}} + - uses: subosito/flutter-action@v2 - name: Install Dependencies working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} - run: dart pub get + run: flutter pub get - name: Format working-directory: ${{ inputs.working_directory }} @@ -64,7 +58,7 @@ runs: working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: | - dart pub global activate coverage + flutter pub global activate coverage dart test -j ${{inputs.concurrency}} --coverage=coverage --platform=${{inputs.platform}} && dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on=${{inputs.report_on}} --check-ignore - name: Upload Coverage diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index ee8d374..3342d52 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -18,7 +18,7 @@ jobs: pull-requests: read outputs: - needs_dart_build: ${{ steps.needs_dart_build.outputs.changes }} + needs_flutter_build: ${{ steps.needs_flutter_build.outputs.changes }} needs_rust_build: ${{ steps.needs_rust_build.outputs.changes }} name: 👀 Detect Changes @@ -29,12 +29,12 @@ jobs: - uses: dorny/paths-filter@v2 name: Build Detection - id: needs_dart_build + id: needs_flutter_build with: filters: | shorebird_code_push: - ./.github/workflows/main.yaml - - ./.github/actions/dart_package/action.yaml + - ./.github/actions/flutter_package/action.yaml - shorebird_code_push/** - uses: dorny/paths-filter@v2 @@ -71,13 +71,13 @@ jobs: codecov_token: ${{ secrets.CODECOV_TOKEN }} working_directory: ${{ matrix.crate }} - build_dart_packages: + build_flutter_packages: needs: changes - if: ${{ needs.changes.outputs.needs_dart_build != '[]' }} + if: ${{ needs.changes.outputs.needs_flutter_build != '[]' }} strategy: matrix: - package: ${{ fromJSON(needs.changes.outputs.needs_dart_build) }} + package: ${{ fromJSON(needs.changes.outputs.needs_flutter_build) }} runs-on: ubuntu-latest @@ -89,15 +89,15 @@ jobs: with: submodules: recursive - - name: 🎯 Build ${{ matrix.package }} - uses: subosito/flutter-action@v2.10.0 + - name: 🐦 Build ${{ matrix.package }} + uses: ./.github/actions/flutter_package with: codecov_token: ${{ secrets.CODECOV_TOKEN }} coverage_excludes: "**/*.g.dart" working_directory: ${{ matrix.package }} ci: - needs: [semantic_pull_request, build_dart_packages, build_rust_crates] + needs: [semantic_pull_request, build_flutter_packages, build_rust_crates] if: ${{ always() }} runs-on: ubuntu-latest diff --git a/shorebird_code_push/.gitignore b/shorebird_code_push/.gitignore index 526da15..57f0116 100644 --- a/shorebird_code_push/.gitignore +++ b/shorebird_code_push/.gitignore @@ -4,4 +4,5 @@ .dart_tool/ .packages build/ -pubspec.lock \ No newline at end of file +pubspec.lock +coverage/ \ No newline at end of file diff --git a/shorebird_code_push/README.md b/shorebird_code_push/README.md index 8cd1295..5d9ec22 100644 --- a/shorebird_code_push/README.md +++ b/shorebird_code_push/README.md @@ -1,7 +1,7 @@ # Shorebird Code Push [![Discord](https://dcbadge.vercel.app/api/server/shorebird)](https://discord.gg/shorebird) - +[![ci](https://github.com/shorebirdtech/updater/actions/workflows/main.yaml/badge.svg)](https://github.com/shorebirdtech/updater/actions/workflows/main.yaml) [![License: MIT][license_badge]][license_link] A Dart package for communicating with the [Shorebird](https://shorebird.dev) diff --git a/shorebird_code_push/example/lib/main.dart b/shorebird_code_push/example/lib/main.dart index 596799c..8a36885 100644 --- a/shorebird_code_push/example/lib/main.dart +++ b/shorebird_code_push/example/lib/main.dart @@ -8,9 +8,7 @@ import 'package:shorebird_code_push/shorebird_code_push.dart'; // a single instance of ShorebirdCodePush in your app. final _shorebirdCodePush = ShorebirdCodePush(); -void main() { - runApp(const MyApp()); -} +void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -38,6 +36,7 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { + final _isShorebirdAvailable = _shorebirdCodePush.isShorebirdAvailable(); int? _currentPatchVersion; bool _isCheckingForUpdate = false; @@ -142,9 +141,13 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final heading = _currentPatchVersion != null + ? '$_currentPatchVersion' + : 'No patch installed'; return Scaffold( appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, + backgroundColor: theme.colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( @@ -153,29 +156,40 @@ class _MyHomePageState extends State { children: [ const Text('Current patch version:'), Text( - _currentPatchVersion != null - ? _currentPatchVersion.toString() - : 'No patch installed', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox( - height: 20, - ), - ElevatedButton( - onPressed: _isCheckingForUpdate ? null : _checkForUpdate, - child: _isCheckingForUpdate - ? const SizedBox( - height: 14, - width: 14, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Text('Check for update'), + heading, + style: theme.textTheme.headlineMedium, ), + const SizedBox(height: 20), + if (!_isShorebirdAvailable) + Text( + 'Shorebird Engine not available.', + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.error, + ), + ), + if (_isShorebirdAvailable) + ElevatedButton( + onPressed: _isCheckingForUpdate ? null : _checkForUpdate, + child: _isCheckingForUpdate + ? const _LoadingIndicator() + : const Text('Check for update'), + ), ], ), ), ); } } + +class _LoadingIndicator extends StatelessWidget { + const _LoadingIndicator(); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ); + } +} diff --git a/shorebird_code_push/lib/shorebird_code_push.dart b/shorebird_code_push/lib/shorebird_code_push.dart index 6c84264..eb0e5a9 100644 --- a/shorebird_code_push/lib/shorebird_code_push.dart +++ b/shorebird_code_push/lib/shorebird_code_push.dart @@ -1,4 +1,5 @@ /// Get info about your Shorebird code push app library shorebird_code_push; -export 'src/shorebird_code_push.dart'; +export 'shorebird_code_push_io.dart' + if (dart.library.html) 'shorebird_code_push_web.dart'; diff --git a/shorebird_code_push/lib/shorebird_code_push_io.dart b/shorebird_code_push/lib/shorebird_code_push_io.dart new file mode 100644 index 0000000..0a20dcd --- /dev/null +++ b/shorebird_code_push/lib/shorebird_code_push_io.dart @@ -0,0 +1,56 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push/src/shorebird_code_push_base.dart'; +import 'package:shorebird_code_push/src/shorebird_code_push_ffi.dart'; +import 'package:shorebird_code_push/src/shorebird_code_push_noop.dart'; +import 'package:shorebird_code_push/src/updater.dart'; + +/// {@template shorebird_code_push} +/// Get info about your Shorebird code push app. +/// {@endtemplate} +class ShorebirdCodePush implements ShorebirdCodePushBase { + /// {@macro shorebird_code_push} + ShorebirdCodePush() : this._(updater: const Updater()); + + /// Constructor used for testing which allows injecting a mock [Updater]. + @visibleForTesting + ShorebirdCodePush.test({Updater updater = const Updater()}) + : this._(updater: updater); + + ShorebirdCodePush._({required Updater updater}) { + try { + // If the Shorebird Engine is not available, this will throw an exception. + updater.currentPatchNumber(); + _delegate = ShorebirdCodePushFfi(updater: updater); + } catch (error) { + // ignore: avoid_print + print('[ShorebirdCodePush]: Error initializing updater: $error'); + _delegate = ShorebirdCodePushNoop(); + } + } + + late final ShorebirdCodePushBase _delegate; + + @override + bool isShorebirdAvailable() => _delegate.isShorebirdAvailable(); + + @override + Future isNewPatchAvailableForDownload() { + return _delegate.isNewPatchAvailableForDownload(); + } + + @override + Future currentPatchNumber() => _delegate.currentPatchNumber(); + + @override + Future nextPatchNumber() => _delegate.nextPatchNumber(); + + @override + Future downloadUpdateIfAvailable() { + return _delegate.downloadUpdateIfAvailable(); + } + + @override + Future isNewPatchReadyToInstall() { + return _delegate.isNewPatchReadyToInstall(); + } +} diff --git a/shorebird_code_push/lib/shorebird_code_push_web.dart b/shorebird_code_push/lib/shorebird_code_push_web.dart new file mode 100644 index 0000000..f540852 --- /dev/null +++ b/shorebird_code_push/lib/shorebird_code_push_web.dart @@ -0,0 +1,8 @@ +import 'package:shorebird_code_push/src/shorebird_code_push_base.dart'; +import 'package:shorebird_code_push/src/shorebird_code_push_noop.dart'; + +/// {@template shorebird_code_push} +/// Get info about your Shorebird code push app. +/// {@endtemplate} +class ShorebirdCodePush extends ShorebirdCodePushNoop + implements ShorebirdCodePushBase {} diff --git a/shorebird_code_push/lib/src/shorebird_code_push.dart b/shorebird_code_push/lib/src/shorebird_code_push.dart deleted file mode 100644 index 424613e..0000000 --- a/shorebird_code_push/lib/src/shorebird_code_push.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'dart:isolate'; - -import 'package:meta/meta.dart'; -import 'package:shorebird_code_push/src/updater.dart'; - -/// A logging function for errors arising from interacting with the native code. -/// -/// Used to override the default behavior of using [print]. -typedef ShorebirdLog = void Function(Object? object); - -/// A function that constructs an [Updater] instance. Used for testing. -@visibleForTesting -typedef UpdaterBuilder = Updater Function(); - -/// {@template shorebird_code_push} -/// Get info about your Shorebird code push app. -/// {@endtemplate} -class ShorebirdCodePush { - /// {@macro shorebird_code_push} - ShorebirdCodePush({ - this.logError = print, - }) : _buildUpdater = Updater.new; - - /// A test-only constructor that allows overriding the Updater constructor. - @visibleForTesting - ShorebirdCodePush.forTest({ - required this.logError, - required UpdaterBuilder buildUpdater, - }) : _buildUpdater = buildUpdater; - - /// Logs error messages arising from interacting with the native code. - /// - /// Defaults to [print]. - final ShorebirdLog logError; - - final UpdaterBuilder _buildUpdater; - static const _loggingPrefix = '[ShorebirdCodePush]'; - - /// Checks whether a new patch is available for download. - /// - /// Runs in a separate isolate to avoid blocking the UI thread. - Future isNewPatchAvailableForDownload() { - return _runInIsolate( - (updater) => updater.checkForUpdate(), - fallbackValue: false, - ); - } - - /// The version of the currently-installed patch. Null if no patch is - /// installed (i.e., the app is running the release version). - Future currentPatchNumber() { - return _runInIsolate( - (updater) { - final patchNumber = updater.currentPatchNumber(); - return patchNumber == 0 ? null : patchNumber; - }, - fallbackValue: null, - ); - } - - /// The version of the patch that will be run on the next app launch. If no - /// new patch has been downloaded, this will be the same as - /// [currentPatchNumber]. - Future nextPatchNumber() { - return _runInIsolate( - (updater) { - final patchNumber = updater.nextPatchNumber(); - return patchNumber == 0 ? null : patchNumber; - }, - fallbackValue: null, - ); - } - - /// Downloads the latest patch, if available. - Future downloadUpdateIfAvailable() async { - await _runInIsolate( - (updater) => updater.downloadUpdate(), - fallbackValue: null, - ); - } - - /// Whether a new patch has been downloaded and is ready to install. - /// - /// If true, the patch number returned by [nextPatchNumber] will be run on the - /// next app launch. - Future isNewPatchReadyToInstall() async { - final patchNumbers = - await Future.wait([currentPatchNumber(), nextPatchNumber()]); - final currentPatch = patchNumbers[0]; - final nextPatch = patchNumbers[1]; - - return nextPatch != null && currentPatch != nextPatch; - } - - void _logError(Object error) { - final logMessage = '$_loggingPrefix $error'; - if (error is ArgumentError) { - // ffi function lookup failures manifest as ArgumentErrors. - logError( - ''' -$logMessage - This is likely because you are not running with the Shorebird Flutter engine (that is, if you ran with `flutter run` instead of `shorebird run`).''', - ); - } else { - logError(logMessage); - } - } - - /// Creates an [Updater] in a separate isolate and runs the given function. If - /// an error occurs, the error is logged and [fallbackValue] is returned. - Future _runInIsolate( - T Function(Updater updater) f, { - required T fallbackValue, - }) async { - try { - // Create a new Updater in the new isolate. - return await Isolate.run(() => f(_buildUpdater())); - } catch (error) { - _logError(error); - return fallbackValue; - } - } -} diff --git a/shorebird_code_push/lib/src/shorebird_code_push_base.dart b/shorebird_code_push/lib/src/shorebird_code_push_base.dart new file mode 100644 index 0000000..b44499f --- /dev/null +++ b/shorebird_code_push/lib/src/shorebird_code_push_base.dart @@ -0,0 +1,32 @@ +/// {@template shorebird_code_push_base} +/// Get info about your Shorebird code push app. +/// {@endtemplate} +abstract class ShorebirdCodePushBase { + /// Whether the Shorebird Engine is available. + bool isShorebirdAvailable(); + + /// Checks whether a new patch is available for download. + /// + /// Runs in a separate isolate to avoid blocking the UI thread. + Future isNewPatchAvailableForDownload(); + + /// The version of the currently-installed patch. `null` if no patch is + /// installed (i.e., the app is running the release version). + /// + /// This will also return `null` if Shorebird is not available. + Future currentPatchNumber(); + + /// The version of the patch that will be run on the next app launch. If no + /// new patch has been downloaded, this will be the same as + /// [currentPatchNumber]. + Future nextPatchNumber(); + + /// Downloads the latest patch, if available. + Future downloadUpdateIfAvailable(); + + /// Whether a new patch has been downloaded and is ready to install. + /// + /// If true, the patch number returned by [nextPatchNumber] will be run on the + /// next app launch. + Future isNewPatchReadyToInstall(); +} diff --git a/shorebird_code_push/lib/src/shorebird_code_push_ffi.dart b/shorebird_code_push/lib/src/shorebird_code_push_ffi.dart new file mode 100644 index 0000000..2a3a696 --- /dev/null +++ b/shorebird_code_push/lib/src/shorebird_code_push_ffi.dart @@ -0,0 +1,60 @@ +import 'dart:isolate'; + +import 'package:shorebird_code_push/src/shorebird_code_push_base.dart'; +import 'package:shorebird_code_push/src/updater.dart'; + +/// {@template shorebird_code_push} +/// Get info about your Shorebird code push app. +/// {@endtemplate} +class ShorebirdCodePushFfi implements ShorebirdCodePushBase { + /// {@macro shorebird_code_push} + ShorebirdCodePushFfi({Updater? updater}) + : _updater = updater ?? const Updater(); + + final Updater _updater; + + @override + Future isNewPatchAvailableForDownload() { + return Isolate.run(_updater.checkForUpdate); + } + + @override + Future currentPatchNumber() { + return Isolate.run(() { + final currentPatchNumber = _updater.currentPatchNumber(); + // 0 means no patch is installed so we return null. + return currentPatchNumber == 0 ? null : currentPatchNumber; + }); + } + + @override + Future nextPatchNumber() { + return Isolate.run( + () { + final patchNumber = _updater.nextPatchNumber(); + // 0 means no patch is next so we return null. + return patchNumber == 0 ? null : patchNumber; + }, + ); + } + + @override + Future downloadUpdateIfAvailable() async { + await Isolate.run(_updater.downloadUpdate); + } + + @override + Future isNewPatchReadyToInstall() async { + final patchNumbers = await Future.wait([ + currentPatchNumber(), + nextPatchNumber(), + ]); + final currentPatch = patchNumbers[0]; + final nextPatch = patchNumbers[1]; + + return nextPatch != null && currentPatch != nextPatch; + } + + @override + bool isShorebirdAvailable() => true; +} diff --git a/shorebird_code_push/lib/src/shorebird_code_push_noop.dart b/shorebird_code_push/lib/src/shorebird_code_push_noop.dart new file mode 100644 index 0000000..923bea0 --- /dev/null +++ b/shorebird_code_push/lib/src/shorebird_code_push_noop.dart @@ -0,0 +1,33 @@ +import 'package:shorebird_code_push/src/shorebird_code_push_base.dart'; + +/// {@template shorebird_code_push_noop} +/// A no-op implementation of [ShorebirdCodePushBase]. +/// +/// This is used when the build does not contain the Shorebird Engine. +/// {@endtemplate} +class ShorebirdCodePushNoop implements ShorebirdCodePushBase { + /// {@macro shorebird_code_push_noop} + ShorebirdCodePushNoop() { + // ignore: avoid_print + print(''' +[ShorebirdCodePush]: Shorebird Engine not available, using no-op implementation. +'''); + } + @override + Future currentPatchNumber() async => null; + + @override + Future downloadUpdateIfAvailable() async {} + + @override + Future isNewPatchAvailableForDownload() async => false; + + @override + Future isNewPatchReadyToInstall() async => false; + + @override + bool isShorebirdAvailable() => false; + + @override + Future nextPatchNumber() async => null; +} diff --git a/shorebird_code_push/lib/src/updater.dart b/shorebird_code_push/lib/src/updater.dart index d9f7b9d..66decec 100644 --- a/shorebird_code_push/lib/src/updater.dart +++ b/shorebird_code_push/lib/src/updater.dart @@ -8,14 +8,13 @@ import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart'; /// translates ffi types into easier to use Dart types. /// {@endtemplate} class Updater { - /// Creates an [Updater] instance using the currently loaded dynamic library. - Updater() { - bindings = UpdaterBindings(ffi.DynamicLibrary.process()); - } + /// {@macro updater} + const Updater(); /// The ffi bindings to the Updater library. @visibleForTesting - static late UpdaterBindings bindings; + static UpdaterBindings bindings = + UpdaterBindings(ffi.DynamicLibrary.process()); /// The currently active patch number. int currentPatchNumber() => bindings.shorebird_current_boot_patch_number(); diff --git a/shorebird_code_push/test/shorebird_code_push_io_test.dart b/shorebird_code_push/test/shorebird_code_push_io_test.dart new file mode 100644 index 0000000..be1cfc8 --- /dev/null +++ b/shorebird_code_push/test/shorebird_code_push_io_test.dart @@ -0,0 +1,127 @@ +import 'dart:async'; + +import 'package:mocktail/mocktail.dart'; +import 'package:shorebird_code_push/shorebird_code_push_io.dart'; +import 'package:shorebird_code_push/src/updater.dart'; +import 'package:test/test.dart'; + +class _MockUpdater extends Mock implements Updater {} + +void main() { + group(ShorebirdCodePush, () { + late List printLogs; + late Updater updater; + late ShorebirdCodePush shorebirdCodePush; + + setUp(() { + printLogs = []; + updater = _MockUpdater(); + when(() => updater.currentPatchNumber()).thenReturn(0); + shorebirdCodePush = runZoned( + () => ShorebirdCodePush.test(updater: updater), + zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) => printLogs.add(line), + ), + ); + }); + + test('can be instantiated', () { + shorebirdCodePush = runZoned( + ShorebirdCodePush.new, + zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) => printLogs.add(line), + ), + ); + expect(shorebirdCodePush, isNotNull); + expect( + printLogs, + [ + startsWith( + '''[ShorebirdCodePush]: Error initializing updater: Invalid argument(s): Failed to lookup symbol''', + ), + equals( + '''[ShorebirdCodePush]: Shorebird Engine not available, using no-op implementation.\n''', + ), + ], + ); + }); + + test('logs error when updater cannot be initialized', () { + final printLogs = []; + final exception = Exception('Failed to lookup symbol'); + when(() => updater.currentPatchNumber()).thenThrow(exception); + runZoned( + () => ShorebirdCodePush.test(updater: updater), + zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) => printLogs.add(line), + ), + ); + expect( + printLogs, + [ + equals('[ShorebirdCodePush]: Error initializing updater: $exception'), + equals( + '''[ShorebirdCodePush]: Shorebird Engine not available, using no-op implementation.\n''', + ), + ], + ); + }); + + group('isShorebirdAvailable', () { + test('proxies to delegate', () { + expect(shorebirdCodePush.isShorebirdAvailable(), isTrue); + }); + }); + + group('isNewPatchAvailableForDownload', () { + test('proxies to delegate', () { + when(() => updater.checkForUpdate()).thenReturn(true); + expectLater( + shorebirdCodePush.isNewPatchAvailableForDownload(), + completion(isTrue), + ); + }); + }); + + group('currentPatchNumber', () { + test('proxies to delegate', () { + when(() => updater.currentPatchNumber()).thenReturn(42); + expectLater( + shorebirdCodePush.currentPatchNumber(), + completion(equals(42)), + ); + }); + }); + + group('nextPatchNumber', () { + test('proxies to delegate', () { + when(() => updater.nextPatchNumber()).thenReturn(42); + expectLater( + shorebirdCodePush.nextPatchNumber(), + completion(equals(42)), + ); + }); + }); + + group('downloadUpdateIfAvailable', () { + test('proxies to delegate', () async { + when(() => updater.downloadUpdate()).thenAnswer((_) async {}); + await expectLater( + shorebirdCodePush.downloadUpdateIfAvailable(), + completes, + ); + }); + }); + + group('isNewPatchReadyToInstall', () { + test('proxies to delegate', () async { + when(() => updater.currentPatchNumber()).thenReturn(0); + when(() => updater.nextPatchNumber()).thenReturn(1); + await expectLater( + shorebirdCodePush.isNewPatchReadyToInstall(), + completion(isTrue), + ); + }); + }); + }); +} diff --git a/shorebird_code_push/test/src/shorebird_code_push_ffi_test.dart b/shorebird_code_push/test/src/shorebird_code_push_ffi_test.dart new file mode 100644 index 0000000..efaaa97 --- /dev/null +++ b/shorebird_code_push/test/src/shorebird_code_push_ffi_test.dart @@ -0,0 +1,154 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:shorebird_code_push/src/shorebird_code_push_ffi.dart'; +import 'package:shorebird_code_push/src/updater.dart'; +import 'package:test/test.dart'; + +class _MockUpdater extends Mock implements Updater {} + +void main() { + group(ShorebirdCodePushFfi, () { + late Updater updater; + late ShorebirdCodePushFfi shorebirdCodePush; + + setUp(() { + updater = _MockUpdater(); + shorebirdCodePush = ShorebirdCodePushFfi(updater: updater); + }); + + group('isShorebirdAvailable', () { + test('returns true', () { + expect(shorebirdCodePush.isShorebirdAvailable(), isTrue); + }); + }); + + group('isNewPatchAvailableForDownload', () { + test('returns false if no update is available', () async { + when(() => updater.checkForUpdate()).thenAnswer((_) => false); + await expectLater( + shorebirdCodePush.isNewPatchAvailableForDownload(), + completion(isFalse), + ); + }); + + test('returns true if an update is available', () async { + when(() => updater.checkForUpdate()).thenAnswer((_) => true); + await expectLater( + shorebirdCodePush.isNewPatchAvailableForDownload(), + completion(isTrue), + ); + }); + + test('surfaces exception if updater throws exception', () async { + when(() => updater.checkForUpdate()).thenThrow(Exception('oh no')); + await expectLater( + () => shorebirdCodePush.isNewPatchAvailableForDownload(), + throwsException, + ); + }); + }); + + group('currentPatchNumber', () { + test('returns null if current patch is reported as 0', () async { + when(() => updater.currentPatchNumber()).thenReturn(0); + await expectLater( + shorebirdCodePush.currentPatchNumber(), + completion(isNull), + ); + }); + + test('forwards the return value of updater.currentPatchNumber', () async { + when(() => updater.currentPatchNumber()).thenReturn(1); + await expectLater( + shorebirdCodePush.currentPatchNumber(), + completion(equals(1)), + ); + }); + + test('surfaces exception if updater throws exception', () async { + when(() => updater.currentPatchNumber()).thenThrow(Exception('oh no')); + await expectLater( + () => shorebirdCodePush.currentPatchNumber(), + throwsException, + ); + }); + }); + + group('nextPatchNumber', () { + test('returns null if current patch is reported as 0', () async { + when(() => updater.nextPatchNumber()).thenReturn(0); + await expectLater( + shorebirdCodePush.nextPatchNumber(), + completion(isNull), + ); + }); + + test('forwards the return value of updater.nextPatchNumber', () async { + when(() => updater.nextPatchNumber()).thenReturn(1); + await expectLater( + shorebirdCodePush.nextPatchNumber(), + completion(equals(1)), + ); + }); + + test('surfaces exception if updater throws exception', () async { + when(() => updater.nextPatchNumber()).thenThrow(Exception('oh no')); + await expectLater( + () => shorebirdCodePush.nextPatchNumber(), + throwsException, + ); + }); + }); + + group('downloadUpdate', () { + test('completes', () async { + when(() => updater.downloadUpdate()).thenReturn(null); + await expectLater( + shorebirdCodePush.downloadUpdateIfAvailable(), + completes, + ); + }); + + test('surfaces exception if updater throws exception', () async { + when(() => updater.downloadUpdate()).thenThrow(Exception('oh no')); + await expectLater( + () => shorebirdCodePush.downloadUpdateIfAvailable(), + throwsException, + ); + }); + }); + + group('isNewPatchReadyToInstall', () { + test('returns false if no new patch is available', () async { + when(() => updater.currentPatchNumber()).thenReturn(1); + when(() => updater.nextPatchNumber()).thenReturn(0); + await expectLater( + shorebirdCodePush.isNewPatchReadyToInstall(), + completion(isFalse), + ); + }); + + test( + 'returns false if the next patch is the same as the current patch', + () async { + when(() => updater.currentPatchNumber()).thenReturn(1); + when(() => updater.nextPatchNumber()).thenReturn(1); + await expectLater( + shorebirdCodePush.isNewPatchReadyToInstall(), + completion(isFalse), + ); + }, + ); + + test( + 'returns true if the next patch number is greater ' + 'than the current patch number', () async { + when(() => updater.currentPatchNumber()).thenReturn(1); + when(() => updater.nextPatchNumber()).thenReturn(2); + await expectLater( + shorebirdCodePush.isNewPatchReadyToInstall(), + completion(isTrue), + ); + }); + }); + }); +} diff --git a/shorebird_code_push/test/src/shorebird_code_push_noop_test.dart b/shorebird_code_push/test/src/shorebird_code_push_noop_test.dart new file mode 100644 index 0000000..7fc409b --- /dev/null +++ b/shorebird_code_push/test/src/shorebird_code_push_noop_test.dart @@ -0,0 +1,72 @@ +// ignore_for_file: prefer_const_constructors + +import 'dart:async'; + +import 'package:shorebird_code_push/src/shorebird_code_push_noop.dart'; +import 'package:test/test.dart'; + +void main() { + group(ShorebirdCodePushNoop, () { + late List printLogs; + late ShorebirdCodePushNoop shorebirdCodePush; + + setUp(() { + printLogs = []; + shorebirdCodePush = runZoned( + ShorebirdCodePushNoop.new, + zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) => printLogs.add(line), + ), + ); + }); + + test('logs warning when instantiated', () { + const expected = ''' +[ShorebirdCodePush]: Shorebird Engine not available, using no-op implementation. +'''; + expect(printLogs, equals([expected])); + }); + + group('isShorebirdAvailable', () { + test('returns false', () { + expect(shorebirdCodePush.isShorebirdAvailable(), isFalse); + }); + }); + + group('isNewPatchAvailableForDownload', () { + test('returns false', () { + expectLater( + shorebirdCodePush.isNewPatchAvailableForDownload(), + completion(isFalse), + ); + }); + }); + + group('currentPatchNumber', () { + test('returns null', () { + expectLater(shorebirdCodePush.currentPatchNumber(), completion(isNull)); + }); + }); + + group('nextPatchNumber', () { + test('returns null', () { + expectLater(shorebirdCodePush.nextPatchNumber(), completion(isNull)); + }); + }); + + group('downloadUpdate', () { + test('completes', () { + expectLater(shorebirdCodePush.downloadUpdateIfAvailable(), completes); + }); + }); + + group('isNewPatchReadyToInstall', () { + test('returns false', () { + expectLater( + shorebirdCodePush.isNewPatchReadyToInstall(), + completion(isFalse), + ); + }); + }); + }); +} diff --git a/shorebird_code_push/test/src/shorebird_code_push_test.dart b/shorebird_code_push/test/src/shorebird_code_push_test.dart deleted file mode 100644 index fd30ba8..0000000 --- a/shorebird_code_push/test/src/shorebird_code_push_test.dart +++ /dev/null @@ -1,136 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:mocktail/mocktail.dart'; -import 'package:shorebird_code_push/shorebird_code_push.dart'; -import 'package:shorebird_code_push/src/updater.dart'; -import 'package:test/test.dart'; - -class _MockUpdater extends Mock implements Updater {} - -void main() { - group('ShorebirdCodePush', () { - late Updater updater; - late ShorebirdCodePush shorebirdCodePush; - Object? loggedError; - - setUp(() { - loggedError = null; - updater = _MockUpdater(); - shorebirdCodePush = ShorebirdCodePush.forTest( - logError: ([object]) => loggedError = object, - buildUpdater: () => updater, - ); - }); - - group('isNewPatchAvailableForDownload', () { - test('returns false if no update is available', () async { - when(() => updater.checkForUpdate()).thenAnswer((_) => false); - expect( - await shorebirdCodePush.isNewPatchAvailableForDownload(), - isFalse, - ); - expect(loggedError, isNull); - }); - - test('returns true if an update is available', () async { - when(() => updater.checkForUpdate()).thenAnswer((_) => true); - expect(await shorebirdCodePush.isNewPatchAvailableForDownload(), true); - expect(loggedError, isNull); - }); - - test('returns false if updater throws exception', () async { - when(() => updater.checkForUpdate()).thenThrow(Exception('oh no')); - expect( - await shorebirdCodePush.isNewPatchAvailableForDownload(), - isFalse, - ); - expect(loggedError, '[ShorebirdCodePush] Exception: oh no'); - }); - }); - - group('currentPatchNumber', () { - test('returns null if current patch is reported as 0', () async { - when(() => updater.currentPatchNumber()).thenReturn(0); - expect(await shorebirdCodePush.currentPatchNumber(), isNull); - expect(loggedError, isNull); - }); - - test('forwards the return value of updater.currentPatchNumber', () async { - when(() => updater.currentPatchNumber()).thenReturn(1); - expect(await shorebirdCodePush.currentPatchNumber(), 1); - expect(loggedError, isNull); - }); - - test('returns null if updater throws exception', () async { - when(() => updater.currentPatchNumber()).thenThrow(Exception('oh no')); - expect(await shorebirdCodePush.currentPatchNumber(), isNull); - expect(loggedError, '[ShorebirdCodePush] Exception: oh no'); - }); - }); - - group('nextPatchNumber', () { - test('returns null if current patch is reported as 0', () async { - when(() => updater.nextPatchNumber()).thenReturn(0); - expect(await shorebirdCodePush.nextPatchNumber(), isNull); - expect(loggedError, isNull); - }); - - test('forwards the return value of updater.nextPatchNumber', () async { - when(() => updater.nextPatchNumber()).thenReturn(1); - expect(await shorebirdCodePush.nextPatchNumber(), 1); - expect(loggedError, isNull); - }); - - test('returns null if updater throws exception', () async { - when(() => updater.nextPatchNumber()).thenThrow(Exception('oh no')); - expect(await shorebirdCodePush.nextPatchNumber(), isNull); - expect(loggedError, '[ShorebirdCodePush] Exception: oh no'); - }); - }); - - group('downloadUpdate', () { - test('forwards the return value of updater.nextPatchNumber', () async { - when(() => updater.downloadUpdate()).thenReturn(null); - await expectLater( - shorebirdCodePush.downloadUpdateIfAvailable(), - completes, - ); - expect(loggedError, isNull); - }); - - test('logs error if updater throws exception', () async { - when(() => updater.downloadUpdate()).thenThrow(Exception('oh no')); - await expectLater( - shorebirdCodePush.downloadUpdateIfAvailable(), - completes, - ); - expect(loggedError, '[ShorebirdCodePush] Exception: oh no'); - }); - }); - - group('isNewPatchReadyToInstall', () { - test('returns false is no new patch is available', () async { - when(() => updater.currentPatchNumber()).thenReturn(1); - when(() => updater.nextPatchNumber()).thenReturn(0); - expect(await shorebirdCodePush.isNewPatchReadyToInstall(), isFalse); - }); - - test( - 'returns false is the next patch is the same as the current patch', - () async { - when(() => updater.currentPatchNumber()).thenReturn(1); - when(() => updater.nextPatchNumber()).thenReturn(1); - expect(await shorebirdCodePush.isNewPatchReadyToInstall(), isFalse); - }, - ); - - test( - '''returns true if the next patch number is greater than the current patch number''', - () async { - when(() => updater.currentPatchNumber()).thenReturn(1); - when(() => updater.nextPatchNumber()).thenReturn(2); - expect(await shorebirdCodePush.isNewPatchReadyToInstall(), isTrue); - }); - }); - }); -} diff --git a/shorebird_code_push/test/src/updater_test.dart b/shorebird_code_push/test/src/updater_test.dart index d973172..b848851 100644 --- a/shorebird_code_push/test/src/updater_test.dart +++ b/shorebird_code_push/test/src/updater_test.dart @@ -13,7 +13,7 @@ void main() { setUp(() { updaterBindings = _MockUpdaterBindings(); - updater = Updater(); + updater = const Updater(); Updater.bindings = updaterBindings; }); @@ -23,8 +23,9 @@ void main() { group('currentPatchNumber', () { test('forwards the result of shorebird_next_boot_patch_number', () { - when(() => updaterBindings.shorebird_current_boot_patch_number()) - .thenReturn(123); + when( + () => updaterBindings.shorebird_current_boot_patch_number(), + ).thenReturn(123); final currentPatchNumber = updater.currentPatchNumber(); expect(currentPatchNumber, 123); }); @@ -32,20 +33,23 @@ void main() { group('checkForUpdate', () { test('forwards the result of shorebird_check_for_update', () { - when(() => updaterBindings.shorebird_check_for_update()) - .thenReturn(true); + when( + () => updaterBindings.shorebird_check_for_update(), + ).thenReturn(true); expect(updater.checkForUpdate(), isTrue); - when(() => updaterBindings.shorebird_check_for_update()) - .thenReturn(false); + when( + () => updaterBindings.shorebird_check_for_update(), + ).thenReturn(false); expect(updater.checkForUpdate(), isFalse); }); }); group('nextPatchNumber', () { test('forwards the result of shorebird_next_boot_patch_number', () { - when(() => updaterBindings.shorebird_next_boot_patch_number()) - .thenReturn(123); + when( + () => updaterBindings.shorebird_next_boot_patch_number(), + ).thenReturn(123); final currentPatchNumber = updater.nextPatchNumber(); expect(currentPatchNumber, 123); });