blob: dcb1d855e09748859194ebae8471e2b7a11c3aa2 [file] [log] [blame]
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:async/async.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
/// A wrapper for [Process] that provides a convenient API for testing its
/// standard IO and interacting with it from a test.
///
/// If the test fails, this will automatically print out any stdout and stderr
/// from the process to aid debugging.
///
/// This may be extended to provide custom implementations of [stdoutStream] and
/// [stderrStream]. These will automatically be picked up by the [stdout] and
/// [stderr] queues, but the debug log will still contain the original output.
class TestProcess {
/// The underlying process.
final Process _process;
/// A human-friendly description of this process.
final String description;
/// A [StreamQueue] that emits each line of stdout from the process.
///
/// A copy of the underlying stream can be retreived using [stdoutStream].
StreamQueue<String> get stdout => _stdout;
StreamQueue<String> _stdout;
/// A [StreamQueue] that emits each line of stderr from the process.
///
/// A copy of the underlying stream can be retreived using [stderrStream].
StreamQueue<String> get stderr => _stderr;
StreamQueue<String> _stderr;
/// A splitter that can emit new copies of [stdout].
final StreamSplitter<String> _stdoutSplitter;
/// A splitter that can emit new copies of [stderr].
final StreamSplitter<String> _stderrSplitter;
/// The standard input sink for this process.
IOSink get stdin => _process.stdin;
/// A buffer of mixed stdout and stderr lines.
final _log = <String>[];
/// Whether [_log] has been passed to [printOnFailure] yet.
bool _loggedOutput = false;
/// Returns a [Future] which completes to the exit code of the process, once
/// it completes.
Future<int> get exitCode => _process.exitCode;
/// The process ID of the process.
int get pid => _process.pid;
/// Completes to [_process]'s exit code if it's exited, otherwise completes to
/// `null` immediately.
Future<int> get _exitCodeOrNull async =>
await exitCode.timeout(Duration.ZERO, onTimeout: () => null);
/// Starts a process.
///
/// [executable], [arguments], [workingDirectory], and [environment] have the
/// same meaning as for [Process.start].
///
/// [description] is a string description of this process; it defaults to the
/// command-line invocation. [encoding] is the [Encoding] that will be used
/// for the process's input and output; it defaults to [UTF8].
///
/// If [forwardStdio] is `true`, the process's stdout and stderr will be
/// printed to the console as they appear. This is only intended to be set
/// temporarily to help when debugging test failures.
static Future<TestProcess> start(
String executable, Iterable<String> arguments,
{String workingDirectory,
Map<String, String> environment,
bool includeParentEnvironment: true,
bool runInShell: false,
String description,
Encoding encoding,
bool forwardStdio: false}) async {
var process = await Process.start(executable, arguments.toList(),
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: runInShell);
if (description == null) {
var humanExecutable = p.isWithin(p.current, executable)
? p.relative(executable)
: executable;
description = "$humanExecutable ${arguments.join(" ")}";
}
encoding ??= UTF8;
return new TestProcess(process, description,
encoding: encoding, forwardStdio: forwardStdio);
}
/// Creates a [TestProcess] for [process].
///
/// The [description], [encoding], and [forwardStdio] are the same as those to
/// [start].
///
/// This is protected, which means it should only be called by subclasses.
@protected
TestProcess(Process process, this.description,
{Encoding encoding, bool forwardStdio: false})
: _process = process,
_stdoutSplitter = new StreamSplitter(process.stdout
.transform(encoding.decoder)
.transform(const LineSplitter())),
_stderrSplitter = new StreamSplitter(process.stderr
.transform(encoding.decoder)
.transform(const LineSplitter())) {
addTearDown(_tearDown);
expect(_process.exitCode.then((_) => _logOutput()), completes,
reason: "Process `$description` never exited.");
_stdout = new StreamQueue(stdoutStream());
_stderr = new StreamQueue(stderrStream());
// Listen eagerly so that the lines are interleaved properly between the two
// streams.
//
// Call [split] explicitly because we don't want to log overridden
// [stdoutStream] or [stderrStream] output.
_stdoutSplitter.split().listen((line) {
if (forwardStdio) print(line);
_log.add(" $line");
});
_stderrSplitter.split().listen((line) {
if (forwardStdio) print(line);
_log.add("[e] $line");
});
}
/// A callback that's run when the test completes.
Future _tearDown() async {
// If the process is already dead, do nothing.
if (await _exitCodeOrNull != null) return;
_process.kill(ProcessSignal.SIGKILL);
// Log output now rather than waiting for the exitCode callback so that
// it's visible even if we time out waiting for the process to die.
await _logOutput();
}
/// Formats the contents of [_log] and passes them to [printOnFailure].
Future _logOutput() async {
if (_loggedOutput) return;
_loggedOutput = true;
var exitCodeOrNull = await _exitCodeOrNull;
// Wait a timer tick to ensure that all available lines have been flushed to
// [_log].
await new Future.delayed(Duration.ZERO);
var buffer = new StringBuffer();
buffer.write("Process `$description` ");
if (exitCodeOrNull == null) {
buffer.writeln("was killed with SIGKILL in a tear-down. Output:");
} else {
buffer.writeln("exited with exitCode $exitCodeOrNull. Output:");
}
buffer.writeln(_log.join("\n"));
printOnFailure(buffer.toString());
}
/// Returns a copy of [stdout] as a single-subscriber stream.
///
/// Each time this is called, it will return a separate copy that will start
/// from the beginning of the process.
///
/// This can be overridden by subclasses to return a derived standard output
/// stream. This stream will then be used for [stdout].
Stream<String> stdoutStream() => _stdoutSplitter.split();
/// Returns a copy of [stderr] as a single-subscriber stream.
///
/// Each time this is called, it will return a separate copy that will start
/// from the beginning of the process.
///
/// This can be overridden by subclasses to return a derived standard output
/// stream. This stream will then be used for [stderr].
Stream<String> stderrStream() => _stderrSplitter.split();
/// Sends [signal] to the process.
///
/// This is meant for sending specific signals. If you just want to kill the
/// process, use [kill] instead.
///
/// Throws an [UnsupportedError] on Windows.
void signal(ProcessSignal signal) {
if (Platform.isWindows) {
throw new UnsupportedError(
"TestProcess.signal() isn't supported on Windows.");
}
_process.kill(signal);
}
/// Kills the process (with SIGKILL on POSIX operating systems), and returns a
/// future that completes once it's dead.
///
/// If this is called after the process is already dead, it does nothing.
Future kill() async {
_process.kill(ProcessSignal.SIGKILL);
await exitCode;
}
/// Waits for the process to exit, and verifies that the exit code matches
/// [expectedExitCode] (if given).
///
/// If this is called after the process is already dead, it verifies its
/// existing exit code.
Future shouldExit([expectedExitCode]) async {
var exitCode = await this.exitCode;
if (expectedExitCode == null) return;
expect(exitCode, expectedExitCode,
reason: "Process `$description` had an unexpected exit code.");
}
}