| // 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(' ')}'; |
| } |