mirror of
https://github.com/flutter/packages.git
synced 2025-06-09 14:19:08 +08:00
[flutter_plugin_tools] Support format on Windows (#4150)
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:
script/tool
@ -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),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user