probe: updater tests on windows
probe-windows / windows (push) Failing after 2m19s

This commit is contained in:
foxyz committed 2026-10-07 22:02:15 +03:00
1 parent e29a5a3b0a
commit 96ef39ab1f
3 files changed
+334 -184

No files matched your search

+216 -121
View File
@@ -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 <stage>/watch-together-<ver>/, then
// `tar -czf out -C <stage> watch-together-<ver>`.
Future<List<int>> _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<List<int>> _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<List<int>> _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<FileSystemException>()));
},
),
throwsA(isA<FileSystemException>()),
);
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 = <String>[];
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<FileSystemException>()));
},
),
throwsA(isA<FileSystemException>()),
);
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<String>)>[];
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',
);
},
);
});
}