halkyon/test/release/app_release_test.dart
Tizian 「ティツィアーン」 34001b1aff
All checks were successful
CI / check (push) Successful in 39m47s
CI / deploy (push) Successful in 59s
fix(release): a snap built in a clone of its own is packed inside it, then moved into the release's folder
snapcraft packs nowhere outside the project it builds, so Todo 0.1.0's release stopped
after its snap was built ("Cannot create packages outside of the project tree").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 09:21:34 +01:00

240 lines
7.4 KiB
Dart

import 'dart:io';
import 'package:test/test.dart';
import '../../apps/learn/release.dart';
import '../../apps/todo/release.dart';
import '../../apps/train/release.dart';
import '../../tool/src/release/apps.dart';
import '../../tool/src/release/version.dart';
import 'fixtures.dart';
void main() {
test("an app's release names its files and tags by its id", () {
expect(testApp.tagPrefix, 'notes-v');
expect(testApp.pubspec, 'apps/notes/halkyon_notes_flutter/pubspec.yaml');
expect(testApp.instance, 'notes.halkyon.eu');
expect(
testApp.apkFile(const Version(0, 19, 0)),
'halkyon-notes-0.19.0.apk',
);
expect(
testApp.snapFile(const Version(0, 19, 0)),
'halkyon-notes_0.19.0_amd64.snap',
);
});
test('a configuration that would release something wrong is refused', () {
expect(testApp.problems(), isEmpty);
expect(
testApp.copyWith(id: 'Notes App').problems(),
contains('The id "Notes App" is not lower-case letters and digits.'),
);
expect(
testApp.copyWith(serverUrl: 'http://notes.halkyon.eu/').problems(),
contains("The server URL http://notes.halkyon.eu/ isn't HTTPS."),
);
expect(
testApp.copyWith(serverUrl: 'https://notes.halkyon.eu').problems(),
contains(
"The server URL https://notes.halkyon.eu doesn't end with a slash, "
'as the app is built with it.',
),
);
expect(
testApp
.copyWith(flutterPackage: 'apps/todo/halkyon_todo_flutter')
.problems(),
contains(
"The Flutter package apps/todo/halkyon_todo_flutter isn't in "
'apps/notes.',
),
);
expect(
testApp.copyWith(applicationId: 'notes').problems(),
contains("The application ID notes isn't one Android takes."),
);
});
test("Learn's release is registered, sound, and names files that exist", () {
expect(releasedApps['learn'], same(learnRelease));
expect(learnRelease.problems(), isEmpty);
// The test runs in the repository's root, as `dart test` does.
for (final path in [
learnRelease.pubspec,
learnRelease.android.certificateFile,
...learnRelease.releaseFiles,
]) {
expect(
FileSystemEntity.typeSync(path),
isNot(FileSystemEntityType.notFound),
reason: path,
);
}
expect(
File('${learnRelease.snap!.directory}/snapcraft.yaml').readAsStringSync(),
contains('name: ${learnRelease.snap!.name}\n'),
);
});
test("Train's release is registered, sound, names files that exist, and "
'has a key of its own', () {
expect(releasedApps['train'], same(trainRelease));
expect(trainRelease.problems(), isEmpty);
for (final path in [
trainRelease.pubspec,
trainRelease.android.certificateFile,
...trainRelease.releaseFiles,
]) {
expect(
FileSystemEntity.typeSync(path),
isNot(FileSystemEntityType.notFound),
reason: path,
);
}
expect(trainRelease.snap, isNull);
expect(
trainRelease.android.certificateFile,
isNot(learnRelease.android.certificateFile),
);
expect(
trainRelease.android.defaultKeystore,
isNot(learnRelease.android.defaultKeystore),
);
expect(
File(
'${trainRelease.fdroid!.listing}/metadata/'
'${trainRelease.applicationId}.yml',
).readAsStringSync(),
contains('NonFreeDep'),
);
});
test("Todo's release is registered, sound, names files that exist, and "
'has a key of its own', () {
expect(releasedApps['todo'], same(todoRelease));
expect(todoRelease.problems(), isEmpty);
for (final path in [
todoRelease.pubspec,
todoRelease.android.certificateFile,
...todoRelease.releaseFiles,
]) {
expect(
FileSystemEntity.typeSync(path),
isNot(FileSystemEntityType.notFound),
reason: path,
);
}
for (final other in [learnRelease, trainRelease]) {
expect(
todoRelease.android.certificateFile,
isNot(other.android.certificateFile),
);
expect(
todoRelease.android.defaultKeystore,
isNot(other.android.defaultKeystore),
);
expect(
todoRelease.android.keystoreVariable,
isNot(other.android.keystoreVariable),
);
}
final listing = File(
'${todoRelease.fdroid!.listing}/metadata/'
'${todoRelease.applicationId}.yml',
).readAsStringSync();
expect(listing, contains('NonFreeNet'));
expect(
File('${todoRelease.fdroid!.listing}/config/categories.yml')
.readAsStringSync(),
contains(
'\n${RegExp(r'Categories:\n - (.+)\n').firstMatch(listing)![1]}:\n',
),
);
});
group("Todo's snap, built in a clone of its own with its folder in place of "
"the root's snap/", () {
final snap = todoRelease.snap!;
final recipe = File('${snap.directory}/snapcraft.yaml').readAsStringSync();
test("is not the root's, and is built outside the repository", () {
expect(learnRelease.snap!.atRoot, isTrue);
expect(snap.atRoot, isFalse);
expect(
snap.projectPath('/home/someone'),
'/home/someone/.cache/halkyon-release/snap/halkyon-todo',
);
});
test('is packed inside its clone, as snapcraft packs nowhere outside the '
"project, and Learn's into the release's folder", () {
expect(
snap.packPath(home: '/home/someone', root: '/repo', out: 'build/x'),
'/home/someone/.cache/halkyon-release/snap/halkyon-todo',
);
expect(
learnRelease.snap!.packPath(
home: '/home/someone',
root: '/repo',
out: 'build/x',
),
'/repo/build/x',
);
});
test('names the snap and the part the release tool takes', () {
expect(recipe, contains('name: ${snap.name}\n'));
expect(recipe, contains('\n ${snap.part}:\n after: [flutter]\n'));
expect(recipe, contains('adopt-info: ${snap.part} '));
});
test('builds Todo for its instance', () {
expect(recipe, contains('cd ${todoRelease.flutterPackage}\n'));
expect(
recipe,
contains('--dart-define=SERVER_URL=${todoRelease.serverUrl}\n'),
);
});
test('has the files it names, as they will stand in the clone', () {
// In the clone, the folder is snap/, the root's snap/local/ beside it.
for (final match in RegExp(
r'(?<![\w/$])snap/([\w./-]+)',
).allMatches(recipe)) {
final inClone = match[1]!;
final path = inClone.startsWith('local')
? 'snap/$inClone'
: '${snap.directory}/$inClone';
if (inClone == 'snapcraft.yaml') {
continue;
}
expect(
FileSystemEntity.typeSync(path),
isNot(FileSystemEntityType.notFound),
reason: match[0],
);
}
expect(
File('${snap.directory}/gui/${snap.name}.desktop').readAsStringSync(),
contains('Exec=${snap.name}\n'),
);
});
});
test("every snap's recipe builds with the Flutter CI pins", () {
final pinned = RegExp(
r'^\s*FLUTTER_VERSION:\s*(\S+)',
multiLine: true,
).firstMatch(File('.forgejo/workflows/ci.yaml').readAsStringSync())![1];
for (final app in releasedApps.values) {
if (app.snap case final snap?) {
expect(
File('${snap.directory}/snapcraft.yaml').readAsStringSync(),
contains("source-tag: '$pinned'"),
reason: app.id,
);
}
}
});
}