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}) |
| 81 : _encoding = encoding { |
| 82 assert(currentSchedule.state == ScheduleState.SET_UP); |
| 83 |
| 84 _updateDescription(executable, arguments); |
| 85 |
| 86 _scheduleStartProcess(executable, arguments, options); |
| 87 |
| 88 _scheduleExceptionCleanup(); |
| 89 |
| 90 var stdoutWithSubscription = _lineStreamWithSubscription( |
| 91 _process.then((p) => p.stdout)); |
| 92 _stdoutSubscription = stdoutWithSubscription.last; |
| 93 var stdoutTee = tee(stdoutWithSubscription.first); |
| 94 _stdout = stdoutTee.first; |
| 95 _stdoutLog = stdoutTee.last; |
| 96 |
| 97 var stderrWithSubscription = _lineStreamWithSubscription( |
| 98 _process.then((p) => p.stderr)); |
| 99 _stderrSubscription = stderrWithSubscription.last; |
| 100 var stderrTee = tee(stderrWithSubscription.first); |
| 101 _stderr = stderrTee.first; |
| 102 _stderrLog = stderrTee.last; |
| 103 } |
| 104 |
| 105 /// Updates [_description] to reflect [executable] and [arguments], which are |
| 106 /// the same values as in [start]. |
| 107 void _updateDescription(executable, arguments) { |
| 108 if (executable is Future) { |
| 109 _description = "future process"; |
| 110 } else if (arguments is Future || arguments.any((e) => e is Future)) { |
| 111 _description = executable; |
| 112 } else { |
| 113 _description = "$executable ${arguments.map((a) => '"$a"').join(' ')}"; |
| 114 } |
| 115 } |
| 116 |
| 117 /// Schedules the process to start and sets [_process]. |
| 118 void _scheduleStartProcess(executable, arguments, options) { |
| 119 var exitCodeCompleter = new Completer(); |
| 120 _exitCode = new ValueFuture(exitCodeCompleter.future); |
| 121 |
| 122 _process = new ValueFuture(schedule(() { |
| 123 if (!_endScheduled) { |
| 124 throw new StateError("Scheduled process '${this.description}' must " |
| 125 "have shouldExit() or kill() called before the test is run."); |
| 126 } |
| 127 |
| 128 _handleExit(exitCodeCompleter); |
| 129 |
| 130 return Future.wait([ |
| 131 new Future.of(() => executable), |
| 132 awaitObject(arguments), |
| 133 new Future.of(() => options) |
| 134 ]).then((results) { |
| 135 executable = results[0]; |
| 136 arguments = results[1]; |
| 137 options = results[2]; |
| 138 _updateDescription(executable, arguments); |
| 139 return Process.start(executable, arguments, options); |
| 140 }); |
| 141 }, "starting process '${this.description}'")); |
| 142 } |
| 143 |
| 144 /// Listens for [_process] to exit and passes the exit code to |
| 145 /// [exitCodeCompleter]. If the process completes earlier than expected, an |
| 146 /// exception will be signaled to the schedule. |
| 147 void _handleExit(Completer exitCodeCompleter) { |
| 148 // We purposefully avoid using wrapFuture here. If an error occurs while a |
| 149 // process is running, we want the schedule to move to the onException |
| 150 // queue where the process will be killed, rather than blocking the tasks |
| 151 // queue waiting for the process to exit. |
| 152 _process.then((p) => p.exitCode).then((exitCode) { |
| 153 if (_endExpected) { |
| 154 exitCodeCompleter.complete(exitCode); |
| 155 return; |
| 156 } |
| 157 |
| 158 wrapFuture(pumpEventQueue().then((_) { |
| 159 if (currentSchedule.currentTask != _taskBeforeEnd) return; |
| 160 // If we're one task before the end was scheduled, wait for that task |
| 161 // to complete and pump the event queue so that _endExpected will be |
| 162 // set. |
| 163 return _taskBeforeEnd.result.then((_) => pumpEventQueue()); |
| 164 }).then((_) { |
| 165 exitCodeCompleter.complete(exitCode); |
| 166 |
| 167 if (!_endExpected) { |
| 168 throw "Process '${this.description}' ended earlier than scheduled " |
| 169 "with exit code $exitCode."; |
| 170 } |
| 171 })); |
| 172 }); |
| 173 } |
| 174 |
| 175 /// Converts a stream of bytes to a stream of lines and returns that along |
| 176 /// with a [StreamSubscription] controlling it. |
| 177 Pair<Stream<String>, StreamSubscription<String>> _lineStreamWithSubscription( |
| 178 Future<Stream<int>> streamFuture) { |
| 179 return streamWithSubscription(futureStream(streamFuture) |
| 180 .handleError((e) => currentSchedule.signalError(e)) |
| 181 .transform(new StringDecoder(_encoding)) |
| 182 .transform(new LineTransformer())); |
| 183 } |
| 184 |
| 185 /// Schedule an exception handler that will clean up the process and provide |
| 186 /// debug information if an error occurs. |
| 187 void _scheduleExceptionCleanup() { |
| 188 currentSchedule.onException.schedule(() { |
| 189 _stdoutSubscription.cancel(); |
| 190 _stderrSubscription.cancel(); |
| 191 |
| 192 if (!_process.hasValue) return; |
| 193 |
| 194 var killedPrematurely = false; |
| 195 if (!_exitCode.hasValue) { |
| 196 var killedPrematurely = true; |
| 197 _endExpected = true; |
| 198 _process.value.kill(); |
| 199 // Ensure that the onException queue waits for the process to actually |
| 200 // exit after being killed. |
| 201 wrapFuture(_process.value.exitCode); |
| 202 } |
| 203 |
| 204 return Future.wait([ |
| 205 _stdoutLog.toList(), |
| 206 _stderrLog.toList() |
| 207 ]).then((results) { |
| 208 var stdout = results[0].join("\n"); |
| 209 var stderr = results[1].join("\n"); |
| 210 |
| 211 var exitDescription = killedPrematurely |
| 212 ? "Process was killed prematurely." |
| 213 : "Process exited with exit code ${_exitCode.value}."; |
| 214 currentSchedule.addDebugInfo( |
| 215 "Results of running '${this.description}':\n" |
| 216 "$exitDescription\n" |
| 217 "Standard output:\n" |
| 218 "${prefixLines(stdout)}\n" |
| 219 "Standard error:\n" |
| 220 "${prefixLines(stderr)}"); |
| 221 }); |
| 222 }, "cleaning up process '${this.description}'"); |
| 223 } |
| 224 |
| 225 /// Reads the next line of stdout from the process. |
| 226 Future<String> nextLine() => schedule(() => streamFirst(_stdout), |
| 227 "reading the next stdout line from process '$description'"); |
| 228 |
| 229 /// Reads the next line of stderr from the process. |
| 230 Future<String> nextErrLine() => schedule(() => streamFirst(_stderr), |
| 231 "reading the next stderr line from process '$description'"); |
| 232 |
| 233 /// Reads the remaining stdout from the process. This should only be called |
| 234 /// after kill() or shouldExit(). |
| 235 Future<String> remainingStdout() { |
| 236 if (!_endScheduled) { |
| 237 throw new StateError("remainingStdout() should only be called after " |
| 238 "kill() or shouldExit()."); |
| 239 } |
| 240 |
| 241 return schedule(() => _stdout.toList().then((lines) => lines.join("\n")), |
| 242 "reading the remaining stdout from process '$description'"); |
| 243 } |
| 244 |
| 245 /// Reads the remaining stderr from the process. This should only be called |
| 246 /// after kill() or shouldExit(). |
| 247 Future<String> remainingStderr() { |
| 248 if (!_endScheduled) { |
| 249 throw new StateError("remainingStderr() should only be called after " |
| 250 "kill() or shouldExit()."); |
| 251 } |
| 252 |
| 253 return schedule(() => _stderr.toList().then((lines) => lines.join("\n")), |
| 254 "reading the remaining stderr from process '$description'"); |
| 255 } |
| 256 |
| 257 /// Writes [line] to the process as stdin. |
| 258 void writeLine(String line) { |
| 259 schedule(() { |
| 260 return _process.then((p) => p.stdin.addString('$line\n', _encoding)); |
| 261 }, "writing '$line' to stdin for process '$description'"); |
| 262 } |
| 263 |
| 264 /// Closes the process's stdin stream. |
| 265 void closeStdin() { |
| 266 schedule(() => _process.then((p) => p.stdin.close()), |
| 267 "closing stdin for process '$description'"); |
| 268 } |
| 269 |
| 270 /// Kills the process, and waits until it's dead. |
| 271 void kill() { |
| 272 if (_endScheduled) { |
| 273 throw new StateError("shouldExit() or kill() already called."); |
| 274 } |
| 275 |
| 276 _endScheduled = true; |
| 277 _taskBeforeEnd = currentSchedule.tasks.contents.last; |
| 278 schedule(() { |
| 279 _endExpected = true; |
| 280 return _process.then((p) => p.kill()).then((_) => _exitCode); |
| 281 }, "waiting for process '$description' to die"); |
| 282 } |
| 283 |
| 284 /// Waits for the process to exit, and verifies that the exit code matches |
| 285 /// [expectedExitCode] (if given). |
| 286 void shouldExit([int expectedExitCode]) { |
| 287 if (_endScheduled) { |
| 288 throw new StateError("shouldExit() or kill() already called."); |
| 289 } |
| 290 |
| 291 _endScheduled = true; |
| 292 _taskBeforeEnd = currentSchedule.tasks.contents.last; |
| 293 schedule(() { |
| 294 _endExpected = true; |
| 295 return _exitCode.then((exitCode) { |
| 296 if (expectedExitCode != null) { |
| 297 expect(exitCode, equals(expectedExitCode)); |
| 298 } |
| 299 }); |
| 300 }, "waiting for process '$description' to exit"); |
| 301 } |
| 302 } |
OLD | NEW |