From 96ef39ab1fb9bc5f5ce0bb79a159c3b05de64b70 Mon Sep 17 00:00:00 2001 From: foxyz Date: Wed, 7 Oct 2026 22:02:15 +0300 Subject: [PATCH] probe: updater tests on windows --- .gitea/workflows/probe.yml | 10 +- apps/desktop/lib/src/update/updater.dart | 171 ++++++++---- apps/desktop/test/updater_test.dart | 337 +++++++++++++++-------- 3 files changed, 334 insertions(+), 184 deletions(-) diff --git a/.gitea/workflows/probe.yml b/.gitea/workflows/probe.yml index 293ef6b..9ffbbee 100644 --- a/.gitea/workflows/probe.yml +++ b/.gitea/workflows/probe.yml @@ -26,11 +26,7 @@ jobs: Add-Content $env:GITHUB_PATH "$F\bin" & "$F\bin\flutter.bat" config --no-analytics --enable-windows-desktop | Out-Null & "$F\bin\flutter.bat" doctor -v - - name: Build zip + - name: Updater tests shell: powershell - run: powershell -NoProfile -ExecutionPolicy Bypass -File tool/package_windows.ps1 - - name: Inspect - shell: powershell - run: | - Get-ChildItem dist - tar.exe -tf (Get-ChildItem dist\*.zip)[0].FullName | Select-Object -First 40 + working-directory: apps/desktop + run: flutter test test/updater_test.dart -r expanded diff --git a/apps/desktop/lib/src/update/updater.dart b/apps/desktop/lib/src/update/updater.dart index 397f40c..8007b6f 100644 --- a/apps/desktop/lib/src/update/updater.dart +++ b/apps/desktop/lib/src/update/updater.dart @@ -25,12 +25,17 @@ final _log = createLogger('update'); const updateManifestUrl = 'https://git.senkochan.moe/Foxyz/sync_replay_project/raw/branch/main/updates/manifest.json'; -const appVersion = String.fromEnvironment('APP_VERSION', - defaultValue: '0.0.0-dev'); +const appVersion = String.fromEnvironment( + 'APP_VERSION', + defaultValue: '0.0.0-dev', +); // Process args as main() received them — restart relaunches with these. List launchArgs = const []; +// Key into the manifest's `assets` map, e.g. linux-x64, windows-x64. +final updatePlatform = '${Platform.operatingSystem}-x64'; + const _stagingDir = '.update-staging'; const _backupDir = '.update-backup'; const _downloadFile = '.update-download'; @@ -41,31 +46,45 @@ class UpdateManifest { final String url; final String? sha256; final String notes; - const UpdateManifest( - {required this.version, - required this.protocol, - required this.url, - this.sha256, - required this.notes}); + const UpdateManifest({ + required this.version, + required this.protocol, + required this.url, + this.sha256, + required this.notes, + }); - factory UpdateManifest.fromJson(Map json) { + // `assets` maps platform → archive URL. Top-level `url` is the Linux + // tarball, kept for builds from before `assets` existed (<= 0.2.1). + factory UpdateManifest.fromJson( + Map json, { + String? platform, + }) { final version = json['version']; final protocol = json['protocol']; - final url = json['url']; + final assets = json['assets']; + final url = assets is Map + ? assets[platform ?? updatePlatform] + : json['url']; if (version is! String || version.isEmpty || protocol is! int || url is! String || url.isEmpty) { - throw const FormatException('update manifest missing version/protocol/url'); + throw FormatException( + assets is Map && version is String + ? 'release $version has no ${platform ?? updatePlatform} build' + : 'update manifest missing version/protocol/url', + ); } final sha = json['sha256']; return UpdateManifest( - version: version, - protocol: protocol, - url: url, - sha256: sha is String && sha.isNotEmpty ? sha : null, - notes: (json['notes'] as String?) ?? ''); + version: version, + protocol: protocol, + url: url, + sha256: sha is String && sha.isNotEmpty ? sha : null, + notes: (json['notes'] as String?) ?? '', + ); } // Newer wire protocol: this build can't join servers running it. @@ -117,10 +136,10 @@ int compareVersions(String a, String b) { final c = na != null && nb != null ? na.compareTo(nb) : na != null - ? -1 - : nb != null - ? 1 - : ia[i].compareTo(ib[i]); + ? -1 + : nb != null + ? 1 + : ia[i].compareTo(ib[i]); if (c != 0) return c; } return ia.length.compareTo(ib.length); @@ -129,8 +148,10 @@ int compareVersions(String a, String b) { bool isNewerVersion(String current, String latest) => compareVersions(latest, current) > 0; -Future fetchManifest( - {HttpClient? http, String url = updateManifestUrl}) async { +Future fetchManifest({ + HttpClient? http, + String url = updateManifestUrl, +}) async { final client = http ?? HttpClient(); try { final req = await client @@ -153,8 +174,12 @@ Future fetchManifest( typedef DownloadProgress = void Function(int received, int? total); -Future downloadFile(String url, File dest, - {HttpClient? http, DownloadProgress? onProgress}) async { +Future downloadFile( + String url, + File dest, { + HttpClient? http, + DownloadProgress? onProgress, +}) async { final client = http ?? HttpClient(); try { final req = await client @@ -361,8 +386,13 @@ typedef ApplyHook = Future Function(String step); // Unpack [archive] into a staging dir inside [root], then swap it in. // On failure the install is left exactly as it was. -Future applyUpdate(File archive, Directory root, - {String? executableName, bool? windows, ApplyHook? hook}) async { +Future applyUpdate( + File archive, + Directory root, { + String? executableName, + bool? windows, + ApplyHook? hook, +}) async { final win = windows ?? Platform.isWindows; final stage = Directory('${root.path}/$_stagingDir'); if (await stage.exists()) await stage.delete(recursive: true); @@ -373,7 +403,8 @@ Future applyUpdate(File archive, Directory root, (executableName != null && !await File('${stage.path}/$executableName').exists())) { throw const FormatException( - 'archive is not a release bundle (missing data/, lib/ or binary)'); + 'archive is not a release bundle (missing data/, lib/ or binary)', + ); } if (win) { await _swapWindows(stage, root, hook); @@ -392,13 +423,17 @@ class _Moved { _Moved(this.name, this.hadOld); } -Future _swapPosix(Directory stage, Directory root, ApplyHook? hook) async { +Future _swapPosix( + Directory stage, + Directory root, + ApplyHook? hook, +) async { final backup = Directory('${root.path}/$_backupDir'); if (await backup.exists()) await backup.delete(recursive: true); await backup.create(); final names = [ for (final e in await stage.list(followLinks: false).toList()) - e.path.substring(stage.path.length + 1) + e.path.substring(stage.path.length + 1), ]..sort(); final done = <_Moved>[]; try { @@ -423,8 +458,10 @@ Future _swapPosix(Directory stage, Directory root, ApplyHook? hook) async if (m.hadOld) await _rename('${backup.path}/${m.name}', cur); } catch (re) { // Leave the backup dir in place so nothing is lost. - _log.severe('rollback of ${m.name} failed: $re ' - '(old files kept in ${backup.path})'); + _log.severe( + 'rollback of ${m.name} failed: $re ' + '(old files kept in ${backup.path})', + ); rethrow; } } @@ -437,10 +474,14 @@ Future _swapPosix(Directory stage, Directory root, ApplyHook? hook) async // UNTESTED on Windows. File-level: a running exe/dll can be renamed but // not deleted or overwritten, so every existing file goes to `.old` // first; leftovers are swept by cleanupUpdateLeftovers on next start. -Future _swapWindows(Directory stage, Directory root, ApplyHook? hook) async { +Future _swapWindows( + Directory stage, + Directory root, + ApplyHook? hook, +) async { final rels = [ await for (final e in stage.list(recursive: true, followLinks: false)) - if (e is File) e.path.substring(stage.path.length + 1) + if (e is File) e.path.substring(stage.path.length + 1), ]..sort(); final done = <(String rel, String? old, bool movedIn)>[]; try { @@ -486,11 +527,14 @@ Future _swapWindows(Directory stage, Directory root, ApplyHook? hook) asyn // Shared by the CLI and the in-app banner: download next to the install // (same filesystem), verify, apply, clean up. -Future downloadAndApply(UpdateManifest manifest, Directory root, - {HttpClient? http, - DownloadProgress? onProgress, - String? executableName, - void Function(String line)? say}) async { +Future downloadAndApply( + UpdateManifest manifest, + Directory root, { + HttpClient? http, + DownloadProgress? onProgress, + String? executableName, + void Function(String line)? say, +}) async { final file = File('${root.path}/$_downloadFile'); try { say?.call('downloading ${manifest.url} ...'); @@ -498,8 +542,10 @@ Future downloadAndApply(UpdateManifest manifest, Directory root, if (manifest.sha256 != null) { await verifySha256(file, manifest.sha256!); } else { - say?.call('no checksum in manifest — skipping verification ' - '(HTTPS transit + gzip CRC still apply).'); + say?.call( + 'no checksum in manifest — skipping verification ' + '(HTTPS transit + gzip CRC still apply).', + ); } say?.call('verified, applying over ${root.path} ...'); await applyUpdate(file, root, executableName: executableName); @@ -512,9 +558,12 @@ Future downloadAndApply(UpdateManifest manifest, Directory root, // exit this process. Future restartApp(String executable, List args) async { final keep = args.where((a) => a != '--upgrade').toList(); - await Process.start(executable, keep, - mode: ProcessStartMode.detached, - workingDirectory: Directory.current.path); + await Process.start( + executable, + keep, + mode: ProcessStartMode.detached, + workingDirectory: Directory.current.path, + ); exit(0); } @@ -524,13 +573,16 @@ String requiredUpdateNote(UpdateManifest m) => // Returns a process exit code: 0 ok / already current, 1 failure, // 2 not a packaged install (update manually). -Future runUpgrade( - {String currentVersion = appVersion, - String manifestUrl = updateManifestUrl, - HttpClient? http, - String? executablePath, - Directory? installRoot}) async { - stdout.writeln('watch-together $currentVersion (proto ${RelayConstants.protoVersion})'); +Future runUpgrade({ + String currentVersion = appVersion, + String manifestUrl = updateManifestUrl, + HttpClient? http, + String? executablePath, + Directory? installRoot, +}) async { + stdout.writeln( + 'watch-together $currentVersion (proto ${RelayConstants.protoVersion})', + ); late final UpdateManifest manifest; try { manifest = await fetchManifest(http: http, url: manifestUrl); @@ -542,7 +594,9 @@ Future runUpgrade( stdout.writeln('already up to date (${manifest.version}).'); return 0; } - stdout.writeln('update available: ${manifest.version} (proto ${manifest.protocol})'); + stdout.writeln( + 'update available: ${manifest.version} (proto ${manifest.protocol})', + ); if (manifest.notes.isNotEmpty) { stdout.writeln(manifest.notes); } @@ -552,15 +606,20 @@ Future runUpgrade( final exe = executablePath ?? currentExecutablePath(); final root = installRoot ?? findInstallRoot(exe); if (root == null) { - stdout.writeln('not a packaged install — download the tarball and update manually.'); + stdout.writeln( + 'not a packaged install — download the tarball and update manually.', + ); return 2; } try { await cleanupUpdateLeftovers(root); - await downloadAndApply(manifest, root, - http: http, - executableName: installRoot == null ? _basename(exe) : null, - say: stdout.writeln); + await downloadAndApply( + manifest, + root, + http: http, + executableName: installRoot == null ? _basename(exe) : null, + say: stdout.writeln, + ); } catch (e) { stdout.writeln('update failed: $e'); return 1; diff --git a/apps/desktop/test/updater_test.dart b/apps/desktop/test/updater_test.dart index 30a8514..1021050 100644 --- a/apps/desktop/test/updater_test.dart +++ b/apps/desktop/test/updater_test.dart @@ -25,9 +25,15 @@ bool _isExec(String path) => File(path).statSync().mode & 0x40 != 0; // Builds the release archive exactly like tool/package_linux.sh: // bundle copied into /watch-together-/, then // `tar -czf out -C watch-together-`. -Future> _releaseTarball(Directory work, - {String version = '0.2.0', bool windows = false, bool bundle = true}) async { - final stage = Directory('${work.path}/pkg-$version-${windows ? 'win' : 'lin'}'); +Future> _releaseTarball( + Directory work, { + String version = '0.2.0', + bool windows = false, + bool bundle = true, +}) async { + final stage = Directory( + '${work.path}/pkg-$version-${windows ? 'win' : 'lin'}', + ); final folder = Directory('${stage.path}/watch-together-$version'); await Directory('${folder.path}/data/flutter_assets').create(recursive: true); await File('${folder.path}/data/flutter_assets/fresh.cfg') @@ -44,8 +50,11 @@ Future> _releaseTarball(Directory work, } await File('${folder.path}/update.sh') .writeAsString('#!/bin/sh\nexec ./watch_together --upgrade\n'); - final r = await Process.run('chmod', - ['755', '${folder.path}/watch_together', '${folder.path}/update.sh']); + final r = await Process.run('chmod', [ + '755', + '${folder.path}/watch_together', + '${folder.path}/update.sh', + ]); expect(r.exitCode, 0); } final out = File('${work.path}/out-$version.tar.gz'); @@ -99,14 +108,16 @@ class _Server { http = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); http.listen((req) async { if (req.uri.path == '/manifest.json' && !brokenManifest) { - req.response.write(jsonEncode({ - 'version': '0.2.0', - 'protocol': RelayConstants.protoVersion, - 'url': '$base/watch-together-0.2.0-linux-x64.tar.gz', - 'sha256': sha256.convert(tarball).toString(), - 'notes': 'test build', - ...manifestExtra, - })); + req.response.write( + jsonEncode({ + 'version': '0.2.0', + 'protocol': RelayConstants.protoVersion, + 'url': '$base/watch-together-0.2.0-linux-x64.tar.gz', + 'sha256': sha256.convert(tarball).toString(), + 'notes': 'test build', + ...manifestExtra, + }), + ); } else if (req.uri.path == '/watch-together-0.2.0-linux-x64.tar.gz') { tarHits++; req.response.contentLength = tarball.length; @@ -145,11 +156,9 @@ void main() { test('manifest rejects missing fields, sha256 is optional', () { expect(() => UpdateManifest.fromJson({}), throwsFormatException); expect( - () => UpdateManifest.fromJson({ - 'version': '0.2.0', - 'protocol': 4, - }), - throwsFormatException); + () => UpdateManifest.fromJson({'version': '0.2.0', 'protocol': 4}), + throwsFormatException, + ); final noSha = UpdateManifest.fromJson({ 'version': '0.2.0', 'protocol': RelayConstants.protoVersion, @@ -158,24 +167,56 @@ void main() { expect(noSha.sha256, isNull); expect(noSha.isRequired, isFalse); expect( - UpdateManifest.fromJson({ - 'version': '0.2.0', - 'protocol': RelayConstants.protoVersion + 1, - 'url': 'https://x/y.tar.gz', - }).isRequired, - isTrue); + UpdateManifest.fromJson({ + 'version': '0.2.0', + 'protocol': RelayConstants.protoVersion + 1, + 'url': 'https://x/y.tar.gz', + }).isRequired, + isTrue, + ); + }); + + test('manifest picks the asset for this platform', () { + final json = { + 'version': '0.2.2', + 'protocol': RelayConstants.protoVersion, + 'url': 'https://x/linux.tar.gz', + 'assets': { + 'linux-x64': 'https://x/linux.tar.gz', + 'windows-x64': 'https://x/windows.zip', + }, + }; + expect( + UpdateManifest.fromJson(json, platform: 'windows-x64').url, + 'https://x/windows.zip', + ); + expect( + UpdateManifest.fromJson(json, platform: 'linux-x64').url, + 'https://x/linux.tar.gz', + ); + expect( + () => UpdateManifest.fromJson(json, platform: 'macos-x64'), + throwsFormatException, + ); }); test('findInstallRoot honors bundle layout', () async { final work = await _work(); try { final root = await _fakeRoot(work); - expect(findInstallRoot('${root.path}/watch_together', windows: false)?.path, - root.path); - expect(findInstallRoot('${work.path}/stray-binary', windows: false), isNull); + expect( + findInstallRoot('${root.path}/watch_together', windows: false)?.path, + root.path, + ); + expect( + findInstallRoot('${work.path}/stray-binary', windows: false), + isNull, + ); final win = await _fakeRoot(await work.createTemp('w'), windows: true); - expect(findInstallRoot('${win.path}/watch_together.exe', windows: true)?.path, - win.path); + expect( + findInstallRoot('${win.path}/watch_together.exe', windows: true)?.path, + win.path, + ); } finally { await work.delete(recursive: true); } @@ -196,19 +237,21 @@ void main() { if (await work.exists()) await work.delete(recursive: true); }); - test('real release layout lands at the install root, modes kept', - () async { + test('real release layout lands at the install root, modes kept', () async { final root = await _fakeRoot(work); final rc = await runUpgrade( - currentVersion: '0.1.0', - manifestUrl: server.manifestUrl, - installRoot: root); + currentVersion: '0.1.0', + manifestUrl: server.manifestUrl, + installRoot: root, + ); expect(rc, 0); expect(server.tarHits, 1); final p = root.path; expect(await File('$p/watch_together').readAsString(), 'binary-v2'); - expect(await File('$p/data/flutter_assets/fresh.cfg').readAsString(), - 'fresh-config'); + expect( + await File('$p/data/flutter_assets/fresh.cfg').readAsString(), + 'fresh-config', + ); expect(Directory('$p/watch-together-0.2.0').existsSync(), isFalse); expect(_isExec('$p/watch_together'), isTrue); expect(_isExec('$p/update.sh'), isTrue); @@ -224,25 +267,31 @@ void main() { test('up to date downloads nothing', () async { final root = await _fakeRoot(work); final rc = await runUpgrade( - currentVersion: '0.2.0', - manifestUrl: server.manifestUrl, - installRoot: root); + currentVersion: '0.2.0', + manifestUrl: server.manifestUrl, + installRoot: root, + ); expect(rc, 0); expect(server.tarHits, 0); - expect(await File('${root.path}/watch_together').readAsString(), - 'binary-v1'); + expect( + await File('${root.path}/watch_together').readAsString(), + 'binary-v1', + ); }); test('bad hash aborts, install untouched', () async { server.manifestExtra = {'sha256': '0' * 64}; final root = await _fakeRoot(work); final rc = await runUpgrade( - currentVersion: '0.1.0', - manifestUrl: server.manifestUrl, - installRoot: root); + currentVersion: '0.1.0', + manifestUrl: server.manifestUrl, + installRoot: root, + ); expect(rc, 1); - expect(await File('${root.path}/watch_together').readAsString(), - 'binary-v1'); + expect( + await File('${root.path}/watch_together').readAsString(), + 'binary-v1', + ); expect(_leftovers(root), isEmpty); }); @@ -250,36 +299,49 @@ void main() { server.manifestExtra = {'sha256': null}; final root = await _fakeRoot(work); final rc = await runUpgrade( - currentVersion: '0.1.0', - manifestUrl: server.manifestUrl, - installRoot: root); + currentVersion: '0.1.0', + manifestUrl: server.manifestUrl, + installRoot: root, + ); expect(rc, 0); - expect(await File('${root.path}/watch_together').readAsString(), - 'binary-v2'); + expect( + await File('${root.path}/watch_together').readAsString(), + 'binary-v2', + ); }); test('unknown layout asks for manual update', () async { final rc = await runUpgrade( - currentVersion: '0.1.0', - manifestUrl: server.manifestUrl, - executablePath: '${work.path}/stray-binary'); + currentVersion: '0.1.0', + manifestUrl: server.manifestUrl, + executablePath: '${work.path}/stray-binary', + ); expect(rc, 2); }); - test('archive that is not a bundle is refused, install untouched', - () async { - server.tarball = await _releaseTarball(work, version: '9.9.9', bundle: false); - final root = await _fakeRoot(work); - final rc = await runUpgrade( + test( + 'archive that is not a bundle is refused, install untouched', + () async { + server.tarball = await _releaseTarball( + work, + version: '9.9.9', + bundle: false, + ); + final root = await _fakeRoot(work); + final rc = await runUpgrade( currentVersion: '0.1.0', manifestUrl: server.manifestUrl, - installRoot: root); - expect(rc, 1); - expect(await File('${root.path}/watch_together').readAsString(), - 'binary-v1'); - expect(File('${root.path}/lib/libgone.so').existsSync(), isTrue); - expect(_leftovers(root), isEmpty); - }); + installRoot: root, + ); + expect(rc, 1); + expect( + await File('${root.path}/watch_together').readAsString(), + 'binary-v1', + ); + expect(File('${root.path}/lib/libgone.so').existsSync(), isTrue); + expect(_leftovers(root), isEmpty); + }, + ); }); group('apply', () { @@ -318,10 +380,16 @@ void main() { test('failure at $step rolls back', () async { final root = await _fakeRoot(work); await expectLater( - applyUpdate(await archive(), root, windows: false, hook: (s) async { + applyUpdate( + await archive(), + root, + windows: false, + hook: (s) async { if (s == step) throw FileSystemException('injected', s); - }), - throwsA(isA())); + }, + ), + throwsA(isA()), + ); await expectUntouched(root); }); } @@ -329,13 +397,20 @@ void main() { test('stages next to the install, never in the system temp', () async { final root = await _fakeRoot(work); final seen = []; - await applyUpdate(await archive(), root, windows: false, hook: (s) async { - if (seen.isEmpty) { - seen.add(Directory('${root.path}/.update-staging').existsSync() - ? 'staged-in-root' - : 'elsewhere'); - } - }); + await applyUpdate( + await archive(), + root, + windows: false, + hook: (s) async { + if (seen.isEmpty) { + seen.add( + Directory('${root.path}/.update-staging').existsSync() + ? 'staged-in-root' + : 'elsewhere', + ); + } + }, + ); expect(seen, ['staged-in-root']); }); @@ -346,12 +421,16 @@ void main() { final f = File('${work.path}/evil.tar.gz') ..writeAsBytesSync(GZipEncoder().encode(TarEncoder().encode(evil))); await expectLater( - applyUpdate(f, root, windows: false), throwsFormatException); + applyUpdate(f, root, windows: false), + throwsFormatException, + ); final link = Archive() ..addFile(ArchiveFile.symlink('w/lib/x.so', '../../../etc/passwd')); f.writeAsBytesSync(GZipEncoder().encode(TarEncoder().encode(link))); await expectLater( - applyUpdate(f, root, windows: false), throwsFormatException); + applyUpdate(f, root, windows: false), + throwsFormatException, + ); expect(File('${work.path}/escape').existsSync(), isFalse); await expectUntouched(root); }); @@ -375,8 +454,10 @@ void main() { final p = root.path; expect(await File('$p/watch_together.exe').readAsString(), 'binary-v2'); expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v2'); - expect(await File('$p/data/flutter_assets/fresh.cfg').readAsString(), - 'fresh-config'); + expect( + await File('$p/data/flutter_assets/fresh.cfg').readAsString(), + 'fresh-config', + ); // Not in use here, so the .old copies were deleted right away. expect(File('$p/watch_together.exe.old').existsSync(), isFalse); expect(_leftovers(root), isEmpty); @@ -385,13 +466,18 @@ void main() { test('windows-style swap rolls back', () async { final root = await _fakeRoot(work, windows: true); await expectLater( - applyUpdate(await archive(windows: true), root, windows: true, - hook: (s) async { + applyUpdate( + await archive(windows: true), + root, + windows: true, + hook: (s) async { if (s == 'in:watch_together.exe') { throw const FileSystemException('injected'); } - }), - throwsA(isA())); + }, + ), + throwsA(isA()), + ); final p = root.path; expect(await File('$p/watch_together.exe').readAsString(), 'binary-v1'); expect(await File('$p/flutter_windows.dll').readAsString(), 'dll-v1'); @@ -416,16 +502,16 @@ void main() { if (await work.exists()) await work.delete(recursive: true); }); - test('check, download with progress, apply, restart same binary', - () async { + test('check, download with progress, apply, restart same binary', () async { final root = await _fakeRoot(work); final restarts = <(String, List)>[]; launchArgs = const ['--some-flag']; final c = UpdateController( - currentVersion: '0.1.0', - manifestUrl: server.manifestUrl, - executablePath: '${root.path}/watch_together', - restart: (exe, args) async => restarts.add((exe, args))); + currentVersion: '0.1.0', + manifestUrl: server.manifestUrl, + executablePath: '${root.path}/watch_together', + restart: (exe, args) async => restarts.add((exe, args)), + ); await c.checkOnce(); expect(c.status.value.phase, UpdatePhase.available); expect(c.status.value.canInstall, isTrue); @@ -438,21 +524,23 @@ void main() { await c.install(); expect(progress.last, 1.0); expect(c.status.value.phase, UpdatePhase.restarting); - expect(await File('${root.path}/watch_together').readAsString(), - 'binary-v2'); + expect( + await File('${root.path}/watch_together').readAsString(), + 'binary-v2', + ); expect(_isExec('${root.path}/watch_together'), isTrue); expect(restarts.single.$1, '${root.path}/watch_together'); expect(restarts.single.$2, ['--some-flag']); }); - test('newer protocol is a required update, cannot be dismissed', - () async { + test('newer protocol is a required update, cannot be dismissed', () async { server.manifestExtra = {'protocol': RelayConstants.protoVersion + 1}; final c = UpdateController( - currentVersion: '0.1.0', - manifestUrl: server.manifestUrl, - executablePath: '${work.path}/stray-binary', - restart: (_, _) async {}); + currentVersion: '0.1.0', + manifestUrl: server.manifestUrl, + executablePath: '${work.path}/stray-binary', + restart: (_, _) async {}, + ); await c.checkOnce(); expect(c.status.value.manifest!.isRequired, isTrue); expect(c.status.value.canInstall, isFalse); @@ -463,39 +551,46 @@ void main() { test('failed check stays quiet; dev builds skip the check', () async { server.brokenManifest = true; final c = UpdateController( - currentVersion: '0.1.0', - manifestUrl: server.manifestUrl, - executablePath: '${work.path}/stray-binary', - restart: (_, _) async {}); + currentVersion: '0.1.0', + manifestUrl: server.manifestUrl, + executablePath: '${work.path}/stray-binary', + restart: (_, _) async {}, + ); await c.checkOnce(); expect(c.status.value.phase, UpdatePhase.idle); server.brokenManifest = false; final dev = UpdateController( - currentVersion: '0.0.0-dev', - manifestUrl: server.manifestUrl, - executablePath: '${work.path}/stray-binary', - restart: (_, _) async {}); + currentVersion: '0.0.0-dev', + manifestUrl: server.manifestUrl, + executablePath: '${work.path}/stray-binary', + restart: (_, _) async {}, + ); await dev.checkOnce(); expect(dev.status.value.phase, UpdatePhase.idle); }); - test('failed install shows error, install untouched, retry works', - () async { - server.manifestExtra = {'sha256': '0' * 64}; - final root = await _fakeRoot(work); - var restarted = false; - final c = UpdateController( + test( + 'failed install shows error, install untouched, retry works', + () async { + server.manifestExtra = {'sha256': '0' * 64}; + final root = await _fakeRoot(work); + var restarted = false; + final c = UpdateController( currentVersion: '0.1.0', manifestUrl: server.manifestUrl, executablePath: '${root.path}/watch_together', - restart: (_, _) async => restarted = true); - await c.checkOnce(); - await c.install(); - expect(c.status.value.phase, UpdatePhase.failed); - expect(c.status.value.error, contains('sha256')); - expect(restarted, isFalse); - expect(await File('${root.path}/watch_together').readAsString(), - 'binary-v1'); - }); + restart: (_, _) async => restarted = true, + ); + await c.checkOnce(); + await c.install(); + expect(c.status.value.phase, UpdatePhase.failed); + expect(c.status.value.error, contains('sha256')); + expect(restarted, isFalse); + expect( + await File('${root.path}/watch_together').readAsString(), + 'binary-v1', + ); + }, + ); }); }