feat(shorebird_code_push): rewrite Dart API (#225)
This commit is contained in:
@@ -58,7 +58,7 @@ jobs:
|
||||
matrix:
|
||||
crate: ${{ fromJSON(needs.changes.outputs.needs_rust_build) }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-latest
|
||||
|
||||
name: 🦀 Build ${{ matrix.crate }}
|
||||
|
||||
@@ -98,12 +98,7 @@ jobs:
|
||||
working_directory: ${{ matrix.package }}
|
||||
|
||||
ci:
|
||||
needs:
|
||||
[
|
||||
semantic_pull_request,
|
||||
build_flutter_packages,
|
||||
build_rust_crates,
|
||||
]
|
||||
needs: [semantic_pull_request, build_flutter_packages, build_rust_crates]
|
||||
if: ${{ always() }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -14,6 +14,33 @@
|
||||
#endif
|
||||
|
||||
|
||||
/**
|
||||
* An unknown error occurred while updating. The update was not installed.
|
||||
* This is a catch-all for errors that don't fit into the other categories.
|
||||
*/
|
||||
#define SHOREBIRD_UPDATE_ERROR -1
|
||||
|
||||
/**
|
||||
* No update is available (e.g. the app is already up-to-date)
|
||||
*/
|
||||
#define SHOREBIRD_NO_UPDATE 0
|
||||
|
||||
/**
|
||||
* An update was installed successfully. It will boot from the update on the
|
||||
* next app launch.
|
||||
*/
|
||||
#define SHOREBIRD_UPDATE_INSTALLED 1
|
||||
|
||||
/**
|
||||
* An error occurred while updating. The update was not installed.
|
||||
*/
|
||||
#define SHOREBIRD_UPDATE_HAD_ERROR 2
|
||||
|
||||
/**
|
||||
* The downloaded patch was not installed because it was invalid.
|
||||
*/
|
||||
#define SHOREBIRD_UPDATE_IS_BAD_PATCH 3
|
||||
|
||||
/**
|
||||
* Struct containing configuration parameters for the updater.
|
||||
* Passed to all updater functions.
|
||||
@@ -69,6 +96,11 @@ typedef struct FileCallbacks {
|
||||
void (*close)(void *file_handle);
|
||||
} FileCallbacks;
|
||||
|
||||
typedef struct UpdateResult {
|
||||
int32_t status;
|
||||
const char *message;
|
||||
} UpdateResult;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif // __cplusplus
|
||||
@@ -114,7 +146,9 @@ SHOREBIRD_EXPORT char *shorebird_next_boot_patch_path(void);
|
||||
* If this function is called with a non-null pointer, it must be a pointer
|
||||
* returned by the updater library.
|
||||
*/
|
||||
SHOREBIRD_EXPORT void shorebird_free_string(char *c_string);
|
||||
SHOREBIRD_EXPORT void shorebird_free_string(const char *c_string);
|
||||
|
||||
SHOREBIRD_EXPORT void shorebird_free_update_result(struct UpdateResult *result);
|
||||
|
||||
/**
|
||||
* Check for an update. Returns true if an update is available.
|
||||
@@ -126,6 +160,12 @@ SHOREBIRD_EXPORT bool shorebird_check_for_update(void);
|
||||
*/
|
||||
SHOREBIRD_EXPORT void shorebird_update(void);
|
||||
|
||||
/**
|
||||
* Synchronously download an update if one is available.
|
||||
* Returns an [UpdateResult] indicating whether the update was successful.
|
||||
*/
|
||||
SHOREBIRD_EXPORT const struct UpdateResult *shorebird_update_with_result(void);
|
||||
|
||||
/**
|
||||
* Start a thread to download an update if one is available.
|
||||
*/
|
||||
|
||||
+261
-3
@@ -14,7 +14,7 @@ use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::updater;
|
||||
use crate::{updater, UpdateStatus};
|
||||
|
||||
use self::c_file::CFileProvder;
|
||||
|
||||
@@ -46,6 +46,28 @@ pub struct AppParameters {
|
||||
pub code_cache_dir: *const libc::c_char,
|
||||
}
|
||||
|
||||
/// An unknown error occurred while updating. The update was not installed.
|
||||
/// This is a catch-all for errors that don't fit into the other categories.
|
||||
pub const SHOREBIRD_UPDATE_ERROR: i32 = -1;
|
||||
|
||||
/// No update is available (e.g. the app is already up-to-date)
|
||||
pub const SHOREBIRD_NO_UPDATE: i32 = 0;
|
||||
|
||||
/// An update was installed successfully. It will boot from the update on the
|
||||
/// next app launch.
|
||||
pub const SHOREBIRD_UPDATE_INSTALLED: i32 = 1;
|
||||
|
||||
/// An error occurred while updating. The update was not installed.
|
||||
pub const SHOREBIRD_UPDATE_HAD_ERROR: i32 = 2;
|
||||
|
||||
/// The downloaded patch was not installed because it was invalid.
|
||||
pub const SHOREBIRD_UPDATE_IS_BAD_PATCH: i32 = 3;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct UpdateResult {
|
||||
pub status: i32,
|
||||
pub message: *const libc::c_char,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct FileCallbacks {
|
||||
@@ -183,6 +205,24 @@ fn path_to_c_string(path: Option<PathBuf>) -> anyhow::Result<*mut c_char> {
|
||||
})
|
||||
}
|
||||
|
||||
fn to_update_result(status: anyhow::Result<UpdateStatus>) -> UpdateResult {
|
||||
let result = match status {
|
||||
Ok(status) => {
|
||||
let message = status.to_string();
|
||||
return UpdateResult {
|
||||
status: status as i32,
|
||||
message: allocate_c_string(message.as_str())
|
||||
.unwrap_or_else(|_| std::ptr::null_mut()),
|
||||
};
|
||||
}
|
||||
Err(err) => UpdateResult {
|
||||
status: SHOREBIRD_UPDATE_ERROR,
|
||||
message: allocate_c_string(&err.to_string()).unwrap_or_else(|_| std::ptr::null_mut()),
|
||||
},
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
/// The path to the patch that will boot on the next run of the app, or NULL if
|
||||
/// there is no next patch.
|
||||
#[no_mangle]
|
||||
@@ -203,12 +243,26 @@ pub extern "C" fn shorebird_next_boot_patch_path() -> *mut c_char {
|
||||
/// If this function is called with a non-null pointer, it must be a pointer
|
||||
/// returned by the updater library.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn shorebird_free_string(c_string: *mut c_char) {
|
||||
pub unsafe extern "C" fn shorebird_free_string(c_string: *const c_char) {
|
||||
if c_string.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
drop(CString::from_raw(c_string));
|
||||
drop(CString::from_raw(c_string as *mut c_char));
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn shorebird_free_update_result(result: *mut UpdateResult) {
|
||||
if result.is_null() {
|
||||
return;
|
||||
}
|
||||
let message = (*result).message;
|
||||
if !message.is_null() {
|
||||
shorebird_free_string(message);
|
||||
}
|
||||
unsafe {
|
||||
drop(Box::from_raw(result));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +282,14 @@ pub extern "C" fn shorebird_update() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Synchronously download an update if one is available.
|
||||
/// Returns an [UpdateResult] indicating whether the update was successful.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn shorebird_update_with_result() -> *const UpdateResult {
|
||||
let result = to_update_result(updater::update());
|
||||
return Box::into_raw(Box::new(result));
|
||||
}
|
||||
|
||||
/// Start a thread to download an update if one is available.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn shorebird_start_update_thread() {
|
||||
@@ -361,6 +423,8 @@ mod test {
|
||||
|
||||
// free_string also doesn't crash with null.
|
||||
unsafe { shorebird_free_string(std::ptr::null_mut()) }
|
||||
// free_update_result also doesn't crash with null.
|
||||
unsafe { shorebird_free_update_result(std::ptr::null_mut()) }
|
||||
}
|
||||
|
||||
#[serial]
|
||||
@@ -499,6 +563,200 @@ mod test {
|
||||
assert_eq!(new, expected_new);
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn patch_success_with_result() {
|
||||
testing_reset_config();
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let base = "hello world";
|
||||
let expected_new: &str = "hello tests";
|
||||
let apk_path = tmp_dir.path().join("base.apk");
|
||||
write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes());
|
||||
let fake_libapp_path = tmp_dir.path().join("lib/arch/ignored.so");
|
||||
let c_params = parameters(&tmp_dir, fake_libapp_path.to_str().unwrap());
|
||||
// app_id is required or shorebird_init will fail.
|
||||
let c_yaml = c_string("app_id: foo");
|
||||
assert!(shorebird_init(&c_params, FileCallbacks::new(), c_yaml));
|
||||
free_c_string(c_yaml);
|
||||
free_parameters(c_params);
|
||||
|
||||
// set up the network hooks to return a patch.
|
||||
testing_set_network_hooks(
|
||||
|_url, _request| {
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
||||
Ok(PatchCheckResponse {
|
||||
patch_available: true,
|
||||
patch: Some(crate::Patch {
|
||||
number: 1,
|
||||
hash: hash.to_owned(),
|
||||
download_url: "ignored".to_owned(),
|
||||
hash_signature: None,
|
||||
}),
|
||||
rolled_back_patch_numbers: None,
|
||||
})
|
||||
},
|
||||
|_url| {
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let patch_bytes: Vec<u8> = vec![
|
||||
40, 181, 47, 253, 0, 128, 177, 0, 0, 223, 177, 0, 0, 0, 16, 0, 0, 6, 0, 0, 0,
|
||||
0, 0, 0, 5, 116, 101, 115, 116, 115, 0,
|
||||
];
|
||||
Ok(patch_bytes)
|
||||
},
|
||||
|_url, _event| Ok(()),
|
||||
);
|
||||
// There is an update available.
|
||||
assert!(shorebird_check_for_update());
|
||||
|
||||
// Go ahead and do the update.
|
||||
let result = shorebird_update_with_result();
|
||||
|
||||
unsafe {
|
||||
assert_eq!(result.read().status, SHOREBIRD_UPDATE_INSTALLED);
|
||||
shorebird_free_update_result(result as *mut UpdateResult);
|
||||
}
|
||||
assert_eq!(shorebird_current_boot_patch_number(), 0);
|
||||
assert_eq!(shorebird_next_boot_patch_number(), 1);
|
||||
|
||||
// Read path contents into memory and check against expected.
|
||||
let c_path = shorebird_next_boot_patch_path();
|
||||
let path = to_rust(c_path).unwrap();
|
||||
unsafe { shorebird_free_string(c_path) };
|
||||
let new = std::fs::read_to_string(path).unwrap();
|
||||
assert_eq!(new, expected_new);
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn patch_check_no_patch_with_result() {
|
||||
testing_reset_config();
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let base = "hello world";
|
||||
let apk_path = tmp_dir.path().join("base.apk");
|
||||
write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes());
|
||||
let fake_libapp_path = tmp_dir.path().join("lib/arch/ignored.so");
|
||||
let c_params = parameters(&tmp_dir, fake_libapp_path.to_str().unwrap());
|
||||
// app_id is required or shorebird_init will fail.
|
||||
let c_yaml = c_string("app_id: foo");
|
||||
assert!(shorebird_init(&c_params, FileCallbacks::new(), c_yaml));
|
||||
free_c_string(c_yaml);
|
||||
free_parameters(c_params);
|
||||
|
||||
// set up the network hooks to return a patch.
|
||||
testing_set_network_hooks(
|
||||
|_url, _request| {
|
||||
Ok(PatchCheckResponse {
|
||||
patch_available: false,
|
||||
patch: None,
|
||||
rolled_back_patch_numbers: None,
|
||||
})
|
||||
},
|
||||
|_url| Err(anyhow::anyhow!("Error")),
|
||||
|_url, _event| Ok(()),
|
||||
);
|
||||
|
||||
// Go ahead and do the update.
|
||||
let result = shorebird_update_with_result();
|
||||
|
||||
unsafe {
|
||||
assert_eq!(result.read().status, SHOREBIRD_NO_UPDATE);
|
||||
shorebird_free_update_result(result as *mut UpdateResult);
|
||||
}
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn patch_check_failure_with_result() {
|
||||
testing_reset_config();
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let base = "hello world";
|
||||
let apk_path = tmp_dir.path().join("base.apk");
|
||||
write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes());
|
||||
let fake_libapp_path = tmp_dir.path().join("lib/arch/ignored.so");
|
||||
let c_params = parameters(&tmp_dir, fake_libapp_path.to_str().unwrap());
|
||||
// app_id is required or shorebird_init will fail.
|
||||
let c_yaml = c_string("app_id: foo");
|
||||
assert!(shorebird_init(&c_params, FileCallbacks::new(), c_yaml));
|
||||
free_c_string(c_yaml);
|
||||
free_parameters(c_params);
|
||||
|
||||
// set up the network hooks to return a patch.
|
||||
testing_set_network_hooks(
|
||||
|_url, _request| Err(anyhow::anyhow!("Error")),
|
||||
|_url| {
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let patch_bytes: Vec<u8> = vec![
|
||||
40, 181, 47, 253, 0, 128, 177, 0, 0, 223, 177, 0, 0, 0, 16, 0, 0, 6, 0, 0, 0,
|
||||
0, 0, 0, 5, 116, 101, 115, 116, 115, 0,
|
||||
];
|
||||
Ok(patch_bytes)
|
||||
},
|
||||
|_url, _event| Ok(()),
|
||||
);
|
||||
|
||||
// Go ahead and do the update.
|
||||
let result = shorebird_update_with_result();
|
||||
|
||||
unsafe {
|
||||
assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR);
|
||||
shorebird_free_update_result(result as *mut UpdateResult);
|
||||
}
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn patch_download_failure_with_result() {
|
||||
testing_reset_config();
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let base = "hello world";
|
||||
let apk_path = tmp_dir.path().join("base.apk");
|
||||
write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes());
|
||||
let fake_libapp_path = tmp_dir.path().join("lib/arch/ignored.so");
|
||||
let c_params = parameters(&tmp_dir, fake_libapp_path.to_str().unwrap());
|
||||
// app_id is required or shorebird_init will fail.
|
||||
let c_yaml = c_string("app_id: foo");
|
||||
assert!(shorebird_init(&c_params, FileCallbacks::new(), c_yaml));
|
||||
free_c_string(c_yaml);
|
||||
free_parameters(c_params);
|
||||
|
||||
// set up the network hooks to return a patch.
|
||||
testing_set_network_hooks(
|
||||
|_url, _request| {
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
||||
Ok(PatchCheckResponse {
|
||||
patch_available: true,
|
||||
patch: Some(crate::Patch {
|
||||
number: 1,
|
||||
hash: hash.to_owned(),
|
||||
download_url: "ignored".to_owned(),
|
||||
hash_signature: None,
|
||||
}),
|
||||
rolled_back_patch_numbers: None,
|
||||
})
|
||||
},
|
||||
|_url| Err(anyhow::anyhow!("Error")),
|
||||
|_url, _event| Ok(()),
|
||||
);
|
||||
|
||||
// Go ahead and do the update.
|
||||
let result = shorebird_update_with_result();
|
||||
|
||||
unsafe {
|
||||
assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR);
|
||||
shorebird_free_update_result(result as *mut UpdateResult);
|
||||
}
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn current_boot_patch_set_after_reporting_launch_start() {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# 2.0.0-dev.1
|
||||
|
||||
- **BREAKING**: revamp the updater API
|
||||
- Remove `ShorebirdCodePush` in favor of `ShorebirdUpdater`
|
||||
|
||||
# 1.1.6
|
||||
|
||||
- Update log messages to explain what "using no-op implementation" means.
|
||||
|
||||
@@ -34,13 +34,11 @@ this:
|
||||
// Import the library
|
||||
import 'package:shorebird_code_push/shorebird_code_push.dart';
|
||||
|
||||
// Create an instance of the ShorebirdCodePush class
|
||||
final shorebirdCodePush = ShorebirdCodePush();
|
||||
// Create an instance of the updater class
|
||||
final updater = ShorebirdUpdater();
|
||||
|
||||
// Launch your app
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
void main() => runApp(const MyApp());
|
||||
|
||||
// [Other code here]
|
||||
|
||||
@@ -49,20 +47,24 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Get the current patch number and print it to the console. It will be
|
||||
// null if no patches are installed.
|
||||
shorebirdCodePush
|
||||
.currentPatchNumber()
|
||||
.then((value) => print('current patch number is $value'));
|
||||
// Get the current patch number and print it to the console.
|
||||
// It will be `null` if no patches are installed.
|
||||
updater.readCurrentPatch().then((currentPatch) {
|
||||
print('The current patch number is: ${currentPatch.number}');
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkForUpdates() async {
|
||||
// Check whether a patch is available to install.
|
||||
final isUpdateAvailable = await shorebirdCodePush.isNewPatchAvailableForDownload();
|
||||
// Check whether a new update is available.
|
||||
final status = await updater.checkForUpdates();
|
||||
|
||||
if (isUpdateAvailable) {
|
||||
// Download the new patch if it's available.
|
||||
await shorebirdCodePush.downloadUpdateIfAvailable();
|
||||
if (status == UpdateStatus.outdated) {
|
||||
try {
|
||||
// Perform the update
|
||||
await updater.update();
|
||||
} on UpdateException catch (error) {
|
||||
// Handle any errors that occur while updating.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
|
||||
import 'package:shorebird_code_push/shorebird_code_push.dart';
|
||||
|
||||
// Create an instance of ShorebirdCodePush. Because this example only contains
|
||||
// a single widget, we create it here, but you will likely only need to create
|
||||
// a single instance of ShorebirdCodePush in your app.
|
||||
final _shorebirdCodePush = ShorebirdCodePush();
|
||||
|
||||
void main() => runApp(const MyApp());
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@@ -21,198 +14,217 @@ class MyApp extends StatelessWidget {
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const MyHomePage(title: 'Shorebird Code Push'),
|
||||
home: const MyHomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({required this.title, super.key});
|
||||
|
||||
final String title;
|
||||
const MyHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
final _isShorebirdAvailable = _shorebirdCodePush.isShorebirdAvailable();
|
||||
int? _currentPatchVersion;
|
||||
bool _isCheckingForUpdate = false;
|
||||
final _updater = ShorebirdUpdater();
|
||||
late final bool _isUpdaterAvailable;
|
||||
var _isCheckingForUpdates = false;
|
||||
Patch? _currentPatch;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Request the current patch number.
|
||||
_shorebirdCodePush.currentPatchNumber().then((currentPatchVersion) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_currentPatchVersion = currentPatchVersion;
|
||||
});
|
||||
// Check whether Shorebird is available.
|
||||
setState(() => _isUpdaterAvailable = _updater.isAvailable);
|
||||
|
||||
// Read the current patch (if there is one.)
|
||||
// `currentPatch` will be `null` if no patch is installed.
|
||||
_updater.readCurrentPatch().then((currentPatch) {
|
||||
setState(() => _currentPatch = currentPatch);
|
||||
}).catchError((Object error) {
|
||||
// If an error occurs, we log it for now.
|
||||
debugPrint('Error reading current patch: $error');
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkForUpdate() async {
|
||||
setState(() {
|
||||
_isCheckingForUpdate = true;
|
||||
});
|
||||
if (_isCheckingForUpdates) return;
|
||||
|
||||
// Ask the Shorebird servers if there is a new patch available.
|
||||
final isUpdateAvailable =
|
||||
await _shorebirdCodePush.isNewPatchAvailableForDownload();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isCheckingForUpdate = false;
|
||||
});
|
||||
|
||||
if (isUpdateAvailable) {
|
||||
_showUpdateAvailableBanner();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('No update available'),
|
||||
),
|
||||
);
|
||||
try {
|
||||
setState(() => _isCheckingForUpdates = true);
|
||||
// Check if there's an update available.
|
||||
final status = await _updater.checkForUpdate();
|
||||
if (!mounted) return;
|
||||
// If there is an update available, show a banner.
|
||||
if (status == UpdateStatus.outdated) _showUpdateAvailableBanner();
|
||||
} catch (error) {
|
||||
// If an error occurs, we log it for now.
|
||||
debugPrint('Error checking for update: $error');
|
||||
} finally {
|
||||
setState(() => _isCheckingForUpdates = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showDownloadingBanner() {
|
||||
ScaffoldMessenger.of(context).showMaterialBanner(
|
||||
const MaterialBanner(
|
||||
content: Text('Downloading...'),
|
||||
actions: [
|
||||
SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentMaterialBanner()
|
||||
..showMaterialBanner(
|
||||
const MaterialBanner(
|
||||
content: Text('Downloading...'),
|
||||
actions: [
|
||||
SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showUpdateAvailableBanner() {
|
||||
ScaffoldMessenger.of(context).showMaterialBanner(
|
||||
MaterialBanner(
|
||||
content: const Text('Update available'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
||||
await _downloadUpdate();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
||||
},
|
||||
child: const Text('Download'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentMaterialBanner()
|
||||
..showMaterialBanner(
|
||||
MaterialBanner(
|
||||
content: const Text('Update available'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
||||
await _downloadUpdate();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
||||
},
|
||||
child: const Text('Download'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRestartBanner() {
|
||||
ScaffoldMessenger.of(context).showMaterialBanner(
|
||||
const MaterialBanner(
|
||||
content: Text('A new patch is ready!'),
|
||||
actions: [
|
||||
TextButton(
|
||||
// Restart the app for the new patch to take effect.
|
||||
onPressed: Restart.restartApp,
|
||||
child: Text('Restart app'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentMaterialBanner()
|
||||
..showMaterialBanner(
|
||||
MaterialBanner(
|
||||
content: const Text('A new patch is ready! Please restart your app.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
||||
},
|
||||
child: const Text('Dismiss'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorBanner() {
|
||||
ScaffoldMessenger.of(context).showMaterialBanner(
|
||||
MaterialBanner(
|
||||
content: const Text('An error occurred while downloading the update.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
||||
},
|
||||
child: const Text('Dismiss'),
|
||||
void _showErrorBanner(Object error) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentMaterialBanner()
|
||||
..showMaterialBanner(
|
||||
MaterialBanner(
|
||||
content: Text(
|
||||
'An error occurred while downloading the update: $error.',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
||||
},
|
||||
child: const Text('Dismiss'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Note: this is only run if an update is reported as available.
|
||||
// [isNewPatchReadyToInstall] returning false does not always indicate an
|
||||
// error with the download.
|
||||
Future<void> _downloadUpdate() async {
|
||||
_showDownloadingBanner();
|
||||
|
||||
await Future.wait([
|
||||
_shorebirdCodePush.downloadUpdateIfAvailable(),
|
||||
// Add an artificial delay so the banner has enough time to animate in.
|
||||
Future<void>.delayed(const Duration(milliseconds: 250)),
|
||||
]);
|
||||
|
||||
final isUpdateReadyToInstall =
|
||||
await _shorebirdCodePush.isNewPatchReadyToInstall();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
||||
if (isUpdateReadyToInstall) {
|
||||
try {
|
||||
// Perform the update (e.g download the latest patch).
|
||||
await _updater.update();
|
||||
if (!mounted) return;
|
||||
// Show a banner to inform the user that the update is ready and that they
|
||||
// need to restart the app.
|
||||
_showRestartBanner();
|
||||
} else {
|
||||
_showErrorBanner();
|
||||
} on UpdateException catch (error) {
|
||||
// If an error occurs, we show a banner with the error message.
|
||||
_showErrorBanner(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final heading = _currentPatchVersion != null
|
||||
? '$_currentPatchVersion'
|
||||
: 'No patch installed';
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
title: const Text('Shorebird Code Push'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text('Current patch version:'),
|
||||
Text(
|
||||
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'),
|
||||
),
|
||||
],
|
||||
body: _isUpdaterAvailable
|
||||
? _CurrentPatchVersion(patch: _currentPatch)
|
||||
: const _ShorebirdUnavailable(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _isCheckingForUpdates ? null : _checkForUpdate,
|
||||
tooltip: 'Check for update',
|
||||
child: _isCheckingForUpdates
|
||||
? const _LoadingIndicator()
|
||||
: const Icon(Icons.refresh),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget that is mounted when Shorebird is not available.
|
||||
class _ShorebirdUnavailable extends StatelessWidget {
|
||||
const _ShorebirdUnavailable();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: Text(
|
||||
'''
|
||||
Shorebird is not available.
|
||||
Please make sure the app was generated via `shorebird release` and that it is running in release mode.''',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget that displays the current patch version.
|
||||
class _CurrentPatchVersion extends StatelessWidget {
|
||||
const _CurrentPatchVersion({required this.patch});
|
||||
|
||||
final Patch? patch;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text('Current patch version:'),
|
||||
Text(
|
||||
patch != null ? '${patch!.number}' : 'No patch installed',
|
||||
style: theme.textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A reusable loading indicator.
|
||||
class _LoadingIndicator extends StatelessWidget {
|
||||
const _LoadingIndicator();
|
||||
|
||||
|
||||
@@ -10,12 +10,10 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
restart_app: ^1.3.2
|
||||
shorebird_code_push:
|
||||
path: ../
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
very_good_analysis: ^5.0.0
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
# This file is used to configure the Shorebird updater used by your application.
|
||||
# Learn more at https://shorebird.dev
|
||||
# This file should be checked into version control.
|
||||
# This file is used to configure the Shorebird updater used by your app.
|
||||
# Learn more at https://docs.shorebird.dev
|
||||
# This file does not contain any sensitive information and should be checked into version control.
|
||||
|
||||
# This is the unique identifier assigned to your app.
|
||||
# It is used by your app to request the correct patches from Shorebird servers.
|
||||
app_id: 1692ba14-0c8d-490e-9593-13815d2ac1cf
|
||||
# Your app_id is the unique identifier assigned to your app.
|
||||
# It is used to identify your app when requesting patches from Shorebird's servers.
|
||||
# It is not a secret and can be shared publicly.
|
||||
app_id: 1692ba14-0c8d-490e-9593-13815d2ac1cf
|
||||
|
||||
# auto_update controls if Shorebird should automatically update in the background on launch.
|
||||
# If auto_update: false, you will need to use package:shorebird_code_push to trigger updates.
|
||||
# https://pub.dev/packages/shorebird_code_push
|
||||
# Uncomment the following line to disable automatic updates.
|
||||
auto_update: false
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/// Get info about your Shorebird code push app
|
||||
library shorebird_code_push;
|
||||
|
||||
export 'src/shorebird_code_push_io.dart'
|
||||
if (dart.library.html) 'src/shorebird_code_push_web.dart';
|
||||
export 'src/shorebird_updater.dart';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,41 +0,0 @@
|
||||
/// {@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.
|
||||
///
|
||||
/// Returns true when there is a new patch for this app on Shorebird servers
|
||||
/// but not yet downloaded to this device.
|
||||
///
|
||||
/// Returns false in all other cases, including when a new patch is installed
|
||||
/// locally but not yet booted from.
|
||||
/// Use [isNewPatchReadyToInstall] to check if a new patch has been downloaded
|
||||
/// and is ready to boot from on next restart.
|
||||
///
|
||||
/// 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.
|
||||
/// Does nothing if there is no new patch 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();
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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';
|
||||
|
||||
/// Applications should not import this file directly, but
|
||||
/// import `package:shorebird_code_push/shorebird_code_push.dart` instead.
|
||||
|
||||
/// {@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) {
|
||||
_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();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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.
|
||||
This occurs when using package:shorebird_code_push in an app that does not
|
||||
contain the Shorebird Engine. Most commonly this is due to building with
|
||||
`flutter build` or `flutter run` instead of `shorebird release`.
|
||||
''');
|
||||
}
|
||||
@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;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'package:shorebird_code_push/src/shorebird_code_push_base.dart';
|
||||
import 'package:shorebird_code_push/src/shorebird_code_push_noop.dart';
|
||||
|
||||
/// Applications should not import this file directly, but
|
||||
/// import `package:shorebird_code_push/shorebird_code_push.dart` instead.
|
||||
|
||||
/// {@template shorebird_code_push}
|
||||
/// Get info about your Shorebird code push app.
|
||||
/// {@endtemplate}
|
||||
class ShorebirdCodePush extends ShorebirdCodePushNoop
|
||||
implements ShorebirdCodePushBase {}
|
||||
@@ -0,0 +1,138 @@
|
||||
import 'package:shorebird_code_push/src/shorebird_updater_io.dart'
|
||||
if (dart.library.js_interop) './shorebird_updater_web.dart';
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
|
||||
/// The reason a call to [ShorebirdUpdater.update] failed.
|
||||
enum UpdateFailureReason {
|
||||
/// No update is available.
|
||||
noUpdate,
|
||||
|
||||
/// The update failed because the patch could not be downloaded.
|
||||
downloadFailed,
|
||||
|
||||
/// The update failed because the patch failed to install.
|
||||
installFailed,
|
||||
|
||||
/// The update failed for an unknown reason.
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// {@template read_patch_exception}
|
||||
/// An exception thrown by [ShorebirdUpdater.readCurrentPatch] and
|
||||
/// [ShorebirdUpdater.readNextPatch] when the read is unsuccessful.
|
||||
/// {@endtemplate}
|
||||
class ReadPatchException implements Exception {
|
||||
/// {@macro update_exception}
|
||||
const ReadPatchException({required this.message});
|
||||
|
||||
/// The human-readable error message.
|
||||
final String message;
|
||||
}
|
||||
|
||||
/// {@template update_exception}
|
||||
/// An exception thrown by [ShorebirdUpdater.update] when the update is
|
||||
/// unsuccessful.
|
||||
/// {@endtemplate}
|
||||
class UpdateException implements Exception {
|
||||
/// {@macro update_exception}
|
||||
const UpdateException({required this.message, required this.reason});
|
||||
|
||||
/// The human-readable error message.
|
||||
final String message;
|
||||
|
||||
/// The reason the update failed.
|
||||
final UpdateFailureReason reason;
|
||||
}
|
||||
|
||||
/// Log message when the Shorebird updater is unavailable in the current
|
||||
/// environment.
|
||||
void logShorebirdEngineUnavailableMessage() {
|
||||
// ignore: avoid_print
|
||||
print('''
|
||||
-------------------------------------------------------------------------------
|
||||
The Shorebird Updater is unavailable in the current environment.
|
||||
-------------------------------------------------------------------------------
|
||||
This occurs when using pkg:shorebird_code_push in an app that does not
|
||||
contain the Shorebird Engine. Most commonly this is due to building with
|
||||
`flutter build` or `flutter run` instead of `shorebird release` or `shorebird preview`.
|
||||
It can also occur when running on an unsupported platform (e.g. web or desktop).
|
||||
''');
|
||||
}
|
||||
|
||||
/// {@template patch}
|
||||
/// An object representing a single patch (over-the-air update).
|
||||
/// {@endtemplate}
|
||||
class Patch {
|
||||
/// {@macro patch}
|
||||
const Patch({required this.number});
|
||||
|
||||
/// The patch number.
|
||||
final int number;
|
||||
}
|
||||
|
||||
/// The current status of the app in terms of whether its up-to-date.
|
||||
enum UpdateStatus {
|
||||
/// The app is up to date (e.g. running the latest patch.)
|
||||
upToDate,
|
||||
|
||||
/// A new update is available for download.
|
||||
outdated,
|
||||
|
||||
/// The app is up to date, but a restart is required for the update to take
|
||||
/// effect.
|
||||
restartRequired,
|
||||
|
||||
/// The update status is unavailable. This occurs when the updater is not
|
||||
/// available in the current build.
|
||||
/// See also:
|
||||
/// * [ShorebirdUpdater.isAvailable] to determine if the updater is
|
||||
/// available.
|
||||
unavailable,
|
||||
}
|
||||
|
||||
/// {@template shorebird_updater}
|
||||
/// Manage updates for a Shorebird app.
|
||||
/// {@endtemplate}
|
||||
abstract class ShorebirdUpdater {
|
||||
/// {@macro shorebird_updater}
|
||||
factory ShorebirdUpdater() => ShorebirdUpdaterImpl(const Updater());
|
||||
|
||||
/// Whether the updater is available on the current platform.
|
||||
/// The most common reasons for this returning false are:
|
||||
/// 1. The app is running in debug mode (Shorebird only supports release
|
||||
/// mode).
|
||||
/// 2. The app was *NOT* built using `shorebird release` and does *NOT*
|
||||
/// contain the Shorebird engine.
|
||||
bool get isAvailable;
|
||||
|
||||
/// Returns information about the currently installed patch.
|
||||
/// Returns `null` if no patch has been installed.
|
||||
/// Returns `null` if the updater is not available.
|
||||
/// Throws a [ReadPatchException] if the read is unsuccessful.
|
||||
Future<Patch?> readCurrentPatch();
|
||||
|
||||
/// Returns information about the most recently downloaded patch.
|
||||
/// Returns the same patch as [readCurrentPatch] if no new patch has been
|
||||
/// downloaded.
|
||||
/// Returns `null` if the updater is not available.
|
||||
/// Throws a [ReadPatchException] if the read is unsuccessful.
|
||||
Future<Patch?> readNextPatch();
|
||||
|
||||
/// Checks for available updates and returns the [UpdateStatus].
|
||||
/// This method should be used to determine the update status before calling
|
||||
/// [update].
|
||||
/// Returns `null` if the updater is not available.
|
||||
Future<UpdateStatus?> checkForUpdate();
|
||||
|
||||
/// Updates the app to the latest patch (if available).
|
||||
/// Note: The app must be restarted for the update to take effect.
|
||||
/// Note: This method does nothing if the updater is not available.
|
||||
///
|
||||
/// Throws an [UpdateException] if a the update call is unsuccessful.
|
||||
///
|
||||
/// See also:
|
||||
/// * [isAvailable], which indicates whether the updater is available.
|
||||
/// * [checkForUpdate], which should be called to check if an update is
|
||||
/// available before calling this method.
|
||||
Future<void> update();
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';
|
||||
import 'package:shorebird_code_push/src/shorebird_updater.dart';
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
|
||||
@visibleForTesting
|
||||
|
||||
/// Type definition for [Isolate.run].
|
||||
typedef IsolateRun = Future<R> Function<R>(
|
||||
FutureOr<R> Function(), {
|
||||
String? debugName,
|
||||
});
|
||||
|
||||
/// {@template shorebird_updater_io}
|
||||
/// The Shorebird IO Updater.
|
||||
/// {@endtemplate}
|
||||
class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
||||
/// {@macro shorebird_updater_io}
|
||||
ShorebirdUpdaterImpl(this._updater, {IsolateRun? run})
|
||||
: _run = run ?? Isolate.run {
|
||||
try {
|
||||
// If the Shorebird Engine is not available, this will throw an exception.
|
||||
// FIXME: Run this in an isolate or refactor the updater to avoid risking
|
||||
// a hang. If another thread is also calling into Shorebird at the same
|
||||
// time the underlying Rust code could block getting the config lock.
|
||||
_updater.currentPatchNumber();
|
||||
_isAvailable = true;
|
||||
} catch (_) {
|
||||
logShorebirdEngineUnavailableMessage();
|
||||
_isAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
late final bool _isAvailable;
|
||||
|
||||
final Updater _updater;
|
||||
|
||||
final IsolateRun _run;
|
||||
|
||||
@override
|
||||
bool get isAvailable => _isAvailable;
|
||||
|
||||
@override
|
||||
Future<Patch?> readCurrentPatch() => _readPatch(_updater.currentPatchNumber);
|
||||
|
||||
@override
|
||||
Future<Patch?> readNextPatch() => _readPatch(_updater.nextPatchNumber);
|
||||
|
||||
Future<Patch?> _readPatch(int Function() fn) async {
|
||||
if (!_isAvailable) return null;
|
||||
return _run(
|
||||
() {
|
||||
try {
|
||||
final patchNumber = fn();
|
||||
return patchNumber > 0 ? Patch(number: patchNumber) : null;
|
||||
} catch (error) {
|
||||
throw ReadPatchException(message: '$error');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UpdateStatus> checkForUpdate() async {
|
||||
if (!_isAvailable) return UpdateStatus.unavailable;
|
||||
|
||||
final isUpdateAvailable = await _run(_updater.checkForUpdate);
|
||||
if (isUpdateAvailable) return UpdateStatus.outdated;
|
||||
|
||||
final (current, next) = await (readCurrentPatch(), readNextPatch()).wait;
|
||||
return next != null && current?.number != next.number
|
||||
? UpdateStatus.restartRequired
|
||||
: UpdateStatus.upToDate;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update() async {
|
||||
if (!_isAvailable) return;
|
||||
|
||||
Pointer<UpdateResult> result = nullptr;
|
||||
|
||||
try {
|
||||
result = await _run(_updater.update);
|
||||
} catch (_) {
|
||||
return _legacyFallback();
|
||||
}
|
||||
|
||||
const unknownErrorMessage = 'An unknown error occurred.';
|
||||
|
||||
try {
|
||||
if (result == nullptr) {
|
||||
throw const UpdateException(
|
||||
reason: UpdateFailureReason.unknown,
|
||||
message: unknownErrorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
final status = result.ref.status;
|
||||
|
||||
if (status == SHOREBIRD_UPDATE_INSTALLED) return;
|
||||
|
||||
final reason = status.toFailureReason();
|
||||
final message = result.ref.message != nullptr
|
||||
? result.ref.message.cast<Utf8>().toDartString()
|
||||
: unknownErrorMessage;
|
||||
throw UpdateException(message: message, reason: reason);
|
||||
} finally {
|
||||
_updater.freeUpdateResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to downloadUpdate if update is not available.
|
||||
Future<void> _legacyFallback() async {
|
||||
await _run(_updater.downloadUpdate);
|
||||
final (current, next) = await (readCurrentPatch(), readNextPatch()).wait;
|
||||
final status = next != null && current?.number != next.number
|
||||
? UpdateStatus.restartRequired
|
||||
: UpdateStatus.upToDate;
|
||||
if (status == UpdateStatus.restartRequired) return;
|
||||
throw const UpdateException(
|
||||
message: '''
|
||||
Downloading update failed but reason is unknown due to legacy updater.
|
||||
Please upgrade the Shorebird Engine for improved error messages.''',
|
||||
reason: UpdateFailureReason.unknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on int {
|
||||
UpdateFailureReason toFailureReason() {
|
||||
switch (this) {
|
||||
case SHOREBIRD_NO_UPDATE:
|
||||
return UpdateFailureReason.noUpdate;
|
||||
case SHOREBIRD_UPDATE_HAD_ERROR:
|
||||
return UpdateFailureReason.downloadFailed;
|
||||
case SHOREBIRD_UPDATE_IS_BAD_PATCH:
|
||||
return UpdateFailureReason.installFailed;
|
||||
case SHOREBIRD_UPDATE_ERROR:
|
||||
return UpdateFailureReason.unknown;
|
||||
default:
|
||||
return UpdateFailureReason.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:shorebird_code_push/src/shorebird_updater.dart';
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
|
||||
/// {@template shorebird_updater_web}
|
||||
/// The Shorebird web updater.
|
||||
/// {@endtemplate}
|
||||
class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
||||
/// {@macro shorebird_updater_web}
|
||||
ShorebirdUpdaterImpl(this._updater) {
|
||||
logShorebirdEngineUnavailableMessage();
|
||||
}
|
||||
|
||||
// ignore: unused_field
|
||||
final Updater _updater;
|
||||
|
||||
@override
|
||||
bool get isAvailable => false;
|
||||
|
||||
@override
|
||||
Future<Patch?> readCurrentPatch() async => null;
|
||||
|
||||
@override
|
||||
Future<Patch?> readNextPatch() async => null;
|
||||
|
||||
@override
|
||||
Future<UpdateStatus> checkForUpdate() async => UpdateStatus.unavailable;
|
||||
|
||||
@override
|
||||
Future<void> update() async {}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:ffi' as ffi;
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';
|
||||
@@ -28,4 +29,12 @@ class Updater {
|
||||
|
||||
/// Downloads the latest patch, if available.
|
||||
void downloadUpdate() => bindings.shorebird_update();
|
||||
|
||||
/// Downloads the latest patch, if available and returns an [UpdateResult]
|
||||
/// to indicate whether the update was successful.
|
||||
Pointer<UpdateResult> update() => bindings.shorebird_update_with_result();
|
||||
|
||||
/// Frees an update result allocated by the updater.
|
||||
void freeUpdateResult(Pointer<UpdateResult> ptr) =>
|
||||
bindings.shorebird_free_update_result(ptr);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
name: shorebird_code_push
|
||||
description: Check for and download Shorebird code push updates from your app.
|
||||
version: 1.1.6
|
||||
version: 2.0.0-dev.1
|
||||
homepage: https://shorebird.dev
|
||||
repository: https://github.com/shorebirdtech/updater/tree/main/shorebird_code_push
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
sdk: ">=3.5.4 <4.0.0"
|
||||
flutter: ">=3.24.4 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
ffi: ^2.0.2
|
||||
@@ -13,9 +14,9 @@ dependencies:
|
||||
|
||||
dev_dependencies:
|
||||
ffigen: ">=8.0.2 <16.0.0"
|
||||
mocktail: ">=0.3.0 <2.0.0"
|
||||
mocktail: ^1.0.0
|
||||
test: ^1.19.2
|
||||
very_good_analysis: ">=5.0.0 <7.0.0"
|
||||
very_good_analysis: ^6.0.0
|
||||
|
||||
ffigen:
|
||||
output: "lib/src/generated/updater_bindings.g.dart"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'dart:async';
|
||||
|
||||
void Function() overridePrint(void Function(List<String> logs) fn) {
|
||||
return () {
|
||||
final printLogs = <String>[];
|
||||
final spec = ZoneSpecification(
|
||||
print: (_, __, ___, String msg) {
|
||||
printLogs.add(msg);
|
||||
},
|
||||
);
|
||||
|
||||
return Zone.current
|
||||
.fork(specification: spec)
|
||||
.run<void>(() => fn(printLogs));
|
||||
};
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shorebird_code_push/src/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,
|
||||
equals(
|
||||
[
|
||||
'''
|
||||
[ShorebirdCodePush]: Shorebird Engine not available, using no-op implementation.
|
||||
This occurs when using package:shorebird_code_push in an app that does not
|
||||
contain the Shorebird Engine. Most commonly this is due to building with
|
||||
`flutter build` or `flutter run` instead of `shorebird release`.\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]: Shorebird Engine not available, using no-op implementation.
|
||||
This occurs when using package:shorebird_code_push in an app that does not
|
||||
contain the Shorebird Engine. Most commonly this is due to building with
|
||||
`flutter build` or `flutter run` instead of `shorebird release`.\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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// 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.
|
||||
This occurs when using package:shorebird_code_push in an app that does not
|
||||
contain the Shorebird Engine. Most commonly this is due to building with
|
||||
`flutter build` or `flutter run` instead of `shorebird release`.\n''';
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';
|
||||
import 'package:shorebird_code_push/src/shorebird_updater.dart';
|
||||
import 'package:shorebird_code_push/src/shorebird_updater_io.dart';
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../override_print.dart';
|
||||
|
||||
class _MockUpdater extends Mock implements Updater {}
|
||||
|
||||
Future<R> run<R>(
|
||||
FutureOr<R> Function() computation, {
|
||||
String? debugName,
|
||||
}) async {
|
||||
return computation();
|
||||
}
|
||||
|
||||
void main() {
|
||||
group(ShorebirdUpdaterImpl, () {
|
||||
late Updater updater;
|
||||
late ShorebirdUpdaterImpl shorebirdUpdater;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Pointer.fromAddress(0));
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
updater = _MockUpdater();
|
||||
});
|
||||
|
||||
group('isAvailable', () {
|
||||
group('when updater is available', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(1);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('returns true', () {
|
||||
expect(shorebirdUpdater.isAvailable, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('when updater is unavailable', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenThrow(Exception('oops'));
|
||||
});
|
||||
|
||||
test(
|
||||
'returns false',
|
||||
overridePrint((_) {
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
expect(shorebirdUpdater.isAvailable, isFalse);
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('readPatch', () {
|
||||
group('when updater is unavailable', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenThrow(Exception('oops'));
|
||||
});
|
||||
|
||||
test(
|
||||
'returns null',
|
||||
overridePrint((_) async {
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
await expectLater(
|
||||
shorebirdUpdater.readCurrentPatch(),
|
||||
completion(isNull),
|
||||
);
|
||||
await expectLater(
|
||||
shorebirdUpdater.readNextPatch(),
|
||||
completion(isNull),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
group('when updater has no installed patches', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
when(updater.nextPatchNumber).thenReturn(0);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('returns null', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.readCurrentPatch(),
|
||||
completion(isNull),
|
||||
);
|
||||
await expectLater(
|
||||
shorebirdUpdater.readNextPatch(),
|
||||
completion(isNull),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when updater has a downloaded patch', () {
|
||||
const currentPatchNumber = 0;
|
||||
const nextPatchNumber = 1;
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(currentPatchNumber);
|
||||
when(updater.nextPatchNumber).thenReturn(nextPatchNumber);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('returns correct patch numbers', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.readCurrentPatch(),
|
||||
completion(isNull),
|
||||
);
|
||||
await expectLater(
|
||||
shorebirdUpdater.readNextPatch(),
|
||||
completion(
|
||||
isA<Patch>().having(
|
||||
(p) => p.number,
|
||||
'number',
|
||||
nextPatchNumber,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when updater has an installed patch', () {
|
||||
const currentPatchNumber = 1;
|
||||
const nextPatchNumber = 1;
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(currentPatchNumber);
|
||||
when(updater.nextPatchNumber).thenReturn(nextPatchNumber);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('returns correct patch numbers', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.readCurrentPatch(),
|
||||
completion(
|
||||
isA<Patch>().having(
|
||||
(p) => p.number,
|
||||
'number',
|
||||
currentPatchNumber,
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
shorebirdUpdater.readNextPatch(),
|
||||
completion(
|
||||
isA<Patch>().having(
|
||||
(p) => p.number,
|
||||
'number',
|
||||
nextPatchNumber,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group(
|
||||
'when updater has an installed patch '
|
||||
'and a new downloaded patch', () {
|
||||
const currentPatchNumber = 1;
|
||||
const nextPatchNumber = 2;
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(currentPatchNumber);
|
||||
when(updater.nextPatchNumber).thenReturn(nextPatchNumber);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('returns correct patch numbers', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.readCurrentPatch(),
|
||||
completion(
|
||||
isA<Patch>().having(
|
||||
(p) => p.number,
|
||||
'number',
|
||||
currentPatchNumber,
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
shorebirdUpdater.readNextPatch(),
|
||||
completion(
|
||||
isA<Patch>().having(
|
||||
(p) => p.number,
|
||||
'number',
|
||||
nextPatchNumber,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when an exception occurs trying to read patches', () {
|
||||
final currentPatchNumberReturnValues = [0, -1];
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenAnswer((_) {
|
||||
final value = currentPatchNumberReturnValues.removeAt(0);
|
||||
if (value < 0) throw Exception('oops');
|
||||
return value;
|
||||
});
|
||||
when(updater.nextPatchNumber).thenThrow(Exception('oops'));
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('throws $ReadPatchException', () async {
|
||||
await expectLater(
|
||||
() => shorebirdUpdater.readCurrentPatch(),
|
||||
throwsA(isA<ReadPatchException>()),
|
||||
);
|
||||
await expectLater(
|
||||
() => shorebirdUpdater.readNextPatch(),
|
||||
throwsA(isA<ReadPatchException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('checkForUpdate', () {
|
||||
group('when updater is unavailable', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenThrow(Exception('oops'));
|
||||
});
|
||||
|
||||
test(
|
||||
'returns UpdateStatus.unavailable',
|
||||
overridePrint((_) async {
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
await expectLater(
|
||||
shorebirdUpdater.checkForUpdate(),
|
||||
completion(equals(UpdateStatus.unavailable)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
group('when updater has an update available', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
when(updater.checkForUpdate).thenReturn(true);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('returns UpdateStatus.outdated', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.checkForUpdate(),
|
||||
completion(equals(UpdateStatus.outdated)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when updater has downloaded an update', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
when(updater.nextPatchNumber).thenReturn(1);
|
||||
when(updater.checkForUpdate).thenReturn(false);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('returns UpdateStatus.restartRequired', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.checkForUpdate(),
|
||||
completion(equals(UpdateStatus.restartRequired)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when updater installed an update and is up to date', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(1);
|
||||
when(updater.nextPatchNumber).thenReturn(1);
|
||||
when(updater.checkForUpdate).thenReturn(false);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('returns UpdateStatus.upToDate', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.checkForUpdate(),
|
||||
completion(equals(UpdateStatus.upToDate)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('update', () {
|
||||
group('when updater is unavailable', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenThrow(Exception('oops'));
|
||||
});
|
||||
|
||||
test(
|
||||
'does nothing',
|
||||
overridePrint((_) async {
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
await expectLater(shorebirdUpdater.update(), completes);
|
||||
verifyNever(updater.downloadUpdate);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
group('when a nullptr result is returned', () {
|
||||
setUp(() {
|
||||
when(() => updater.currentPatchNumber()).thenReturn(0);
|
||||
when(() => updater.update()).thenReturn(nullptr);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('throws $UpdateException', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update,
|
||||
throwsA(
|
||||
isA<UpdateException>()
|
||||
.having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
'An unknown error occurred.',
|
||||
)
|
||||
.having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
UpdateFailureReason.unknown,
|
||||
),
|
||||
),
|
||||
);
|
||||
verify(updater.update).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when no update is available', () {
|
||||
setUp(() {
|
||||
when(() => updater.currentPatchNumber()).thenReturn(0);
|
||||
final result = calloc.allocate<UpdateResult>(sizeOf<UpdateResult>());
|
||||
result.ref.status = SHOREBIRD_NO_UPDATE;
|
||||
result.ref.message = 'oops'.toNativeUtf8().cast<Char>();
|
||||
addTearDown(() {
|
||||
calloc
|
||||
..free(result.ref.message)
|
||||
..free(result);
|
||||
});
|
||||
when(() => updater.update()).thenReturn(result);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('throws $UpdateException', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update,
|
||||
throwsA(
|
||||
isA<UpdateException>()
|
||||
.having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
'oops',
|
||||
)
|
||||
.having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
UpdateFailureReason.noUpdate,
|
||||
),
|
||||
),
|
||||
);
|
||||
verify(updater.update).called(1);
|
||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when an error occurs during download', () {
|
||||
setUp(() {
|
||||
when(() => updater.currentPatchNumber()).thenReturn(0);
|
||||
final result = calloc.allocate<UpdateResult>(sizeOf<UpdateResult>());
|
||||
result.ref.status = SHOREBIRD_UPDATE_HAD_ERROR;
|
||||
result.ref.message = 'oops'.toNativeUtf8().cast<Char>();
|
||||
addTearDown(() {
|
||||
calloc
|
||||
..free(result.ref.message)
|
||||
..free(result);
|
||||
});
|
||||
when(() => updater.update()).thenReturn(result);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('throws $UpdateException', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update,
|
||||
throwsA(
|
||||
isA<UpdateException>()
|
||||
.having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
'oops',
|
||||
)
|
||||
.having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
UpdateFailureReason.downloadFailed,
|
||||
),
|
||||
),
|
||||
);
|
||||
verify(updater.update).called(1);
|
||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when the downloaded patch is bad', () {
|
||||
setUp(() {
|
||||
when(() => updater.currentPatchNumber()).thenReturn(0);
|
||||
final result = calloc.allocate<UpdateResult>(sizeOf<UpdateResult>());
|
||||
result.ref.status = SHOREBIRD_UPDATE_IS_BAD_PATCH;
|
||||
result.ref.message = 'oops'.toNativeUtf8().cast<Char>();
|
||||
addTearDown(() {
|
||||
calloc
|
||||
..free(result.ref.message)
|
||||
..free(result);
|
||||
});
|
||||
when(() => updater.update()).thenReturn(result);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('throws $UpdateException', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update,
|
||||
throwsA(
|
||||
isA<UpdateException>()
|
||||
.having((e) => e.message, 'message', 'oops')
|
||||
.having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
UpdateFailureReason.installFailed,
|
||||
),
|
||||
),
|
||||
);
|
||||
verify(updater.update).called(1);
|
||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when an unknown error occurs', () {
|
||||
setUp(() {
|
||||
when(() => updater.currentPatchNumber()).thenReturn(0);
|
||||
final result = calloc.allocate<UpdateResult>(sizeOf<UpdateResult>());
|
||||
result.ref.status = SHOREBIRD_UPDATE_ERROR;
|
||||
addTearDown(() => calloc.free(result));
|
||||
when(() => updater.update()).thenReturn(result);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('throws $UpdateException', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update,
|
||||
throwsA(
|
||||
isA<UpdateException>()
|
||||
.having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
'An unknown error occurred.',
|
||||
)
|
||||
.having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
UpdateFailureReason.unknown,
|
||||
),
|
||||
),
|
||||
);
|
||||
verify(updater.update).called(1);
|
||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when an outdated version of the engine is used', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
when(updater.nextPatchNumber).thenReturn(1);
|
||||
when(() => updater.update()).thenThrow(Exception('oops'));
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('falls back to downloadUpdate', () async {
|
||||
await expectLater(shorebirdUpdater.update(), completes);
|
||||
verify(updater.update).called(1);
|
||||
verify(updater.downloadUpdate).called(1);
|
||||
verifyNever(() => updater.freeUpdateResult(any()));
|
||||
});
|
||||
|
||||
group('when update fails', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
when(updater.nextPatchNumber).thenReturn(0);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('throws if legacy update fails', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update,
|
||||
throwsA(
|
||||
isA<UpdateException>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
'''
|
||||
Downloading update failed but reason is unknown due to legacy updater.
|
||||
Please upgrade the Shorebird Engine for improved error messages.''',
|
||||
).having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
UpdateFailureReason.unknown,
|
||||
),
|
||||
),
|
||||
);
|
||||
verify(updater.update).called(1);
|
||||
verify(updater.downloadUpdate).called(1);
|
||||
verifyNever(() => updater.freeUpdateResult(any()));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('when an unsupported status code is returned', () {
|
||||
setUp(() {
|
||||
when(() => updater.currentPatchNumber()).thenReturn(0);
|
||||
final result = calloc.allocate<UpdateResult>(sizeOf<UpdateResult>());
|
||||
result.ref.status = -42; // invalid status code
|
||||
result.ref.message = nullptr;
|
||||
addTearDown(() => calloc.free(result));
|
||||
when(() => updater.update()).thenReturn(result);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('throws $UpdateException', () async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update,
|
||||
throwsA(
|
||||
isA<UpdateException>()
|
||||
.having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
'An unknown error occurred.',
|
||||
)
|
||||
.having(
|
||||
(e) => e.reason,
|
||||
'reason',
|
||||
UpdateFailureReason.unknown,
|
||||
),
|
||||
),
|
||||
);
|
||||
verify(updater.update).called(1);
|
||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when download succeeds', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
final result = calloc.allocate<UpdateResult>(sizeOf<UpdateResult>());
|
||||
result.ref.status = SHOREBIRD_UPDATE_INSTALLED;
|
||||
addTearDown(() => calloc.free(result));
|
||||
when(() => updater.update()).thenReturn(result);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('completes', () async {
|
||||
await expectLater(shorebirdUpdater.update(), completes);
|
||||
verify(updater.update).called(1);
|
||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:shorebird_code_push/shorebird_code_push.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../override_print.dart';
|
||||
|
||||
void main() {
|
||||
group(ShorebirdUpdater, () {
|
||||
test(
|
||||
'can be instantiated',
|
||||
overridePrint((_) {
|
||||
expect(ShorebirdUpdater.new, returnsNormally);
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shorebird_code_push/src/shorebird_updater.dart';
|
||||
import 'package:shorebird_code_push/src/shorebird_updater_web.dart';
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../override_print.dart';
|
||||
|
||||
class _MockUpdater extends Mock implements Updater {}
|
||||
|
||||
void main() {
|
||||
group(ShorebirdUpdaterImpl, () {
|
||||
late Updater updater;
|
||||
late ShorebirdUpdaterImpl shorebirdUpdater;
|
||||
|
||||
setUp(() {
|
||||
updater = _MockUpdater();
|
||||
});
|
||||
|
||||
test(
|
||||
'logs unavailable error',
|
||||
overridePrint((logs) {
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater);
|
||||
expect(
|
||||
logs,
|
||||
contains(
|
||||
isA<String>().having(
|
||||
(s) => s,
|
||||
'message',
|
||||
contains(
|
||||
'''The Shorebird Updater is unavailable in the current environment.''',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
group('isAvailable', () {
|
||||
test(
|
||||
'returns false',
|
||||
overridePrint((_) {
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater);
|
||||
expect(shorebirdUpdater.isAvailable, isFalse);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
group('readPatch', () {
|
||||
test(
|
||||
'returns null',
|
||||
overridePrint((_) async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.readCurrentPatch(),
|
||||
completion(isNull),
|
||||
);
|
||||
await expectLater(
|
||||
shorebirdUpdater.readNextPatch(),
|
||||
completion(isNull),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
group('checkForUpdate', () {
|
||||
test(
|
||||
'returns UpdateStatus.unavailable',
|
||||
overridePrint((_) async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.checkForUpdate(),
|
||||
completion(equals(UpdateStatus.unavailable)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
group('update', () {
|
||||
test(
|
||||
'does nothing',
|
||||
overridePrint((_) async {
|
||||
await expectLater(shorebirdUpdater.update(), completes);
|
||||
verifyNever(updater.downloadUpdate);
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
@@ -10,6 +13,10 @@ void main() {
|
||||
late UpdaterBindings updaterBindings;
|
||||
late Updater updater;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Pointer.fromAddress(0));
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
updaterBindings = _MockUpdaterBindings();
|
||||
|
||||
@@ -62,5 +69,25 @@ void main() {
|
||||
verify(() => updaterBindings.shorebird_update()).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('update', () {
|
||||
test('calls bindings.shorebird_update_with_result', () {
|
||||
when(
|
||||
() => updaterBindings.shorebird_update_with_result(),
|
||||
).thenReturn(nullptr);
|
||||
updater.update();
|
||||
verify(() => updaterBindings.shorebird_update_with_result()).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('freeUpdateResult', () {
|
||||
test('calls bindings.shorebird_free_update_result', () {
|
||||
final result = calloc.allocate<UpdateResult>(sizeOf<UpdateResult>());
|
||||
updater.freeUpdateResult(result);
|
||||
verify(
|
||||
() => updaterBindings.shorebird_free_update_result(any()),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user