// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/output_utils.dart'; import 'package:flutter_plugin_tools/src/common/package_looping_command.dart'; import 'package:git/git.dart'; import 'package:test/test.dart'; import '../mocks.dart'; import '../util.dart'; // Constants for colorized output start and end. const String _startElapsedTimeColor = '\x1B[90m'; const String _startErrorColor = '\x1B[31m'; const String _startHeadingColor = '\x1B[36m'; const String _startSkipColor = '\x1B[90m'; const String _startSkipWithWarningColor = '\x1B[93m'; const String _startSuccessColor = '\x1B[32m'; const String _startWarningColor = '\x1B[33m'; const String _endColor = '\x1B[0m'; // The filename within a package containing warnings to log during runForPackage. enum _ResultFileType { /// A file containing errors to return. errors, /// A file containing warnings that should be logged. warns, /// A file indicating that the package should be skipped, and why. skips, /// A file indicating that the package should throw. throws, } // The filename within a package containing errors to return from runForPackage. const String _errorFile = 'errors'; // The filename within a package indicating that it should be skipped. const String _skipFile = 'skip'; // The filename within a package containing warnings to log during runForPackage. const String _warningFile = 'warnings'; // The filename within a package indicating that it should throw. const String _throwFile = 'throw'; /// Writes a file to [package] to control the behavior of /// [TestPackageLoopingCommand] for that package. void _addResultFile( RepositoryPackage package, _ResultFileType type, { String? contents, }) { final File file = package.directory.childFile(_filenameForType(type)); file.createSync(); if (contents != null) { file.writeAsStringSync(contents); } } String _filenameForType(_ResultFileType type) { switch (type) { case _ResultFileType.errors: return _errorFile; case _ResultFileType.warns: return _warningFile; case _ResultFileType.skips: return _skipFile; case _ResultFileType.throws: return _throwFile; } } void main() { late MockPlatform mockPlatform; late Directory packagesDir; late Directory thirdPartyPackagesDir; late GitDir gitDir; late RecordingProcessRunner gitProcessRunner; setUp(() { mockPlatform = MockPlatform(); (:packagesDir, processRunner: _, :gitProcessRunner, :gitDir) = configureBaseCommandMocks(platform: mockPlatform); // Correct color handling is part of the behavior being tested here. useColorForOutput = true; thirdPartyPackagesDir = packagesDir.parent .childDirectory('third_party') .childDirectory('packages'); }); tearDown(() { // Restore the default behavior. useColorForOutput = io.stdout.supportsAnsiEscapes; }); /// Creates a TestPackageLoopingCommand with the given configuration. TestPackageLoopingCommand createTestCommand({ bool hasLongOutput = true, PackageLoopingType packageLoopingType = PackageLoopingType.topLevelOnly, bool failsDuringInit = false, bool warnsDuringInit = false, bool captureOutput = false, String? customFailureListHeader, String? customFailureListFooter, }) { return TestPackageLoopingCommand( packagesDir, platform: mockPlatform, hasLongOutput: hasLongOutput, packageLoopingType: packageLoopingType, failsDuringInit: failsDuringInit, warnsDuringInit: warnsDuringInit, customFailureListHeader: customFailureListHeader, customFailureListFooter: customFailureListFooter, captureOutput: captureOutput, gitDir: gitDir, ); } /// Runs [command] with the given [arguments], and returns its output. Future> runCommand( TestPackageLoopingCommand command, { List arguments = const [], void Function(Error error)? errorHandler, }) async { late CommandRunner runner; runner = CommandRunner( 'test_package_looping_command', 'Test for base package looping functionality', ); runner.addCommand(command); return runCapturingPrint(runner, [ command.name, ...arguments, ], errorHandler: errorHandler); } group('tool exit', () { test('is handled during initializeRun', () async { final TestPackageLoopingCommand command = createTestCommand( failsDuringInit: true, ); expect(() => runCommand(command), throwsA(isA())); }); test('does not stop looping on error', () async { createFakePackage('package_a', packagesDir); final RepositoryPackage failingPackage = createFakePlugin( 'package_b', packagesDir, ); createFakePackage('package_c', packagesDir); _addResultFile(failingPackage, _ResultFileType.errors); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); Error? commandError; final List output = await runCommand( command, errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ '${_startHeadingColor}Running for package_a...$_endColor', '${_startHeadingColor}Running for package_b...$_endColor', '${_startHeadingColor}Running for package_c...$_endColor', ]), ); }); test('does not stop looping on exceptions', () async { createFakePackage('package_a', packagesDir); final RepositoryPackage failingPackage = createFakePlugin( 'package_b', packagesDir, ); createFakePackage('package_c', packagesDir); _addResultFile(failingPackage, _ResultFileType.throws); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); Error? commandError; final List output = await runCommand( command, errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ '${_startHeadingColor}Running for package_a...$_endColor', '${_startHeadingColor}Running for package_b...$_endColor', '${_startHeadingColor}Running for package_c...$_endColor', ]), ); }); }); group('file filtering', () { test('runs command if the changed files list is empty', () async { createFakePackage('package_a', packagesDir); gitProcessRunner.mockProcessesForExecutable['git-diff'] = [FakeProcessInfo(MockProcess(stdout: ''))]; final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand(command); expect( output, containsAllInOrder([ '${_startHeadingColor}Running for package_a...$_endColor', ]), ); }); test('runs command if any files are not ignored', () async { createFakePackage('package_a', packagesDir); gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' skip/a other skip/b ''', ), ), ]; final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand(command); expect( output, containsAllInOrder([ '${_startHeadingColor}Running for package_a...$_endColor', ]), ); }); test('skips commands if all files should be ignored', () async { createFakePackage('package_a', packagesDir); gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ FakeProcessInfo( MockProcess( stdout: ''' skip/a skip/b ''', ), ), ]; final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand(command); expect( output, isNot(containsAllInOrder([contains('Running for package_a')])), ); expect( output, containsAllInOrder([ '${_startSkipColor}SKIPPING ALL PACKAGES: No changed files affect this command$_endColor', ]), ); }); }); group('package iteration', () { test('includes plugins and packages', () async { final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, ); final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, ); final TestPackageLoopingCommand command = createTestCommand(); await runCommand(command); expect( command.checkedPackages, unorderedEquals([plugin.path, package.path]), ); }); test('includes third_party/packages', () async { final RepositoryPackage package1 = createFakePackage( 'a_package', packagesDir, ); final RepositoryPackage package2 = createFakePackage( 'another_package', thirdPartyPackagesDir, ); final TestPackageLoopingCommand command = createTestCommand(); await runCommand(command); expect( command.checkedPackages, unorderedEquals([package1.path, package2.path]), ); }); test('includes all subpackages when requested', () async { final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, examples: ['example1', 'example2'], ); final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, ); final RepositoryPackage subPackage = createFakePackage( 'sub_package', package.directory, examples: [], ); final TestPackageLoopingCommand command = createTestCommand( packageLoopingType: PackageLoopingType.includeAllSubpackages, ); await runCommand(command); expect( command.checkedPackages, unorderedEquals([ plugin.path, getExampleDir(plugin).childDirectory('example1').path, getExampleDir(plugin).childDirectory('example2').path, package.path, getExampleDir(package).path, subPackage.path, ]), ); }); test('includes examples when requested', () async { final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, examples: ['example1', 'example2'], ); final RepositoryPackage package = createFakePackage( 'a_package', packagesDir, ); final RepositoryPackage subPackage = createFakePackage( 'sub_package', package.directory, ); final TestPackageLoopingCommand command = createTestCommand( packageLoopingType: PackageLoopingType.includeExamples, ); await runCommand(command); expect( command.checkedPackages, unorderedEquals([ plugin.path, getExampleDir(plugin).childDirectory('example1').path, getExampleDir(plugin).childDirectory('example2').path, package.path, getExampleDir(package).path, ]), ); expect(command.checkedPackages, isNot(contains(subPackage.path))); }); test('excludes subpackages when main package is excluded', () async { final RepositoryPackage excluded = createFakePlugin( 'a_plugin', packagesDir, examples: ['example1', 'example2'], ); final RepositoryPackage included = createFakePackage( 'a_package', packagesDir, ); final RepositoryPackage subpackage = createFakePackage( 'sub_package', excluded.directory, ); final TestPackageLoopingCommand command = createTestCommand( packageLoopingType: PackageLoopingType.includeAllSubpackages, ); await runCommand(command, arguments: ['--exclude=a_plugin']); final Iterable examples = excluded.getExamples(); expect( command.checkedPackages, unorderedEquals([included.path, getExampleDir(included).path]), ); expect(command.checkedPackages, isNot(contains(excluded.path))); expect(examples.length, 2); for (final example in examples) { expect(command.checkedPackages, isNot(contains(example.path))); } expect(command.checkedPackages, isNot(contains(subpackage.path))); }); test('excludes examples when main package is excluded', () async { final RepositoryPackage excluded = createFakePlugin( 'a_plugin', packagesDir, examples: ['example1', 'example2'], ); final RepositoryPackage included = createFakePackage( 'a_package', packagesDir, ); final TestPackageLoopingCommand command = createTestCommand( packageLoopingType: PackageLoopingType.includeExamples, ); await runCommand(command, arguments: ['--exclude=a_plugin']); final Iterable examples = excluded.getExamples(); expect( command.checkedPackages, unorderedEquals([included.path, getExampleDir(included).path]), ); expect(command.checkedPackages, isNot(contains(excluded.path))); expect(examples.length, 2); for (final example in examples) { expect(command.checkedPackages, isNot(contains(example.path))); } }); test('skips unsupported Flutter versions when requested', () async { final RepositoryPackage excluded = createFakePlugin( 'a_plugin', packagesDir, flutterConstraint: '>=2.10.0', ); final RepositoryPackage included = createFakePackage( 'a_package', packagesDir, ); final TestPackageLoopingCommand command = createTestCommand( packageLoopingType: PackageLoopingType.includeAllSubpackages, hasLongOutput: false, ); final List output = await runCommand( command, arguments: ['--skip-if-not-supporting-flutter-version=2.5.0'], ); expect( command.checkedPackages, unorderedEquals([included.path, getExampleDir(included).path]), ); expect(command.checkedPackages, isNot(contains(excluded.path))); expect( output, containsAllInOrder([ '${_startHeadingColor}Running for a_package...$_endColor', '${_startHeadingColor}Running for a_plugin...$_endColor', '$_startSkipColor SKIPPING: Does not support Flutter 2.5.0$_endColor', ]), ); }); test('skips unsupported Dart versions when requested', () async { final RepositoryPackage excluded = createFakePackage( 'excluded_package', packagesDir, dartConstraint: '>=2.18.0 <4.0.0', ); final RepositoryPackage included = createFakePackage( 'a_package', packagesDir, ); final TestPackageLoopingCommand command = createTestCommand( packageLoopingType: PackageLoopingType.includeAllSubpackages, hasLongOutput: false, ); final List output = await runCommand( command, arguments: [ '--skip-if-not-supporting-flutter-version=3.0.0', // Flutter 3.0.0 -> Dart 2.17.0 ], ); expect( command.checkedPackages, unorderedEquals([included.path, getExampleDir(included).path]), ); expect(command.checkedPackages, isNot(contains(excluded.path))); expect( output, containsAllInOrder([ '${_startHeadingColor}Running for a_package...$_endColor', '${_startHeadingColor}Running for excluded_package...$_endColor', '$_startSkipColor SKIPPING: Does not support Dart 2.17.0$_endColor', ]), ); }); }); group('output', () { test('has the expected package headers for long-form output', () async { createFakePlugin('package_a', packagesDir); createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand(command); const separator = '============================================================'; expect( output, containsAllInOrder([ '$_startHeadingColor\n$separator\n|| Running for package_a\n$separator\n$_endColor', '$_startHeadingColor\n$separator\n|| Running for package_b\n$separator\n$_endColor', ]), ); }); test('has the expected package headers for short-form output', () async { createFakePlugin('package_a', packagesDir); createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand(command); expect( output, containsAllInOrder([ '${_startHeadingColor}Running for package_a...$_endColor', '${_startHeadingColor}Running for package_b...$_endColor', ]), ); }); test('prints timing info in long-form output when requested', () async { createFakePlugin('package_a', packagesDir); createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand( command, arguments: ['--log-timing'], ); const separator = '============================================================'; expect( output, containsAllInOrder([ '$_startHeadingColor\n$separator\n|| Running for package_a [@0:00]\n$separator\n$_endColor', '$_startElapsedTimeColor\n[package_a completed in 0m 0s]$_endColor', '$_startHeadingColor\n$separator\n|| Running for package_b [@0:00]\n$separator\n$_endColor', '$_startElapsedTimeColor\n[package_b completed in 0m 0s]$_endColor', ]), ); }); test('prints timing info in short-form output when requested', () async { createFakePlugin('package_a', packagesDir); createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand( command, arguments: ['--log-timing'], ); expect( output, containsAllInOrder([ '$_startHeadingColor[0:00] Running for package_a...$_endColor', '$_startHeadingColor[0:00] Running for package_b...$_endColor', ]), ); // Short-form output should not include elapsed time. expect(output, isNot(contains('[package_a completed in 0m 0s]'))); }); test('shows the success message when nothing fails', () async { createFakePackage('package_a', packagesDir); createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand(command); expect( output, containsAllInOrder([ '\n', '${_startSuccessColor}No issues found!$_endColor', ]), ); }); test( 'shows failure summaries when something fails without extra details', () async { createFakePackage('package_a', packagesDir); final RepositoryPackage failingPackage1 = createFakePlugin( 'package_b', packagesDir, ); createFakePackage('package_c', packagesDir); final RepositoryPackage failingPackage2 = createFakePlugin( 'package_d', packagesDir, ); _addResultFile(failingPackage1, _ResultFileType.errors); _addResultFile(failingPackage2, _ResultFileType.errors); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); Error? commandError; final List output = await runCommand( command, errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ '\n', '${_startErrorColor}The following packages had errors:$_endColor', '$_startErrorColor package_b$_endColor', '$_startErrorColor package_d$_endColor', '${_startErrorColor}See above for full details.$_endColor', ]), ); }, ); test('uses custom summary header and footer if provided', () async { createFakePackage('package_a', packagesDir); final RepositoryPackage failingPackage1 = createFakePlugin( 'package_b', packagesDir, ); createFakePackage('package_c', packagesDir); final RepositoryPackage failingPackage2 = createFakePlugin( 'package_d', packagesDir, ); _addResultFile(failingPackage1, _ResultFileType.errors); _addResultFile(failingPackage2, _ResultFileType.errors); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, customFailureListHeader: 'This is a custom header', customFailureListFooter: 'And a custom footer!', ); Error? commandError; final List output = await runCommand( command, errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ '\n', '${_startErrorColor}This is a custom header$_endColor', '$_startErrorColor package_b$_endColor', '$_startErrorColor package_d$_endColor', '${_startErrorColor}And a custom footer!$_endColor', ]), ); }); test( 'shows failure summaries when something fails with extra details', () async { createFakePackage('package_a', packagesDir); final RepositoryPackage failingPackage1 = createFakePlugin( 'package_b', packagesDir, ); createFakePackage('package_c', packagesDir); final RepositoryPackage failingPackage2 = createFakePlugin( 'package_d', packagesDir, ); _addResultFile( failingPackage1, _ResultFileType.errors, contents: 'just one detail', ); _addResultFile( failingPackage2, _ResultFileType.errors, contents: 'first detail\nsecond detail', ); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); Error? commandError; final List output = await runCommand( command, errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ '\n', '${_startErrorColor}The following packages had errors:$_endColor', '$_startErrorColor package_b:\n just one detail$_endColor', '$_startErrorColor package_d:\n first detail\n second detail$_endColor', '${_startErrorColor}See above for full details.$_endColor', ]), ); }, ); test('is captured, not printed, when requested', () async { createFakePlugin('package_a', packagesDir); createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = createTestCommand( captureOutput: true, ); final List output = await runCommand(command); expect(output, isEmpty); // None of the output should be colorized when captured. const separator = '============================================================'; expect( command.capturedOutput, containsAllInOrder([ '\n$separator\n|| Running for package_a\n$separator\n', '\n$separator\n|| Running for package_b\n$separator\n', 'No issues found!', ]), ); }); test('logs skips', () async { createFakePackage('package_a', packagesDir); final RepositoryPackage skipPackage = createFakePackage( 'package_b', packagesDir, ); _addResultFile( skipPackage, _ResultFileType.skips, contents: 'For a reason', ); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand(command); expect( output, containsAllInOrder([ '${_startHeadingColor}Running for package_a...$_endColor', '${_startHeadingColor}Running for package_b...$_endColor', '$_startSkipColor SKIPPING: For a reason$_endColor', ]), ); }); test('logs exclusions', () async { createFakePackage('package_a', packagesDir); createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand( command, arguments: ['--exclude=package_b'], ); expect( output, containsAllInOrder([ '${_startHeadingColor}Running for package_a...$_endColor', '${_startSkipColor}Not running for package_b; excluded$_endColor', ]), ); }); test('logs warnings', () async { final RepositoryPackage warnPackage = createFakePackage( 'package_a', packagesDir, ); _addResultFile( warnPackage, _ResultFileType.warns, contents: 'Warning 1\nWarning 2', ); createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand(command); expect( output, containsAllInOrder([ '${_startHeadingColor}Running for package_a...$_endColor', '${_startWarningColor}Warning 1$_endColor', '${_startWarningColor}Warning 2$_endColor', '${_startHeadingColor}Running for package_b...$_endColor', ]), ); }); test('logs unhandled exceptions as errors', () async { createFakePackage('package_a', packagesDir); final RepositoryPackage failingPackage = createFakePlugin( 'package_b', packagesDir, ); createFakePackage('package_c', packagesDir); _addResultFile(failingPackage, _ResultFileType.throws); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); Error? commandError; final List output = await runCommand( command, errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ '${_startErrorColor}Exception: Uh-oh$_endColor', '${_startErrorColor}The following packages had errors:$_endColor', '$_startErrorColor package_b:\n Unhandled exception$_endColor', ]), ); }); test('prints run summary on success', () async { final RepositoryPackage warnPackage1 = createFakePackage( 'package_a', packagesDir, ); _addResultFile( warnPackage1, _ResultFileType.warns, contents: 'Warning 1\nWarning 2', ); createFakePackage('package_b', packagesDir); final RepositoryPackage skipPackage = createFakePackage( 'package_c', packagesDir, ); _addResultFile( skipPackage, _ResultFileType.skips, contents: 'For a reason', ); final RepositoryPackage skipAndWarnPackage = createFakePackage( 'package_d', packagesDir, ); _addResultFile( skipAndWarnPackage, _ResultFileType.warns, contents: 'Warning', ); _addResultFile( skipAndWarnPackage, _ResultFileType.skips, contents: 'See warning', ); final RepositoryPackage warnPackage2 = createFakePackage( 'package_e', packagesDir, ); _addResultFile( warnPackage2, _ResultFileType.warns, contents: 'Warning 1\nWarning 2', ); createFakePackage('package_f', packagesDir); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand(command); expect( output, containsAllInOrder([ '------------------------------------------------------------', 'Ran for 4 package(s) (2 with warnings)', 'Skipped 2 package(s) (1 with warnings)', '\n', '${_startSuccessColor}No issues found!$_endColor', ]), ); // The long-form summary should not be printed for short-form commands. expect(output, isNot(contains('Run summary:'))); expect(output, isNot(contains(contains('package a - ran')))); }); test('counts exclusions as skips in run summary', () async { createFakePackage('package_a', packagesDir); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, ); final List output = await runCommand( command, arguments: ['--exclude=package_a'], ); expect( output, containsAllInOrder([ '------------------------------------------------------------', 'Skipped 1 package(s)', '\n', '${_startSuccessColor}No issues found!$_endColor', ]), ); }); test('prints long-form run summary for long-output commands', () async { final RepositoryPackage warnPackage1 = createFakePackage( 'package_a', packagesDir, ); _addResultFile( warnPackage1, _ResultFileType.warns, contents: 'Warning 1\nWarning 2', ); createFakePackage('package_b', packagesDir); final RepositoryPackage skipPackage = createFakePackage( 'package_c', packagesDir, ); _addResultFile( skipPackage, _ResultFileType.skips, contents: 'For a reason', ); final RepositoryPackage skipAndWarnPackage = createFakePackage( 'package_d', packagesDir, ); _addResultFile( skipAndWarnPackage, _ResultFileType.warns, contents: 'Warning', ); _addResultFile( skipAndWarnPackage, _ResultFileType.skips, contents: 'See warning', ); final RepositoryPackage warnPackage2 = createFakePackage( 'package_e', packagesDir, ); _addResultFile( warnPackage2, _ResultFileType.warns, contents: 'Warning 1\nWarning 2', ); createFakePackage('package_f', packagesDir); final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand(command); expect( output, containsAllInOrder([ '------------------------------------------------------------', 'Run overview:', ' package_a - ${_startWarningColor}ran (with warning)$_endColor', ' package_b - ${_startSuccessColor}ran$_endColor', ' package_c - ${_startSkipColor}skipped$_endColor', ' package_d - ${_startSkipWithWarningColor}skipped (with warning)$_endColor', ' package_e - ${_startWarningColor}ran (with warning)$_endColor', ' package_f - ${_startSuccessColor}ran$_endColor', '', 'Ran for 4 package(s) (2 with warnings)', 'Skipped 2 package(s) (1 with warnings)', '\n', '${_startSuccessColor}No issues found!$_endColor', ]), ); }); test('prints exclusions as skips in long-form run summary', () async { createFakePackage('package_a', packagesDir); final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand( command, arguments: ['--exclude=package_a'], ); expect( output, containsAllInOrder([ ' package_a - ${_startSkipColor}excluded$_endColor', '', 'Skipped 1 package(s)', '\n', '${_startSuccessColor}No issues found!$_endColor', ]), ); }); test('handles warnings outside of runForPackage', () async { createFakePackage('package_a', packagesDir); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, warnsDuringInit: true, ); final List output = await runCommand(command); expect( output, containsAllInOrder([ '${_startWarningColor}Warning during initializeRun$_endColor', '${_startHeadingColor}Running for package_a...$_endColor', '${_startWarningColor}Warning during completeRun$_endColor', '------------------------------------------------------------', 'Ran for 1 package(s)', '2 warnings not associated with a package', '\n', '${_startSuccessColor}No issues found!$_endColor', ]), ); }); }); } class TestPackageLoopingCommand extends PackageLoopingCommand { TestPackageLoopingCommand( super.packagesDir, { required super.platform, this.hasLongOutput = true, this.packageLoopingType = PackageLoopingType.topLevelOnly, this.customFailureListHeader, this.customFailureListFooter, this.failsDuringInit = false, this.warnsDuringInit = false, this.captureOutput = false, super.processRunner, super.gitDir, }); final List checkedPackages = []; final List capturedOutput = []; final String? customFailureListHeader; final String? customFailureListFooter; final bool failsDuringInit; final bool warnsDuringInit; @override bool hasLongOutput; @override PackageLoopingType packageLoopingType; @override String get failureListHeader => customFailureListHeader ?? super.failureListHeader; @override String get failureListFooter => customFailureListFooter ?? super.failureListFooter; @override bool captureOutput; @override final String name = 'loop-test'; @override final String description = 'sample package looping command'; @override bool shouldIgnoreFile(String path) { return path.startsWith('skip/'); } @override Future initializeRun() async { if (warnsDuringInit) { logWarning('Warning during initializeRun'); } if (failsDuringInit) { throw ToolExit(2); } } @override Future runForPackage(RepositoryPackage package) async { checkedPackages.add(package.path); final File warningFile = package.directory.childFile(_warningFile); if (warningFile.existsSync()) { final List warnings = warningFile.readAsLinesSync(); warnings.forEach(logWarning); } final File skipFile = package.directory.childFile(_skipFile); if (skipFile.existsSync()) { return PackageResult.skip(skipFile.readAsStringSync()); } final File errorFile = package.directory.childFile(_errorFile); if (errorFile.existsSync()) { return PackageResult.fail(errorFile.readAsLinesSync()); } final File throwFile = package.directory.childFile(_throwFile); if (throwFile.existsSync()) { throw Exception('Uh-oh'); } return PackageResult.success(); } @override Future completeRun() async { if (warnsDuringInit) { logWarning('Warning during completeRun'); } } @override Future handleCapturedOutput(List output) async { capturedOutput.addAll(output); } }