Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(195)

Side by Side Diff: pkg/scheduled_test/lib/scheduled_process.dart

Issue 12377093: Add a ScheduledProcess class to pkg/scheduled_test. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Code review changes. Created 7 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | pkg/scheduled_test/lib/scheduled_test.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « no previous file | pkg/scheduled_test/lib/scheduled_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698