forked from flutter/flutter
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathrun_command.dart
More file actions
229 lines (202 loc) · 8.38 KB
/
run_command.dart
File metadata and controls
229 lines (202 loc) · 8.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
// 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, savedStderr;
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(io.stdout.add);
process.stderr.listen(io.stdout.add);
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, 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;
}
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,
}