run_command.dart 8.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6
import 'dart:convert';
7
import 'dart:core' hide print;
8
import 'dart:io' as io;
9 10 11

import 'package:path/path.dart' as path;

12
import 'utils.dart';
13

14 15 16 17 18 19 20
/// 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.
Dan Field's avatar
Dan Field committed
21
Stream<String> runAndGetStdout(String executable, List<String> arguments, {
22 23
  String? workingDirectory,
  Map<String, String>? environment,
Dan Field's avatar
Dan Field committed
24 25
  bool expectNonZeroExit = false,
}) async* {
26
  final StreamController<String> output = StreamController<String>();
27
  final Future<CommandResult?> command = runCommand(
28 29
    executable,
    arguments,
Dan Field's avatar
Dan Field committed
30 31
    workingDirectory: workingDirectory,
    environment: environment,
32 33 34 35 36 37
    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);
    },
Dan Field's avatar
Dan Field committed
38 39
  );

40 41 42
  // Close the stream controller after the command is complete. Otherwise,
  // the yield* will never finish.
  command.whenComplete(output.close);
43

44 45 46 47 48 49 50 51 52 53 54
  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;
55 56
  final Future<List<List<int>>>? _savedStdout;
  final Future<List<List<int>>>? _savedStderr;
57 58 59 60 61 62 63 64 65

  /// 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.
66 67
    final String? flattenedStdout = _savedStdout != null ? _flattenToString((await _savedStdout)!) : null;
    final String? flattenedStderr = _savedStderr != null ? _flattenToString((await _savedStderr)!) : null;
68
    return CommandResult._(exitCode, _time.elapsed, flattenedStdout, flattenedStderr);
Dan Field's avatar
Dan Field committed
69 70 71
  }
}

72 73 74 75 76 77 78 79 80 81 82
/// 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.
83
  final String? flattenedStdout;
84 85

  /// Standard error output decoded as a string using UTF8 decoder.
86
  final String? flattenedStderr;
87 88 89 90
}

/// Starts the `executable` and returns a command object representing the
/// running process.
91 92 93 94 95
///
/// `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.
96 97 98 99
///
/// `outputMode` controls where the standard output from the command process
/// goes. See [OutputMode].
Future<Command> startCommand(String executable, List<String> arguments, {
100 101
  String? workingDirectory,
  Map<String, String>? environment,
102
  OutputMode outputMode = OutputMode.print,
103 104
  bool Function(String)? removeLine,
  void Function(String, io.Process)? outputListener,
105 106
}) async {
  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
107
  final String relativeWorkingDir = path.relative(workingDirectory ?? io.Directory.current.path);
108 109
  printProgress('RUNNING', relativeWorkingDir, commandDescription);

110
  final Stopwatch time = Stopwatch()..start();
111
  final io.Process process = await io.Process.start(executable, arguments,
112 113 114 115
    workingDirectory: workingDirectory,
    environment: environment,
  );

116 117
  Future<List<List<int>>> savedStdout = Future<List<List<int>>>.value(<List<int>>[]);
  Future<List<List<int>>> savedStderr = Future<List<List<int>>>.value(<List<int>>[]);
118 119 120 121
  final Stream<List<int>> stdoutSource = process.stdout
    .transform<String>(const Utf8Decoder())
    .transform(const LineSplitter())
    .where((String line) => removeLine == null || !removeLine(line))
122 123 124 125 126 127 128
    .map((String line) {
      final String formattedLine = '$line\n';
      if (outputListener != null) {
        outputListener(formattedLine, process);
      }
      return formattedLine;
    })
129
    .transform(const Utf8Encoder());
130 131
  switch (outputMode) {
    case OutputMode.print:
132 133 134 135 136 137 138 139
      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));
      });
140 141 142 143 144
      break;
    case OutputMode.capture:
      savedStdout = stdoutSource.toList();
      savedStderr = process.stderr.toList();
      break;
145 146
  }

147 148 149 150 151 152 153 154 155 156 157 158 159
  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.
///
160
/// Returns the result of the finished process.
161 162 163 164
///
/// `outputMode` controls where the standard output from the command process
/// goes. See [OutputMode].
Future<CommandResult> runCommand(String executable, List<String> arguments, {
165 166
  String? workingDirectory,
  Map<String, String>? environment,
167
  bool expectNonZeroExit = false,
168 169
  int? expectedExitCode,
  String? failureMessage,
170
  OutputMode outputMode = OutputMode.print,
171 172
  bool Function(String)? removeLine,
  void Function(String, io.Process)? outputListener,
173 174 175
}) async {
  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
  final String relativeWorkingDir = path.relative(workingDirectory ?? io.Directory.current.path);
176

177 178 179 180 181 182 183 184 185 186
  final Command command = await startCommand(executable, arguments,
    workingDirectory: workingDirectory,
    environment: environment,
    outputMode: outputMode,
    removeLine: removeLine,
    outputListener: outputListener,
  );

  final CommandResult result = await command.onExit;

187
  if ((result.exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && result.exitCode != expectedExitCode)) {
188 189
    // Print the output when we get unexpected results (unless output was
    // printed already).
190 191 192 193
    switch (outputMode) {
      case OutputMode.print:
        break;
      case OutputMode.capture:
194
        io.stdout.writeln(result.flattenedStdout);
195
        io.stdout.writeln(result.flattenedStderr);
196
        break;
197
    }
198 199 200 201
    exitWithError(<String>[
      if (failureMessage != null)
        failureMessage
      else
202
        '${bold}ERROR: ${red}Last command exited with ${result.exitCode} (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset',
203 204 205
      '${bold}Command: $green$commandDescription$reset',
      '${bold}Relative working directory: $cyan$relativeWorkingDir$reset',
    ]);
206
  }
207 208
  print('$clock ELAPSED TIME: ${prettyPrintDuration(result.elapsedTime)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
  return result;
209
}
210 211

/// Flattens a nested list of UTF-8 code units into a single string.
212 213
String _flattenToString(List<List<int>> chunks) =>
  utf8.decode(chunks.expand<int>((List<int> ints) => ints).toList());
214

215 216 217
/// 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'
218
  /// standard output stream (i.e. stderr is redirected to stdout).
219 220 221 222 223 224 225 226 227 228 229 230
  ///
  /// 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,
231
}