// Copyright 2014 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:core' hide print; import 'dart:io' as io; import 'package:path/path.dart' as path; import 'utils.dart'; /// 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. Stream<String> runAndGetStdout(String executable, List<String> arguments, { String? workingDirectory, Map<String, String>? environment, bool expectNonZeroExit = false, }) async* { final StreamController<String> output = StreamController<String>(); final Future<CommandResult?> command = runCommand( executable, arguments, workingDirectory: workingDirectory, environment: environment, 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); }, ); // Close the stream controller after the command is complete. Otherwise, // the yield* will never finish. command.whenComplete(output.close); 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); } } /// 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. /// /// `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. /// /// `outputMode` controls where the standard output from the command process /// goes. See [OutputMode]. Future<Command> startCommand(String executable, List<String> arguments, { String? workingDirectory, Map<String, String>? environment, OutputMode outputMode = OutputMode.print, 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); printProgress('RUNNING', relativeWorkingDir, commandDescription); final Stopwatch time = Stopwatch()..start(); final io.Process process = await io.Process.start(executable, arguments, workingDirectory: workingDirectory, environment: environment, ); Future<List<List<int>>> savedStdout = Future<List<List<int>>>.value(<List<int>>[]); Future<List<List<int>>> savedStderr = Future<List<List<int>>>.value(<List<int>>[]); final Stream<List<int>> stdoutSource = process.stdout .transform<String>(const Utf8Decoder()) .transform(const LineSplitter()) .where((String line) => removeLine == null || !removeLine(line)) .map((String line) { final String formattedLine = '$line\n'; if (outputListener != null) { outputListener(formattedLine, process); } return formattedLine; }) .transform(const Utf8Encoder()); switch (outputMode) { case OutputMode.print: stdoutSource.listen((List<int> output) { io.stdout.add(output); savedStdout.then((List<List<int>> list) => list.add(output)); }); process.stderr.listen((List<int> output) { io.stdout.add(output); savedStdout.then((List<List<int>> list) => list.add(output)); }); break; case OutputMode.capture: savedStdout = stdoutSource.toList(); savedStderr = process.stderr.toList(); break; } 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. /// /// `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 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); 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)) { // Print the output when we get unexpected results (unless output was // printed already). switch (outputMode) { case OutputMode.print: break; case OutputMode.capture: io.stdout.writeln(result.flattenedStdout); io.stdout.writeln(result.flattenedStderr); break; } exitWithError(<String>[ if (failureMessage != null) failureMessage else '${bold}ERROR: ${red}Last command exited with ${result.exitCode} (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset', '${bold}Command: $green$commandDescription$reset', '${bold}Relative working directory: $cyan$relativeWorkingDir$reset', ]); } print('$clock ELAPSED TIME: ${prettyPrintDuration(result.elapsedTime)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset'); return result; } /// Flattens a nested list of UTF-8 code units into a single string. String _flattenToString(List<List<int>> chunks) => utf8.decode(chunks.expand<int>((List<int> ints) => ints).toList()); /// 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' /// standard output stream (i.e. stderr is redirected to stdout). /// /// 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, }