refactor(shorebird_code_push): ffi + noop implementation and web compat (#55)
This commit is contained in:
+5
-11
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
pubspec.lock
|
||||
pubspec.lock
|
||||
coverage/
|
||||
@@ -1,7 +1,7 @@
|
||||
# Shorebird Code Push
|
||||
|
||||
[](https://discord.gg/shorebird)
|
||||
|
||||
[](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)
|
||||
|
||||
@@ -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<MyHomePage> {
|
||||
final _isShorebirdAvailable = _shorebirdCodePush.isShorebirdAvailable();
|
||||
int? _currentPatchVersion;
|
||||
bool _isCheckingForUpdate = false;
|
||||
|
||||
@@ -142,9 +141,13 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
|
||||
@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<MyHomePage> {
|
||||
children: <Widget>[
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<bool> isNewPatchAvailableForDownload() {
|
||||
return _delegate.isNewPatchAvailableForDownload();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> currentPatchNumber() => _delegate.currentPatchNumber();
|
||||
|
||||
@override
|
||||
Future<int?> nextPatchNumber() => _delegate.nextPatchNumber();
|
||||
|
||||
@override
|
||||
Future<void> downloadUpdateIfAvailable() {
|
||||
return _delegate.downloadUpdateIfAvailable();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isNewPatchReadyToInstall() {
|
||||
return _delegate.isNewPatchReadyToInstall();
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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<bool> 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<int?> 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<int?> nextPatchNumber() {
|
||||
return _runInIsolate(
|
||||
(updater) {
|
||||
final patchNumber = updater.nextPatchNumber();
|
||||
return patchNumber == 0 ? null : patchNumber;
|
||||
},
|
||||
fallbackValue: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Downloads the latest patch, if available.
|
||||
Future<void> 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<bool> 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<T> _runInIsolate<T>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<bool> 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<int?> 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<int?> nextPatchNumber();
|
||||
|
||||
/// Downloads the latest patch, if available.
|
||||
Future<void> 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<bool> isNewPatchReadyToInstall();
|
||||
}
|
||||
@@ -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<bool> isNewPatchAvailableForDownload() {
|
||||
return Isolate.run(_updater.checkForUpdate);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> 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<int?> 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<void> downloadUpdateIfAvailable() async {
|
||||
await Isolate.run(_updater.downloadUpdate);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> 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;
|
||||
}
|
||||
@@ -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<int?> currentPatchNumber() async => null;
|
||||
|
||||
@override
|
||||
Future<void> downloadUpdateIfAvailable() async {}
|
||||
|
||||
@override
|
||||
Future<bool> isNewPatchAvailableForDownload() async => false;
|
||||
|
||||
@override
|
||||
Future<bool> isNewPatchReadyToInstall() async => false;
|
||||
|
||||
@override
|
||||
bool isShorebirdAvailable() => false;
|
||||
|
||||
@override
|
||||
Future<int?> nextPatchNumber() async => null;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String> 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 = <String>[];
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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<String> 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user