| // 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<void> runner; |
| late String javaFormatPath; |
| |
| setUp(() { |
| fileSystem = MemoryFileSystem(); |
| mockPlatform = MockPlatform(); |
| packagesDir = createPackagesDirectory(fileSystem: fileSystem); |
| processRunner = RecordingProcessRunner(); |
| analyzeCommand = FormatCommand( |
| packagesDir, |
| processRunner: processRunner, |
| platform: mockPlatform, |
| ); |
| |
| // Create the java formatter file 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); |
| |
| runner = CommandRunner<void>('format_command', 'Test for format_command'); |
| runner.addCommand(analyzeCommand); |
| }); |
| |
| /// Returns a modified version of a list of [relativePaths] that are relative |
| /// to [package] to instead be relative to [packagesDir]. |
| List<String> getPackagesDirRelativePaths( |
| RepositoryPackage package, List<String> 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] with name [packageName] such that each path will |
| /// be 99 characters long relative to [packagesDir]. |
| /// |
| /// This is for each of testing batching, since it means each file will |
| /// consume 100 characters of the batch length. |
| List<String> get99CharacterPathExtraFiles(String packageName, int count) { |
| final int padding = 99 - |
| packageName.length - |
| 1 - // the path separator after the package name |
| 1 - // the path separator after the padding |
| 10; // the file name |
| const int filenameBase = 10000; |
| |
| final p.Context path = analyzeCommand.path; |
| return <String>[ |
| for (int i = filenameBase; i < filenameBase + count; ++i) |
| path.join('a' * padding, '$i.dart'), |
| ]; |
| } |
| |
| test('formats .dart files', () async { |
| const List<String> files = <String>[ |
| 'lib/a.dart', |
| 'lib/src/b.dart', |
| 'lib/src/c.dart', |
| ]; |
| final RepositoryPackage plugin = createFakePlugin( |
| 'a_plugin', |
| packagesDir, |
| extraFiles: files, |
| ); |
| |
| await runCapturingPrint(runner, <String>['format']); |
| |
| expect( |
| processRunner.recordedCalls, |
| orderedEquals(<ProcessCall>[ |
| ProcessCall( |
| 'dart', |
| <String>['format', ...getPackagesDirRelativePaths(plugin, files)], |
| packagesDir.path), |
| ])); |
| }); |
| |
| test('does not format .dart files with pragma', () async { |
| const List<String> formattedFiles = <String>[ |
| '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: <String>[ |
| ...formattedFiles, |
| unformattedFile, |
| ], |
| ); |
| |
| 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, <String>['format']); |
| |
| expect( |
| processRunner.recordedCalls, |
| orderedEquals(<ProcessCall>[ |
| ProcessCall( |
| 'dart', |
| <String>[ |
| 'format', |
| ...getPackagesDirRelativePaths(plugin, formattedFiles) |
| ], |
| packagesDir.path), |
| ])); |
| }); |
| |
| test('fails if dart format fails', () async { |
| const List<String> files = <String>[ |
| 'lib/a.dart', |
| 'lib/src/b.dart', |
| 'lib/src/c.dart', |
| ]; |
| createFakePlugin('a_plugin', packagesDir, extraFiles: files); |
| |
| processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['format']) |
| ]; |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['format'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Failed to format Dart files: exit code 1.'), |
| ])); |
| }); |
| |
| test('formats .java files', () async { |
| const List<String> files = <String>[ |
| '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, |
| ); |
| |
| await runCapturingPrint(runner, <String>['format']); |
| |
| expect( |
| processRunner.recordedCalls, |
| orderedEquals(<ProcessCall>[ |
| const ProcessCall('java', <String>['-version'], null), |
| ProcessCall( |
| 'java', |
| <String>[ |
| '-jar', |
| javaFormatPath, |
| '--replace', |
| ...getPackagesDirRelativePaths(plugin, files) |
| ], |
| packagesDir.path), |
| ])); |
| }); |
| |
| test('fails with a clear message if Java is not in the path', () async { |
| const List<String> files = <String>[ |
| 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', |
| 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', |
| ]; |
| createFakePlugin('a_plugin', packagesDir, extraFiles: files); |
| |
| processRunner.mockProcessesForExecutable['java'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['-version']) |
| ]; |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['format'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'Unable to run "java". Make sure that it is in your path, or ' |
| 'provide a full path with --java.'), |
| ])); |
| }); |
| |
| test('fails if Java formatter fails', () async { |
| const List<String> files = <String>[ |
| 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', |
| 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', |
| ]; |
| createFakePlugin('a_plugin', packagesDir, extraFiles: files); |
| |
| processRunner.mockProcessesForExecutable['java'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess(), <String>['-version']), // check for working java |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['-jar']), // format |
| ]; |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['format'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Failed to format Java files: exit code 1.'), |
| ])); |
| }); |
| |
| test('honors --java flag', () async { |
| const List<String> files = <String>[ |
| '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, |
| ); |
| |
| await runCapturingPrint(runner, <String>['format', '--java=/path/to/java']); |
| |
| expect( |
| processRunner.recordedCalls, |
| orderedEquals(<ProcessCall>[ |
| const ProcessCall('/path/to/java', <String>['--version'], null), |
| ProcessCall( |
| '/path/to/java', |
| <String>[ |
| '-jar', |
| javaFormatPath, |
| '--replace', |
| ...getPackagesDirRelativePaths(plugin, files) |
| ], |
| packagesDir.path), |
| ])); |
| }); |
| |
| test('formats c-ish files', () async { |
| const List<String> files = <String>[ |
| '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, |
| ); |
| |
| await runCapturingPrint(runner, <String>['format']); |
| |
| expect( |
| processRunner.recordedCalls, |
| orderedEquals(<ProcessCall>[ |
| const ProcessCall('clang-format', <String>['--version'], null), |
| ProcessCall( |
| 'clang-format', |
| <String>[ |
| '-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<String> files = <String>[ |
| 'linux/foo_plugin.cc', |
| 'macos/Classes/Foo.h', |
| ]; |
| createFakePlugin('a_plugin', packagesDir, extraFiles: files); |
| |
| processRunner.mockProcessesForExecutable['clang-format'] = |
| <FakeProcessInfo>[FakeProcessInfo(MockProcess(exitCode: 1))]; |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['format'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Unable to run "clang-format". Make sure that it is in your ' |
| 'path, or provide a full path with --clang-format.'), |
| ])); |
| }); |
| |
| test('falls back to working clang-format in the path', () async { |
| const List<String> files = <String>[ |
| 'linux/foo_plugin.cc', |
| 'macos/Classes/Foo.h', |
| ]; |
| final RepositoryPackage plugin = createFakePlugin( |
| 'a_plugin', |
| packagesDir, |
| extraFiles: files, |
| ); |
| |
| processRunner.mockProcessesForExecutable['clang-format'] = |
| <FakeProcessInfo>[FakeProcessInfo(MockProcess(exitCode: 1))]; |
| processRunner.mockProcessesForExecutable['which'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess( |
| stdout: |
| '/usr/local/bin/clang-format\n/path/to/working-clang-format'), |
| <String>['-a', 'clang-format']) |
| ]; |
| processRunner.mockProcessesForExecutable['/usr/local/bin/clang-format'] = |
| <FakeProcessInfo>[FakeProcessInfo(MockProcess(exitCode: 1))]; |
| await runCapturingPrint(runner, <String>['format']); |
| |
| expect( |
| processRunner.recordedCalls, |
| containsAll(<ProcessCall>[ |
| const ProcessCall( |
| '/path/to/working-clang-format', <String>['--version'], null), |
| ProcessCall( |
| '/path/to/working-clang-format', |
| <String>[ |
| '-i', |
| '--style=file', |
| ...getPackagesDirRelativePaths(plugin, files) |
| ], |
| packagesDir.path), |
| ])); |
| }); |
| |
| test('honors --clang-format flag', () async { |
| const List<String> files = <String>[ |
| 'windows/foo_plugin.cpp', |
| ]; |
| final RepositoryPackage plugin = createFakePlugin( |
| 'a_plugin', |
| packagesDir, |
| extraFiles: files, |
| ); |
| |
| await runCapturingPrint( |
| runner, <String>['format', '--clang-format=/path/to/clang-format']); |
| |
| expect( |
| processRunner.recordedCalls, |
| orderedEquals(<ProcessCall>[ |
| const ProcessCall( |
| '/path/to/clang-format', <String>['--version'], null), |
| ProcessCall( |
| '/path/to/clang-format', |
| <String>[ |
| '-i', |
| '--style=file', |
| ...getPackagesDirRelativePaths(plugin, files) |
| ], |
| packagesDir.path), |
| ])); |
| }); |
| |
| test('fails if clang-format fails', () async { |
| const List<String> files = <String>[ |
| 'linux/foo_plugin.cc', |
| 'macos/Classes/Foo.h', |
| ]; |
| createFakePlugin('a_plugin', packagesDir, extraFiles: files); |
| |
| processRunner.mockProcessesForExecutable['clang-format'] = |
| <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(), |
| <String>['--version']), // check for working clang-format |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['-i']), // format |
| ]; |
| Error? commandError; |
| final List<String> output = await runCapturingPrint( |
| runner, <String>['format'], errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains( |
| 'Failed to format C, C++, and Objective-C files: exit code 1.'), |
| ])); |
| }); |
| |
| test('skips known non-repo files', () async { |
| const List<String> skipFiles = <String>[ |
| '/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<String> clangFiles = <String>['ios/Classes/Foo.h']; |
| const List<String> dartFiles = <String>['lib/a.dart']; |
| const List<String> javaFiles = <String>[ |
| 'android/src/main/java/io/flutter/plugins/a_plugin/a.java' |
| ]; |
| final RepositoryPackage plugin = createFakePlugin( |
| 'a_plugin', |
| packagesDir, |
| extraFiles: <String>[ |
| ...skipFiles, |
| // Include some files that should be formatted to validate that it's |
| // correctly filtering even when running the commands. |
| ...clangFiles, |
| ...dartFiles, |
| ...javaFiles, |
| ], |
| ); |
| |
| await runCapturingPrint(runner, <String>['format']); |
| |
| expect( |
| processRunner.recordedCalls, |
| containsAll(<ProcessCall>[ |
| ProcessCall( |
| 'clang-format', |
| <String>[ |
| '-i', |
| '--style=file', |
| ...getPackagesDirRelativePaths(plugin, clangFiles) |
| ], |
| packagesDir.path), |
| ProcessCall( |
| 'dart', |
| <String>[ |
| 'format', |
| ...getPackagesDirRelativePaths(plugin, dartFiles) |
| ], |
| packagesDir.path), |
| ProcessCall( |
| 'java', |
| <String>[ |
| '-jar', |
| javaFormatPath, |
| '--replace', |
| ...getPackagesDirRelativePaths(plugin, javaFiles) |
| ], |
| packagesDir.path), |
| ])); |
| }); |
| |
| test('fails if files are changed with --fail-on-change', () async { |
| const List<String> files = <String>[ |
| 'linux/foo_plugin.cc', |
| 'macos/Classes/Foo.h', |
| ]; |
| createFakePlugin('a_plugin', packagesDir, extraFiles: files); |
| |
| const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; |
| processRunner.mockProcessesForExecutable['git'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess(stdout: changedFilePath), <String>['ls-files']), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['format', '--fail-on-change'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| 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 <<DONE'), |
| ])); |
| }); |
| |
| test('fails if git ls-files fails', () async { |
| const List<String> files = <String>[ |
| 'linux/foo_plugin.cc', |
| 'macos/Classes/Foo.h', |
| ]; |
| createFakePlugin('a_plugin', packagesDir, extraFiles: files); |
| |
| processRunner.mockProcessesForExecutable['git'] = <FakeProcessInfo>[ |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['ls-files']) |
| ]; |
| Error? commandError; |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['format', '--fail-on-change'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| contains('Unable to determine changed files.'), |
| ])); |
| }); |
| |
| test('reports git diff failures', () async { |
| const List<String> files = <String>[ |
| 'linux/foo_plugin.cc', |
| 'macos/Classes/Foo.h', |
| ]; |
| createFakePlugin('a_plugin', packagesDir, extraFiles: files); |
| |
| const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; |
| processRunner.mockProcessesForExecutable['git'] = <FakeProcessInfo>[ |
| FakeProcessInfo( |
| MockProcess(stdout: changedFilePath), <String>['ls-files']), |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['diff']), |
| ]; |
| |
| Error? commandError; |
| final List<String> output = |
| await runCapturingPrint(runner, <String>['format', '--fail-on-change'], |
| errorHandler: (Error e) { |
| commandError = e; |
| }); |
| |
| expect(commandError, isA<ToolExit>()); |
| expect( |
| output, |
| containsAllInOrder(<Matcher>[ |
| 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<String> batch1 = |
| get99CharacterPathExtraFiles(pluginName, batchSize + 1); |
| final String extraFile = batch1.removeLast(); |
| |
| createFakePlugin( |
| pluginName, |
| packagesDir, |
| extraFiles: <String>[...batch1, extraFile], |
| ); |
| |
| await runCapturingPrint(runner, <String>['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', |
| <String>[ |
| 'format', |
| '$pluginName\\$extraFile', |
| ], |
| packagesDir.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<String> batch = |
| get99CharacterPathExtraFiles(pluginName, batchSize + 1); |
| |
| createFakePlugin( |
| pluginName, |
| packagesDir, |
| extraFiles: batch, |
| ); |
| |
| await runCapturingPrint(runner, <String>['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<String> batch1 = |
| get99CharacterPathExtraFiles(pluginName, batchSize + 1); |
| final String extraFile = batch1.removeLast(); |
| |
| createFakePlugin( |
| pluginName, |
| packagesDir, |
| extraFiles: <String>[...batch1, extraFile], |
| ); |
| |
| await runCapturingPrint(runner, <String>['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', |
| <String>[ |
| 'format', |
| '$pluginName/$extraFile', |
| ], |
| packagesDir.path), |
| )); |
| }); |
| } |