run_command.dart 8.35 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 22 23 24 25
Stream<String> runAndGetStdout(String executable, List<String> arguments, {
  String workingDirectory,
  Map<String, String> environment,
  bool expectNonZeroExit = false,
}) async* {
26 27 28 29
  final StreamController<String> output = StreamController<String>();
  final Future<CommandResult> command = runCommand(
    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 55 56 57 58 59 60 61 62 63 64 65 66 67 68
  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);
Dan Field's avatar
Dan Field committed
69 70 71
  }
}

72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
/// 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.
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
  bool Function(String) removeLine,
104
  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 116
    workingDirectory: workingDirectory,
    environment: environment,
  );

  Future<List<List<int>>> savedStdout, savedStderr;
117 118 119 120
  final Stream<List<int>> stdoutSource = process.stdout
    .transform<String>(const Utf8Decoder())
    .transform(const LineSplitter())
    .where((String line) => removeLine == null || !removeLine(line))
121 122 123 124 125 126 127
    .map((String line) {
      final String formattedLine = '$line\n';
      if (outputListener != null) {
        outputListener(formattedLine, process);
      }
      return formattedLine;
    })
128
    .transform(const Utf8Encoder());
129 130
  switch (outputMode) {
    case OutputMode.print:
131 132
      stdoutSource.listen(io.stdout.add);
      process.stderr.listen(io.stderr.add);
133 134 135 136 137
      break;
    case OutputMode.capture:
      savedStdout = stdoutSource.toList();
      savedStderr = process.stderr.toList();
      break;
138 139
  }

140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
  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;
173 174
  }

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

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

213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
/// 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,
229
}