// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/format_command.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; void main() { late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; late RecordingProcessRunner processRunner; late FormatCommand analyzeCommand; late CommandRunner runner; late String javaFormatPath; late String kotlinFormatPath; setUp(() { fileSystem = MemoryFileSystem(); mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); analyzeCommand = FormatCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, ); // Create the Java and Kotlin formatter files that the command checks for, // to avoid a download. final p.Context path = analyzeCommand.path; javaFormatPath = path.join(path.dirname(path.fromUri(mockPlatform.script)), 'google-java-format-1.3-all-deps.jar'); fileSystem.file(javaFormatPath).createSync(recursive: true); kotlinFormatPath = path.join( path.dirname(path.fromUri(mockPlatform.script)), 'ktfmt-0.46-jar-with-dependencies.jar'); fileSystem.file(kotlinFormatPath).createSync(recursive: true); runner = CommandRunner('format_command', 'Test for format_command'); runner.addCommand(analyzeCommand); }); /// Creates the .dart_tool directory for [package] to simulate (as much as /// this command requires) `pub get` having been run. void fakePubGet(RepositoryPackage package) { package.directory.childDirectory('.dart_tool').createSync(); } /// Returns a modified version of a list of [relativePaths] that are relative /// to [package] to instead be relative to [packagesDir]. List getPackagesDirRelativePaths( RepositoryPackage package, List relativePaths) { final p.Context path = analyzeCommand.path; final String relativeBase = path.relative(package.path, from: packagesDir.path); return relativePaths .map((String relativePath) => path.join(relativeBase, relativePath)) .toList(); } /// Returns a list of [count] relative paths to pass to [createFakePlugin] /// or [createFakePackage] such that each path will be 99 characters long /// relative to the package directory. /// /// This is for each of testing batching, since it means each file will /// consume 100 characters of the batch length. List get99CharacterPathExtraFiles(int count) { const int padding = 99 - 1 - // the path separator after the padding 10; // the file name const int filenameBase = 10000; final p.Context path = analyzeCommand.path; return [ for (int i = filenameBase; i < filenameBase + count; ++i) path.join('a' * padding, '$i.dart'), ]; } test('formats .dart files', () async { const List files = [ 'lib/a.dart', 'lib/src/b.dart', 'lib/src/c.dart', ]; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, ); fakePubGet(plugin); await runCapturingPrint(runner, ['format']); expect( processRunner.recordedCalls, orderedEquals([ ProcessCall('dart', const ['format', ...files], plugin.path), ])); }); test('does not format .dart files with pragma', () async { const List formattedFiles = [ 'lib/a.dart', 'lib/src/b.dart', 'lib/src/c.dart', ]; const String unformattedFile = 'lib/src/d.dart'; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: [ ...formattedFiles, unformattedFile, ], ); fakePubGet(plugin); final p.Context posixContext = p.posix; childFileWithSubcomponents( plugin.directory, posixContext.split(unformattedFile)) .writeAsStringSync( '// copyright bla bla\n// This file is hand-formatted.\ncode...'); await runCapturingPrint(runner, ['format']); expect( processRunner.recordedCalls, orderedEquals([ ProcessCall( 'dart', const ['format', ...formattedFiles], plugin.path), ])); }); test('fails if dart format fails', () async { const List files = [ 'lib/a.dart', 'lib/src/b.dart', 'lib/src/c.dart', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); processRunner.mockProcessesForExecutable['dart'] = [ FakeProcessInfo(MockProcess(exitCode: 1), ['format']) ]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Failed to format Dart files: exit code 1.'), ])); }); test('skips dart if --no-dart flag is provided', () async { const List files = [ 'lib/a.dart', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); await runCapturingPrint(runner, ['format', '--no-dart']); expect(processRunner.recordedCalls, orderedEquals([])); }); test('formats .java files', () async { const List files = [ 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', ]; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, ); fakePubGet(plugin); await runCapturingPrint(runner, ['format']); expect( processRunner.recordedCalls, orderedEquals([ const ProcessCall('java', ['-version'], null), ProcessCall( 'java', [ '-jar', javaFormatPath, '--replace', ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); }); test('fails with a clear message if Java is not in the path', () async { const List files = [ 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); processRunner.mockProcessesForExecutable['java'] = [ FakeProcessInfo(MockProcess(exitCode: 1), ['-version']) ]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains( 'Unable to run "java". Make sure that it is in your path, or ' 'provide a full path with --java-path.'), ])); }); test('fails if Java formatter fails', () async { const List files = [ 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); processRunner.mockProcessesForExecutable['java'] = [ FakeProcessInfo( MockProcess(), ['-version']), // check for working java FakeProcessInfo(MockProcess(exitCode: 1), ['-jar']), // format ]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Failed to format Java files: exit code 1.'), ])); }); test('honors --java-path flag', () async { const List files = [ 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', ]; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, ); fakePubGet(plugin); await runCapturingPrint( runner, ['format', '--java-path=/path/to/java']); expect( processRunner.recordedCalls, orderedEquals([ const ProcessCall('/path/to/java', ['--version'], null), ProcessCall( '/path/to/java', [ '-jar', javaFormatPath, '--replace', ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); }); test('skips Java if --no-java flag is provided', () async { const List files = [ 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); await runCapturingPrint(runner, ['format', '--no-java']); expect(processRunner.recordedCalls, orderedEquals([])); }); test('formats c-ish files', () async { const List files = [ 'ios/Classes/Foo.h', 'ios/Classes/Foo.m', 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', 'macos/Classes/Foo.mm', 'windows/foo_plugin.cpp', ]; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, ); fakePubGet(plugin); await runCapturingPrint(runner, ['format']); expect( processRunner.recordedCalls, orderedEquals([ const ProcessCall('clang-format', ['--version'], null), ProcessCall( 'clang-format', [ '-i', '--style=file', ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); }); test('fails with a clear message if clang-format is not in the path', () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); processRunner.mockProcessesForExecutable['clang-format'] = [FakeProcessInfo(MockProcess(exitCode: 1))]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Unable to run "clang-format". Make sure that it is in your ' 'path, or provide a full path with --clang-format-path.'), ])); }); test('falls back to working clang-format in the path', () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', ]; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, ); fakePubGet(plugin); processRunner.mockProcessesForExecutable['clang-format'] = [FakeProcessInfo(MockProcess(exitCode: 1))]; processRunner.mockProcessesForExecutable['which'] = [ FakeProcessInfo( MockProcess( stdout: '/usr/local/bin/clang-format\n/path/to/working-clang-format'), ['-a', 'clang-format']) ]; processRunner.mockProcessesForExecutable['/usr/local/bin/clang-format'] = [FakeProcessInfo(MockProcess(exitCode: 1))]; await runCapturingPrint(runner, ['format']); expect( processRunner.recordedCalls, containsAll([ const ProcessCall( '/path/to/working-clang-format', ['--version'], null), ProcessCall( '/path/to/working-clang-format', [ '-i', '--style=file', ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); }); test('honors --clang-format-path flag', () async { const List files = [ 'windows/foo_plugin.cpp', ]; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, ); fakePubGet(plugin); await runCapturingPrint(runner, ['format', '--clang-format-path=/path/to/clang-format']); expect( processRunner.recordedCalls, orderedEquals([ const ProcessCall( '/path/to/clang-format', ['--version'], null), ProcessCall( '/path/to/clang-format', [ '-i', '--style=file', ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); }); test('fails if clang-format fails', () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); processRunner.mockProcessesForExecutable['clang-format'] = [ FakeProcessInfo(MockProcess(), ['--version']), // check for working clang-format FakeProcessInfo(MockProcess(exitCode: 1), ['-i']), // format ]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains( 'Failed to format C, C++, and Objective-C files: exit code 1.'), ])); }); test('skips clang-format if --no-clang-format flag is provided', () async { const List files = [ 'linux/foo_plugin.cc', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); await runCapturingPrint(runner, ['format', '--no-clang-format']); expect(processRunner.recordedCalls, orderedEquals([])); }); group('kotlin-format', () { test('formats .kt files', () async { const List files = [ 'android/src/main/kotlin/io/flutter/plugins/a_plugin/a.kt', 'android/src/main/kotlin/io/flutter/plugins/a_plugin/b.kt', ]; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, ); fakePubGet(plugin); await runCapturingPrint(runner, ['format']); expect( processRunner.recordedCalls, orderedEquals([ const ProcessCall('java', ['-version'], null), ProcessCall( 'java', [ '-jar', kotlinFormatPath, ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); }); test('fails if Kotlin formatter fails', () async { const List files = [ 'android/src/main/kotlin/io/flutter/plugins/a_plugin/a.kt', 'android/src/main/kotlin/io/flutter/plugins/a_plugin/b.kt', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); processRunner.mockProcessesForExecutable['java'] = [ FakeProcessInfo( MockProcess(), ['-version']), // check for working java FakeProcessInfo(MockProcess(exitCode: 1), ['-jar']), // format ]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Failed to format Kotlin files: exit code 1.'), ])); }); test('skips Kotlin if --no-kotlin flag is provided', () async { const List files = [ 'android/src/main/kotlin/io/flutter/plugins/a_plugin/a.kt', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); await runCapturingPrint(runner, ['format', '--no-kotlin']); expect(processRunner.recordedCalls, orderedEquals([])); }); }); group('swift-format', () { test('formats Swift if --swift-format flag is provided', () async { const List files = [ 'macos/foo.swift', ]; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, ); fakePubGet(plugin); await runCapturingPrint(runner, [ 'format', '--swift', '--swift-format-path=/path/to/swift-format' ]); expect( processRunner.recordedCalls, orderedEquals([ const ProcessCall( '/path/to/swift-format', ['--version'], null, ), ProcessCall( '/path/to/swift-format', ['-i', ...getPackagesDirRelativePaths(plugin, files)], packagesDir.path, ), ProcessCall( '/path/to/swift-format', [ 'lint', '--parallel', '--strict', ...getPackagesDirRelativePaths(plugin, files), ], packagesDir.path, ), ])); }); test('skips Swift if --no-swift flag is provided', () async { const List files = [ 'macos/foo.swift', ]; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, ); fakePubGet(plugin); await runCapturingPrint(runner, ['format', '--no-swift']); expect(processRunner.recordedCalls, orderedEquals([])); }); test('fails with a clear message if swift-format is not in the path', () async { const List files = [ 'macos/foo.swift', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); processRunner.mockProcessesForExecutable['swift-format'] = [ FakeProcessInfo(MockProcess(exitCode: 1), ['--version']), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['format', '--swift'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains( 'Unable to run "swift-format". Make sure that it is in your path, or ' 'provide a full path with --swift-format-path.'), ])); }); test('fails if swift-format lint fails', () async { const List files = [ 'macos/foo.swift', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); processRunner.mockProcessesForExecutable['swift-format'] = [ FakeProcessInfo(MockProcess(), ['--version']), // check for working swift-format FakeProcessInfo(MockProcess(), ['-i']), FakeProcessInfo(MockProcess(exitCode: 1), [ 'lint', '--parallel', '--strict', ]), ]; Error? commandError; final List output = await runCapturingPrint(runner, [ 'format', '--swift', '--swift-format-path=swift-format' ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Failed to lint Swift files: exit code 1.'), ])); }); test('fails if swift-format fails', () async { const List files = [ 'macos/foo.swift', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); processRunner.mockProcessesForExecutable['swift-format'] = [ FakeProcessInfo(MockProcess(), ['--version']), // check for working swift-format FakeProcessInfo(MockProcess(exitCode: 1), ['-i']), ]; Error? commandError; final List output = await runCapturingPrint(runner, [ 'format', '--swift', '--swift-format-path=swift-format' ], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Failed to format Swift files: exit code 1.'), ])); }); }); test('skips known non-repo files', () async { const List skipFiles = [ '/example/build/SomeFramework.framework/Headers/SomeFramework.h', '/example/Pods/APod.framework/Headers/APod.h', '.dart_tool/internals/foo.cc', '.dart_tool/internals/Bar.java', '.dart_tool/internals/baz.dart', ]; const List clangFiles = ['ios/Classes/Foo.h']; const List dartFiles = ['lib/a.dart']; const List javaFiles = [ 'android/src/main/java/io/flutter/plugins/a_plugin/a.java' ]; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: [ ...skipFiles, // Include some files that should be formatted to validate that it's // correctly filtering even when running the commands. ...clangFiles, ...dartFiles, ...javaFiles, ], ); fakePubGet(plugin); await runCapturingPrint(runner, ['format']); expect( processRunner.recordedCalls, containsAll([ ProcessCall( 'clang-format', [ '-i', '--style=file', ...getPackagesDirRelativePaths(plugin, clangFiles) ], packagesDir.path), ProcessCall( 'dart', const ['format', ...dartFiles], plugin.path), ProcessCall( 'java', [ '-jar', javaFormatPath, '--replace', ...getPackagesDirRelativePaths(plugin, javaFiles) ], packagesDir.path), ])); }); test('skips GeneratedPluginRegistrant.swift', () async { const String sourceFile = 'macos/Classes/Foo.swift'; final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: [ sourceFile, 'example/macos/Flutter/GeneratedPluginRegistrant.swift', ], ); fakePubGet(plugin); await runCapturingPrint(runner, [ 'format', '--swift', '--swift-format-path=/path/to/swift-format' ]); expect( processRunner.recordedCalls, orderedEquals([ const ProcessCall( '/path/to/swift-format', ['--version'], null, ), ProcessCall( '/path/to/swift-format', [ '-i', ...getPackagesDirRelativePaths(plugin, [sourceFile]) ], packagesDir.path, ), ProcessCall( '/path/to/swift-format', [ 'lint', '--parallel', '--strict', ...getPackagesDirRelativePaths(plugin, [sourceFile]), ], packagesDir.path, ), ])); }); test('fails if files are changed with --fail-on-change', () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; processRunner.mockProcessesForExecutable['git'] = [ FakeProcessInfo( MockProcess(stdout: changedFilePath), ['ls-files']), ]; Error? commandError; final List output = await runCapturingPrint(runner, ['format', '--fail-on-change'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('These files are not formatted correctly'), contains(changedFilePath), // Ensure the error message links to instructions. contains( 'https://github.com/flutter/packages/blob/main/script/tool/README.md#format-code'), contains('patch -p1 <[ ProcessCall( 'git', [ 'ls-files', '--modified', packagesDir.path, thirdPartyDir.path ], packagesDir.parent.path, ), ])); }); test('fails if git ls-files fails', () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); processRunner.mockProcessesForExecutable['git'] = [ FakeProcessInfo(MockProcess(exitCode: 1), ['ls-files']) ]; Error? commandError; final List output = await runCapturingPrint(runner, ['format', '--fail-on-change'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('Unable to determine changed files.'), ])); }); test('reports git diff failures', () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', ]; final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, extraFiles: files); fakePubGet(plugin); const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; processRunner.mockProcessesForExecutable['git'] = [ FakeProcessInfo( MockProcess(stdout: changedFilePath), ['ls-files']), FakeProcessInfo(MockProcess(exitCode: 1), ['diff']), ]; Error? commandError; final List output = await runCapturingPrint(runner, ['format', '--fail-on-change'], errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('These files are not formatted correctly'), contains(changedFilePath), contains('Unable to determine diff.'), ])); }); test('Batches moderately long file lists on Windows', () async { mockPlatform.isWindows = true; const String pluginName = 'a_plugin'; // -1 since the command itself takes some length. const int batchSize = (windowsCommandLineMax ~/ 100) - 1; // Make the file list one file longer than would fit in the batch. final List batch1 = get99CharacterPathExtraFiles(batchSize + 1); final String extraFile = batch1.removeLast(); final RepositoryPackage package = createFakePlugin( pluginName, packagesDir, extraFiles: [...batch1, extraFile], ); fakePubGet(package); await runCapturingPrint(runner, ['format']); // Ensure that it was batched... expect(processRunner.recordedCalls.length, 2); // ... and that the spillover into the second batch was only one file. expect( processRunner.recordedCalls, contains( ProcessCall( 'dart', [ 'format', extraFile, ], package.path), )); }); // Validates that the Windows limit--which is much lower than the limit on // other platforms--isn't being used on all platforms, as that would make // formatting slower on Linux and macOS. test('Does not batch moderately long file lists on non-Windows', () async { const String pluginName = 'a_plugin'; // -1 since the command itself takes some length. const int batchSize = (windowsCommandLineMax ~/ 100) - 1; // Make the file list one file longer than would fit in a Windows batch. final List batch = get99CharacterPathExtraFiles(batchSize + 1); final RepositoryPackage plugin = createFakePlugin( pluginName, packagesDir, extraFiles: batch, ); fakePubGet(plugin); await runCapturingPrint(runner, ['format']); expect(processRunner.recordedCalls.length, 1); }); test('Batches extremely long file lists on non-Windows', () async { const String pluginName = 'a_plugin'; // -1 since the command itself takes some length. const int batchSize = (nonWindowsCommandLineMax ~/ 100) - 1; // Make the file list one file longer than would fit in the batch. final List batch1 = get99CharacterPathExtraFiles(batchSize + 1); final String extraFile = batch1.removeLast(); final RepositoryPackage package = createFakePlugin( pluginName, packagesDir, extraFiles: [...batch1, extraFile], ); fakePubGet(package); await runCapturingPrint(runner, ['format']); // Ensure that it was batched... expect(processRunner.recordedCalls.length, 2); // ... and that the spillover into the second batch was only one file. expect( processRunner.recordedCalls, contains( ProcessCall( 'dart', [ 'format', extraFile, ], package.path), )); }); }