| // 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, | 
 | } |