F-Droid's Time is for clocks and timers; a to-do list with a calendar belongs under Task and Calendar & Agenda, the developer's choice of both. No other app used Time, so the repository no longer lists it. The release test now checks every category the listing names. ADR 0033 gets D26 (the review's fixes so far) and D27. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
239 lines
7.6 KiB
Dart
239 lines
7.6 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.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,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|