mirror of
https://github.com/danog/dart-sass.git
synced 2024-12-02 09:37:49 +01:00
a88318f5a9
Closes #1980 Closes #1959
219 lines
7.6 KiB
Dart
219 lines
7.6 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.
|
|
|
|
@TestOn('vm')
|
|
|
|
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/src/embedded/embedded_sass.pb.dart';
|
|
import 'package:sass/src/embedded/utils.dart';
|
|
import 'package:sass/src/embedded/util/length_delimited_transformer.dart';
|
|
|
|
import 'utils.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.
|
|
///
|
|
/// The initial int is the compilation ID.
|
|
StreamQueue<(int, OutboundMessage)> get outbound => _outbound;
|
|
late StreamQueue<(int, OutboundMessage)> _outbound;
|
|
|
|
/// A [StreamQueue] that emits each line of stderr from the process.
|
|
StreamQueue<String> get stderr => _stderr;
|
|
late StreamQueue<String> _stderr;
|
|
|
|
/// A splitter that can emit new copies of [outbound].
|
|
final StreamSplitter<(int, 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.
|
|
///
|
|
/// The initial int is the compilation ID.
|
|
final Sink<(int, 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.
|
|
var _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 {
|
|
var exitCode =
|
|
await this.exitCode.timeout(Duration.zero, onTimeout: () => -1);
|
|
return exitCode == -1 ? null : exitCode;
|
|
}
|
|
|
|
/// 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("sass"),
|
|
[...pkg.executableArgs("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((packet) {
|
|
var (compilationId, buffer) = parsePacket(packet);
|
|
return (compilationId, OutboundMessage.fromBuffer(buffer));
|
|
})),
|
|
_stderrSplitter = StreamSplitter(process.stderr
|
|
.transform(utf8.decoder)
|
|
.transform(const LineSplitter())),
|
|
inbound = StreamSinkTransformer<(int, InboundMessage),
|
|
List<int>>.fromHandlers(handleData: (pair, sink) {
|
|
var (compilationId, message) = pair;
|
|
sink.add(serializePacket(compilationId, message));
|
|
}).bind(
|
|
StreamSinkTransformer.fromStreamTransformer(lengthDelimitedEncoder)
|
|
.bind(process.stdin)) {
|
|
addTearDown(_tearDown);
|
|
expect(_process.exitCode.then((_) => _logOutput()), completes,
|
|
reason: "Process `sass --embedded` never exited.");
|
|
|
|
_outbound = StreamQueue(_outboundSplitter.split());
|
|
_stderr = StreamQueue(_stderrSplitter.split());
|
|
|
|
_outboundSplitter.split().listen((pair) {
|
|
for (var line in pair.$2.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<void> _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<void> _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<void>.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());
|
|
}
|
|
|
|
/// Sends [message] to the process with the default compilation ID.
|
|
void send(InboundMessage message) =>
|
|
inbound.add((defaultCompilationId, message));
|
|
|
|
/// Fetches the next message from [outbound] and asserts that it has the
|
|
/// default compilation ID.
|
|
Future<OutboundMessage> receive() async {
|
|
var (actualCompilationId, message) = await outbound.next;
|
|
expect(actualCompilationId, equals(defaultCompilationId),
|
|
reason: "Expected default compilation ID");
|
|
return message;
|
|
}
|
|
|
|
/// Closes the process's stdin and waits for it to exit gracefully.
|
|
Future<void> close() async {
|
|
stdin.close();
|
|
await shouldExit(0);
|
|
}
|
|
|
|
/// 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<void> 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<void> shouldExit([int? expectedExitCode]) async {
|
|
var exitCode = await this.exitCode;
|
|
if (expectedExitCode == null) return;
|
|
expect(exitCode, expectedExitCode,
|
|
reason: "Process `dart_sass_embedded` had an unexpected exit code.");
|
|
}
|
|
}
|