blob: 21454b836bb55e54cca20cc72d21117145b51e57 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. 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:fxtest/fxtest.dart';
import 'package:fxutils/fxutils.dart';
/// Disposable runner which executes a single test and presents its output in
/// realtime (via streams) and aggregates for the end (via a [StringBuffer]).
class TestRunner {
final StreamController<String> _stdoutController;
final StartProcess _startProcess;
TestRunner({StartProcess? startProcess})
: _stdoutController = StreamController<String>(),
_startProcess = startProcess ?? Process.start;
Stream<String> get output => _stdoutController.stream;
void addOutput(String content) => _stdoutController.sink.add(content);
/// Wrapper which runs commands and not only collects all output for a
/// [ProcessResult], but also optionally emits realtime events from nested
/// stdout.
///
/// The [run] method takes an optional [realtimeOutputSink], which is used to
/// flush output from the process running individual tests. To achieve this,
/// two intermediate streams are created, one each for the sub-process's
/// [stdout] and [stderr], with listeners that maintain complete copies for
/// our value and which dump each update into [realtimeOutputSink] if it was
/// supplied.
Future<ProcessResult> run(
String command,
List<String> args, {
required String workingDirectory,
Map<String, String>? environment,
}) async {
var processArgs = _buildProcessArgs(command, args);
Process process = await _startProcess(
processArgs.command,
processArgs.args,
environment: environment,
workingDirectory: workingDirectory,
);
var utf8decoder = Utf8Decoder(allowMalformed: true);
var _stdOut = StringBuffer();
var stdOutFinish = Future(() async {
var lines =
process.stdout.transform(utf8decoder).transform(LineSplitter());
await for (var val in lines) {
_stdOut.writeln(val);
addOutput(val);
}
});
var _stdErr = StringBuffer();
var stdErrFinish = Future(() async {
var lines =
process.stderr.transform(utf8decoder).transform(LineSplitter());
await for (var val in lines) {
_stdErr.writeln(val);
addOutput(val);
}
});
// Wait for test to actually end. This means waiting for the test process
// to terminate and for the output streams to be flushed.
var results =
await Future.wait([process.exitCode, stdOutFinish, stdErrFinish]);
var _exitCode = results[0] ?? -1;
await close();
// Return the same thing as if we'd used `Process.run`.
return ProcessResult(
process.pid,
_exitCode,
_stdOut.toString(),
_stdErr.toString(),
);
}
Future close() {
return _stdoutController.close();
}
_ProcessArgs _buildProcessArgs(String command, List<String> args) {
return _ProcessArgs(command, args);
}
}
class SymbolizingTestRunner extends TestRunner {
final String fx;
SymbolizingTestRunner({
required this.fx,
}) : assert(fx != '');
factory SymbolizingTestRunner.builder(TestsConfig testsConfig) =>
SymbolizingTestRunner(fx: testsConfig.fxEnv.fx);
@override
_ProcessArgs _buildProcessArgs(String command, List<String> args) {
return _ProcessArgs('bash', [
// `-o pipefail` forwards the exitcode from `command` (the test itself)
// instead of the always-zero exitcode from symbolize
'-o',
'pipefail',
// `-c` keeps the pipe in the domain of `bash`, and not in the domain of
// `fx shell ...`, which leads to "can't find fx" errors (since that's
// not on the device)
'-c',
[command, ...args, '|', fx, 'symbolize'].join(' ')
]);
}
}
class _ProcessArgs {
final String command;
final List<String> args;
_ProcessArgs(this.command, this.args);
@override
String toString() => '$command ${args.join(' ')}';
}