refactor(shorebird_code_push): ffi + noop implementation and web compat (#55)

This commit is contained in:
Felix Angelov
2023-07-14 15:30:35 -05:00
committed by GitHub
parent d3ec0a3aaf
commit 4ceb37dc1e
18 changed files with 615 additions and 319 deletions
@@ -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
+9 -9
View File
@@ -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
+2 -1
View File
@@ -4,4 +4,5 @@
.dart_tool/
.packages
build/
pubspec.lock
pubspec.lock
coverage/
+1 -1
View File
@@ -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)
+37 -23
View File
@@ -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;
}
+4 -5
View File
@@ -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 -9
View File
@@ -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);
});