halkyon/test/deploy/images_test.dart

126 lines
3.9 KiB
Dart
Raw Normal View History

import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
import '../../tool/src/images.dart';
// The images CI's checks never build: the deploy does, on the server, and
// a package missing from one failed it there (26 Oct 2026, `halkyon_core`
// kept out by `.dockerignore`, `halkyon_llm` never copied). Their list is
// `dartImages`, whose resolution tool/check.dart replays.
/// The sidecars' images, which copy single files of the repository's: kept
/// out of the build's context, they failed it (29 Oct 2026, Train's running
/// profile, `apps/train/` left out whole).
const _sidecars = [
'deploy/brouter/Dockerfile',
'deploy/maps/Dockerfile',
'deploy/photon/Dockerfile',
];
/// The packages [package] takes by path, and theirs, with itself: pub
/// resolves every workspace member's dev dependencies too, so an image
/// needs those as well.
Set<String> _needed(String package) {
final needed = <String>{};
final waiting = [package];
while (waiting.isNotEmpty) {
final each = waiting.removeLast();
if (!needed.add(each)) {
continue;
}
final pubspec =
loadYaml(File('$each/pubspec.yaml').readAsStringSync()) as YamlMap;
for (final section in ['dependencies', 'dev_dependencies']) {
final dependencies = pubspec[section] as YamlMap? ?? YamlMap();
for (final dependency in dependencies.values) {
if (dependency case {'path': final String path}) {
waiting.add(p.normalize(p.join(each, path)));
}
}
}
}
return needed;
}
/// The folders the image's build stage copies in as they are named.
Set<String> _copied(String dockerfile) => {
for (final match in RegExp(
r'^COPY ((?:apps|packages)/\S+) \1$',
multiLine: true,
).allMatches(dockerfile))
match[1]!,
};
/// The members `tool/workspace_only.sh` keeps in the image's workspace.
List<String> _members(String dockerfile) => RegExp(
r'^RUN sh tool/workspace_only\.sh (.+)$',
multiLine: true,
).firstMatch(dockerfile)![1]!.split(' ');
/// The lines of `.dockerignore` that name a folder, without the slash.
Set<String> _ignored() => {
for (final line in File('.dockerignore').readAsLinesSync())
if (line.trim() case final folder
when folder.isNotEmpty && !folder.startsWith('#'))
folder.replaceFirst(RegExp(r'/$'), ''),
};
void main() {
for (final path in dartImages) {
group(path, () {
final dockerfile = File(path).readAsStringSync();
final package = _members(dockerfile).first;
test('builds the package it lies in', () {
expect(package, p.dirname(path));
});
test('copies every package the build resolves, and keeps each in the '
'workspace', () {
final needed = _needed(package);
expect(_copied(dockerfile), needed);
expect(_members(dockerfile).toSet(), needed);
});
test("keeps none of them out of the build's context", () {
final ignored = _ignored();
for (final each in _needed(package)) {
expect(
ignored.where(
(folder) => each == folder || p.isWithin(folder, each),
),
isEmpty,
reason: '.dockerignore leaves $each out',
);
}
});
});
}
for (final path in _sidecars) {
test("$path copies files that are in the build's context", () {
final ignored = _ignored();
final sources = [
for (final match in RegExp(
r'^COPY (?:--\S+ )*(\S+) \S+$',
multiLine: true,
).allMatches(File(path).readAsStringSync()))
match[1]!,
];
expect(sources, isNotEmpty);
for (final source in sources) {
expect(File(source).existsSync(), isTrue, reason: source);
expect(
ignored.where(
(folder) => source == folder || p.isWithin(folder, source),
),
isEmpty,
reason: '.dockerignore leaves $source out',
);
}
});
}
}