Skip to content

Commit d916bb9

Browse files
authored
Add the package implementation. (dart-archive/test_process#1)
1 parent a8137d1 commit d916bb9

File tree

3 files changed

+362
-1
lines changed

3 files changed

+362
-1
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// Copyright (c) 2017, 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+
import 'dart:async';
6+
import 'dart:convert';
7+
import 'dart:io';
8+
9+
import 'package:async/async.dart';
10+
import 'package:meta/meta.dart';
11+
import 'package:path/path.dart' as p;
12+
import 'package:test/test.dart';
13+
14+
/// A wrapper for [Process] that provides a convenient API for testing its
15+
/// standard IO and interacting with it from a test.
16+
///
17+
/// If the test fails, this will automatically print out any stdout and stderr
18+
/// from the process to aid debugging.
19+
///
20+
/// This may be extended to provide custom implementations of [stdoutStream] and
21+
/// [stderrStream]. These will automatically be picked up by the [stdout] and
22+
/// [stderr] queues, but the debug log will still contain the original output.
23+
class TestProcess {
24+
/// The underlying process.
25+
final Process _process;
26+
27+
/// A human-friendly description of this process.
28+
final String description;
29+
30+
/// A [StreamQueue] that emits each line of stdout from the process.
31+
///
32+
/// A copy of the underlying stream can be retreived using [stdoutStream].
33+
StreamQueue<String> get stdout => _stdout;
34+
StreamQueue<String> _stdout;
35+
36+
/// A [StreamQueue] that emits each line of stderr from the process.
37+
///
38+
/// A copy of the underlying stream can be retreived using [stderrStream].
39+
StreamQueue<String> get stderr => _stderr;
40+
StreamQueue<String> _stderr;
41+
42+
/// A splitter that can emit new copies of [stdout].
43+
final StreamSplitter<String> _stdoutSplitter;
44+
45+
/// A splitter that can emit new copies of [stderr].
46+
final StreamSplitter<String> _stderrSplitter;
47+
48+
/// The standard input sink for this process.
49+
IOSink get stdin => _process.stdin;
50+
51+
/// A buffer of mixed stdout and stderr lines.
52+
final _log = <String>[];
53+
54+
/// Whether [_log] has been passed to [printOnFailure] yet.
55+
bool _loggedOutput = false;
56+
57+
/// Completes to [_process]'s exit code if it's exited, otherwise completes to
58+
/// `null` immediately.
59+
Future<int> get _exitCodeOrNull async =>
60+
await _process.exitCode.timeout(Duration.ZERO, onTimeout: () => null);
61+
62+
/// Starts a process.
63+
///
64+
/// [executable], [arguments], [workingDirectory], and [environment] have the
65+
/// same meaning as for [Process.start].
66+
///
67+
/// [description] is a string description of this process; it defaults to the
68+
/// command-line invocation. [encoding] is the [Encoding] that will be used
69+
/// for the process's input and output; it defaults to [UTF8].
70+
///
71+
/// If [forwardStdio] is `true`, the process's stdout and stderr will be
72+
/// printed to the console as they appear. This is only intended to be set
73+
/// temporarily to help when debugging test failures.
74+
static Future<TestProcess> start(
75+
String executable,
76+
Iterable<String> arguments,
77+
{String workingDirectory,
78+
Map<String, String> environment,
79+
bool includeParentEnvironment: true,
80+
bool runInShell: false,
81+
String description,
82+
Encoding encoding,
83+
bool forwardStdio: false}) async {
84+
var process = await Process.start(executable, arguments.toList(),
85+
workingDirectory: workingDirectory,
86+
environment: environment,
87+
includeParentEnvironment: includeParentEnvironment,
88+
runInShell: runInShell);
89+
90+
if (description == null) {
91+
var humanExecutable = p.isWithin(p.current, executable)
92+
? p.relative(executable)
93+
: executable;
94+
description = "$humanExecutable ${arguments.join(" ")}";
95+
}
96+
97+
encoding ??= UTF8;
98+
return new TestProcess(
99+
process, description, encoding: encoding, forwardStdio: forwardStdio);
100+
}
101+
102+
/// Creates a [TestProcess] for [process].
103+
///
104+
/// The [description], [encoding], and [forwardStdio] are the same as those to
105+
/// [start].
106+
///
107+
/// This is protected, which means it should only be called by subclasses.
108+
@protected
109+
TestProcess(Process process, this.description, {Encoding encoding,
110+
bool forwardStdio: false})
111+
: _process = process,
112+
_stdoutSplitter = new StreamSplitter(process.stdout
113+
.transform(encoding.decoder).transform(const LineSplitter())),
114+
_stderrSplitter = new StreamSplitter(process.stderr
115+
.transform(encoding.decoder).transform(const LineSplitter())) {
116+
addTearDown(_tearDown);
117+
expect(_process.exitCode.then((_) => _logOutput()), completes,
118+
reason: "Process `$description` never exited.");
119+
120+
_stdout = new StreamQueue(stdoutStream());
121+
_stderr = new StreamQueue(stderrStream());
122+
123+
// Listen eagerly so that the lines are interleaved properly between the two
124+
// streams.
125+
stdoutStream().listen((line) {
126+
if (forwardStdio) print(line);
127+
_log.add(" $line");
128+
});
129+
130+
stderrStream().listen((line) {
131+
if (forwardStdio) print(line);
132+
_log.add("[e] $line");
133+
});
134+
}
135+
136+
/// A callback that's run when the test completes.
137+
Future _tearDown() async {
138+
// If the process is already dead, do nothing.
139+
if (await _exitCodeOrNull != null) return;
140+
141+
_process.kill(ProcessSignal.SIGKILL);
142+
143+
// Log output now rather than waiting for the exitCode callback so that
144+
// it's visible even if we time out waiting for the process to die.
145+
await _logOutput();
146+
}
147+
148+
/// Formats the contents of [_log] and passes them to [printOnFailure].
149+
Future _logOutput() async {
150+
if (_loggedOutput) return;
151+
_loggedOutput = true;
152+
153+
var exitCode = await _exitCodeOrNull;
154+
155+
// Wait a timer tick to ensure that all available lines have been flushed to
156+
// [_log].
157+
await new Future.delayed(Duration.ZERO);
158+
159+
var buffer = new StringBuffer();
160+
buffer.write("Process `$description` ");
161+
if ((await _exitCodeOrNull) == null) {
162+
buffer.writeln("was killed with SIGKILL in a tear-down. Output:");
163+
} else {
164+
buffer.writeln("exited with exitCode $exitCode. Output:");
165+
}
166+
167+
buffer.writeln(_log.join("\n"));
168+
printOnFailure(buffer.toString());
169+
}
170+
171+
/// Returns a copy of [stdout] as a single-subscriber stream.
172+
///
173+
/// Each time this is called, it will return a separate copy that will start
174+
/// from the beginning of the process.
175+
///
176+
/// This can be overridden by subclasses to return a derived standard output
177+
/// stream. This stream will then be used for [stdout].
178+
Stream<String> stdoutStream() => _stdoutSplitter.split();
179+
180+
/// Returns a copy of [stderr] as a single-subscriber stream.
181+
///
182+
/// Each time this is called, it will return a separate copy that will start
183+
/// from the beginning of the process.
184+
///
185+
/// This can be overridden by subclasses to return a derived standard output
186+
/// stream. This stream will then be used for [stderr].
187+
Stream<String> stderrStream() => _stderrSplitter.split();
188+
189+
/// Sends [signal] to the process.
190+
///
191+
/// This is meant for sending specific signals. If you just want to kill the
192+
/// process, use [kill] instead.
193+
///
194+
/// Throws an [UnsupportedError] on Windows.
195+
void signal(ProcessSignal signal) {
196+
if (Platform.isWindows) {
197+
throw new UnsupportedError(
198+
"TestProcess.signal() isn't supported on Windows.");
199+
}
200+
201+
_process.kill(signal);
202+
}
203+
204+
/// Kills the process (with SIGKILL on POSIX operating systems), and returns a
205+
/// future that completes once it's dead.
206+
///
207+
/// If this is called after the process is already dead, it does nothing.
208+
Future kill() async {
209+
_process.kill(ProcessSignal.SIGKILL);
210+
await _process.exitCode;
211+
}
212+
213+
/// Waits for the process to exit, and verifies that the exit code matches
214+
/// [expectedExitCode] (if given).
215+
///
216+
/// If this is called after the process is already dead, it verifies its
217+
/// existing exit code.
218+
Future shouldExit([expectedExitCode]) async {
219+
var exitCode = await _process.exitCode;
220+
if (expectedExitCode == null) return;
221+
expect(exitCode, expectedExitCode,
222+
reason: "Process `$description` had an unexpected exit code.");
223+
}
224+
}

pkgs/test_process/pubspec.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: test_process
2-
version: 1.0.0-dev
2+
version: 1.0.0-rc.1
33
description: A library for testing subprocesses.
44
author: Dart Team <misc@dartlang.org>
55
homepage: https://github.com/dart-lang/test_process
@@ -8,4 +8,10 @@ environment:
88
sdk: '>=1.8.0 <2.0.0'
99

1010
dependencies:
11+
async: "^1.12.0"
12+
meta: ">=0.9.0 <2.0.0"
13+
path: "^1.0.0"
1114
test: "^0.12.19"
15+
16+
dev_dependencies:
17+
test_descriptor: "^1.0.0"
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright (c) 2017, 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+
import 'dart:async';
6+
import 'dart:io';
7+
8+
import 'package:path/path.dart' as p;
9+
import 'package:test/test.dart';
10+
11+
import 'package:test_descriptor/test_descriptor.dart' as d;
12+
import 'package:test_process/test_process.dart';
13+
14+
final throwsTestFailure = throwsA(new isInstanceOf<TestFailure>());
15+
16+
void main() {
17+
group("shouldExit()", () {
18+
test("succeeds when the process exits with the given exit code", () async {
19+
var process = await startDartProcess('exitCode = 42;');
20+
await process.shouldExit(greaterThan(12));
21+
});
22+
23+
test("fails when the process exits with a different exit code", () async {
24+
var process = await startDartProcess('exitCode = 1;');
25+
expect(process.shouldExit(greaterThan(12)), throwsTestFailure);
26+
});
27+
28+
test("allows any exit code without an assertion", () async {
29+
var process = await startDartProcess('exitCode = 1;');
30+
await process.shouldExit();
31+
});
32+
});
33+
34+
test("kill() stops the process", () async {
35+
var process = await startDartProcess('while (true);');
36+
37+
// Should terminate.
38+
await process.kill();
39+
});
40+
41+
group("stdout and stderr", () {
42+
test("expose the process's standard io", () async {
43+
var process = await startDartProcess(r'''
44+
print("hello");
45+
stderr.writeln("hi");
46+
print("\nworld");
47+
''');
48+
49+
expect(process.stdout,
50+
emitsInOrder(['hello', '', 'world', emitsDone]));
51+
expect(process.stderr, emitsInOrder(['hi', emitsDone]));
52+
await process.shouldExit(0);
53+
});
54+
55+
test("close when the process exits", () async {
56+
var process = await startDartProcess('');
57+
expect(expectLater(process.stdout, emits('hello')),
58+
throwsTestFailure);
59+
expect(expectLater(process.stderr, emits('world')),
60+
throwsTestFailure);
61+
await process.shouldExit(0);
62+
});
63+
});
64+
65+
test("stdoutStream() and stderrStream() copy the process's standard io",
66+
() async {
67+
var process = await startDartProcess(r'''
68+
print("hello");
69+
stderr.writeln("hi");
70+
print("\nworld");
71+
''');
72+
73+
expect(process.stdoutStream(),
74+
emitsInOrder(['hello', '', 'world', emitsDone]));
75+
expect(process.stdoutStream(),
76+
emitsInOrder(['hello', '', 'world', emitsDone]));
77+
78+
expect(process.stderrStream(), emitsInOrder(['hi', emitsDone]));
79+
expect(process.stderrStream(), emitsInOrder(['hi', emitsDone]));
80+
81+
await process.shouldExit(0);
82+
83+
expect(process.stdoutStream(),
84+
emitsInOrder(['hello', '', 'world', emitsDone]));
85+
expect(process.stderrStream(), emitsInOrder(['hi', emitsDone]));
86+
});
87+
88+
test("stdin writes to the process", () async {
89+
var process = await startDartProcess(r'''
90+
stdinLines.listen((line) => print("> $line"));
91+
''');
92+
93+
process.stdin.writeln("hello");
94+
await expectLater(process.stdout, emits("> hello"));
95+
process.stdin.writeln("world");
96+
await expectLater(process.stdout, emits("> world"));
97+
await process.kill();
98+
});
99+
100+
test("signal sends a signal to the process", () async {
101+
var process = await startDartProcess(r'''
102+
ProcessSignal.SIGHUP.watch().listen((_) => print("HUP"));
103+
print("ready");
104+
''');
105+
106+
await expectLater(process.stdout, emits('ready'));
107+
process.signal(ProcessSignal.SIGHUP);
108+
await expectLater(process.stdout, emits('HUP'));
109+
process.kill();
110+
}, testOn: "!windows");
111+
}
112+
113+
/// Starts a Dart process running [script] in a main method.
114+
Future<TestProcess> startDartProcess(String script) {
115+
var dartPath = p.join(d.sandbox, 'test.dart');
116+
new File(dartPath).writeAsStringSync('''
117+
import 'dart:async';
118+
import 'dart:convert';
119+
import 'dart:io';
120+
121+
var stdinLines = stdin
122+
.transform(UTF8.decoder)
123+
.transform(new LineSplitter());
124+
125+
void main() {
126+
$script
127+
}
128+
''');
129+
130+
return TestProcess.start(Platform.executable, ['--checked', dartPath]);
131+
}

0 commit comments

Comments
 (0)