This commit is contained in:
1 parent
e29a5a3b0a
commit
96ef39ab1f
3 files changed
+334
-184
No files matched your search
+216
-121
@@ -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',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user