halkyon/test/release/app_release_test.dart

239 lines
7.6 KiB
Dart
Raw Normal View History

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.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'));
// Task and Calendar & Agenda (ADR 0033, D24), each one the repository
// lists.
final categories = [
for (final match in RegExp(r'^ - (.+)$', multiLine: true).allMatches(
RegExp(r'Categories:\n((?: - .+\n)+)').firstMatch(listing)![1]!,
))
match[1]!,
];
expect(categories, ['Task', 'Calendar & Agenda']);
final listed = File('${todoRelease.fdroid!.listing}/config/categories.yml')
.readAsStringSync();
for (final category in categories) {
expect(listed, contains('\n$category:\n'));
}
});
test("Learn's snap is the root's; Todo's and Train's are built in clones of "
'their own, outside the repository', () {
expect(learnRelease.snap!.atRoot, isTrue);
for (final (app, name) in [
(todoRelease, 'halkyon-todo'),
(trainRelease, 'halkyon-train'),
]) {
final snap = app.snap!;
expect(snap.atRoot, isFalse, reason: app.id);
expect(
snap.projectPath('/home/someone'),
'/home/someone/.cache/halkyon-release/snap/$name',
);
expect(snap.storeChannel, 'edge', reason: app.id);
}
});
for (final app in [todoRelease, trainRelease]) {
group("${app.name}'s snap, built in a clone of its own with its folder in "
"place of the root's snap/", () {
final snap = app.snap!;
final recipe = File('${snap.directory}/snapcraft.yaml')
.readAsStringSync();
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 the app for its instance', () {
expect(recipe, contains('cd ${app.flutterPackage}\n'));
expect(recipe, contains('--dart-define=SERVER_URL=${app.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,
);
}
}
});
}