diff --git a/.ci/targets/dart_unit_tests.yaml b/.ci/targets/dart_unit_tests.yaml index 6c5d6c4288d6..291c4e1c1102 100644 --- a/.ci/targets/dart_unit_tests.yaml +++ b/.ci/targets/dart_unit_tests.yaml @@ -11,3 +11,6 @@ tasks: - name: Dart unit tests - pathified script: .ci/scripts/dart_unit_tests_pathified.sh args: ["--platform=vm"] + - name: Dart fix tests + script: .ci/scripts/tool_runner.sh + args: ["test-dart-fixes"] diff --git a/packages/go_router/tool/run_tests.dart b/packages/go_router/tool/run_tests.dart deleted file mode 100644 index ca09bed0f7cd..000000000000 --- a/packages/go_router/tool/run_tests.dart +++ /dev/null @@ -1,101 +0,0 @@ -// 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. - -// Called from the custom-tests CI action. -// -// usage: dart run tool/run_tests.dart - -// ignore_for_file: avoid_print - -import 'dart:async'; -import 'dart:io'; -import 'package:io/io.dart' as io; -import 'package:path/path.dart' as p; - -// This test runner simulates a consumption of go_router that checks if -// the dart fixes are applied correctly. -// This is done by copying the `test_fixes/` directory to a temp directory -// that references `go_router`, and running `dart fix --compare-to-golden` -// on the temp directory. -Future main(List args) async { - final Directory goRouterPackageRoot = File.fromUri(Platform.script).parent.parent; - - final Directory testTempDir = await Directory.systemTemp.createTemp(); - - // Cleans up the temp directory and exits with a given statusCode. - Future cleanUpAndExit(int statusCode) async { - await testTempDir.delete(recursive: true); - exit(statusCode); - } - - // Copy the test_fixes folder to the temporary testFixesTargetDir. - // - // This also creates the proper pubspec.yaml in the temp directory. - await _prepareTemplate(packageRoot: goRouterPackageRoot, testTempDir: testTempDir); - - // Run dart pub get in the temp directory to set it up. - final int pubGetStatusCode = await _runProcess('dart', [ - 'pub', - 'get', - ], workingDirectory: testTempDir.path); - - if (pubGetStatusCode != 0) { - await cleanUpAndExit(pubGetStatusCode); - } - - // Run dart fix --compare-to-golden in the temp directory. - final int dartFixStatusCode = await _runProcess('dart', [ - 'fix', - '--compare-to-golden', - ], workingDirectory: testTempDir.path); - - await cleanUpAndExit(dartFixStatusCode); -} - -Future _prepareTemplate({ - required Directory packageRoot, - required Directory testTempDir, -}) async { - // The src test_fixes directory. - final testFixesSrcDir = Directory(p.join(packageRoot.path, 'test_fixes')); - - // Copy from src `test_fixes/` to the temp directory. - await io.copyPath(testFixesSrcDir.path, testTempDir.path); - - // The pubspec.yaml file to create. - final targetPubspecFile = File(p.join(testTempDir.path, 'pubspec.yaml')); - - final targetYaml = - ''' -name: test_fixes -publish_to: "none" -version: 1.0.0 - -environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" - -dependencies: - flutter: - sdk: flutter - go_router: - path: ${packageRoot.path} -'''; - - await targetPubspecFile.writeAsString(targetYaml); -} - -Future _streamOutput(Future processFuture) async { - final Process process = await processFuture; - unawaited(stdout.addStream(process.stdout)); - unawaited(stderr.addStream(process.stderr)); - return process; -} - -Future _runProcess(String command, List arguments, {String? workingDirectory}) async { - final Process process = await _streamOutput( - Process.start(command, arguments, workingDirectory: workingDirectory), - ); - return process.exitCode; -} diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index 92f775917c1f..861058af0e18 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -74,6 +74,9 @@ class RepositoryPackage { /// The test directory containing the package's Dart tests. Directory get testDirectory => directory.childDirectory('test'); + /// The test directory containing the tests for the package's dart fixes. + Directory get dartFixTestDirectory => directory.childDirectory('test_fixes'); + /// The path to the script that is run by the `custom-test` command. File get customTestScript => directory.childDirectory('tool').childFile('run_tests.dart'); diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 89c7bf9525b0..48bc4bf4f6bb 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -30,6 +30,7 @@ import 'podspec_check_command.dart'; import 'publish_check_command.dart'; import 'publish_command.dart'; import 'remove_dev_dependencies_command.dart'; +import 'test_dart_fixes_command.dart'; import 'update_dependency_command.dart'; import 'update_excerpts_command.dart'; import 'update_min_sdk_command.dart'; @@ -75,6 +76,7 @@ void main(List args) { ..addCommand(PublishCheckCommand(packagesDir)) ..addCommand(PublishCommand(packagesDir)) ..addCommand(RemoveDevDependenciesCommand(packagesDir)) + ..addCommand(TestDartFixesCommand(packagesDir)) ..addCommand(UpdateDependencyCommand(packagesDir)) ..addCommand(UpdateExcerptsCommand(packagesDir)) ..addCommand(UpdateMinSdkCommand(packagesDir)) diff --git a/script/tool/lib/src/test_dart_fixes_command.dart b/script/tool/lib/src/test_dart_fixes_command.dart new file mode 100644 index 000000000000..53d2e771dd2c --- /dev/null +++ b/script/tool/lib/src/test_dart_fixes_command.dart @@ -0,0 +1,146 @@ +// 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 'package:file/file.dart'; +import 'package:meta/meta.dart'; + +import 'common/file_filters.dart'; +import 'common/package_looping_command.dart'; +import 'common/pub_utils.dart'; +import 'common/repository_package.dart'; + +/// A command to run dart fix tests for packages that have a test_fixes +/// directory. +class TestDartFixesCommand extends PackageLoopingCommand { + /// Creates an instance of the test dart fixes command. + TestDartFixesCommand(super.packagesDir, {super.processRunner, super.platform, super.gitDir}); + + /// A map of the test directory used for each package passed to runForPackage. + @visibleForTesting + final testDirectories = {}; + + @override + final String name = 'test-dart-fixes'; + + @override + List get aliases => []; + + @override + final String description = + 'Runs the Dart fix tests for all packages.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + PackageLoopingType get packageLoopingType => PackageLoopingType.topLevelOnly; + + @override + bool shouldIgnoreFile(String path) { + return isRepoLevelNonCodeImpactingFile(path) || + isNativeCodeFile(path) || + isPackageSupportFile(path); + } + + @override + Future runForPackage(RepositoryPackage package) async { + // Only run for packages that have a fix_tests directory. + if (!package.dartFixTestDirectory.existsSync()) { + return PackageResult.skip('No ${package.dartFixTestDirectory} directory.'); + } + + final Directory testDirectory; + try { + testDirectory = await _createTestDirectory(package); + } catch (error) { + return PackageResult.fail(['Failed to create temporary test directory: $error']); + } + + PackageResult result; + try { + final bool success = await _runDartFixTests(testDirectory); + if (!success) { + throw Exception('Failed to run dart fix tests.'); + } + result = PackageResult.success(); + } catch (error) { + result = PackageResult.fail(['Dart fix tests failed: $error']); + } + if (testDirectory.existsSync()) { + await testDirectory.delete(recursive: true); + } + testDirectories[package.displayName] = testDirectory; + return result; + } + + /// Create and prepare a temporary directory in which to run the dart fix + /// tests. + /// + /// It is the responsibility of the caller to delete this directory and its + /// contents when done. + static Future _createTestDirectory(RepositoryPackage package) async { + final FileSystem fileSystem = package.directory.fileSystem; + final Directory testDirectory = await fileSystem.systemTempDirectory.createTemp(); + + // Copy from `test_fixes/` to the temp directory. + for (final FileSystemEntity entity in package.dartFixTestDirectory.listSync(recursive: true)) { + final String relativePath = fileSystem.path.relative( + entity.path, + from: package.dartFixTestDirectory.path, + ); + final String destPath = fileSystem.path.join(testDirectory.path, relativePath); + if (entity is Directory) { + fileSystem.directory(destPath).createSync(recursive: true); + } else if (entity is File) { + fileSystem.file(destPath).parent.createSync(recursive: true); + entity.copySync(destPath); + } + } + + // The pubspec.yaml file to create. + final File targetPubspecFile = fileSystem.file( + fileSystem.path.join(testDirectory.path, 'pubspec.yaml'), + ); + + final targetYaml = + ''' +name: test_fixes +publish_to: "none" +version: 1.0.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + ${package.directory.basename}: + path: ${package.directory.path} +'''; + + await targetPubspecFile.writeAsString(targetYaml); + return testDirectory; + } + + /// Run the dart fix tests for the package in the given temporary directory. + /// + /// Resolves with the status code of the command. + Future _runDartFixTests(Directory testDirectory) async { + // Run flutter pub get in the temp directory to set it up. + final bool success = await runPubGet(RepositoryPackage(testDirectory), processRunner, platform); + + if (!success) { + return success; + } + + // Run dart fix --compare-to-golden in the temp directory. + final int exitCode = await processRunner.runAndStream('dart', [ + 'fix', + '--compare-to-golden', + ], workingDir: testDirectory); + + return exitCode == 0; + } +} diff --git a/script/tool/test/test_dart_fixes_command_test.dart b/script/tool/test/test_dart_fixes_command_test.dart new file mode 100644 index 000000000000..7794a3a261e4 --- /dev/null +++ b/script/tool/test/test_dart_fixes_command_test.dart @@ -0,0 +1,160 @@ +// 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 '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/test_dart_fixes_command.dart'; +import 'package:git/git.dart'; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('TestDartFixesCommand', () { + late Platform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late TestDartFixesCommand command; + + setUp(() { + mockPlatform = MockPlatform(); + final GitDir gitDir; + (:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = configureBaseCommandMocks( + platform: mockPlatform, + ); + command = TestDartFixesCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: gitDir, + ); + + runner = CommandRunner('test_dart_fixes', 'Test for $TestDartFixesCommand'); + runner.addCommand(command); + }); + + test('runs on each package with a test_fixes directory', () async { + final RepositoryPackage package1 = createFakePackage( + 'package1', + packagesDir, + examples: [], + extraFiles: ['test_fixes/empty.dart', 'test_fixes/empty.dart.expect'], + ); + final RepositoryPackage package2 = createFakePackage( + 'package2', + packagesDir, + examples: [], + extraFiles: ['test_fixes/empty.dart', 'test_fixes/empty.dart.expect'], + ); + createFakePackage('package3', packagesDir, examples: []); + + final List output = await runCapturingPrint(runner, ['test-dart-fixes']); + + expect( + output, + containsAllInOrder([ + contains('Running for package1'), + contains('Running for package2'), + contains('Running for package3'), + contains("SKIPPING: No MemoryDirectory: '/packages/package3/test_fixes' directory."), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), const [ + 'pub', + 'get', + ], command.testDirectories[package1.displayName]!.path), + ProcessCall('dart', const [ + 'fix', + '--compare-to-golden', + ], command.testDirectories[package1.displayName]!.path), + ProcessCall(getFlutterCommand(mockPlatform), const [ + 'pub', + 'get', + ], command.testDirectories[package2.displayName]!.path), + ProcessCall('dart', const [ + 'fix', + '--compare-to-golden', + ], command.testDirectories[package2.displayName]!.path), + ]), + ); + }); + + test('fails when dart fix test fails', () async { + final RepositoryPackage packageThatFails = createFakePackage( + 'package1', + packagesDir, + examples: [], + extraFiles: ['test_fixes/fix.dart', 'test_fixes/fix.dart.expect'], + ); + packageThatFails.dartFixTestDirectory.childFile('fix.dart').writeAsStringSync('a'); + packageThatFails.dartFixTestDirectory.childFile('fix.dart.expect').writeAsStringSync('b'); + + final RepositoryPackage packageThatSucceeds = createFakePackage( + 'package2', + packagesDir, + examples: [], + extraFiles: ['test_fixes/fix.dart', 'test_fixes/fix.dart.expect'], + ); + packageThatSucceeds.dartFixTestDirectory.childFile('fix.dart').writeAsStringSync('c'); + packageThatSucceeds.dartFixTestDirectory.childFile('fix.dart.expect').writeAsStringSync('c'); + + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo(_MockDartFixProcess(processRunner, packagesDir.fileSystem), const [ + 'fix', + '--compare-to-golden', + ]), + FakeProcessInfo(_MockDartFixProcess(processRunner, packagesDir.fileSystem), const [ + 'fix', + '--compare-to-golden', + ]), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['test-dart-fixes'], + errorHandler: (e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' package1'), + isNot(contains('package2')), + ]), + ); + }); + }); +} + +/// Fails when fix.dart does not equal fix.dart.expect. +class _MockDartFixProcess extends MockProcess { + _MockDartFixProcess(this.processRunner, this.fileSystem); + final RecordingProcessRunner processRunner; + final FileSystem fileSystem; + + @override + Future get exitCode async { + final ProcessCall call = processRunner.recordedCalls.last; + final String? workingDir = call.workingDir; + final File fixDart = fileSystem.file(fileSystem.path.join(workingDir!, 'fix.dart')); + final File fixExpect = fileSystem.file(fileSystem.path.join(workingDir, 'fix.dart.expect')); + if (!fixDart.existsSync() || !fixExpect.existsSync()) { + return 1; + } + return fixDart.readAsStringSync() == fixExpect.readAsStringSync() ? 0 : 1; + } +}