diff --git a/apps/desktop/test/updater_test.dart b/apps/desktop/test/updater_test.dart index 1021050..d81fa46 100644 --- a/apps/desktop/test/updater_test.dart +++ b/apps/desktop/test/updater_test.dart @@ -20,16 +20,21 @@ Future _work() async { return base.createTemp('wt-up-test'); } +// Linux-layout tests check exec bits and symlinks, which NTFS lacks. +final _posixOnly = Platform.isWindows ? 'POSIX install layout' : null; + 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-`. +// `tar -czf out -C watch-together-`. [zip]: like +// tool/package_windows.ps1 (`tar -a -c -f out.zip`, bsdtar — Windows only). Future> _releaseTarball( Directory work, { String version = '0.2.0', bool windows = false, bool bundle = true, + bool zip = false, }) async { final stage = Directory( '${work.path}/pkg-$version-${windows ? 'win' : 'lin'}', @@ -50,16 +55,18 @@ Future> _releaseTarball( } 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', - ]); - expect(r.exitCode, 0); + if (!Platform.isWindows) { + 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'); + final out = File('${work.path}/out-$version.${zip ? 'zip' : 'tar.gz'}'); final r = await Process.run('tar', [ - '-czf', + ...zip ? ['-a', '-c', '-f'] : ['-czf'], out.path, '-C', stage.path, @@ -80,7 +87,9 @@ Future _fakeRoot(Directory work, {bool windows = false}) async { await Directory('${root.path}/lib').create(); await File('${root.path}/lib/libgone.so').writeAsString('lib-v1'); await File('${root.path}/watch_together').writeAsString('binary-v1'); - await Process.run('chmod', ['755', '${root.path}/watch_together']); + if (!Platform.isWindows) { + await Process.run('chmod', ['755', '${root.path}/watch_together']); + } } await File('${root.path}/user-notes.txt').writeAsString('mine'); return root; @@ -262,7 +271,7 @@ void main() { expect(File('$p/data/old.cfg').existsSync(), isFalse); expect(await File('$p/user-notes.txt').readAsString(), 'mine'); expect(_leftovers(root), isEmpty); - }); + }, skip: _posixOnly); test('up to date downloads nothing', () async { final root = await _fakeRoot(work); @@ -391,7 +400,7 @@ void main() { throwsA(isA()), ); await expectUntouched(root); - }); + }, skip: _posixOnly); } test('stages next to the install, never in the system temp', () async { @@ -485,6 +494,42 @@ void main() { expect(File('$p/flutter_windows.dll.old').existsSync(), isFalse); expect(_leftovers(root), isEmpty); }); + + // The real thing: the release zip (bsdtar, like package_windows.ps1) + // applied over an install whose exe is running. Windows refuses to + // delete a running image but allows renaming it, so it is parked as + // .old and swept on the next start. + test('windows: zip over a running exe', () async { + final root = await _fakeRoot(work, windows: true); + final exe = '${root.path}/watch_together.exe'; + await File(r'C:\Windows\System32\PING.EXE').copy(exe); + final zip = File('${work.path}/u.zip') + ..writeAsBytesSync( + await _releaseTarball(work, windows: true, zip: true), + ); + final running = await Process.start(exe, ['-n', '60', '127.0.0.1']); + try { + expect( + () => File(exe).deleteSync(), + throwsA(isA()), + reason: 'precondition: a running exe is locked', + ); + await applyUpdate(zip, root, windows: true); + expect(await File(exe).readAsString(), 'binary-v2'); + expect(File('$exe.old').existsSync(), isTrue); + expect( + await File('${root.path}/data/flutter_assets/fresh.cfg') + .readAsString(), + 'fresh-config', + ); + expect(_leftovers(root), isEmpty); + } finally { + running.kill(); + await running.exitCode; + } + await cleanupUpdateLeftovers(root); + expect(File('$exe.old').existsSync(), isFalse); + }, skip: Platform.isWindows ? null : 'needs Windows'); }); group('in-app controller', () {