import 'dart:io'; import 'package:test/test.dart'; import '../../tool/src/images.dart'; /// tool/check.dart's replay of the images' resolution (4 Nov 2026: two /// deploys failed on what pub refuses in Dart's image alone). void main() { group('a build stage read', () { const dockerfile = r''' # A comment. FROM dart:3.13.4 AS build WORKDIR /app COPY pubspec.yaml pubspec.lock ./ COPY tool/workspace_only.sh tool/ RUN sh tool/workspace_only.sh apps/a \ packages/b RUN dart pub get WORKDIR /app/apps/a RUN dart build cli --target bin/main.dart --output build FROM ubuntu:24.04 COPY --from=build /app/apps/a/build /srv '''; test('names its Dart and its steps up to and including dart pub get, ' 'continued lines joined', () { final resolution = imageResolution(dockerfile); expect(resolution.dartVersion, '3.13.4'); expect( [ for (final step in resolution.steps) switch (step) { CopyStep(:final sources, :final destination) => 'COPY ${sources.join(' ')} -> $destination', RunStep(:final command) => 'RUN $command', WorkdirStep(:final path) => 'WORKDIR $path', }, ], [ 'WORKDIR /app', 'COPY pubspec.yaml pubspec.lock -> ./', 'COPY tool/workspace_only.sh -> tool/', 'RUN sh tool/workspace_only.sh apps/a packages/b', 'RUN dart pub get', ], ); }); test('refuses a stage without Dart, without pub get, or with an ' 'instruction it would replay wrongly', () { expect( () => imageResolution('FROM ubuntu:24.04\nRUN true\n'), throwsFormatException, ); expect( () => imageResolution('FROM dart:3.13.4 AS build\nRUN true\n'), throwsFormatException, ); expect( () => imageResolution( 'FROM dart:3.13.4 AS build\nENV A=b\nRUN dart pub get\n', ), throwsFormatException, ); }); }); group("COPY's files land as Docker puts them", () { const files = [ 'pubspec.yaml', 'pubspec.lock', 'tool/workspace_only.sh', 'tool/check.dart', 'apps/a/pubspec.yaml', 'apps/a/lib/a.dart', 'apps/ab/pubspec.yaml', ]; test('a folder: its content under the destination, nothing beside it', () { expect(copiedFiles(const CopyStep(['apps/a'], 'apps/a'), files), { 'apps/a/pubspec.yaml': '/app/apps/a/pubspec.yaml', 'apps/a/lib/a.dart': '/app/apps/a/lib/a.dart', }); }); test('files into a folder named with a slash, or several at once', () { expect( copiedFiles(const CopyStep(['tool/workspace_only.sh'], 'tool/'), files), {'tool/workspace_only.sh': '/app/tool/workspace_only.sh'}, ); expect( copiedFiles( const CopyStep(['pubspec.yaml', 'pubspec.lock'], './'), files, ), { 'pubspec.yaml': '/app/pubspec.yaml', 'pubspec.lock': '/app/pubspec.lock', }, ); }); test('a file as the destination named without a slash', () { expect( copiedFiles( const CopyStep(['pubspec.yaml'], 'other.yaml'), files, workdir: '/app/x', ), {'pubspec.yaml': '/app/x/other.yaml'}, ); }); }); test("the SDK's archive is this machine's", () { expect( dartSdkArchive('linux', '3.13.4 (stable) (Tue) on "linux_x64"'), 'dartsdk-linux-x64-release.zip', ); expect( dartSdkArchive('linux', '3.13.4 (stable) (Tue) on "linux_arm64"'), 'dartsdk-linux-arm64-release.zip', ); }); group('the repository', () { test('every image of the list is replayed up to its pub get', () { for (final path in dartImages) { final resolution = imageResolution(File(path).readAsStringSync()); expect(resolution.dartVersion, matches(RegExp(r'^\d+\.\d+\.\d+$'))); expect(resolution.steps.last, isA(), reason: path); } }); test('every Dockerfile building with Dart is on the list, so none ' 'escapes the replay', () { final dockerfiles = Process.runSync('git', [ 'ls-files', '*Dockerfile', ]).stdout.toString().split('\n').where((path) => path.isNotEmpty); final dart = [ for (final path in dockerfiles) if (File(path) .readAsStringSync() .contains(RegExp('^FROM dart:', multiLine: true))) path, ]; expect(dart.toSet(), dartImages.toSet()); }); }); }