| 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_test.scheduled_process; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'dart:convert'; | |
| 9 import 'dart:io'; | |
| 10 | |
| 11 import 'package:stack_trace/stack_trace.dart'; | |
| 12 | |
| 13 import 'scheduled_stream.dart'; | |
| 14 import 'scheduled_test.dart'; | |
| 15 import 'src/utils.dart'; | |
| 16 import 'src/value_future.dart'; | |
| 17 | |
| 18 /// A class representing a [Process] that is scheduled to run in the course of | |
| 19 /// the test. This class allows actions on the process to be scheduled | |
| 20 /// synchronously. All operations on this class are scheduled. | |
| 21 /// | |
| 22 /// Before running the test, either [shouldExit] or [kill] must be called on | |
| 23 /// this to ensure that the process terminates when expected. Note that [kill] | |
| 24 /// is using SIGKILL, to ensure the process is killed on Mac OS X (an early | |
| 25 /// SIGTERM on Mac OS X may be ignored). | |
| 26 /// | |
| 27 /// If the test fails, this will automatically print out any stdout and stderr | |
| 28 /// from the process to aid debugging. | |
| 29 class ScheduledProcess { | |
| 30 /// A description of the process. Used for error reporting. | |
| 31 String get description => _description; | |
| 32 String _description; | |
| 33 | |
| 34 /// Whether a description was passed explicitly by the user. | |
| 35 bool _explicitDescription; | |
| 36 | |
| 37 /// The encoding used for the process's input and output streams. | |
| 38 final Encoding _encoding; | |
| 39 | |
| 40 /// The process that's scheduled to run. | |
| 41 ValueFuture<Process> _process; | |
| 42 | |
| 43 /// A fork of [_stdout] that records the standard output of the process. Used | |
| 44 /// for debugging information. | |
| 45 Stream<String> _stdoutLog; | |
| 46 | |
| 47 /// A line-by-line view of the standard output stream of the process. | |
| 48 ScheduledStream<String> get stdout => _stdout; | |
| 49 ScheduledStream<String> _stdout; | |
| 50 | |
| 51 /// A canceller that controls both [_stdout] and [_stdoutLog]. | |
| 52 StreamCanceller _stdoutCanceller; | |
| 53 | |
| 54 /// A fork of [_stderr] that records the standard error of the process. Used | |
| 55 /// for debugging information. | |
| 56 Stream<String> _stderrLog; | |
| 57 | |
| 58 /// A line-by-line view of the standard error stream of the process. | |
| 59 ScheduledStream<String> get stderr => _stderr; | |
| 60 ScheduledStream<String> _stderr; | |
| 61 | |
| 62 /// A canceller that controls both [_stderr] and [_stderrLog]. | |
| 63 StreamCanceller _stderrCanceller; | |
| 64 | |
| 65 /// The exit code of the process that's scheduled to run. This will naturally | |
| 66 /// only complete once the process has terminated. | |
| 67 ValueFuture<int> _exitCode; | |
| 68 | |
| 69 /// Whether the user has scheduled the end of this process by calling either | |
| 70 /// [shouldExit] or [kill]. | |
| 71 bool get _endScheduled => _scheduledExitTask != null; | |
| 72 | |
| 73 /// The task where this process is scheduled to exit -- either by waiting to | |
| 74 /// exit ([shouldExit]) or by killing the process ([kill]). | |
| 75 /// | |
| 76 /// It's legal for the process to exit before this task runs. This can happen | |
| 77 /// for example if there's still standard output to read from the process | |
| 78 /// after it exits. | |
| 79 Task _scheduledExitTask; | |
| 80 | |
| 81 /// The task during which the process actually exited. | |
| 82 Task _actualExitTask; | |
| 83 | |
| 84 /// Schedules a process to start. [executable], [arguments], | |
| 85 /// [workingDirectory], and [environment] have the same meaning as for | |
| 86 /// [Process.start]. [description] is a string description of this process; it | |
| 87 /// defaults to the command-line invocation. [encoding] is the [Encoding] that | |
| 88 /// will be used for the process's input and output. | |
| 89 /// | |
| 90 /// [executable], [arguments], [workingDirectory], and [environment] may be | |
| 91 /// either a [Future] or a concrete value. If any are [Future]s, the process | |
| 92 /// won't start until the [Future]s have completed. In addition, [arguments] | |
| 93 /// may be a [List] containing a mix of strings and [Future]s. | |
| 94 ScheduledProcess.start(executable, arguments, | |
| 95 {workingDirectory, environment, String description, | |
| 96 Encoding encoding: UTF8}) | |
| 97 : _encoding = encoding, | |
| 98 _explicitDescription = description != null, | |
| 99 _description = description { | |
| 100 assert(currentSchedule.state == ScheduleState.SET_UP); | |
| 101 | |
| 102 _updateDescription(executable, arguments); | |
| 103 | |
| 104 _scheduleStartProcess(executable, arguments, workingDirectory, environment); | |
| 105 | |
| 106 _scheduleExceptionCleanup(); | |
| 107 | |
| 108 var stdoutWithCanceller = _lineStreamWithCanceller( | |
| 109 _process.then((p) => Chain.track(p.stdout))); | |
| 110 _stdoutCanceller = stdoutWithCanceller.last; | |
| 111 _stdoutLog = stdoutWithCanceller.first; | |
| 112 | |
| 113 var stderrWithCanceller = _lineStreamWithCanceller( | |
| 114 _process.then((p) => Chain.track(p.stderr))); | |
| 115 _stderrCanceller = stderrWithCanceller.last; | |
| 116 _stderrLog = stderrWithCanceller.first; | |
| 117 | |
| 118 _stdout = new ScheduledStream<String>(stdoutStream()); | |
| 119 _stderr = new ScheduledStream<String>(stderrStream()); | |
| 120 } | |
| 121 | |
| 122 /// Updates [_description] to reflect [executable] and [arguments], which are | |
| 123 /// the same values as in [start]. | |
| 124 void _updateDescription(executable, arguments) { | |
| 125 if (_explicitDescription) return; | |
| 126 if (executable is Future) { | |
| 127 _description = "future process"; | |
| 128 } else if (arguments is Future || arguments.any((e) => e is Future)) { | |
| 129 _description = executable; | |
| 130 } else { | |
| 131 _description = "$executable ${arguments.map((a) => '"$a"').join(' ')}"; | |
| 132 } | |
| 133 } | |
| 134 | |
| 135 /// Schedules the process to start and sets [_process]. | |
| 136 void _scheduleStartProcess(executable, | |
| 137 arguments, | |
| 138 workingDirectory, | |
| 139 environment) { | |
| 140 var exitCodeCompleter = new Completer(); | |
| 141 _exitCode = new ValueFuture(exitCodeCompleter.future); | |
| 142 | |
| 143 _process = new ValueFuture(schedule(() { | |
| 144 if (!_endScheduled) { | |
| 145 throw new StateError("Scheduled process '$description' must " | |
| 146 "have shouldExit() or kill() called before the test is run."); | |
| 147 } | |
| 148 | |
| 149 _handleExit(exitCodeCompleter); | |
| 150 | |
| 151 return Future.wait([ | |
| 152 new Future.sync(() => executable), | |
| 153 awaitObject(arguments), | |
| 154 new Future.sync(() => workingDirectory), | |
| 155 new Future.sync(() => environment) | |
| 156 ]).then((results) { | |
| 157 executable = results[0]; | |
| 158 arguments = results[1]; | |
| 159 workingDirectory = results[2]; | |
| 160 environment = results[3]; | |
| 161 _updateDescription(executable, arguments); | |
| 162 return Chain.track( | |
| 163 Process.start(executable, | |
| 164 arguments, | |
| 165 workingDirectory: workingDirectory, | |
| 166 environment: environment)).then((process) { | |
| 167 process.stdin.encoding = UTF8; | |
| 168 return process; | |
| 169 }); | |
| 170 }); | |
| 171 }, "starting process '$description'")); | |
| 172 } | |
| 173 | |
| 174 /// Listens for [_process] to exit and passes the exit code to | |
| 175 /// [exitCodeCompleter]. If the process completes earlier than expected, an | |
| 176 /// exception will be signaled to the schedule. | |
| 177 void _handleExit(Completer exitCodeCompleter) { | |
| 178 // We purposefully avoid using wrapFuture here. If an error occurs while a | |
| 179 // process is running, we want the schedule to move to the onException | |
| 180 // queue where the process will be killed, rather than blocking the tasks | |
| 181 // queue waiting for the process to exit. | |
| 182 _process.then((p) => Chain.track(p.exitCode)).then((exitCode) { | |
| 183 _actualExitTask = currentSchedule.currentTask; | |
| 184 exitCodeCompleter.complete(exitCode); | |
| 185 }); | |
| 186 } | |
| 187 | |
| 188 /// Converts a stream of byte lists to a stream of lines and returns that | |
| 189 /// along with a [StreamCanceller] controlling it. | |
| 190 Pair<Stream<String>, StreamCanceller> _lineStreamWithCanceller( | |
| 191 Future<Stream<List<int>>> streamFuture) { | |
| 192 // Ignore errors from the future. They'll be reported through [schedule]. | |
| 193 streamFuture = streamFuture.catchError((_) => new Stream.fromIterable([])); | |
| 194 return streamWithCanceller(futureStream(streamFuture) | |
| 195 .handleError(currentSchedule.signalError) | |
| 196 .map((chunk) { | |
| 197 // Whenever the process produces any sort of output, reset the schedule's | |
| 198 // timer. | |
| 199 currentSchedule.heartbeat(); | |
| 200 return chunk; | |
| 201 }) | |
| 202 .transform(_encoding.decoder) | |
| 203 .transform(new LineSplitter())); | |
| 204 } | |
| 205 | |
| 206 /// Schedule an exception handler that will clean up the process and provide | |
| 207 /// debug information if an error occurs. | |
| 208 void _scheduleExceptionCleanup() { | |
| 209 currentSchedule.onException.schedule(() { | |
| 210 _stdoutCanceller(); | |
| 211 _stderrCanceller(); | |
| 212 | |
| 213 if (!_process.hasValue) return null; | |
| 214 | |
| 215 var killedPrematurely = false; | |
| 216 if (!_exitCode.hasValue) { | |
| 217 killedPrematurely = true; | |
| 218 _process.value.kill(ProcessSignal.SIGKILL); | |
| 219 // Ensure that the onException queue waits for the process to actually | |
| 220 // exit after being killed. | |
| 221 wrapFuture(_process.value.exitCode, "waiting for process " | |
| 222 "'$description' to die"); | |
| 223 } | |
| 224 | |
| 225 return Future.wait([ | |
| 226 _stdoutLog.toList(), | |
| 227 _stderrLog.toList() | |
| 228 ]).then((results) { | |
| 229 var stdout = results[0].join("\n"); | |
| 230 var stderr = results[1].join("\n"); | |
| 231 | |
| 232 var exitDescription; | |
| 233 if (killedPrematurely) { | |
| 234 exitDescription = "Process was killed prematurely."; | |
| 235 } else { | |
| 236 exitDescription = "Process exited with exit code ${_exitCode.value}"; | |
| 237 if (_actualExitTask != _scheduledExitTask) { | |
| 238 var taskString = _actualExitTask.toString(); | |
| 239 if (taskString.contains("\n")) { | |
| 240 exitDescription += " in task:\n${prefixLines(taskString)}"; | |
| 241 } else { | |
| 242 exitDescription += " in task $taskString"; | |
| 243 } | |
| 244 } | |
| 245 exitDescription += "."; | |
| 246 } | |
| 247 | |
| 248 currentSchedule.addDebugInfo( | |
| 249 "Results of running '$description':\n" | |
| 250 "$exitDescription\n" | |
| 251 "Standard output:\n" | |
| 252 "${prefixLines(stdout)}\n" | |
| 253 "Standard error:\n" | |
| 254 "${prefixLines(stderr)}"); | |
| 255 }); | |
| 256 }, "cleaning up process '$description'"); | |
| 257 } | |
| 258 | |
| 259 /// Returns a stream that will emit anything the process emits via the | |
| 260 /// process's standard output from now on. | |
| 261 /// | |
| 262 /// This stream will be independent from any other methods that deal with | |
| 263 /// standard output, including other calls to [stdoutStream]. | |
| 264 /// | |
| 265 /// This can be overridden by subclasses to return a derived standard output | |
| 266 /// stream. This stream will then be used for [stdout] and [stderr]. | |
| 267 Stream<String> stdoutStream() { | |
| 268 var pair = tee(_stdoutLog); | |
| 269 _stdoutLog = pair.first; | |
| 270 return pair.last; | |
| 271 } | |
| 272 | |
| 273 /// Returns a stream that will emit anything the process emits via the | |
| 274 /// process's standard error from now on. | |
| 275 /// | |
| 276 /// This stream will be independent from any other methods that deal with | |
| 277 /// standard error, including other calls to [stderrStream]. | |
| 278 Stream<String> stderrStream() { | |
| 279 var pair = tee(_stderrLog); | |
| 280 _stderrLog = pair.first; | |
| 281 return pair.last; | |
| 282 } | |
| 283 | |
| 284 /// Writes [line] to the process as stdin. | |
| 285 void writeLine(String line) { | |
| 286 schedule(() { | |
| 287 return _process.then((p) => p.stdin.writeln('$line')); | |
| 288 }, "writing '$line' to stdin for process '$description'"); | |
| 289 } | |
| 290 | |
| 291 /// Closes the process's stdin stream. | |
| 292 void closeStdin() { | |
| 293 schedule(() => _process.then((p) => p.stdin.close()), | |
| 294 "closing stdin for process '$description'"); | |
| 295 } | |
| 296 | |
| 297 /// Kills the process, and waits until it's dead. | |
| 298 void kill() { | |
| 299 if (_endScheduled) { | |
| 300 throw new StateError("shouldExit() or kill() already called."); | |
| 301 } | |
| 302 | |
| 303 schedule(() { | |
| 304 return _process | |
| 305 .then((p) => p.kill(ProcessSignal.SIGKILL)) | |
| 306 .then((_) => _exitCode); | |
| 307 }, "waiting for process '$description' to die"); | |
| 308 _scheduledExitTask = currentSchedule.tasks.contents.last; | |
| 309 } | |
| 310 | |
| 311 /// Waits for the process to exit, and verifies that the exit code matches | |
| 312 /// [expectedExitCode] (if given). | |
| 313 void shouldExit([int expectedExitCode]) { | |
| 314 if (_endScheduled) { | |
| 315 throw new StateError("shouldExit() or kill() already called."); | |
| 316 } | |
| 317 | |
| 318 schedule(() { | |
| 319 return _exitCode.then((exitCode) { | |
| 320 if (expectedExitCode != null) { | |
| 321 expect(exitCode, equals(expectedExitCode)); | |
| 322 } | |
| 323 }); | |
| 324 }, "waiting for process '$description' to exit"); | |
| 325 _scheduledExitTask = currentSchedule.tasks.contents.last; | |
| 326 } | |
| 327 } | |
| OLD | NEW |