| // 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(); |
| } |
| } |