1
0
mirror of https://github.com/flutter/packages.git synced 2025-06-09 14:19:08 +08:00

[flutter_plugin_tools] Support format on Windows ()

Allows `format` to run successfully on Windows:
- Ensures that no calls exceed the command length limit.
- Allows specifying a `java` path to make it easier to run without a system Java (e.g., by pointing to the `java` binary in an Android Studio installation).
- Adds clear error messages when `java` or `clang-format` is missing since it's very non-obvious what's wrong otherwise.

Bumps the version, which I intended to do in the previous PR but apparently didn't push to the PR.
This commit is contained in:
stuartmorgan
2021-07-12 17:57:39 -07:00
committed by GitHub
parent 77460f03f2
commit 5f7735d16c
4 changed files with 383 additions and 44 deletions

@ -19,8 +19,8 @@ void main() {
late FileSystem fileSystem;
late MockPlatform mockPlatform;
late Directory packagesDir;
late p.Context path;
late RecordingProcessRunner processRunner;
late FormatCommand analyzeCommand;
late CommandRunner<void> runner;
late String javaFormatPath;
@ -29,7 +29,7 @@ void main() {
mockPlatform = MockPlatform();
packagesDir = createPackagesDirectory(fileSystem: fileSystem);
processRunner = RecordingProcessRunner();
final FormatCommand analyzeCommand = FormatCommand(
analyzeCommand = FormatCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
@ -37,7 +37,7 @@ void main() {
// Create the java formatter file that the command checks for, to avoid
// a download.
path = analyzeCommand.path;
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);
@ -46,13 +46,39 @@ void main() {
runner.addCommand(analyzeCommand);
});
List<String> _getAbsolutePaths(
/// Returns a modified version of a list of [relativePaths] that are relative
/// to [package] to instead be relative to [packagesDir].
List<String> _getPackagesDirRelativePaths(
Directory 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(package.path, relativePath))
.map((String relativePath) => path.join(relativeBase, relativePath))
.toList();
}
/// Returns a list of [count] relative paths to pass to [createFakePlugin]
/// with name [pluginName] 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 pluginName, int count) {
final int padding = 99 -
pluginName.length -
1 - // the path separator after the plugin 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',
@ -71,8 +97,11 @@ void main() {
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'flutter',
<String>['format', ..._getAbsolutePaths(pluginDir, files)],
getFlutterCommand(mockPlatform),
<String>[
'format',
..._getPackagesDirRelativePaths(pluginDir, files)
],
packagesDir.path),
]));
});
@ -85,9 +114,8 @@ void main() {
];
createFakePlugin('a_plugin', packagesDir, extraFiles: files);
processRunner.mockProcessesForExecutable['flutter'] = <io.Process>[
MockProcess.failing()
];
processRunner.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] =
<io.Process>[MockProcess.failing()];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['format'], errorHandler: (Error e) {
@ -118,19 +146,20 @@ void main() {
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
const ProcessCall('java', <String>['--version'], null),
ProcessCall(
'java',
<String>[
'-jar',
javaFormatPath,
'--replace',
..._getAbsolutePaths(pluginDir, files)
..._getPackagesDirRelativePaths(pluginDir, files)
],
packagesDir.path),
]));
});
test('fails if Java formatter fails', () async {
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',
@ -146,6 +175,33 @@ void main() {
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'] = <io.Process>[
MockProcess.succeeding(), // check for working java
MockProcess.failing(), // format
];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['format'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
@ -154,6 +210,35 @@ void main() {
]));
});
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 Directory pluginDir = 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(pluginDir, files)
],
packagesDir.path),
]));
});
test('formats c-ish files', () async {
const List<String> files = <String>[
'ios/Classes/Foo.h',
@ -174,12 +259,69 @@ void main() {
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
const ProcessCall('clang-format', <String>['--version'], null),
ProcessCall(
'clang-format',
<String>[
'-i',
'--style=Google',
..._getAbsolutePaths(pluginDir, files)
..._getPackagesDirRelativePaths(pluginDir, 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'] = <io.Process>[
MockProcess.failing()
];
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('honors --clang-format flag', () async {
const List<String> files = <String>[
'windows/foo_plugin.cpp',
];
final Directory pluginDir = 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=Google',
..._getPackagesDirRelativePaths(pluginDir, files)
],
packagesDir.path),
]));
@ -193,7 +335,8 @@ void main() {
createFakePlugin('a_plugin', packagesDir, extraFiles: files);
processRunner.mockProcessesForExecutable['clang-format'] = <io.Process>[
MockProcess.failing()
MockProcess.succeeding(), // check for working clang-format
MockProcess.failing(), // format
];
Error? commandError;
final List<String> output = await runCapturingPrint(
@ -246,12 +389,15 @@ void main() {
<String>[
'-i',
'--style=Google',
..._getAbsolutePaths(pluginDir, clangFiles)
..._getPackagesDirRelativePaths(pluginDir, clangFiles)
],
packagesDir.path),
ProcessCall(
'flutter',
<String>['format', ..._getAbsolutePaths(pluginDir, dartFiles)],
getFlutterCommand(mockPlatform),
<String>[
'format',
..._getPackagesDirRelativePaths(pluginDir, dartFiles)
],
packagesDir.path),
ProcessCall(
'java',
@ -259,7 +405,7 @@ void main() {
'-jar',
javaFormatPath,
'--replace',
..._getAbsolutePaths(pluginDir, javaFiles)
..._getPackagesDirRelativePaths(pluginDir, javaFiles)
],
packagesDir.path),
]));
@ -348,4 +494,97 @@ void main() {
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(
getFlutterCommand(mockPlatform),
<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(
getFlutterCommand(mockPlatform),
<String>[
'format',
'$pluginName/$extraFile',
],
packagesDir.path),
));
});
}