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 |