Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library scheduled_process; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'dart:io'; | |
| 9 | |
| 10 import 'scheduled_test.dart'; | |
| 11 import 'src/utils.dart'; | |
| 12 import 'src/value_future.dart'; | |
| 13 | |
| 14 /// A class representing a [Process] that is scheduled to run in the course of | |
| 15 /// the test. This class allows actions on the process to be scheduled | |
| 16 /// synchronously. All operations on this class are scheduled. | |
| 17 /// | |
| 18 /// Before running the test, either [shouldExit] or [kill] must be called on | |
| 19 /// this to ensure that the process terminates when expected. | |
| 20 /// | |
| 21 /// If the test fails, this will automatically print out any stdout and stderr | |
| 22 /// from the process to aid debugging. | |
| 23 class ScheduledProcess { | |
| 24 // A description of the process. Used for error reporting. | |
| 25 String get description => _description; | |
| 26 String _description; | |
| 27 | |
| 28 /// The encoding used for the process's input and output streams. | |
| 29 final Encoding _encoding; | |
| 30 | |
| 31 /// The process that's scheduled to run. | |
| 32 ValueFuture<Process> _process; | |
| 33 | |
| 34 /// A fork of [_stdout] that records the standard output of the process. Used | |
| 35 /// for debugging information. | |
| 36 Stream<String> _stdoutLog; | |
| 37 | |
| 38 /// A line-by-line view of the standard output stream of the process. | |
| 39 Stream<String> _stdout; | |
| 40 | |
| 41 /// A subscription that controls both [_stdout] and [_stdoutLog]. | |
| 42 StreamSubscription<String> _stdoutSubscription; | |
| 43 | |
| 44 /// A fork of [_stderr] that records the standard error of the process. Used | |
| 45 /// for debugging information. | |
| 46 Stream<String> _stderrLog; | |
| 47 | |
| 48 /// A line-by-line view of the standard error stream of the process. | |
| 49 Stream<String> _stderr; | |
| 50 | |
| 51 /// A subscription that controls both [_stderr] and [_stderrLog]. | |
| 52 StreamSubscription<String> _stderrSubscription; | |
| 53 | |
| 54 /// The exit code of the process that's scheduled to run. This will naturally | |
| 55 /// only complete once the process has terminated. | |
| 56 ValueFuture<int> _exitCode; | |
| 57 | |
| 58 /// Whether the user has scheduled the end of this process by calling either | |
| 59 /// [shouldExit] or [kill]. | |
| 60 var _endScheduled = false; | |
| 61 | |
| 62 /// The task that runs immediately before this process is scheduled to end. If | |
| 63 /// the process ends during this task, we treat that as expected. | |
| 64 Task _taskBeforeEnd; | |
| 65 | |
| 66 /// Whether the process is expected to terminate at this point. | |
| 67 var _endExpected = false; | |
| 68 | |
| 69 /// Schedules a process to start. [executable], [arguments], and [options] | |
| 70 /// have the same meaning as for [Process.start]. [description] is a string | |
| 71 /// description of this process; it defaults to the command-line invocation. | |
| 72 /// [encoding] is the [Encoding] that will be used for the process's input and | |
| 73 /// output. | |
| 74 /// | |
| 75 /// [executable], [arguments], and [options] may be either a [Future] or a | |
| 76 /// concrete value. If any are [Future]s, the process won't start until the | |
| 77 /// [Future]s have completed. In addition, [arguments] may be a [List] | |
| 78 /// containing a mix of strings and [Future]s. | |
| 79 ScheduledProcess.start(executable, arguments, | |
| 80 {options, String description, Encoding encoding: Encoding.UTF_8}) | |
|
Bob Nystrom
2013/03/04 23:52:00
This is quite a large method with a bunch of async
| |
| 81 : _encoding = encoding { | |
| 82 assert(currentSchedule.state == ScheduleState.SET_UP); | |
| 83 | |
| 84 if (executable is Future) { | |
| 85 _description = "future process"; | |
| 86 } else if (arguments is Future || arguments.any((e) => e is Future)) { | |
| 87 _description = executable; | |
| 88 } else { | |
| 89 _description = "$executable ${arguments.map((a) => '"$a"').join(' ')}"; | |
| 90 } | |
|
Bob Nystrom
2013/03/04 23:52:00
How about pulling this into a separate _updateDesc
nweiz
2013/03/05 02:16:09
Done.
| |
| 91 | |
| 92 var exitCodeCompleter = new Completer(); | |
| 93 _exitCode = new ValueFuture(exitCodeCompleter.future); | |
| 94 | |
| 95 _process = new ValueFuture(schedule(() { | |
| 96 if (!_endScheduled) { | |
| 97 throw new StateError("Scheduled process '${this.description}' must " | |
| 98 "have shouldExit() or kill() called before the test is run."); | |
| 99 } | |
| 100 | |
| 101 // We purposefully avoid using wrapFuture here. If an error occurs while a | |
| 102 // process is running, we want the schedule to move to the onException | |
| 103 // queue where the process will be killed, rather than blocking the tasks | |
| 104 // queue waiting for the process to exit. | |
| 105 _process.then((p) => p.exitCode).then((exitCode) { | |
|
Bob Nystrom
2013/03/04 23:52:00
Asynchronously accessing the result of a future st
nweiz
2013/03/05 02:16:09
Done.
| |
| 106 if (_endExpected) { | |
| 107 exitCodeCompleter.complete(exitCode); | |
| 108 return; | |
| 109 } | |
| 110 | |
| 111 wrapFuture(pumpEventQueue().then((_) { | |
| 112 if (currentSchedule.currentTask != _taskBeforeEnd) return; | |
| 113 // If we're one task before the end was scheduled, wait for that task | |
| 114 // to complete and pump the event queue so that _endExpected will be | |
| 115 // set. | |
| 116 return _taskBeforeEnd.result.then((_) => pumpEventQueue()); | |
| 117 }).then((_) { | |
| 118 exitCodeCompleter.complete(exitCode); | |
| 119 | |
| 120 if (!_endExpected) { | |
| 121 throw "Process '${this.description}' ended earlier than scheduled " | |
| 122 "with exit code $exitCode."; | |
| 123 } | |
| 124 })); | |
| 125 }); | |
| 126 | |
| 127 return Future.wait([ | |
| 128 new Future.of(() => executable), | |
| 129 awaitObject(arguments), | |
| 130 new Future.of(() => options) | |
| 131 ]).then((results) { | |
| 132 executable = results[0]; | |
| 133 arguments = results[1]; | |
| 134 options = results[2]; | |
| 135 _description = "$executable ${arguments.map((a) => '"$a"').join(' ')}"; | |
|
Bob Nystrom
2013/03/04 23:52:00
Call _updateDescription() here.
nweiz
2013/03/05 02:16:09
Done.
| |
| 136 return Process.start(executable, arguments, options); | |
| 137 }); | |
| 138 }, "starting process '${this.description}'")); | |
| 139 | |
| 140 var stdoutWithSubscription = _lineStreamWithSubscription( | |
| 141 _process.then((p) => p.stdout)); | |
| 142 _stdoutSubscription = stdoutWithSubscription.last; | |
| 143 var stdoutTee = tee(stdoutWithSubscription.first); | |
| 144 _stdout = stdoutTee.first; | |
| 145 _stdoutLog = stdoutTee.last; | |
| 146 | |
| 147 var stderrWithSubscription = _lineStreamWithSubscription( | |
| 148 _process.then((p) => p.stderr)); | |
| 149 _stderrSubscription = stderrWithSubscription.last; | |
| 150 var stderrTee = tee(stderrWithSubscription.first); | |
| 151 _stderr = stderrTee.first; | |
| 152 _stderrLog = stderrTee.last; | |
| 153 | |
| 154 currentSchedule.onException.schedule(() { | |
|
Bob Nystrom
2013/03/04 23:52:00
Move this to a separate fn.
nweiz
2013/03/05 02:16:09
Done.
| |
| 155 _stdoutSubscription.cancel(); | |
| 156 _stderrSubscription.cancel(); | |
| 157 | |
| 158 if (!_process.hasValue) return; | |
| 159 | |
| 160 var killedPrematurely = false; | |
| 161 if (!_exitCode.hasValue) { | |
| 162 var killedPrematurely = true; | |
| 163 _endExpected = true; | |
| 164 _process.value.kill(); | |
| 165 // Ensure that the onException queue waits for the process to actually | |
| 166 // exit after being killed. | |
| 167 wrapFuture(_process.value.exitCode); | |
| 168 } | |
| 169 | |
| 170 return Future.wait([ | |
| 171 _stdoutLog.toList(), | |
| 172 _stderrLog.toList() | |
| 173 ]).then((results) { | |
| 174 var stdout = results[0].join("\n"); | |
| 175 var stderr = results[1].join("\n"); | |
| 176 | |
| 177 var exitDescription = killedPrematurely | |
| 178 ? "Process was killed prematurely." | |
| 179 : "Process exited with exit code ${_exitCode.value}."; | |
| 180 currentSchedule.addDebugInfo( | |
| 181 "Results of running '${this.description}':\n" | |
| 182 "$exitDescription\n" | |
| 183 "Standard output:\n" | |
| 184 "${prefixLines(stdout)}\n" | |
| 185 "Standard error:\n" | |
| 186 "${prefixLines(stderr)}"); | |
| 187 }); | |
| 188 }, "cleaning up process '${this.description}'"); | |
| 189 } | |
| 190 | |
| 191 /// Converts a stream of bytes to a stream of lines and returns that along | |
| 192 /// with a [StreamSubscription] controlling it. | |
| 193 Pair<Stream<String>, StreamSubscription<String>> _lineStreamWithSubscription( | |
| 194 Future<Stream<int>> streamFuture) { | |
| 195 return streamWithSubscription(futureStream(streamFuture) | |
| 196 .handleError((e) => currentSchedule.signalError(e)) | |
| 197 .transform(new StringDecoder(_encoding)) | |
| 198 .transform(new LineTransformer())); | |
| 199 } | |
| 200 | |
| 201 /// Reads the next line of stdout from the process. | |
| 202 Future<String> nextLine() => schedule(() => streamFirst(_stdout), | |
| 203 "reading the next stdout line from process '$description'"); | |
| 204 | |
| 205 /// Reads the next line of stderr from the process. | |
| 206 Future<String> nextErrLine() => schedule(() => streamFirst(_stderr), | |
| 207 "reading the next stderr line from process '$description'"); | |
| 208 | |
| 209 /// Reads the remaining stdout from the process. This should only be called | |
| 210 /// after kill() or shouldExit(). | |
| 211 Future<String> remainingStdout() { | |
| 212 if (!_endScheduled) { | |
| 213 throw new StateError("remainingStdout() should only be called after " | |
| 214 "kill() or shouldExit()."); | |
| 215 } | |
| 216 | |
| 217 return schedule(() => _stdout.toList().then((lines) => lines.join("\n")), | |
| 218 "reading the remaining stdout from process '$description'"); | |
| 219 } | |
| 220 | |
| 221 /// Reads the remaining stderr from the process. This should only be called | |
| 222 /// after kill() or shouldExit(). | |
| 223 Future<String> remainingStderr() { | |
| 224 if (!_endScheduled) { | |
| 225 throw new StateError("remainingStderr() should only be called after " | |
| 226 "kill() or shouldExit()."); | |
| 227 } | |
| 228 | |
| 229 return schedule(() => _stderr.toList().then((lines) => lines.join("\n")), | |
| 230 "reading the remaining stderr from process '$description'"); | |
| 231 } | |
| 232 | |
| 233 /// Writes [line] to the process as stdin. | |
| 234 void writeLine(String line) { | |
| 235 schedule(() { | |
| 236 return _process.then((p) => p.stdin.addString('$line\n', _encoding)); | |
| 237 }, "writing '$line' to stdin for process '$description'"); | |
| 238 } | |
| 239 | |
| 240 /// Closes the process's stdin stream. | |
| 241 void closeStdin() { | |
| 242 schedule(() => _process.then((p) => p.stdin.close()), | |
| 243 "closing stdin for process '$description'"); | |
| 244 } | |
| 245 | |
| 246 /// Kills the process, and waits until it's dead. | |
| 247 void kill() { | |
| 248 if (_endScheduled) { | |
| 249 throw new StateError("shouldExit() or kill() already called."); | |
| 250 } | |
| 251 | |
| 252 _endScheduled = true; | |
| 253 _taskBeforeEnd = currentSchedule.tasks.contents.last; | |
| 254 schedule(() { | |
| 255 _endExpected = true; | |
| 256 return _process.then((p) => p.kill()).then((_) => _exitCode); | |
| 257 }, "waiting for process '$description' to die"); | |
| 258 } | |
| 259 | |
| 260 /// Waits for the process to exit, and verifies that the exit code matches | |
| 261 /// [expectedExitCode] (if given). | |
| 262 void shouldExit([int expectedExitCode]) { | |
| 263 if (_endScheduled) { | |
| 264 throw new StateError("shouldExit() or kill() already called."); | |
| 265 } | |
| 266 | |
| 267 _endScheduled = true; | |
| 268 _taskBeforeEnd = currentSchedule.tasks.contents.last; | |
| 269 schedule(() { | |
| 270 _endExpected = true; | |
| 271 return _exitCode.then((exitCode) { | |
| 272 if (expectedExitCode != null) { | |
| 273 expect(exitCode, equals(expectedExitCode)); | |
| 274 } | |
| 275 }); | |
| 276 }, "waiting for process '$description' to exit"); | |
| 277 } | |
| 278 } | |
| OLD | NEW |