2019-11-27 23:04:02 +00:00
|
|
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
2018-08-30 14:30:25 +00:00
|
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
|
|
// found in the LICENSE file.
|
|
|
|
|
2020-10-20 03:56:25 +00:00
|
|
|
import 'dart:async';
|
2018-08-30 14:30:25 +00:00
|
|
|
import 'dart:convert';
|
2020-01-02 19:47:28 +00:00
|
|
|
import 'dart:core' hide print;
|
2020-10-20 03:56:25 +00:00
|
|
|
import 'dart:io' as io;
|
2018-08-30 14:30:25 +00:00
|
|
|
|
|
|
|
import 'package:path/path.dart' as path;
|
|
|
|
|
2020-01-02 19:47:28 +00:00
|
|
|
import 'utils.dart';
|
2018-08-30 14:30:25 +00:00
|
|
|
|
2020-10-20 03:56:25 +00:00
|
|
|
/// Runs the `executable` and returns standard output as a stream of lines.
|
|
|
|
///
|
|
|
|
/// The returned stream reaches its end immediately after the command exits.
|
|
|
|
///
|
|
|
|
/// If `expectNonZeroExit` is false and the process exits with a non-zero exit
|
|
|
|
/// code fails the test immediately by exiting the test process with exit code
|
|
|
|
/// 1.
|
2019-03-06 21:13:45 +00:00
|
|
|
Stream<String> runAndGetStdout(String executable, List<String> arguments, {
|
|
|
|
String workingDirectory,
|
|
|
|
Map<String, String> environment,
|
|
|
|
bool expectNonZeroExit = false,
|
|
|
|
}) async* {
|
2020-10-20 03:56:25 +00:00
|
|
|
final StreamController<String> output = StreamController<String>();
|
|
|
|
final Future<CommandResult> command = runCommand(
|
|
|
|
executable,
|
|
|
|
arguments,
|
2019-03-06 21:13:45 +00:00
|
|
|
workingDirectory: workingDirectory,
|
|
|
|
environment: environment,
|
2020-10-20 03:56:25 +00:00
|
|
|
expectNonZeroExit: expectNonZeroExit,
|
|
|
|
// Capture the output so it's not printed to the console by default.
|
|
|
|
outputMode: OutputMode.capture,
|
|
|
|
outputListener: (String line, io.Process process) {
|
|
|
|
output.add(line);
|
|
|
|
},
|
2019-03-06 21:13:45 +00:00
|
|
|
);
|
|
|
|
|
2020-10-20 03:56:25 +00:00
|
|
|
// Close the stream controller after the command is complete. Otherwise,
|
|
|
|
// the yield* will never finish.
|
|
|
|
command.whenComplete(output.close);
|
2019-03-13 19:58:10 +00:00
|
|
|
|
2020-10-20 03:56:25 +00:00
|
|
|
yield* output.stream;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Represents a running process launched using [startCommand].
|
|
|
|
class Command {
|
|
|
|
Command._(this.process, this._time, this._savedStdout, this._savedStderr);
|
|
|
|
|
|
|
|
/// The raw process that was launched for this command.
|
|
|
|
final io.Process process;
|
|
|
|
|
|
|
|
final Stopwatch _time;
|
|
|
|
final Future<List<List<int>>> _savedStdout;
|
|
|
|
final Future<List<List<int>>> _savedStderr;
|
|
|
|
|
|
|
|
/// Evaluates when the [process] exits.
|
|
|
|
///
|
|
|
|
/// Returns the result of running the command.
|
|
|
|
Future<CommandResult> get onExit async {
|
|
|
|
final int exitCode = await process.exitCode;
|
|
|
|
_time.stop();
|
|
|
|
|
|
|
|
// Saved output is null when OutputMode.print is used.
|
|
|
|
final String flattenedStdout = _savedStdout != null ? _flattenToString(await _savedStdout) : null;
|
|
|
|
final String flattenedStderr = _savedStderr != null ? _flattenToString(await _savedStderr) : null;
|
|
|
|
return CommandResult._(exitCode, _time.elapsed, flattenedStdout, flattenedStderr);
|
2019-03-06 21:13:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-20 03:56:25 +00:00
|
|
|
/// The result of running a command using [startCommand] and [runCommand];
|
|
|
|
class CommandResult {
|
|
|
|
CommandResult._(this.exitCode, this.elapsedTime, this.flattenedStdout, this.flattenedStderr);
|
|
|
|
|
|
|
|
/// The exit code of the process.
|
|
|
|
final int exitCode;
|
|
|
|
|
|
|
|
/// The amount of time it took the process to complete.
|
|
|
|
final Duration elapsedTime;
|
|
|
|
|
|
|
|
/// Standard output decoded as a string using UTF8 decoder.
|
|
|
|
final String flattenedStdout;
|
|
|
|
|
|
|
|
/// Standard error output decoded as a string using UTF8 decoder.
|
|
|
|
final String flattenedStderr;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Starts the `executable` and returns a command object representing the
|
|
|
|
/// running process.
|
2020-02-14 02:34:08 +00:00
|
|
|
///
|
|
|
|
/// `outputListener` is called for every line of standard output from the
|
|
|
|
/// process, and is given the [Process] object. This can be used to interrupt
|
|
|
|
/// an indefinitely running process, for example, by waiting until the process
|
|
|
|
/// emits certain output.
|
2020-10-20 03:56:25 +00:00
|
|
|
///
|
|
|
|
/// `outputMode` controls where the standard output from the command process
|
|
|
|
/// goes. See [OutputMode].
|
|
|
|
Future<Command> startCommand(String executable, List<String> arguments, {
|
2018-08-30 14:30:25 +00:00
|
|
|
String workingDirectory,
|
|
|
|
Map<String, String> environment,
|
2019-08-09 22:10:45 +00:00
|
|
|
OutputMode outputMode = OutputMode.print,
|
2019-07-10 15:48:01 +00:00
|
|
|
bool Function(String) removeLine,
|
2020-10-20 03:56:25 +00:00
|
|
|
void Function(String, io.Process) outputListener,
|
2018-08-30 14:30:25 +00:00
|
|
|
}) async {
|
|
|
|
final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
|
2020-10-20 03:56:25 +00:00
|
|
|
final String relativeWorkingDir = path.relative(workingDirectory ?? io.Directory.current.path);
|
2018-08-30 14:30:25 +00:00
|
|
|
printProgress('RUNNING', relativeWorkingDir, commandDescription);
|
|
|
|
|
2019-08-20 21:53:39 +00:00
|
|
|
final Stopwatch time = Stopwatch()..start();
|
2020-10-20 03:56:25 +00:00
|
|
|
final io.Process process = await io.Process.start(executable, arguments,
|
2018-08-30 14:30:25 +00:00
|
|
|
workingDirectory: workingDirectory,
|
|
|
|
environment: environment,
|
|
|
|
);
|
|
|
|
|
|
|
|
Future<List<List<int>>> savedStdout, savedStderr;
|
2019-07-10 15:48:01 +00:00
|
|
|
final Stream<List<int>> stdoutSource = process.stdout
|
|
|
|
.transform<String>(const Utf8Decoder())
|
|
|
|
.transform(const LineSplitter())
|
|
|
|
.where((String line) => removeLine == null || !removeLine(line))
|
2020-02-14 02:34:08 +00:00
|
|
|
.map((String line) {
|
|
|
|
final String formattedLine = '$line\n';
|
|
|
|
if (outputListener != null) {
|
|
|
|
outputListener(formattedLine, process);
|
|
|
|
}
|
|
|
|
return formattedLine;
|
|
|
|
})
|
2019-07-10 15:48:01 +00:00
|
|
|
.transform(const Utf8Encoder());
|
2019-08-10 00:01:07 +00:00
|
|
|
switch (outputMode) {
|
|
|
|
case OutputMode.print:
|
|
|
|
await Future.wait<void>(<Future<void>>[
|
2020-10-20 03:56:25 +00:00
|
|
|
io.stdout.addStream(stdoutSource),
|
|
|
|
io.stderr.addStream(process.stderr),
|
2019-08-10 00:01:07 +00:00
|
|
|
]);
|
|
|
|
break;
|
|
|
|
case OutputMode.capture:
|
|
|
|
savedStdout = stdoutSource.toList();
|
|
|
|
savedStderr = process.stderr.toList();
|
|
|
|
break;
|
2018-08-30 14:30:25 +00:00
|
|
|
}
|
|
|
|
|
2020-10-20 03:56:25 +00:00
|
|
|
return Command._(process, time, savedStdout, savedStderr);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Runs the `executable` and waits until the process exits.
|
|
|
|
///
|
|
|
|
/// If the process exits with a non-zero exit code, exits this process with
|
|
|
|
/// exit code 1, unless `expectNonZeroExit` is set to true.
|
|
|
|
///
|
|
|
|
/// `outputListener` is called for every line of standard output from the
|
|
|
|
/// process, and is given the [Process] object. This can be used to interrupt
|
|
|
|
/// an indefinitely running process, for example, by waiting until the process
|
|
|
|
/// emits certain output.
|
|
|
|
///
|
|
|
|
/// Returns the result of the finished process, or null if `skip` is true.
|
|
|
|
///
|
|
|
|
/// `outputMode` controls where the standard output from the command process
|
|
|
|
/// goes. See [OutputMode].
|
|
|
|
Future<CommandResult> runCommand(String executable, List<String> arguments, {
|
|
|
|
String workingDirectory,
|
|
|
|
Map<String, String> environment,
|
|
|
|
bool expectNonZeroExit = false,
|
|
|
|
int expectedExitCode,
|
|
|
|
String failureMessage,
|
|
|
|
OutputMode outputMode = OutputMode.print,
|
|
|
|
bool skip = false,
|
|
|
|
bool Function(String) removeLine,
|
|
|
|
void Function(String, io.Process) outputListener,
|
|
|
|
}) async {
|
|
|
|
final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
|
|
|
|
final String relativeWorkingDir = path.relative(workingDirectory ?? io.Directory.current.path);
|
|
|
|
if (skip) {
|
|
|
|
printProgress('SKIPPING', relativeWorkingDir, commandDescription);
|
|
|
|
return null;
|
2019-08-09 22:10:45 +00:00
|
|
|
}
|
|
|
|
|
2020-10-20 03:56:25 +00:00
|
|
|
final Command command = await startCommand(executable, arguments,
|
|
|
|
workingDirectory: workingDirectory,
|
|
|
|
environment: environment,
|
|
|
|
outputMode: outputMode,
|
|
|
|
removeLine: removeLine,
|
|
|
|
outputListener: outputListener,
|
|
|
|
);
|
|
|
|
|
|
|
|
final CommandResult result = await command.onExit;
|
|
|
|
|
|
|
|
if ((result.exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && result.exitCode != expectedExitCode)) {
|
2019-08-09 22:10:45 +00:00
|
|
|
// Print the output when we get unexpected results (unless output was
|
|
|
|
// printed already).
|
2019-08-10 00:01:07 +00:00
|
|
|
switch (outputMode) {
|
|
|
|
case OutputMode.print:
|
|
|
|
break;
|
|
|
|
case OutputMode.capture:
|
2020-10-20 03:56:25 +00:00
|
|
|
io.stdout.writeln(result.flattenedStdout);
|
|
|
|
io.stderr.writeln(result.flattenedStderr);
|
2019-08-10 00:01:07 +00:00
|
|
|
break;
|
2018-08-30 14:30:25 +00:00
|
|
|
}
|
2020-01-02 19:47:28 +00:00
|
|
|
exitWithError(<String>[
|
|
|
|
if (failureMessage != null)
|
|
|
|
failureMessage
|
|
|
|
else
|
2020-10-20 03:56:25 +00:00
|
|
|
'${bold}ERROR: ${red}Last command exited with ${result.exitCode} (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset',
|
2020-01-02 19:47:28 +00:00
|
|
|
'${bold}Command: $green$commandDescription$reset',
|
|
|
|
'${bold}Relative working directory: $cyan$relativeWorkingDir$reset',
|
|
|
|
]);
|
2018-08-30 14:30:25 +00:00
|
|
|
}
|
2020-10-20 03:56:25 +00:00
|
|
|
print('$clock ELAPSED TIME: ${prettyPrintDuration(result.elapsedTime)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
|
|
|
|
return result;
|
2018-08-30 14:30:25 +00:00
|
|
|
}
|
2019-08-09 22:10:45 +00:00
|
|
|
|
|
|
|
/// Flattens a nested list of UTF-8 code units into a single string.
|
2019-08-15 18:12:08 +00:00
|
|
|
String _flattenToString(List<List<int>> chunks) =>
|
|
|
|
utf8.decode(chunks.expand<int>((List<int> ints) => ints).toList());
|
2019-08-09 22:10:45 +00:00
|
|
|
|
2020-10-20 03:56:25 +00:00
|
|
|
/// Specifies what to do with the command output from [runCommand] and [startCommand].
|
|
|
|
enum OutputMode {
|
|
|
|
/// Forwards standard output and standard error streams to the test process'
|
|
|
|
/// respective standard streams.
|
|
|
|
///
|
|
|
|
/// Use this mode if all you want is print the output of the command to the
|
|
|
|
/// console. The output is no longer available after the process exits.
|
|
|
|
print,
|
|
|
|
|
|
|
|
/// Saves standard output and standard error streams in memory.
|
|
|
|
///
|
|
|
|
/// Captured output can be retrieved from the [CommandResult] object.
|
|
|
|
///
|
|
|
|
/// Use this mode in tests that need to inspect the output of a command, or
|
|
|
|
/// when the output should not be printed to console.
|
|
|
|
capture,
|
2019-08-09 22:10:45 +00:00
|
|
|
}
|