blob: 1dc9676e899bb653c4e6aaefbdf3064fec89846e [file] [log] [blame]
// 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.
// @dart = 2.6
import 'dart:async';
import 'dart:io' as io;
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'environment.dart';
import 'exceptions.dart';
/// Clears the terminal screen and places the cursor at the top left corner.
///
/// This works on Linux and Mac. On Windows, it's a no-op.
void clearTerminalScreen() {
if (!io.Platform.isWindows) {
// See: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
print("\x1B[2J\x1B[1;2H");
}
}
class FilePath {
FilePath.fromCwd(String relativePath)
: _absolutePath = path.absolute(relativePath);
FilePath.fromWebUi(String relativePath)
: _absolutePath = path.join(environment.webUiRootDir.path, relativePath);
final String _absolutePath;
String get absolute => _absolutePath;
String get relativeToCwd => path.relative(_absolutePath);
String get relativeToWebUi =>
path.relative(_absolutePath, from: environment.webUiRootDir.path);
@override
bool operator ==(Object other) {
return other is FilePath && other._absolutePath == _absolutePath;
}
@override
String toString() => _absolutePath;
}
/// Runs [executable] merging its output into the current process' standard out and standard error.
Future<int> runProcess(
String executable,
List<String> arguments, {
String workingDirectory,
bool mustSucceed: false,
Map<String, String> environment = const <String, String>{},
}) async {
final io.Process process = await io.Process.start(
executable,
arguments,
workingDirectory: workingDirectory,
// Running the process in a system shell for Windows. Otherwise
// the process is not able to get Dart from path.
runInShell: io.Platform.isWindows,
mode: io.ProcessStartMode.inheritStdio,
environment: environment,
);
final int exitCode = await process.exitCode;
if (mustSucceed && exitCode != 0) {
throw ProcessException(
description: 'Sub-process failed.',
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
exitCode: exitCode,
);
}
return exitCode;
}
/// Runs [executable]. Do not follow the exit code or the output.
Future<void> startProcess(
String executable,
List<String> arguments, {
String workingDirectory,
bool mustSucceed: false,
}) async {
final io.Process process = await io.Process.start(
executable,
arguments,
workingDirectory: workingDirectory,
// Running the process in a system shell for Windows. Otherwise
// the process is not able to get Dart from path.
runInShell: io.Platform.isWindows,
mode: io.ProcessStartMode.inheritStdio,
);
processesToCleanUp.add(process);
}
/// Runs [executable] and returns its standard output as a string.
///
/// If the process fails, throws a [ProcessException].
Future<String> evalProcess(
String executable,
List<String> arguments, {
String workingDirectory,
}) async {
final io.ProcessResult result = await io.Process.run(
executable,
arguments,
workingDirectory: workingDirectory,
);
if (result.exitCode != 0) {
throw ProcessException(
description: result.stderr as String,
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
exitCode: result.exitCode,
);
}
return result.stdout as String;
}
Future<void> runFlutter(
String workingDirectory,
List<String> arguments, {
bool useSystemFlutter = false,
}) async {
final String executable =
useSystemFlutter ? 'flutter' : environment.flutterCommand.path;
arguments.add('--local-engine=host_debug_unopt');
final int exitCode = await runProcess(
executable,
arguments,
workingDirectory: workingDirectory,
);
if (exitCode != 0) {
throw ToolException('ERROR: Failed to run $executable with '
'arguments ${arguments.toString()}. Exited with exit code $exitCode');
}
}
@immutable
class ProcessException implements Exception {
ProcessException({
@required this.description,
@required this.executable,
@required this.arguments,
@required this.workingDirectory,
@required this.exitCode,
});
final String description;
final String executable;
final List<String> arguments;
final String workingDirectory;
final int exitCode;
@override
String toString() {
final StringBuffer message = StringBuffer();
message
..writeln(description)
..writeln('Command: $executable ${arguments.join(' ')}')
..writeln(
'Working directory: ${workingDirectory ?? io.Directory.current.path}')
..writeln('Exit code: $exitCode');
return '$message';
}
}
/// Adds utility methods
mixin ArgUtils<T> on Command<T> {
/// Extracts a boolean argument from [argResults].
bool boolArg(String name) => argResults[name] as bool;
/// Extracts a string argument from [argResults].
String stringArg(String name) => argResults[name] as String;
/// Extracts a integer argument from [argResults].
///
/// If the argument value cannot be parsed as [int] throws an [ArgumentError].
int intArg(String name) {
final String rawValue = stringArg(name);
if (rawValue == null) {
return null;
}
final int value = int.tryParse(rawValue);
if (value == null) {
throw ArgumentError(
'Argument $name should be an integer value but was "$rawValue"',
);
}
return value;
}
}
/// Parses additional options that can be used for all tests.
class GeneralTestsArgumentParser {
static final GeneralTestsArgumentParser _singletonInstance =
GeneralTestsArgumentParser._();
/// The [GeneralTestsArgumentParser] singleton.
static GeneralTestsArgumentParser get instance => _singletonInstance;
GeneralTestsArgumentParser._();
/// If target name is provided integration tests can run that one test
/// instead of running all the tests.
bool verbose = false;
void populateOptions(ArgParser argParser) {
argParser
..addFlag(
'verbose',
defaultsTo: false,
help: 'Flag to indicate extra logs should also be printed.',
);
}
/// Populate results of the arguments passed.
void parseOptions(ArgResults argResults) {
verbose = argResults['verbose'] as bool;
}
}
bool get isVerboseLoggingEnabled => GeneralTestsArgumentParser.instance.verbose;
/// There might be proccesses started during the tests.
///
/// Use this list to store those Processes, for cleaning up before shutdown.
final List<io.Process> processesToCleanUp = <io.Process>[];
/// There might be temporary directories created during the tests.
///
/// Use this list to store those directories and for deleteing them before
/// shutdown.
final List<io.Directory> temporaryDirectories = <io.Directory>[];
typedef AsyncCallback = Future<void> Function();
/// There might be additional cleanup needs to be done after the tools ran.
///
/// Add these operations here to make sure that they will run before felt
/// exit.
final List<AsyncCallback> cleanupCallbacks = <AsyncCallback>[];
/// Cleanup the remaning processes, close open browsers, delete temp files.
void cleanup() async {
// Cleanup remaining processes if any.
if (processesToCleanUp.length > 0) {
for (io.Process process in processesToCleanUp) {
process.kill();
}
}
// Delete temporary directories.
if (temporaryDirectories.length > 0) {
for (io.Directory directory in temporaryDirectories) {
if (!directory.existsSync()) {
directory.deleteSync(recursive: true);
}
}
}
for (final AsyncCallback callback in cleanupCallbacks) {
await callback();
}
}