| // 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'; | |
| import 'package:meta/meta.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 _stdOut = StringBuffer(); | |
| var stdOutFinish = Future(() async { | |
| var lines = | |
| process.stdout.transform(utf8.decoder).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(utf8.decoder).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]; | |
| 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 != null && 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(' ')}'; | |
| } |