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

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: 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
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})
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 }
OLDNEW
« no previous file with comments | « no previous file | pkg/scheduled_test/lib/scheduled_test.dart » ('j') | pkg/scheduled_test/lib/src/schedule.dart » ('J')

Powered by Google App Engine
This is Rietveld 408576698