mirror of
https://github.com/flutter/packages.git
synced 2025-08-06 17:28:42 +08:00

The purpose of this PR is to make running all unit tests on Windows pass (vs failing a large portion of the tests as currently happens). This does not mean that the commands actually work when run on Windows, or that Windows support is tested, only that it's possible to actually run the tests themselves. This is prep for actually supporting parts of the tool on Windows in future PRs. Major changes: - Make the tests significantly more hermetic: - Make almost all tools take a `Platform` constructor argument that can be used to inject a mock platform to control what OS the command acts like it is running on under test. - Add a path `Context` object to the base command, whose style matches the `Platform`, and use that almost everywhere instead of the top-level `path` functions. - In cases where Posix behavior is always required (such as parsing `git` output), explicitly use the `posix` context object for `path` functions. - Start laying the groundwork for actual Windows support: - Replace all uses of `flutter` as a command with a getter that returns `flutter` or `flutter.bat` as appropriate. - For user messages that include relative paths, use a helper that always uses Posix-style relative paths for consistent output. This bumps the version since quite a few changes have built up, and having a cut point before starting to make more changes to the commands to support Windows seems like a good idea. Part of https://github.com/flutter/flutter/issues/86113
371 lines
11 KiB
Dart
371 lines
11 KiB
Dart
// 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 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io' as io;
|
|
|
|
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/plugin_utils.dart';
|
|
import 'package:flutter_plugin_tools/src/common/process_runner.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:platform/platform.dart';
|
|
import 'package:quiver/collection.dart';
|
|
|
|
/// Returns the exe name that command will use when running Flutter on
|
|
/// [platform].
|
|
String getFlutterCommand(Platform platform) =>
|
|
platform.isWindows ? 'flutter.bat' : 'flutter';
|
|
|
|
/// Creates a packages directory in the given location.
|
|
///
|
|
/// If [parentDir] is set the packages directory will be created there,
|
|
/// otherwise [fileSystem] must be provided and it will be created an arbitrary
|
|
/// location in that filesystem.
|
|
Directory createPackagesDirectory(
|
|
{Directory? parentDir, FileSystem? fileSystem}) {
|
|
assert(parentDir != null || fileSystem != null,
|
|
'One of parentDir or fileSystem must be provided');
|
|
assert(fileSystem == null || fileSystem is MemoryFileSystem,
|
|
'If using a real filesystem, parentDir must be provided');
|
|
final Directory packagesDir =
|
|
(parentDir ?? fileSystem!.currentDirectory).childDirectory('packages');
|
|
packagesDir.createSync();
|
|
return packagesDir;
|
|
}
|
|
|
|
/// Creates a plugin package with the given [name] in [packagesDirectory].
|
|
///
|
|
/// [platformSupport] is a map of platform string to the support details for
|
|
/// that platform.
|
|
///
|
|
/// [extraFiles] is an optional list of plugin-relative paths, using Posix
|
|
/// separators, of extra files to create in the plugin.
|
|
Directory createFakePlugin(
|
|
String name,
|
|
Directory parentDirectory, {
|
|
List<String> examples = const <String>['example'],
|
|
List<String> extraFiles = const <String>[],
|
|
Map<String, PlatformSupport> platformSupport =
|
|
const <String, PlatformSupport>{},
|
|
String? version = '0.0.1',
|
|
}) {
|
|
final Directory pluginDirectory = createFakePackage(name, parentDirectory,
|
|
isFlutter: true,
|
|
examples: examples,
|
|
extraFiles: extraFiles,
|
|
version: version);
|
|
|
|
createFakePubspec(
|
|
pluginDirectory,
|
|
name: name,
|
|
isFlutter: true,
|
|
isPlugin: true,
|
|
platformSupport: platformSupport,
|
|
version: version,
|
|
);
|
|
|
|
return pluginDirectory;
|
|
}
|
|
|
|
/// Creates a plugin package with the given [name] in [packagesDirectory].
|
|
///
|
|
/// [extraFiles] is an optional list of package-relative paths, using unix-style
|
|
/// separators, of extra files to create in the package.
|
|
Directory createFakePackage(
|
|
String name,
|
|
Directory parentDirectory, {
|
|
List<String> examples = const <String>['example'],
|
|
List<String> extraFiles = const <String>[],
|
|
bool isFlutter = false,
|
|
String? version = '0.0.1',
|
|
}) {
|
|
final Directory packageDirectory = parentDirectory.childDirectory(name);
|
|
packageDirectory.createSync(recursive: true);
|
|
|
|
createFakePubspec(packageDirectory, name: name, isFlutter: isFlutter);
|
|
createFakeCHANGELOG(packageDirectory, '''
|
|
## $version
|
|
* Some changes.
|
|
''');
|
|
|
|
if (examples.length == 1) {
|
|
final Directory exampleDir = packageDirectory.childDirectory(examples.first)
|
|
..createSync();
|
|
createFakePubspec(exampleDir,
|
|
name: '${name}_example', isFlutter: isFlutter, publishTo: 'none');
|
|
} else if (examples.isNotEmpty) {
|
|
final Directory exampleDir = packageDirectory.childDirectory('example')
|
|
..createSync();
|
|
for (final String example in examples) {
|
|
final Directory currentExample = exampleDir.childDirectory(example)
|
|
..createSync();
|
|
createFakePubspec(currentExample,
|
|
name: example, isFlutter: isFlutter, publishTo: 'none');
|
|
}
|
|
}
|
|
|
|
final FileSystem fileSystem = packageDirectory.fileSystem;
|
|
final p.Context posixContext = p.posix;
|
|
for (final String file in extraFiles) {
|
|
final List<String> newFilePath = <String>[
|
|
packageDirectory.path,
|
|
...posixContext.split(file)
|
|
];
|
|
final File newFile = fileSystem.file(fileSystem.path.joinAll(newFilePath));
|
|
newFile.createSync(recursive: true);
|
|
}
|
|
|
|
return packageDirectory;
|
|
}
|
|
|
|
void createFakeCHANGELOG(Directory parent, String texts) {
|
|
parent.childFile('CHANGELOG.md').createSync();
|
|
parent.childFile('CHANGELOG.md').writeAsStringSync(texts);
|
|
}
|
|
|
|
/// Creates a `pubspec.yaml` file with a flutter dependency.
|
|
///
|
|
/// [platformSupport] is a map of platform string to the support details for
|
|
/// that platform. If empty, no `plugin` entry will be created unless `isPlugin`
|
|
/// is set to `true`.
|
|
void createFakePubspec(
|
|
Directory parent, {
|
|
String name = 'fake_package',
|
|
bool isFlutter = true,
|
|
bool isPlugin = false,
|
|
Map<String, PlatformSupport> platformSupport =
|
|
const <String, PlatformSupport>{},
|
|
String publishTo = 'http://no_pub_server.com',
|
|
String? version,
|
|
}) {
|
|
isPlugin |= platformSupport.isNotEmpty;
|
|
parent.childFile('pubspec.yaml').createSync();
|
|
String yaml = '''
|
|
name: $name
|
|
''';
|
|
if (isFlutter) {
|
|
if (isPlugin) {
|
|
yaml += '''
|
|
flutter:
|
|
plugin:
|
|
platforms:
|
|
''';
|
|
for (final MapEntry<String, PlatformSupport> platform
|
|
in platformSupport.entries) {
|
|
yaml += _pluginPlatformSection(platform.key, platform.value, name);
|
|
}
|
|
}
|
|
|
|
yaml += '''
|
|
dependencies:
|
|
flutter:
|
|
sdk: flutter
|
|
''';
|
|
}
|
|
if (version != null) {
|
|
yaml += '''
|
|
version: $version
|
|
''';
|
|
}
|
|
if (publishTo.isNotEmpty) {
|
|
yaml += '''
|
|
publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being published by a broken test.
|
|
''';
|
|
}
|
|
parent.childFile('pubspec.yaml').writeAsStringSync(yaml);
|
|
}
|
|
|
|
String _pluginPlatformSection(
|
|
String platform, PlatformSupport type, String packageName) {
|
|
if (type == PlatformSupport.federated) {
|
|
return '''
|
|
$platform:
|
|
default_package: ${packageName}_$platform
|
|
''';
|
|
}
|
|
switch (platform) {
|
|
case kPlatformAndroid:
|
|
return '''
|
|
android:
|
|
package: io.flutter.plugins.fake
|
|
pluginClass: FakePlugin
|
|
''';
|
|
case kPlatformIos:
|
|
return '''
|
|
ios:
|
|
pluginClass: FLTFakePlugin
|
|
''';
|
|
case kPlatformLinux:
|
|
return '''
|
|
linux:
|
|
pluginClass: FakePlugin
|
|
''';
|
|
case kPlatformMacos:
|
|
return '''
|
|
macos:
|
|
pluginClass: FakePlugin
|
|
''';
|
|
case kPlatformWeb:
|
|
return '''
|
|
web:
|
|
pluginClass: FakePlugin
|
|
fileName: ${packageName}_web.dart
|
|
''';
|
|
case kPlatformWindows:
|
|
return '''
|
|
windows:
|
|
pluginClass: FakePlugin
|
|
''';
|
|
default:
|
|
assert(false);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
typedef _ErrorHandler = void Function(Error error);
|
|
|
|
/// Run the command [runner] with the given [args] and return
|
|
/// what was printed.
|
|
/// A custom [errorHandler] can be used to handle the runner error as desired without throwing.
|
|
Future<List<String>> runCapturingPrint(
|
|
CommandRunner<void> runner, List<String> args,
|
|
{_ErrorHandler? errorHandler}) async {
|
|
final List<String> prints = <String>[];
|
|
final ZoneSpecification spec = ZoneSpecification(
|
|
print: (_, __, ___, String message) {
|
|
prints.add(message);
|
|
},
|
|
);
|
|
try {
|
|
await Zone.current
|
|
.fork(specification: spec)
|
|
.run<Future<void>>(() => runner.run(args));
|
|
} on Error catch (e) {
|
|
if (errorHandler == null) {
|
|
rethrow;
|
|
}
|
|
errorHandler(e);
|
|
}
|
|
|
|
return prints;
|
|
}
|
|
|
|
/// A mock [ProcessRunner] which records process calls.
|
|
class RecordingProcessRunner extends ProcessRunner {
|
|
final List<ProcessCall> recordedCalls = <ProcessCall>[];
|
|
|
|
/// Maps an executable to a list of processes that should be used for each
|
|
/// successive call to it via [run], [runAndStream], or [start].
|
|
final Map<String, List<io.Process>> mockProcessesForExecutable =
|
|
<String, List<io.Process>>{};
|
|
|
|
/// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int].
|
|
String? resultStdout;
|
|
|
|
/// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int].
|
|
String? resultStderr;
|
|
|
|
// Deprecated--do not add new uses. Use mockProcessesForExecutable instead.
|
|
io.Process? processToReturn;
|
|
|
|
@override
|
|
Future<int> runAndStream(
|
|
String executable,
|
|
List<String> args, {
|
|
Directory? workingDir,
|
|
bool exitOnError = false,
|
|
}) async {
|
|
recordedCalls.add(ProcessCall(executable, args, workingDir?.path));
|
|
final io.Process? processToReturn = _getProcessToReturn(executable);
|
|
final int exitCode =
|
|
processToReturn == null ? 0 : await processToReturn.exitCode;
|
|
if (exitOnError && (exitCode != 0)) {
|
|
throw io.ProcessException(executable, args);
|
|
}
|
|
return Future<int>.value(exitCode);
|
|
}
|
|
|
|
/// Returns [io.ProcessResult] created from [mockProcessesForExecutable],
|
|
/// [resultStdout], and [resultStderr].
|
|
@override
|
|
Future<io.ProcessResult> run(
|
|
String executable,
|
|
List<String> args, {
|
|
Directory? workingDir,
|
|
bool exitOnError = false,
|
|
bool logOnError = false,
|
|
Encoding stdoutEncoding = io.systemEncoding,
|
|
Encoding stderrEncoding = io.systemEncoding,
|
|
}) async {
|
|
recordedCalls.add(ProcessCall(executable, args, workingDir?.path));
|
|
|
|
final io.Process? process = _getProcessToReturn(executable);
|
|
final io.ProcessResult result = process == null
|
|
? io.ProcessResult(1, 0, '', '')
|
|
: io.ProcessResult(process.pid, await process.exitCode,
|
|
resultStdout ?? process.stdout, resultStderr ?? process.stderr);
|
|
|
|
if (exitOnError && (result.exitCode != 0)) {
|
|
throw io.ProcessException(executable, args);
|
|
}
|
|
|
|
return Future<io.ProcessResult>.value(result);
|
|
}
|
|
|
|
@override
|
|
Future<io.Process> start(String executable, List<String> args,
|
|
{Directory? workingDirectory}) async {
|
|
recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path));
|
|
return Future<io.Process>.value(_getProcessToReturn(executable));
|
|
}
|
|
|
|
io.Process? _getProcessToReturn(String executable) {
|
|
io.Process? process;
|
|
final List<io.Process>? processes = mockProcessesForExecutable[executable];
|
|
if (processes != null && processes.isNotEmpty) {
|
|
process = mockProcessesForExecutable[executable]!.removeAt(0);
|
|
}
|
|
// Fall back to `processToReturn` for backwards compatibility.
|
|
return process ?? processToReturn;
|
|
}
|
|
}
|
|
|
|
/// A recorded process call.
|
|
@immutable
|
|
class ProcessCall {
|
|
const ProcessCall(this.executable, this.args, this.workingDir);
|
|
|
|
/// The executable that was called.
|
|
final String executable;
|
|
|
|
/// The arguments passed to [executable] in the call.
|
|
final List<String> args;
|
|
|
|
/// The working directory this process was called from.
|
|
final String? workingDir;
|
|
|
|
@override
|
|
bool operator ==(dynamic other) {
|
|
return other is ProcessCall &&
|
|
executable == other.executable &&
|
|
listsEqual(args, other.args) &&
|
|
workingDir == other.workingDir;
|
|
}
|
|
|
|
@override
|
|
int get hashCode =>
|
|
(executable.hashCode) ^ (args.hashCode) ^ (workingDir?.hashCode ?? 0);
|
|
|
|
@override
|
|
String toString() {
|
|
final List<String> command = <String>[executable, ...args];
|
|
return '"${command.join(' ')}" in $workingDir';
|
|
}
|
|
}
|