| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import 'dart:async'; |
| 6 | import 'dart:convert'; |
| 7 | import 'dart:core' hide print; |
| 8 | import 'dart:io' as io; |
| 9 | |
| 10 | import 'package:path/path.dart' as path; |
| 11 | |
| 12 | import 'utils.dart'; |
| 13 | |
| 14 | /// Runs the `executable` and returns standard output as a stream of lines. |
| 15 | /// |
| 16 | /// The returned stream reaches its end immediately after the command exits. |
| 17 | /// |
| 18 | /// If `expectNonZeroExit` is false and the process exits with a non-zero exit |
| 19 | /// code fails the test immediately by exiting the test process with exit code |
| 20 | /// 1. |
| 21 | Stream<String> runAndGetStdout( |
| 22 | String executable, |
| 23 | List<String> arguments, { |
| 24 | String? workingDirectory, |
| 25 | Map<String, String>? environment, |
| 26 | bool expectNonZeroExit = false, |
| 27 | }) async* { |
| 28 | final StreamController<String> output = StreamController<String>(); |
| 29 | final Future<CommandResult?> command = runCommand( |
| 30 | executable, |
| 31 | arguments, |
| 32 | workingDirectory: workingDirectory, |
| 33 | environment: environment, |
| 34 | expectNonZeroExit: expectNonZeroExit, |
| 35 | // Capture the output so it's not printed to the console by default. |
| 36 | outputMode: OutputMode.capture, |
| 37 | outputListener: (String line, io.Process process) { |
| 38 | output.add(line); |
| 39 | }, |
| 40 | ); |
| 41 | |
| 42 | // Close the stream controller after the command is complete. Otherwise, |
| 43 | // the yield* will never finish. |
| 44 | command.whenComplete(output.close); |
| 45 | |
| 46 | yield* output.stream; |
| 47 | } |
| 48 | |
| 49 | /// Represents a running process launched using [startCommand]. |
| 50 | class Command { |
| 51 | Command._(this.process, this._time, this._savedStdout, this._savedStderr); |
| 52 | |
| 53 | /// The raw process that was launched for this command. |
| 54 | final io.Process process; |
| 55 | final Stopwatch _time; |
| 56 | final Future<String> _savedStdout; |
| 57 | final Future<String> _savedStderr; |
| 58 | } |
| 59 | |
| 60 | /// The result of running a command using [startCommand] and [runCommand]; |
| 61 | class CommandResult { |
| 62 | CommandResult._(this.exitCode, this.elapsedTime, this.flattenedStdout, this.flattenedStderr); |
| 63 | |
| 64 | /// The exit code of the process. |
| 65 | final int exitCode; |
| 66 | |
| 67 | /// The amount of time it took the process to complete. |
| 68 | final Duration elapsedTime; |
| 69 | |
| 70 | /// Standard output decoded as a string using UTF8 decoder. |
| 71 | final String? flattenedStdout; |
| 72 | |
| 73 | /// Standard error output decoded as a string using UTF8 decoder. |
| 74 | final String? flattenedStderr; |
| 75 | } |
| 76 | |
| 77 | /// Starts the `executable` and returns a command object representing the |
| 78 | /// running process. |
| 79 | /// |
| 80 | /// `outputListener` is called for every line of standard output from the |
| 81 | /// process, and is given the [Process] object. This can be used to interrupt |
| 82 | /// an indefinitely running process, for example, by waiting until the process |
| 83 | /// emits certain output. |
| 84 | /// |
| 85 | /// `outputMode` controls where the standard output from the command process |
| 86 | /// goes. See [OutputMode]. |
| 87 | Future<Command> startCommand( |
| 88 | String executable, |
| 89 | List<String> arguments, { |
| 90 | String? workingDirectory, |
| 91 | Map<String, String>? environment, |
| 92 | OutputMode outputMode = OutputMode.print, |
| 93 | bool Function(String)? removeLine, |
| 94 | void Function(String, io.Process)? outputListener, |
| 95 | }) async { |
| 96 | final String relativeWorkingDir = path.relative(workingDirectory ?? io.Directory.current.path); |
| 97 | final String commandDescription = |
| 98 | ' ${path.relative(executable, from: workingDirectory)} ${arguments.join(' ' )}' ; |
| 99 | print('RUNNING: cd $cyan$relativeWorkingDir$reset; $green$commandDescription$reset' ); |
| 100 |
|
| 101 | final Stopwatch time = Stopwatch()..start();
|
| 102 | print('workingDirectory: $workingDirectory, executable: $executable, arguments: $arguments' );
|
| 103 | final io.Process process = await io.Process.start(
|
| 104 | executable,
|
| 105 | arguments,
|
| 106 | workingDirectory: workingDirectory,
|
| 107 | environment: environment,
|
| 108 | );
|
| 109 | return Command._(
|
| 110 | process,
|
| 111 | time,
|
| 112 | process.stdout
|
| 113 | .transform<String>(const Utf8Decoder())
|
| 114 | .transform(const LineSplitter())
|
| 115 | .where((String line) => removeLine == null || !removeLine(line))
|
| 116 | .map<String>((String line) {
|
| 117 | final String formattedLine = ' $line\n' ;
|
| 118 | if (outputListener != null) {
|
| 119 | outputListener(formattedLine, process);
|
| 120 | }
|
| 121 | switch (outputMode) {
|
| 122 | case OutputMode.print:
|
| 123 | print(line);
|
| 124 | case OutputMode.capture:
|
| 125 | break;
|
| 126 | }
|
| 127 | return line;
|
| 128 | })
|
| 129 | .join('\n' ),
|
| 130 | process.stderr
|
| 131 | .transform<String>(const Utf8Decoder())
|
| 132 | .transform(const LineSplitter())
|
| 133 | .map<String>((String line) {
|
| 134 | switch (outputMode) {
|
| 135 | case OutputMode.print:
|
| 136 | print(line);
|
| 137 | case OutputMode.capture:
|
| 138 | break;
|
| 139 | }
|
| 140 | return line;
|
| 141 | })
|
| 142 | .join('\n' ),
|
| 143 | );
|
| 144 | }
|
| 145 |
|
| 146 | /// Runs the `executable` and waits until the process exits.
|
| 147 | ///
|
| 148 | /// If the process exits with a non-zero exit code and `expectNonZeroExit` is
|
| 149 | /// false, calls foundError (which does not terminate execution!).
|
| 150 | ///
|
| 151 | /// `outputListener` is called for every line of standard output from the
|
| 152 | /// process, and is given the [Process] object. This can be used to interrupt
|
| 153 | /// an indefinitely running process, for example, by waiting until the process
|
| 154 | /// emits certain output.
|
| 155 | ///
|
| 156 | /// Returns the result of the finished process.
|
| 157 | ///
|
| 158 | /// `outputMode` controls where the standard output from the command process
|
| 159 | /// goes. See [OutputMode].
|
| 160 | Future<CommandResult> runCommand(
|
| 161 | String executable,
|
| 162 | List<String> arguments, {
|
| 163 | String? workingDirectory,
|
| 164 | Map<String, String>? environment,
|
| 165 | bool expectNonZeroExit = false,
|
| 166 | int? expectedExitCode,
|
| 167 | String? failureMessage,
|
| 168 | OutputMode outputMode = OutputMode.print,
|
| 169 | bool Function(String)? removeLine,
|
| 170 | void Function(String, io.Process)? outputListener,
|
| 171 | }) async {
|
| 172 | final String commandDescription =
|
| 173 | ' ${path.relative(executable, from: workingDirectory)} ${arguments.join(' ' )}' ;
|
| 174 | final String relativeWorkingDir = workingDirectory ?? path.relative(io.Directory.current.path);
|
| 175 | if (dryRun) {
|
| 176 | printProgress(_prettyPrintRunCommand(executable, arguments, workingDirectory));
|
| 177 | return CommandResult._(
|
| 178 | 0,
|
| 179 | Duration.zero,
|
| 180 | ' $executable ${arguments.join(' ' )}' ,
|
| 181 | 'Simulated execution due to --dry-run' ,
|
| 182 | );
|
| 183 | }
|
| 184 |
|
| 185 | final Command command = await startCommand(
|
| 186 | executable,
|
| 187 | arguments,
|
| 188 | workingDirectory: workingDirectory,
|
| 189 | environment: environment,
|
| 190 | outputMode: outputMode,
|
| 191 | removeLine: removeLine,
|
| 192 | outputListener: outputListener,
|
| 193 | );
|
| 194 |
|
| 195 | final CommandResult result = CommandResult._(
|
| 196 | await command.process.exitCode,
|
| 197 | command._time.elapsed,
|
| 198 | await command._savedStdout,
|
| 199 | await command._savedStderr,
|
| 200 | );
|
| 201 |
|
| 202 | if ((result.exitCode == 0) == expectNonZeroExit ||
|
| 203 | (expectedExitCode != null && result.exitCode != expectedExitCode)) {
|
| 204 | // Print the output when we get unexpected results (unless output was
|
| 205 | // printed already).
|
| 206 | switch (outputMode) {
|
| 207 | case OutputMode.print:
|
| 208 | break;
|
| 209 | case OutputMode.capture:
|
| 210 | print(result.flattenedStdout);
|
| 211 | print(result.flattenedStderr);
|
| 212 | }
|
| 213 | final String allOutput = ' ${result.flattenedStdout}\n ${result.flattenedStderr}' ;
|
| 214 | foundError(<String>[
|
| 215 | if (failureMessage != null) failureMessage,
|
| 216 | ' ${bold}Command: $green$commandDescription$reset' ,
|
| 217 | if (failureMessage == null)
|
| 218 | ' $bold${red}Command exited with exit code ${result.exitCode} but expected ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero' ) : 'zero' } exit code. $reset' ,
|
| 219 | ' ${bold}Working directory: $cyan${path.absolute(relativeWorkingDir)}$reset' ,
|
| 220 | if (allOutput.isNotEmpty && allOutput.length < 512)
|
| 221 | ' ${bold}stdout and stderr output:\n $allOutput' ,
|
| 222 | ]);
|
| 223 | } else {
|
| 224 | print(
|
| 225 | 'ELAPSED TIME: ${prettyPrintDuration(result.elapsedTime)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset' ,
|
| 226 | );
|
| 227 | }
|
| 228 | return result;
|
| 229 | }
|
| 230 |
|
| 231 | final String _flutterRoot = path.dirname(
|
| 232 | path.dirname(path.dirname(path.fromUri(io.Platform.script))),
|
| 233 | );
|
| 234 |
|
| 235 | String _prettyPrintRunCommand(String executable, List<String> arguments, String? workingDirectory) {
|
| 236 | final StringBuffer output = StringBuffer();
|
| 237 |
|
| 238 | // Print CWD relative to the root.
|
| 239 | output.write('|> ' );
|
| 240 | output.write(path.relative(executable, from: _flutterRoot));
|
| 241 | if (workingDirectory != null) {
|
| 242 | output.write(' ( ${path.relative(workingDirectory, from: _flutterRoot)})' );
|
| 243 | }
|
| 244 | output.writeln(': ' );
|
| 245 | output.writeAll(arguments.map((String a) => ' $a' ), '\n' );
|
| 246 |
|
| 247 | return output.toString();
|
| 248 | }
|
| 249 |
|
| 250 | /// Specifies what to do with the command output from [runCommand] and [startCommand].
|
| 251 | enum OutputMode {
|
| 252 | /// Forwards standard output and standard error streams to the test process'
|
| 253 | /// standard output stream (i.e. stderr is redirected to stdout).
|
| 254 | ///
|
| 255 | /// Use this mode if all you want is print the output of the command to the
|
| 256 | /// console. The output is no longer available after the process exits.
|
| 257 | print,
|
| 258 |
|
| 259 | /// Saves standard output and standard error streams in memory.
|
| 260 | ///
|
| 261 | /// Captured output can be retrieved from the [CommandResult] object.
|
| 262 | ///
|
| 263 | /// Use this mode in tests that need to inspect the output of a command, or
|
| 264 | /// when the output should not be printed to console.
|
| 265 | capture,
|
| 266 | }
|
| 267 |
|