feat(shorebird_code_push): track support (#232)
* feat(shorebird_code_push): track support * cleanup and add todos * docs * run ffigen * use c_char instead of char * Add channel support * tests * tests * update podfile.lock * Update example to include tracks selector --------- Co-authored-by: Bryan Oltman <bryan@shorebird.dev> Co-authored-by: Bryan Oltman <bryanoltman@gmail.com>
This commit is contained in:
@@ -151,9 +151,15 @@ 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.
|
||||
* Check for an update on the first non-null channel of:
|
||||
* 1. `c_channel`
|
||||
* 2. The channel specified in shorebird.yaml
|
||||
* 3. The default "stable" channel
|
||||
*
|
||||
* Returns true if an update exists that has not yet been downloaded.
|
||||
*/
|
||||
SHOREBIRD_EXPORT bool shorebird_check_for_update(void);
|
||||
SHOREBIRD_EXPORT
|
||||
bool shorebird_check_for_downloadable_update(const char *c_channel);
|
||||
|
||||
/**
|
||||
* Synchronously download an update if one is available.
|
||||
@@ -161,10 +167,15 @@ SHOREBIRD_EXPORT bool shorebird_check_for_update(void);
|
||||
SHOREBIRD_EXPORT void shorebird_update(void);
|
||||
|
||||
/**
|
||||
* Synchronously download an update if one is available.
|
||||
* Synchronously download an update on the first non-null channel of:
|
||||
* 1. `c_channel`
|
||||
* 2. The channel specified in shorebird.yaml
|
||||
* 3. The default "stable" channel
|
||||
*
|
||||
* Returns an [UpdateResult] indicating whether the update was successful.
|
||||
*/
|
||||
SHOREBIRD_EXPORT const struct UpdateResult *shorebird_update_with_result(void);
|
||||
SHOREBIRD_EXPORT
|
||||
const struct UpdateResult *shorebird_update_with_result(const char *c_channel);
|
||||
|
||||
/**
|
||||
* Start a thread to download an update if one is available.
|
||||
|
||||
+64
-20
@@ -68,6 +68,7 @@ pub struct UpdateResult {
|
||||
pub status: i32,
|
||||
pub message: *const libc::c_char,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct FileCallbacks {
|
||||
@@ -94,6 +95,13 @@ fn to_rust(c_string: *const libc::c_char) -> anyhow::Result<String> {
|
||||
Ok(c_str.to_str()?.to_string())
|
||||
}
|
||||
|
||||
fn to_rust_option(c_string: *const c_char) -> anyhow::Result<Option<String>> {
|
||||
if c_string.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(to_rust(c_string)?))
|
||||
}
|
||||
|
||||
/// Converts a Rust string to a C string, caller must free the C string.
|
||||
fn allocate_c_string(rust_string: &str) -> anyhow::Result<*mut c_char> {
|
||||
let c_str = CString::new(rust_string)?;
|
||||
@@ -266,27 +274,47 @@ pub unsafe extern "C" fn shorebird_free_update_result(result: *mut UpdateResult)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for an update. Returns true if an update is available.
|
||||
/// Check for an update on the first non-null channel of:
|
||||
/// 1. `c_channel`
|
||||
/// 2. The channel specified in shorebird.yaml
|
||||
/// 3. The default "stable" channel
|
||||
///
|
||||
/// Returns true if an update exists that has not yet been downloaded.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn shorebird_check_for_update() -> bool {
|
||||
log_on_error(updater::check_for_update, "checking for update", false)
|
||||
pub extern "C" fn shorebird_check_for_downloadable_update(c_channel: *const c_char) -> bool {
|
||||
log_on_error(
|
||||
|| {
|
||||
let channel = to_rust_option(c_channel)?;
|
||||
updater::check_for_downloadable_update(channel.as_deref())
|
||||
},
|
||||
"checking for update",
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Synchronously download an update if one is available.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn shorebird_update() {
|
||||
log_on_error(
|
||||
|| updater::update().map(|result| shorebird_info!("Update result: {}", result)),
|
||||
|| updater::update(None).map(|result| shorebird_info!("Update result: {}", result)),
|
||||
"downloading update",
|
||||
(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Synchronously download an update if one is available.
|
||||
/// Synchronously download an update on the first non-null channel of:
|
||||
/// 1. `c_channel`
|
||||
/// 2. The channel specified in shorebird.yaml
|
||||
/// 3. The default "stable" channel
|
||||
///
|
||||
/// 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());
|
||||
pub extern "C" fn shorebird_update_with_result(c_channel: *const c_char) -> *const UpdateResult {
|
||||
let channel = to_rust_option(c_channel);
|
||||
let result = match channel {
|
||||
Ok(channel) => to_update_result(updater::update(channel.as_deref())),
|
||||
Err(err) => to_update_result(Err(err)),
|
||||
};
|
||||
return Box::into_raw(Box::new(result));
|
||||
}
|
||||
|
||||
@@ -522,7 +550,10 @@ mod test {
|
||||
|
||||
// set up the network hooks to return a patch.
|
||||
testing_set_network_hooks(
|
||||
|_url, _request| {
|
||||
|_url, request| {
|
||||
// We didn't specify a channel in either the shorebird_check_for_downloadable_update
|
||||
// call or the shorebird.yaml, so we should default to "stable".
|
||||
assert_eq!(request.channel, "stable");
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
||||
Ok(PatchCheckResponse {
|
||||
@@ -547,7 +578,7 @@ mod test {
|
||||
|_url, _event| Ok(()),
|
||||
);
|
||||
// There is an update available.
|
||||
assert!(shorebird_check_for_update());
|
||||
assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
|
||||
|
||||
// Go ahead and do the update.
|
||||
shorebird_update();
|
||||
@@ -565,7 +596,7 @@ mod test {
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn patch_success_with_result() {
|
||||
fn patch_success_with_result() -> anyhow::Result<()> {
|
||||
testing_reset_config();
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
|
||||
@@ -584,7 +615,8 @@ mod test {
|
||||
|
||||
// set up the network hooks to return a patch.
|
||||
testing_set_network_hooks(
|
||||
|_url, _request| {
|
||||
|_url, request| {
|
||||
assert_eq!(request.channel, "beta");
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
||||
Ok(PatchCheckResponse {
|
||||
@@ -609,10 +641,12 @@ mod test {
|
||||
|_url, _event| Ok(()),
|
||||
);
|
||||
// There is an update available.
|
||||
assert!(shorebird_check_for_update());
|
||||
let channel_c_str = allocate_c_string("beta")?;
|
||||
assert!(shorebird_check_for_downloadable_update(channel_c_str));
|
||||
|
||||
// Go ahead and do the update.
|
||||
let result = shorebird_update_with_result();
|
||||
let result = shorebird_update_with_result(channel_c_str);
|
||||
unsafe { shorebird_free_string(channel_c_str) };
|
||||
|
||||
unsafe {
|
||||
assert_eq!(result.read().status, SHOREBIRD_UPDATE_INSTALLED);
|
||||
@@ -627,6 +661,8 @@ mod test {
|
||||
unsafe { shorebird_free_string(c_path) };
|
||||
let new = std::fs::read_to_string(path).unwrap();
|
||||
assert_eq!(new, expected_new);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[serial]
|
||||
@@ -661,7 +697,7 @@ mod test {
|
||||
);
|
||||
|
||||
// Go ahead and do the update.
|
||||
let result = shorebird_update_with_result();
|
||||
let result = shorebird_update_with_result(std::ptr::null());
|
||||
|
||||
unsafe {
|
||||
assert_eq!(result.read().status, SHOREBIRD_NO_UPDATE);
|
||||
@@ -702,7 +738,7 @@ mod test {
|
||||
);
|
||||
|
||||
// Go ahead and do the update.
|
||||
let result = shorebird_update_with_result();
|
||||
let result = shorebird_update_with_result(std::ptr::null());
|
||||
|
||||
unsafe {
|
||||
assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR);
|
||||
@@ -712,7 +748,7 @@ mod test {
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn patch_download_failure_with_result() {
|
||||
fn patch_download_failure_with_result() -> anyhow::Result<()> {
|
||||
testing_reset_config();
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
|
||||
@@ -730,7 +766,11 @@ mod test {
|
||||
|
||||
// set up the network hooks to return a patch.
|
||||
testing_set_network_hooks(
|
||||
|_url, _request| {
|
||||
|_url, request| {
|
||||
// shorebird_update_with_result was called with the beta channel, ensure that is
|
||||
// piped through to the network request.
|
||||
assert_eq!(request.channel, "beta");
|
||||
|
||||
// Generated by `string_patch "hello world" "hello tests"`
|
||||
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
||||
Ok(PatchCheckResponse {
|
||||
@@ -749,12 +789,16 @@ mod test {
|
||||
);
|
||||
|
||||
// Go ahead and do the update.
|
||||
let result = shorebird_update_with_result();
|
||||
let channel_c_str = allocate_c_string("beta")?;
|
||||
let result = shorebird_update_with_result(channel_c_str);
|
||||
unsafe { shorebird_free_string(channel_c_str) };
|
||||
|
||||
unsafe {
|
||||
assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR);
|
||||
shorebird_free_update_result(result as *mut UpdateResult);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[serial]
|
||||
@@ -806,7 +850,7 @@ mod test {
|
||||
assert_eq!(shorebird_current_boot_patch_number(), 0);
|
||||
|
||||
// There is an update available.
|
||||
assert!(shorebird_check_for_update());
|
||||
assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
|
||||
// Go ahead and do the update.
|
||||
shorebird_update();
|
||||
|
||||
@@ -913,7 +957,7 @@ mod test {
|
||||
shorebird_start_update_thread();
|
||||
// Wait for the thread to start.
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
assert!(updater::update().is_err());
|
||||
assert!(updater::update(None).is_err());
|
||||
}
|
||||
// Unlock the lock, and wait for the thread to finish.
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
+37
-22
@@ -242,12 +242,24 @@ pub fn should_auto_update() -> anyhow::Result<bool> {
|
||||
with_config(|config| Ok(config.auto_update))
|
||||
}
|
||||
|
||||
/// Synchronously checks for an update and returns true if an update is available.
|
||||
pub fn check_for_update() -> anyhow::Result<bool> {
|
||||
/// Synchronously checks for an update on the first non-null channel of:
|
||||
/// 1. `c_channel`
|
||||
/// 2. The channel specified in shorebird.yaml
|
||||
/// 3. The default "stable" channel
|
||||
///
|
||||
/// Returns true if an update is available for download. Will return false if the update is already
|
||||
/// downloaded and ready to install.
|
||||
pub fn check_for_downloadable_update(channel: Option<&str>) -> anyhow::Result<bool> {
|
||||
let (request, url, request_fn) = with_config(|config| {
|
||||
// Get the required info to make the request.
|
||||
let mut config = config.clone();
|
||||
|
||||
match channel {
|
||||
Some(channel) => config.channel = channel.to_string(),
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok((
|
||||
PatchCheckRequest::new(config),
|
||||
PatchCheckRequest::new(&config),
|
||||
patches_check_url(&config.base_url),
|
||||
config.network_hooks.patch_check_request_fn,
|
||||
))
|
||||
@@ -319,7 +331,7 @@ fn copy_update_config() -> anyhow::Result<UpdateConfig> {
|
||||
|
||||
// Callers must possess the Updater lock, but we don't care about the contents
|
||||
// since they're empty.
|
||||
fn update_internal(_: &UpdaterLockState) -> anyhow::Result<UpdateStatus> {
|
||||
fn update_internal(_: &UpdaterLockState, channel: Option<&str>) -> anyhow::Result<UpdateStatus> {
|
||||
// Only one copy of Update can be running at a time.
|
||||
// Update will take the global Updater lock.
|
||||
// Update will need to take the Config lock at times, but will only
|
||||
@@ -337,7 +349,10 @@ fn update_internal(_: &UpdaterLockState) -> anyhow::Result<UpdateStatus> {
|
||||
// Takes Config lock and installs patch.
|
||||
// Saves state to disk (holds Config lock while writing).
|
||||
|
||||
let config = copy_update_config()?;
|
||||
let mut config = copy_update_config()?;
|
||||
if channel.is_some() {
|
||||
config.channel = channel.unwrap().to_string();
|
||||
}
|
||||
|
||||
// We discard any events if we have more than 3 queued to make sure
|
||||
// we don't stall the client.
|
||||
@@ -460,8 +475,8 @@ fn should_install_patch(patch_number: usize) -> Result<ShouldInstallPatchCheckRe
|
||||
}
|
||||
|
||||
/// Synchronously checks for an update and downloads and installs it if available.
|
||||
pub fn update() -> anyhow::Result<UpdateStatus> {
|
||||
with_updater_thread_lock(update_internal)
|
||||
pub fn update(channel: Option<&str>) -> anyhow::Result<UpdateStatus> {
|
||||
with_updater_thread_lock(|lock_state| update_internal(lock_state, channel))
|
||||
}
|
||||
|
||||
/// Given a path to a patch file, and a base file, apply the patch to the base
|
||||
@@ -642,7 +657,7 @@ pub fn report_launch_success() -> anyhow::Result<()> {
|
||||
/// and install it if available.
|
||||
pub fn start_update_thread() {
|
||||
std::thread::spawn(move || {
|
||||
let result = update();
|
||||
let result = update(None);
|
||||
let status = match result {
|
||||
Ok(status) => status,
|
||||
Err(err) => {
|
||||
@@ -949,7 +964,7 @@ mod tests {
|
||||
let apk_path = tmp_dir.path().join("base.apk");
|
||||
write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes());
|
||||
|
||||
let result = super::update()?;
|
||||
let result = super::update(None)?;
|
||||
assert_eq!(result, crate::UpdateStatus::UpdateInstalled);
|
||||
|
||||
// This is gross.
|
||||
@@ -1011,7 +1026,7 @@ mod tests {
|
||||
let apk_path = tmp_dir.path().join("base.apk");
|
||||
write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes());
|
||||
|
||||
let result = super::update()?;
|
||||
let result = super::update(None)?;
|
||||
assert_eq!(result, crate::UpdateStatus::UpdateInstalled);
|
||||
|
||||
// This is gross.
|
||||
@@ -1147,7 +1162,7 @@ mod tests {
|
||||
// Make sure we're starting with no next boot patch.
|
||||
assert!(updater_state.next_boot_patch().is_none());
|
||||
|
||||
let result = super::update()?;
|
||||
let result = super::update(None)?;
|
||||
|
||||
// Ensure that we've skipped the known bad patch.
|
||||
assert_eq!(result, crate::UpdateStatus::UpdateIsBadPatch);
|
||||
@@ -1182,7 +1197,7 @@ mod tests {
|
||||
|
||||
install_fake_patch(patch_number)?;
|
||||
|
||||
let update_status = super::update()?;
|
||||
let update_status = super::update(None)?;
|
||||
|
||||
assert_eq!(update_status, crate::UpdateStatus::NoUpdate);
|
||||
|
||||
@@ -1242,7 +1257,7 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
super::update().unwrap();
|
||||
super::update(None).unwrap();
|
||||
// Only 3 events should have been sent.
|
||||
event_mock.expect(3).assert();
|
||||
|
||||
@@ -1296,7 +1311,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Invoke check_for_update to kick off a patch check request
|
||||
let _ = std::thread::spawn(crate::check_for_update);
|
||||
let _ = std::thread::spawn(|| crate::check_for_downloadable_update(None));
|
||||
|
||||
// Call with_config to get the config lock. This should complete before the patch check request is resolved.
|
||||
let config_thread = std::thread::spawn(|| with_config(|_| Ok(())));
|
||||
@@ -1358,7 +1373,7 @@ mod rollback_tests {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let update_result = crate::update();
|
||||
let update_result = crate::update(None);
|
||||
assert_eq!(update_result.unwrap(), crate::UpdateStatus::NoUpdate);
|
||||
|
||||
Ok(())
|
||||
@@ -1393,7 +1408,7 @@ mod rollback_tests {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
crate::update()?;
|
||||
crate::update(None)?;
|
||||
|
||||
with_mut_state(|state| {
|
||||
assert!(state.next_boot_patch().is_none());
|
||||
@@ -1462,7 +1477,7 @@ mod rollback_tests {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let update_result = crate::update();
|
||||
let update_result = crate::update(None);
|
||||
assert_eq!(update_result.unwrap(), crate::UpdateStatus::UpdateInstalled);
|
||||
|
||||
with_mut_state(|state| {
|
||||
@@ -1475,7 +1490,7 @@ mod rollback_tests {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod check_for_update_tests {
|
||||
mod check_for_downloadable_update_tests {
|
||||
use anyhow::Result;
|
||||
use serial_test::serial;
|
||||
use tempdir::TempDir;
|
||||
@@ -1518,7 +1533,7 @@ mod check_for_update_tests {
|
||||
|
||||
install_fake_patch(patch_number)?;
|
||||
|
||||
let is_update_available = crate::check_for_update()?;
|
||||
let is_update_available = crate::check_for_downloadable_update(None)?;
|
||||
assert!(!is_update_available);
|
||||
|
||||
Ok(())
|
||||
@@ -1536,7 +1551,7 @@ mod check_for_update_tests {
|
||||
report_launch_start()?;
|
||||
report_launch_failure()?;
|
||||
|
||||
let is_update_available = crate::check_for_update()?;
|
||||
let is_update_available = crate::check_for_downloadable_update(None)?;
|
||||
assert!(!is_update_available);
|
||||
|
||||
Ok(())
|
||||
@@ -1550,7 +1565,7 @@ mod check_for_update_tests {
|
||||
let tmp_dir = TempDir::new("example").unwrap();
|
||||
init_for_testing(&tmp_dir, Some(&server.url()));
|
||||
|
||||
let is_update_available = crate::check_for_update()?;
|
||||
let is_update_available = crate::check_for_downloadable_update(None)?;
|
||||
assert!(is_update_available);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -13,4 +13,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.1
|
||||
|
||||
@@ -29,6 +29,7 @@ class MyHomePage extends StatefulWidget {
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
final _updater = ShorebirdUpdater();
|
||||
late final bool _isUpdaterAvailable;
|
||||
var _currentTrack = UpdateTrack.stable;
|
||||
var _isCheckingForUpdates = false;
|
||||
Patch? _currentPatch;
|
||||
|
||||
@@ -54,10 +55,20 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
try {
|
||||
setState(() => _isCheckingForUpdates = true);
|
||||
// Check if there's an update available.
|
||||
final status = await _updater.checkForUpdate();
|
||||
final status = await _updater.checkForUpdate(track: _currentTrack);
|
||||
if (!mounted) return;
|
||||
// If there is an update available, show a banner.
|
||||
if (status == UpdateStatus.outdated) _showUpdateAvailableBanner();
|
||||
switch (status) {
|
||||
case UpdateStatus.upToDate:
|
||||
_showNoUpdateAvailableBanner();
|
||||
case UpdateStatus.outdated:
|
||||
_showUpdateAvailableBanner();
|
||||
case UpdateStatus.restartRequired:
|
||||
_showRestartBanner();
|
||||
case UpdateStatus.unavailable:
|
||||
// Do nothing, there is already a warning displayed at the top of the
|
||||
// screen.
|
||||
}
|
||||
} catch (error) {
|
||||
// If an error occurs, we log it for now.
|
||||
debugPrint('Error checking for update: $error');
|
||||
@@ -88,7 +99,9 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
..hideCurrentMaterialBanner()
|
||||
..showMaterialBanner(
|
||||
MaterialBanner(
|
||||
content: const Text('Update available'),
|
||||
content: Text(
|
||||
'Update available for the ${_currentTrack.name} track.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@@ -104,6 +117,26 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showNoUpdateAvailableBanner() {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentMaterialBanner()
|
||||
..showMaterialBanner(
|
||||
MaterialBanner(
|
||||
content: Text(
|
||||
'No update available on the ${_currentTrack.name} track.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
||||
},
|
||||
child: const Text('Dismiss'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRestartBanner() {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentMaterialBanner()
|
||||
@@ -145,8 +178,10 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
Future<void> _downloadUpdate() async {
|
||||
_showDownloadingBanner();
|
||||
try {
|
||||
// Perform the update (e.g download the latest patch).
|
||||
await _updater.update();
|
||||
// Perform the update (e.g download the latest patch on [_currentTrack]).
|
||||
// Note that [track] is optional. Not passing it will default to the
|
||||
// stable track.
|
||||
await _updater.update(track: _currentTrack);
|
||||
if (!mounted) return;
|
||||
// Show a banner to inform the user that the update is ready and that they
|
||||
// need to restart the app.
|
||||
@@ -166,9 +201,21 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
backgroundColor: theme.colorScheme.inversePrimary,
|
||||
title: const Text('Shorebird Code Push'),
|
||||
),
|
||||
body: _isUpdaterAvailable
|
||||
? _CurrentPatchVersion(patch: _currentPatch)
|
||||
: const _ShorebirdUnavailable(),
|
||||
body: Column(
|
||||
children: [
|
||||
if (!_isUpdaterAvailable) const _ShorebirdUnavailable(),
|
||||
const Spacer(),
|
||||
_CurrentPatchVersion(patch: _currentPatch),
|
||||
const SizedBox(height: 12),
|
||||
_TrackPicker(
|
||||
currentTrack: _currentTrack,
|
||||
onChanged: (track) {
|
||||
setState(() => _currentTrack = track);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _isCheckingForUpdates ? null : _checkForUpdate,
|
||||
tooltip: 'Check for update',
|
||||
@@ -224,6 +271,47 @@ class _CurrentPatchVersion extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget that allows selection of update track.
|
||||
class _TrackPicker extends StatelessWidget {
|
||||
const _TrackPicker({
|
||||
required this.currentTrack,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final UpdateTrack currentTrack;
|
||||
|
||||
final ValueChanged<UpdateTrack> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const Text('Update track:'),
|
||||
SegmentedButton<UpdateTrack>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
label: Text('Stable'),
|
||||
value: UpdateTrack.stable,
|
||||
),
|
||||
ButtonSegment(
|
||||
label: Text('Beta'),
|
||||
icon: Icon(Icons.science),
|
||||
value: UpdateTrack.beta,
|
||||
),
|
||||
ButtonSegment(
|
||||
label: Text('Staging'),
|
||||
icon: Icon(Icons.construction),
|
||||
value: UpdateTrack.staging,
|
||||
),
|
||||
],
|
||||
selected: {currentTrack},
|
||||
onSelectionChanged: (tracks) => onChanged(tracks.single),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A reusable loading indicator.
|
||||
class _LoadingIndicator extends StatelessWidget {
|
||||
const _LoadingIndicator();
|
||||
|
||||
@@ -8,4 +8,5 @@ export 'src/shorebird_updater.dart'
|
||||
ShorebirdUpdater,
|
||||
UpdateException,
|
||||
UpdateFailureReason,
|
||||
UpdateStatus;
|
||||
UpdateStatus,
|
||||
UpdateTrack;
|
||||
|
||||
@@ -189,13 +189,13 @@ class UpdaterBindings {
|
||||
_waitpidPtr.asFunction<int Function(int, ffi.Pointer<ffi.Int>, int)>();
|
||||
|
||||
int waitid(
|
||||
idtype_t arg0,
|
||||
Dart__uint32_t arg1,
|
||||
int arg0,
|
||||
int arg1,
|
||||
ffi.Pointer<siginfo_t> arg2,
|
||||
int arg3,
|
||||
) {
|
||||
return _waitid(
|
||||
arg0.value,
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
@@ -204,8 +204,8 @@ class UpdaterBindings {
|
||||
|
||||
late final _waitidPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Int Function(ffi.UnsignedInt, id_t, ffi.Pointer<siginfo_t>,
|
||||
ffi.Int)>>('waitid');
|
||||
ffi.Int Function(
|
||||
ffi.Int32, id_t, ffi.Pointer<siginfo_t>, ffi.Int)>>('waitid');
|
||||
late final _waitid = _waitidPtr
|
||||
.asFunction<int Function(int, int, ffi.Pointer<siginfo_t>, int)>();
|
||||
|
||||
@@ -2456,16 +2456,26 @@ class UpdaterBindings {
|
||||
late final _shorebird_free_update_result = _shorebird_free_update_resultPtr
|
||||
.asFunction<void Function(ffi.Pointer<UpdateResult>)>();
|
||||
|
||||
/// Check for an update. Returns true if an update is available.
|
||||
bool shorebird_check_for_update() {
|
||||
return _shorebird_check_for_update();
|
||||
/// Check for an update on the first non-null channel of:
|
||||
/// 1. `c_channel`
|
||||
/// 2. The channel specified in shorebird.yaml
|
||||
/// 3. The default "stable" channel
|
||||
///
|
||||
/// Returns true if an update exists that has not yet been downloaded.
|
||||
bool shorebird_check_for_downloadable_update(
|
||||
ffi.Pointer<ffi.Char> c_channel,
|
||||
) {
|
||||
return _shorebird_check_for_downloadable_update(
|
||||
c_channel,
|
||||
);
|
||||
}
|
||||
|
||||
late final _shorebird_check_for_updatePtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Bool Function()>>(
|
||||
'shorebird_check_for_update');
|
||||
late final _shorebird_check_for_update =
|
||||
_shorebird_check_for_updatePtr.asFunction<bool Function()>();
|
||||
late final _shorebird_check_for_downloadable_updatePtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Bool Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'shorebird_check_for_downloadable_update');
|
||||
late final _shorebird_check_for_downloadable_update =
|
||||
_shorebird_check_for_downloadable_updatePtr
|
||||
.asFunction<bool Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
/// Synchronously download an update if one is available.
|
||||
void shorebird_update() {
|
||||
@@ -2477,17 +2487,26 @@ class UpdaterBindings {
|
||||
late final _shorebird_update =
|
||||
_shorebird_updatePtr.asFunction<void Function()>();
|
||||
|
||||
/// Synchronously download an update if one is available.
|
||||
/// Synchronously download an update on the first non-null channel of:
|
||||
/// 1. `c_channel`
|
||||
/// 2. The channel specified in shorebird.yaml
|
||||
/// 3. The default "stable" channel
|
||||
///
|
||||
/// Returns an [UpdateResult] indicating whether the update was successful.
|
||||
ffi.Pointer<UpdateResult> shorebird_update_with_result() {
|
||||
return _shorebird_update_with_result();
|
||||
ffi.Pointer<UpdateResult> shorebird_update_with_result(
|
||||
ffi.Pointer<ffi.Char> c_channel,
|
||||
) {
|
||||
return _shorebird_update_with_result(
|
||||
c_channel,
|
||||
);
|
||||
}
|
||||
|
||||
late final _shorebird_update_with_resultPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<UpdateResult> Function()>>(
|
||||
'shorebird_update_with_result');
|
||||
late final _shorebird_update_with_resultPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<UpdateResult> Function(
|
||||
ffi.Pointer<ffi.Char>)>>('shorebird_update_with_result');
|
||||
late final _shorebird_update_with_result = _shorebird_update_with_resultPtr
|
||||
.asFunction<ffi.Pointer<UpdateResult> Function()>();
|
||||
.asFunction<ffi.Pointer<UpdateResult> Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
/// Start a thread to download an update if one is available.
|
||||
void shorebird_start_update_thread() {
|
||||
@@ -2640,20 +2659,10 @@ final class _opaque_pthread_t extends ffi.Struct {
|
||||
external ffi.Array<ffi.Char> __opaque;
|
||||
}
|
||||
|
||||
enum idtype_t {
|
||||
P_ALL(0),
|
||||
P_PID(1),
|
||||
P_PGID(2);
|
||||
|
||||
final int value;
|
||||
const idtype_t(this.value);
|
||||
|
||||
static idtype_t fromValue(int value) => switch (value) {
|
||||
0 => P_ALL,
|
||||
1 => P_PID,
|
||||
2 => P_PGID,
|
||||
_ => throw ArgumentError("Unknown value for idtype_t: $value"),
|
||||
};
|
||||
abstract class idtype_t {
|
||||
static const int P_ALL = 0;
|
||||
static const int P_PID = 1;
|
||||
static const int P_PGID = 2;
|
||||
}
|
||||
|
||||
final class __darwin_arm_exception_state extends ffi.Struct {
|
||||
@@ -4060,6 +4069,8 @@ const int __MAC_14_5 = 140500;
|
||||
|
||||
const int __MAC_15_0 = 150000;
|
||||
|
||||
const int __MAC_15_1 = 150100;
|
||||
|
||||
const int __IPHONE_2_0 = 20000;
|
||||
|
||||
const int __IPHONE_2_1 = 20100;
|
||||
@@ -4220,6 +4231,8 @@ const int __IPHONE_17_5 = 170500;
|
||||
|
||||
const int __IPHONE_18_0 = 180000;
|
||||
|
||||
const int __IPHONE_18_1 = 180100;
|
||||
|
||||
const int __WATCHOS_1_0 = 10000;
|
||||
|
||||
const int __WATCHOS_2_0 = 20000;
|
||||
@@ -4316,6 +4329,8 @@ const int __WATCHOS_10_5 = 100500;
|
||||
|
||||
const int __WATCHOS_11_0 = 110000;
|
||||
|
||||
const int __WATCHOS_11_1 = 110100;
|
||||
|
||||
const int __TVOS_9_0 = 90000;
|
||||
|
||||
const int __TVOS_9_1 = 90100;
|
||||
@@ -4414,6 +4429,8 @@ const int __TVOS_17_5 = 170500;
|
||||
|
||||
const int __TVOS_18_0 = 180000;
|
||||
|
||||
const int __TVOS_18_1 = 180100;
|
||||
|
||||
const int __BRIDGEOS_2_0 = 20000;
|
||||
|
||||
const int __BRIDGEOS_3_0 = 30000;
|
||||
@@ -4468,6 +4485,8 @@ const int __BRIDGEOS_8_5 = 80500;
|
||||
|
||||
const int __BRIDGEOS_9_0 = 90000;
|
||||
|
||||
const int __BRIDGEOS_9_1 = 90100;
|
||||
|
||||
const int __DRIVERKIT_19_0 = 190000;
|
||||
|
||||
const int __DRIVERKIT_20_0 = 200000;
|
||||
@@ -4496,6 +4515,8 @@ const int __DRIVERKIT_23_5 = 230500;
|
||||
|
||||
const int __DRIVERKIT_24_0 = 240000;
|
||||
|
||||
const int __DRIVERKIT_24_1 = 240100;
|
||||
|
||||
const int __VISIONOS_1_0 = 10000;
|
||||
|
||||
const int __VISIONOS_1_1 = 10100;
|
||||
@@ -4504,6 +4525,8 @@ const int __VISIONOS_1_2 = 10200;
|
||||
|
||||
const int __VISIONOS_2_0 = 20000;
|
||||
|
||||
const int __VISIONOS_2_1 = 20100;
|
||||
|
||||
const int MAC_OS_X_VERSION_10_0 = 1000;
|
||||
|
||||
const int MAC_OS_X_VERSION_10_1 = 1010;
|
||||
@@ -4628,9 +4651,11 @@ const int MAC_OS_VERSION_14_5 = 140500;
|
||||
|
||||
const int MAC_OS_VERSION_15_0 = 150000;
|
||||
|
||||
const int __MAC_OS_X_VERSION_MIN_REQUIRED = 140000;
|
||||
const int MAC_OS_VERSION_15_1 = 150100;
|
||||
|
||||
const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150000;
|
||||
const int __MAC_OS_X_VERSION_MIN_REQUIRED = 150000;
|
||||
|
||||
const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150100;
|
||||
|
||||
const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1;
|
||||
|
||||
|
||||
@@ -121,8 +121,7 @@ abstract class ShorebirdUpdater {
|
||||
/// 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();
|
||||
Future<UpdateStatus> checkForUpdate({UpdateTrack? track});
|
||||
|
||||
/// Updates the app to the latest patch (if available).
|
||||
/// Future will complete once the update is fully downloaded and ready
|
||||
@@ -136,5 +135,17 @@ abstract class ShorebirdUpdater {
|
||||
/// * [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();
|
||||
Future<void> update({UpdateTrack? track});
|
||||
}
|
||||
|
||||
/// The track to check for updates on.
|
||||
enum UpdateTrack {
|
||||
/// The staging track used for internal testing.
|
||||
staging,
|
||||
|
||||
/// The beta track used for public testing.
|
||||
beta,
|
||||
|
||||
/// The stable track used for general availability.
|
||||
stable,
|
||||
}
|
||||
|
||||
@@ -66,12 +66,16 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UpdateStatus> checkForUpdate() async {
|
||||
Future<UpdateStatus> checkForUpdate({UpdateTrack? track}) async {
|
||||
if (!_isAvailable) return UpdateStatus.unavailable;
|
||||
|
||||
final isUpdateAvailable = await _run(_updater.checkForUpdate);
|
||||
// First, check to see whether an update is available for download.
|
||||
final isUpdateAvailable =
|
||||
await _run(() => _updater.checkForDownloadableUpdate(track: track));
|
||||
if (isUpdateAvailable) return UpdateStatus.outdated;
|
||||
|
||||
// If no new update is available for download, see if a new patch exists
|
||||
// on disk that requires a restart.
|
||||
final (current, next) = await (readCurrentPatch(), readNextPatch()).wait;
|
||||
return next != null && current?.number != next.number
|
||||
? UpdateStatus.restartRequired
|
||||
@@ -79,13 +83,13 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update() async {
|
||||
Future<void> update({UpdateTrack? track}) async {
|
||||
if (!_isAvailable) return;
|
||||
|
||||
Pointer<UpdateResult> result = nullptr;
|
||||
|
||||
try {
|
||||
result = await _run(_updater.update);
|
||||
result = await _run(() => _updater.update(track: track));
|
||||
} catch (_) {
|
||||
return _legacyFallback();
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
||||
Future<Patch?> readNextPatch() async => null;
|
||||
|
||||
@override
|
||||
Future<UpdateStatus> checkForUpdate() async => UpdateStatus.unavailable;
|
||||
Future<UpdateStatus> checkForUpdate({UpdateTrack? track}) async =>
|
||||
UpdateStatus.unavailable;
|
||||
|
||||
@override
|
||||
Future<void> update() async {}
|
||||
Future<void> update({UpdateTrack? track}) async {}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:ffi' as ffi;
|
||||
import 'dart:ffi';
|
||||
|
||||
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';
|
||||
|
||||
/// {@template updater}
|
||||
/// A wrapper around the generated [UpdaterBindings] that, when necessary,
|
||||
@@ -20,9 +22,6 @@ class Updater {
|
||||
/// The currently active patch number.
|
||||
int currentPatchNumber() => bindings.shorebird_current_boot_patch_number();
|
||||
|
||||
/// Whether a new patch is available.
|
||||
bool checkForUpdate() => bindings.shorebird_check_for_update();
|
||||
|
||||
/// The next patch number that will be loaded. Will be the same as
|
||||
/// currentPatchNumber if no new patch is available.
|
||||
int nextPatchNumber() => bindings.shorebird_next_boot_patch_number();
|
||||
@@ -30,9 +29,20 @@ class Updater {
|
||||
/// Downloads the latest patch, if available.
|
||||
void downloadUpdate() => bindings.shorebird_update();
|
||||
|
||||
// New Methods added to support v2.0.0 of the Dart APIs //
|
||||
|
||||
/// Whether a new patch is available for download.
|
||||
bool checkForDownloadableUpdate({UpdateTrack? track}) =>
|
||||
bindings.shorebird_check_for_downloadable_update(
|
||||
track == null ? ffi.nullptr : track.name.toNativeUtf8().cast<Char>(),
|
||||
);
|
||||
|
||||
/// 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();
|
||||
Pointer<UpdateResult> update({UpdateTrack? track}) =>
|
||||
bindings.shorebird_update_with_result(
|
||||
track == null ? ffi.nullptr : track.name.toNativeUtf8().cast<Char>(),
|
||||
);
|
||||
|
||||
/// Frees an update result allocated by the updater.
|
||||
void freeUpdateResult(Pointer<UpdateResult> ptr) =>
|
||||
|
||||
@@ -242,7 +242,7 @@ void main() {
|
||||
group('when updater has an update available', () {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
when(updater.checkForUpdate).thenReturn(true);
|
||||
when(updater.checkForDownloadableUpdate).thenReturn(true);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
@@ -258,7 +258,7 @@ void main() {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
when(updater.nextPatchNumber).thenReturn(1);
|
||||
when(updater.checkForUpdate).thenReturn(false);
|
||||
when(updater.checkForDownloadableUpdate).thenReturn(false);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
@@ -274,7 +274,7 @@ void main() {
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(1);
|
||||
when(updater.nextPatchNumber).thenReturn(1);
|
||||
when(updater.checkForUpdate).thenReturn(false);
|
||||
when(updater.checkForDownloadableUpdate).thenReturn(false);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
@@ -285,6 +285,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when a track is provided', () {
|
||||
const track = UpdateTrack.beta;
|
||||
|
||||
setUp(() {
|
||||
when(updater.currentPatchNumber).thenReturn(0);
|
||||
when(
|
||||
() => updater.checkForDownloadableUpdate(track: track),
|
||||
).thenReturn(true);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('forwards the provided track to the underlying updater call',
|
||||
() async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.checkForUpdate(track: track),
|
||||
completion(equals(UpdateStatus.outdated)),
|
||||
);
|
||||
verify(() => updater.checkForDownloadableUpdate(track: track))
|
||||
.called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('update', () {
|
||||
@@ -565,6 +587,29 @@ Please upgrade the Shorebird Engine for improved error messages.''',
|
||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when a track is provided', () {
|
||||
const track = UpdateTrack.beta;
|
||||
|
||||
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(track: track)).thenReturn(result);
|
||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||
});
|
||||
|
||||
test('forwards the provided track to the underlying updater call',
|
||||
() async {
|
||||
await expectLater(
|
||||
shorebirdUpdater.update(track: track),
|
||||
completes,
|
||||
);
|
||||
verify(() => updater.update(track: track)).called(1);
|
||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:ffi';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shorebird_code_push/shorebird_code_push.dart';
|
||||
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';
|
||||
import 'package:shorebird_code_push/src/updater.dart';
|
||||
import 'package:test/test.dart';
|
||||
@@ -38,17 +39,55 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('checkForUpdate', () {
|
||||
group('checkForDownloadableUpdate', () {
|
||||
test('forwards the result of shorebird_check_for_update', () {
|
||||
when(
|
||||
() => updaterBindings.shorebird_check_for_update(),
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
nullptr,
|
||||
),
|
||||
).thenReturn(true);
|
||||
expect(updater.checkForUpdate(), isTrue);
|
||||
expect(updater.checkForDownloadableUpdate(), isTrue);
|
||||
|
||||
when(
|
||||
() => updaterBindings.shorebird_check_for_update(),
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
nullptr,
|
||||
),
|
||||
).thenReturn(false);
|
||||
expect(updater.checkForUpdate(), isFalse);
|
||||
expect(updater.checkForDownloadableUpdate(), isFalse);
|
||||
});
|
||||
|
||||
group('when a track is provided', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
any(),
|
||||
),
|
||||
).thenReturn(true);
|
||||
});
|
||||
|
||||
test('forwards the result of shorebird_check_for_update', () {
|
||||
expect(
|
||||
updater.checkForDownloadableUpdate(track: UpdateTrack.beta),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
expect(
|
||||
updater.checkForDownloadableUpdate(track: UpdateTrack.stable),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
final captured = verify(
|
||||
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||
captureAny(),
|
||||
),
|
||||
).captured;
|
||||
expect(
|
||||
captured.map(
|
||||
(cstr) => (cstr as Pointer<Char>).cast<Utf8>().toDartString(),
|
||||
),
|
||||
equals(['beta', 'stable']),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,10 +112,12 @@ void main() {
|
||||
group('update', () {
|
||||
test('calls bindings.shorebird_update_with_result', () {
|
||||
when(
|
||||
() => updaterBindings.shorebird_update_with_result(),
|
||||
() => updaterBindings.shorebird_update_with_result(nullptr),
|
||||
).thenReturn(nullptr);
|
||||
updater.update();
|
||||
verify(() => updaterBindings.shorebird_update_with_result()).called(1);
|
||||
verify(
|
||||
() => updaterBindings.shorebird_update_with_result(nullptr),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user