Files
packages/script/tool/test/util.dart
stuartmorgan 10486b0ceb [flutter_plugin_tools] Split common.dart (#4057)
common.dart is a large-and-growing file containing all shared code,
which makes it hard to navigate. To make maintenance easier, this splits
the file (and its test file) into separate files for each major
component or category.
2021-06-16 12:37:30 -07:00

331 lines
9.9 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/plugin_utils.dart';
import 'package:flutter_plugin_tools/src/common/process_runner.dart';
import 'package:meta/meta.dart';
import 'package:quiver/collection.dart';
/// 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].
Directory createFakePlugin(
String name,
Directory packagesDirectory, {
bool withSingleExample = false,
List<String> withExamples = const <String>[],
List<List<String>> withExtraFiles = const <List<String>>[],
bool isFlutter = true,
// TODO(stuartmorgan): Change these platform switches to support type enums.
bool isAndroidPlugin = false,
bool isIosPlugin = false,
bool isWebPlugin = false,
bool isLinuxPlugin = false,
bool isMacOsPlugin = false,
bool isWindowsPlugin = false,
bool includeChangeLog = false,
bool includeVersion = false,
String version = '0.0.1',
String parentDirectoryName = '',
}) {
assert(!(withSingleExample && withExamples.isNotEmpty),
'cannot pass withSingleExample and withExamples simultaneously');
Directory parentDirectory = packagesDirectory;
if (parentDirectoryName != '') {
parentDirectory = parentDirectory.childDirectory(parentDirectoryName);
}
final Directory pluginDirectory = parentDirectory.childDirectory(name);
pluginDirectory.createSync(recursive: true);
createFakePubspec(pluginDirectory,
name: name,
isFlutter: isFlutter,
androidSupport: isAndroidPlugin ? PlatformSupport.inline : null,
iosSupport: isIosPlugin ? PlatformSupport.inline : null,
webSupport: isWebPlugin ? PlatformSupport.inline : null,
linuxSupport: isLinuxPlugin ? PlatformSupport.inline : null,
macosSupport: isMacOsPlugin ? PlatformSupport.inline : null,
windowsSupport: isWindowsPlugin ? PlatformSupport.inline : null,
version: includeVersion ? version : null);
if (includeChangeLog) {
createFakeCHANGELOG(pluginDirectory, '''
## 0.0.1
* Some changes.
''');
}
if (withSingleExample) {
final Directory exampleDir = pluginDirectory.childDirectory('example')
..createSync();
createFakePubspec(exampleDir,
name: '${name}_example', isFlutter: isFlutter, publishTo: 'none');
} else if (withExamples.isNotEmpty) {
final Directory exampleDir = pluginDirectory.childDirectory('example')
..createSync();
for (final String example in withExamples) {
final Directory currentExample = exampleDir.childDirectory(example)
..createSync();
createFakePubspec(currentExample,
name: example, isFlutter: isFlutter, publishTo: 'none');
}
}
final FileSystem fileSystem = pluginDirectory.fileSystem;
for (final List<String> file in withExtraFiles) {
final List<String> newFilePath = <String>[pluginDirectory.path, ...file];
final File newFile = fileSystem.file(fileSystem.path.joinAll(newFilePath));
newFile.createSync(recursive: true);
}
return pluginDirectory;
}
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.
void createFakePubspec(
Directory parent, {
String name = 'fake_package',
bool isFlutter = true,
PlatformSupport? androidSupport,
PlatformSupport? iosSupport,
PlatformSupport? linuxSupport,
PlatformSupport? macosSupport,
PlatformSupport? webSupport,
PlatformSupport? windowsSupport,
String publishTo = 'http://no_pub_server.com',
String? version,
}) {
parent.childFile('pubspec.yaml').createSync();
String yaml = '''
name: $name
flutter:
plugin:
platforms:
''';
if (androidSupport != null) {
yaml += _pluginPlatformSection('android', androidSupport, name);
}
if (iosSupport != null) {
yaml += _pluginPlatformSection('ios', iosSupport, name);
}
if (webSupport != null) {
yaml += _pluginPlatformSection('web', webSupport, name);
}
if (linuxSupport != null) {
yaml += _pluginPlatformSection('linux', linuxSupport, name);
}
if (macosSupport != null) {
yaml += _pluginPlatformSection('macos', macosSupport, name);
}
if (windowsSupport != null) {
yaml += _pluginPlatformSection('windows', windowsSupport, name);
}
if (isFlutter) {
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 'android':
return '''
android:
package: io.flutter.plugins.fake
pluginClass: FakePlugin
''';
case 'ios':
return '''
ios:
pluginClass: FLTFakePlugin
''';
case 'linux':
return '''
linux:
pluginClass: FakePlugin
''';
case 'macos':
return '''
macos:
pluginClass: FakePlugin
''';
case 'web':
return '''
web:
pluginClass: FakePlugin
fileName: ${packageName}_web.dart
''';
case 'windows':
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 {
io.Process? processToReturn;
final List<ProcessCall> recordedCalls = <ProcessCall>[];
/// 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;
@override
Future<int> runAndStream(
String executable,
List<String> args, {
Directory? workingDir,
bool exitOnError = false,
}) async {
recordedCalls.add(ProcessCall(executable, args, workingDir?.path));
return Future<int>.value(
processToReturn == null ? 0 : await processToReturn!.exitCode);
}
/// Returns [io.ProcessResult] created from [processToReturn], [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 = processToReturn;
final io.ProcessResult result = process == null
? io.ProcessResult(1, 1, '', '')
: io.ProcessResult(process.pid, await process.exitCode,
resultStdout ?? process.stdout, resultStderr ?? process.stderr);
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(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';
}
}