dart-sass/test/embedded_process.dart
2020-07-21 16:21:24 -07:00

185 lines
6.5 KiB
Dart

// Copyright 2019 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:async/async.dart';
import 'package:cli_pkg/testing.dart' as pkg;
import 'package:test/test.dart';
import 'package:sass_embedded/src/embedded_sass.pb.dart';
import 'package:sass_embedded/src/util/length_delimited_transformer.dart';
/// A wrapper for [Process] that provides a convenient API for testing the
/// embedded Sass process.
///
/// If the test fails, this will automatically print out any stderr and protocol
/// buffers from the process to aid debugging.
///
/// This API is based on the `test_process` package.
class EmbeddedProcess {
/// The underlying process.
final Process _process;
/// A [StreamQueue] that emits each outbound protocol buffer from the process.
StreamQueue<OutboundMessage> get outbound => _outbound;
StreamQueue<OutboundMessage> _outbound;
/// A [StreamQueue] that emits each line of stderr from the process.
StreamQueue<String> get stderr => _stderr;
StreamQueue<String> _stderr;
/// A splitter that can emit new copies of [outbound].
final StreamSplitter<OutboundMessage> _outboundSplitter;
/// A splitter that can emit new copies of [stderr].
final StreamSplitter<String> _stderrSplitter;
/// A sink into which inbound messages can be passed to the process.
final Sink<InboundMessage> inbound;
/// The raw standard input byte sink.
IOSink get stdin => _process.stdin;
/// A log that includes lines from [stderr] and human-friendly serializations
/// of protocol buffers from [outbound]
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], [workingDirectory], [environment],
/// [includeParentEnvironment], and [runInShell] have the same meaning as for
/// [Process.start].
///
/// If [forwardOutput] is `true`, the process's [outbound] messages 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<EmbeddedProcess> start(
{String workingDirectory,
Map<String, String> environment,
bool includeParentEnvironment = true,
bool runInShell = false,
bool forwardOutput = false}) async {
var process = await Process.start(
pkg.executableRunner("dart-sass-embedded"),
pkg.executableArgs("dart-sass-embedded"),
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: runInShell);
return EmbeddedProcess._(process, forwardOutput: forwardOutput);
}
/// Creates a [EmbeddedProcess] for [process].
///
/// The [forwardOutput] argument is the same as that to [start].
EmbeddedProcess._(Process process, {bool forwardOutput = false})
: _process = process,
_outboundSplitter = StreamSplitter(process.stdout
.transform(lengthDelimitedDecoder)
.map((message) => OutboundMessage.fromBuffer(message))),
_stderrSplitter = StreamSplitter(process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())),
inbound = StreamSinkTransformer<InboundMessage, List<int>>.fromHandlers(
handleData: (message, sink) =>
sink.add(message.writeToBuffer())).bind(
StreamSinkTransformer.fromStreamTransformer(lengthDelimitedEncoder)
.bind(process.stdin)) {
addTearDown(_tearDown);
expect(_process.exitCode.then((_) => _logOutput()), completes,
reason: "Process `dart_sass_embedded` never exited.");
_outbound = StreamQueue(_outboundSplitter.split());
_stderr = StreamQueue(_stderrSplitter.split());
_outboundSplitter.split().listen((message) {
for (var line in message.toDebugString().split("\n")) {
if (forwardOutput) print(line);
_log.add(" $line");
}
});
_stderrSplitter.split().listen((line) {
if (forwardOutput) 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 Future.delayed(Duration.zero);
var buffer = StringBuffer();
buffer.write("Process `dart_sass_embedded` ");
if (exitCodeOrNull == null) {
buffer.write("was killed with SIGKILL in a tear-down.");
} else {
buffer.write("exited with exitCode $exitCodeOrNull.");
}
buffer.writeln(" Output:");
buffer.writeln(_log.join("\n"));
printOnFailure(buffer.toString());
}
/// 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 `dart_sass_embedded` had an unexpected exit code.");
}
}