blob: 7b29ea81277a32f242b5992de842e52a85312365 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. 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:collection';
import 'dart:convert';
// We need to use the 'io' prefix here, otherwise io.exitCode will shadow
// CommandOutput.exitCode in subclasses of CommandOutput.
import 'dart:io' as io;
import 'dart:math' as math;
import 'android.dart';
import 'browser_controller.dart';
import 'command.dart';
import 'command_output.dart';
import 'configuration.dart';
import 'dependency_graph.dart';
import 'output_log.dart';
import 'runtime_configuration.dart';
import 'test_case.dart';
import 'test_file.dart';
import 'test_progress.dart';
import 'test_suite.dart';
import 'utils.dart';
const unhandledCompilerExceptionExitCode = 253;
const parseFailExitCode = 245;
const _cannotOpenDisplayMessage = 'Gtk-WARNING **: cannot open display';
const _failedToRunCommandMessage = 'Failed to run command. return code=1';
typedef _StepFunction = Future<AdbCommandResult> Function();
class ProcessQueue {
final TestConfiguration _globalConfiguration;
final void Function() _allDone;
final Graph<Command> _graph = Graph();
final List<EventListener> _eventListener;
ProcessQueue(
this._globalConfiguration,
int maxProcesses,
int maxBrowserProcesses,
List<TestSuite> testSuites,
this._eventListener,
this._allDone,
[bool verbose = false,
AdbDevicePool adbDevicePool]) {
void setupForListing(TestCaseEnqueuer testCaseEnqueuer) {
_graph.sealed.listen((_) {
var testCases = testCaseEnqueuer.remainingTestCases.toList();
testCases.sort((a, b) => a.displayName.compareTo(b.displayName));
print("\nGenerating all matching test cases ....\n");
for (var testCase in testCases) {
eventFinishedTestCase(testCase);
var outcomes = testCase.expectedOutcomes.map((o) => '$o').toList()
..sort();
print("${testCase.displayName} "
"Expectations: ${outcomes.join(', ')} "
"Configuration: '${testCase.configurationString}'");
}
eventAllTestsKnown();
});
}
TestCaseEnqueuer testCaseEnqueuer;
CommandQueue commandQueue;
void setupForRunning(TestCaseEnqueuer testCaseEnqueuer) {
Timer _debugTimer;
// If we haven't seen a single test finishing during a 10 minute period
// something is definitely wrong, so we dump the debugging information.
final debugTimerDuration = const Duration(minutes: 10);
void cancelDebugTimer() {
if (_debugTimer != null) {
_debugTimer.cancel();
}
}
void resetDebugTimer() {
cancelDebugTimer();
_debugTimer = Timer(debugTimerDuration, () {
print("The debug timer of test.dart expired. Please report this issue"
" to dart-engprod@ and provide the following information:");
print("");
print("Graph is sealed: ${_graph.isSealed}");
print("");
_graph.dumpCounts();
print("");
var unfinishedNodeStates = [
NodeState.initialized,
NodeState.waiting,
NodeState.enqueuing,
NodeState.processing
];
for (var nodeState in unfinishedNodeStates) {
if (_graph.stateCount(nodeState) > 0) {
print("Commands in state '$nodeState':");
print("=================================");
print("");
for (var node in _graph.nodes) {
if (node.state == nodeState) {
var command = node.data;
var testCases = testCaseEnqueuer.command2testCases[command];
print(" Command: $command");
for (var testCase in testCases) {
print(" Enqueued by: ${testCase.configurationString} "
"-- ${testCase.displayName}");
}
print("");
}
}
print("");
print("");
}
}
if (commandQueue != null) {
commandQueue.dumpState();
}
});
}
// When the graph building is finished, notify event listeners.
_graph.sealed.listen((_) {
eventAllTestsKnown();
});
// Queue commands as they become "runnable"
CommandEnqueuer(_graph);
// CommandExecutor will execute commands
var executor = CommandExecutorImpl(
_globalConfiguration, maxProcesses, maxBrowserProcesses,
adbDevicePool: adbDevicePool);
// Run "runnable commands" using [executor] subject to
// maxProcesses/maxBrowserProcesses constraint
commandQueue = CommandQueue(_graph, testCaseEnqueuer, executor,
maxProcesses, maxBrowserProcesses, verbose);
// Finish test cases when all commands were run (or some failed)
var testCaseCompleter =
TestCaseCompleter(_graph, testCaseEnqueuer, commandQueue);
testCaseCompleter.finishedTestCases.listen((TestCase finishedTestCase) {
resetDebugTimer();
eventFinishedTestCase(finishedTestCase);
}, onDone: () {
// Wait until the commandQueue/exectutor is done (it may need to stop
// batch runners, browser controllers, ....)
commandQueue.done.then((_) {
cancelDebugTimer();
eventAllTestsDone();
});
});
resetDebugTimer();
}
// Build up the dependency graph
testCaseEnqueuer = TestCaseEnqueuer(_graph, eventTestAdded);
// Either list or run the tests
if (_globalConfiguration.listTests) {
setupForListing(testCaseEnqueuer);
} else {
setupForRunning(testCaseEnqueuer);
}
// Start enqueing all TestCases
testCaseEnqueuer.enqueueTestSuites(testSuites);
}
void eventFinishedTestCase(TestCase testCase) {
for (var listener in _eventListener) {
listener.done(testCase);
}
}
void eventTestAdded(TestCase testCase) {
for (var listener in _eventListener) {
listener.testAdded();
}
}
void eventAllTestsKnown() {
for (var listener in _eventListener) {
listener.allTestsKnown();
}
}
void eventAllTestsDone() {
for (var listener in _eventListener) {
listener.allDone();
}
_allDone();
}
}
/// [TestCaseEnqueuer] takes a list of TestSuites, generates TestCases and
/// builds a dependency graph of all commands in every TestSuite.
///
/// It maintains three helper data structures:
///
/// - command2node: A mapping from a [Command] to a node in the dependency
/// graph.
///
/// - command2testCases: A mapping from [Command] to all TestCases that it is
/// part of.
///
/// - remainingTestCases: A set of TestCases that were enqueued but are not
/// finished.
///
/// [Command] and it's subclasses all have hashCode/operator== methods defined
/// on them, so we can safely use them as keys in Map/Set objects.
class TestCaseEnqueuer {
final Graph<Command> graph;
final Function _onTestCaseAdded;
final command2node = <Command, Node<Command>>{};
final command2testCases = <Command, List<TestCase>>{};
final remainingTestCases = <TestCase>{};
TestCaseEnqueuer(this.graph, this._onTestCaseAdded);
void enqueueTestSuites(List<TestSuite> testSuites) {
// Cache information about test cases per test suite. For multiple
// configurations there is no need to repeatedly search the file
// system, generate tests, and search test files for options.
var testCache = <String, List<TestFile>>{};
for (var suite in testSuites) {
suite.findTestCases(_add, testCache);
}
// We're finished with building the dependency graph.
graph.seal();
}
/// Adds a test case to the list of active test cases, and adds its commands
/// to the dependency graph of commands.
///
/// If the repeat flag is > 1, replicates the test case and its commands,
/// adding an index field with a distinct value to each of the copies.
///
/// Each copy of the test case depends on the previous copy of the test
/// case completing, with its first command having a dependency on the last
/// command of the previous copy of the test case. This dependency is
/// marked as a "timingDependency", so that it doesn't depend on the previous
/// test completing successfully, just on it completing.
void _add(TestCase testCase) {
Node<Command> lastNode;
for (var i = 0; i < testCase.configuration.repeat; ++i) {
if (i > 0) {
testCase = testCase.indexedCopy(i);
}
remainingTestCases.add(testCase);
var isFirstCommand = true;
for (var command in testCase.commands) {
// Make exactly *one* node in the dependency graph for every command.
// This ensures that we never have two commands c1 and c2 in the graph
// with "c1 == c2".
var node = command2node[command];
if (node == null) {
var requiredNodes =
(lastNode != null) ? [lastNode] : <Node<Command>>[];
node = graph.add(command, requiredNodes,
timingDependency: isFirstCommand);
command2node[command] = node;
command2testCases[command] = <TestCase>[];
}
// Keep mapping from command to all testCases that refer to it.
command2testCases[command].add(testCase);
lastNode = node;
isFirstCommand = false;
}
_onTestCaseAdded(testCase);
}
}
}
/// [CommandEnqueuer] will:
///
/// - Change node.state to NodeState.enqueuing as soon as all dependencies have
/// a state of NodeState.Successful.
/// - Change node.state to NodeState.unableToRun if one or more dependencies
/// have a state of NodeState.failed/NodeState.unableToRun.
class CommandEnqueuer {
static const _initStates = [NodeState.initialized, NodeState.waiting];
static const _finishedStates = [
NodeState.successful,
NodeState.failed,
NodeState.unableToRun
];
final Graph<Command> _graph;
CommandEnqueuer(this._graph) {
_graph.added.listen(_changeNodeStateIfNecessary);
_graph.changed.listen((event) {
if (event.from == NodeState.waiting ||
event.from == NodeState.processing) {
if (_finishedStates.contains(event.to)) {
for (var dependentNode in event.node.neededFor) {
_changeNodeStateIfNecessary(dependentNode);
}
}
}
});
}
/// Called when either a new node was added or if one of it's dependencies
/// changed it's state.
void _changeNodeStateIfNecessary(Node<Command> node) {
if (_initStates.contains(node.state)) {
var allDependenciesFinished =
node.dependencies.every((dep) => _finishedStates.contains(dep.state));
var anyDependenciesUnsuccessful = node.dependencies.any((dep) =>
[NodeState.failed, NodeState.unableToRun].contains(dep.state));
var allDependenciesSuccessful =
node.dependencies.every((dep) => dep.state == NodeState.successful);
var newState = NodeState.waiting;
if (allDependenciesSuccessful ||
(allDependenciesFinished && node.timingDependency)) {
newState = NodeState.enqueuing;
} else if (anyDependenciesUnsuccessful) {
newState = NodeState.unableToRun;
}
if (node.state != newState) {
_graph.changeState(node, newState);
}
}
}
}
/// [CommandQueue] will listen for nodes entering the NodeState.enqueuing state,
/// queue them up and run them. While nodes are processed they will be in the
/// NodeState.processing state. After running a command, the node will change
/// to a state of NodeState.Successful or NodeState.failed.
///
/// It provides a synchronous stream [completedCommands] which provides the
/// [CommandOutput]s for the finished commands.
///
/// It provides a [done] future, which will complete once there are no more
/// nodes left in the states Initialized/Waiting/Enqueing/Processing
/// and the [executor] has cleaned up its resources.
class CommandQueue {
final Graph<Command> graph;
final CommandExecutor executor;
final TestCaseEnqueuer enqueuer;
final _runQueue = Queue<Command>();
final _commandOutputStream = StreamController<CommandOutput>(sync: true);
final _completer = Completer<Null>();
int _numProcesses = 0;
final int _maxProcesses;
int _numBrowserProcesses = 0;
final int _maxBrowserProcesses;
bool _finishing = false;
final bool _verbose;
CommandQueue(this.graph, this.enqueuer, this.executor, this._maxProcesses,
this._maxBrowserProcesses, this._verbose) {
graph.changed.listen((event) {
if (event.to == NodeState.enqueuing) {
assert(event.from == NodeState.initialized ||
event.from == NodeState.waiting);
graph.changeState(event.node, NodeState.processing);
var command = event.node.data;
if (event.node.dependencies.isNotEmpty) {
_runQueue.addFirst(command);
} else {
_runQueue.add(command);
}
Timer.run(_tryRunNextCommand);
} else if (event.to == NodeState.unableToRun) {
_checkDone();
}
});
// We're finished if the graph is sealed and all nodes are in a finished
// state (Successful, Failed or UnableToRun).
// So we're calling '_checkDone()' to check whether that condition is met
// and we can cleanup.
graph.sealed.listen((event) {
_checkDone();
});
}
Stream<CommandOutput> get completedCommands => _commandOutputStream.stream;
Future get done => _completer.future;
void _tryRunNextCommand() {
_checkDone();
if (_numProcesses < _maxProcesses && _runQueue.isNotEmpty) {
var command = _runQueue.removeFirst();
var isBrowserCommand = command is BrowserTestCommand;
if (isBrowserCommand && _numBrowserProcesses == _maxBrowserProcesses) {
// If there is no free browser runner, put it back into the queue.
_runQueue.add(command);
// Don't lose a process.
Timer(const Duration(milliseconds: 100), _tryRunNextCommand);
return;
}
_numProcesses++;
if (isBrowserCommand) _numBrowserProcesses++;
var node = enqueuer.command2node[command];
Iterable<TestCase> testCases = enqueuer.command2testCases[command];
// If a command is part of many TestCases we set the timeout to be
// the maximum over all [TestCase.timeout]s. At some point, we might
// eliminate [TestCase.timeout] completely and move it to [Command].
var timeout =
testCases.map((TestCase test) => test.timeout).fold(0, math.max);
if (_verbose) {
print('Running "${command.displayName}" command: $command');
}
executor.runCommand(node, command, timeout).then((CommandOutput output) {
assert(command == output.command);
_commandOutputStream.add(output);
if (output.canRunDependendCommands) {
graph.changeState(node, NodeState.successful);
} else {
graph.changeState(node, NodeState.failed);
}
_numProcesses--;
if (isBrowserCommand) _numBrowserProcesses--;
// Don't lose a process
Timer.run(_tryRunNextCommand);
});
}
}
void _checkDone() {
if (!_finishing &&
_runQueue.isEmpty &&
_numProcesses == 0 &&
graph.isSealed &&
graph.stateCount(NodeState.initialized) == 0 &&
graph.stateCount(NodeState.waiting) == 0 &&
graph.stateCount(NodeState.enqueuing) == 0 &&
graph.stateCount(NodeState.processing) == 0) {
_finishing = true;
executor.cleanup().then((_) {
_completer.complete();
_commandOutputStream.close();
});
}
}
void dumpState() {
print("");
print("CommandQueue state:");
print(" Processes: used: $_numProcesses max: $_maxProcesses");
print(" BrowserProcesses: used: $_numBrowserProcesses "
"max: $_maxBrowserProcesses");
print(" Finishing: $_finishing");
print(" Queue (length = ${_runQueue.length}):");
for (var command in _runQueue) {
print(" $command");
}
}
}
/// [CommandExecutor] is responsible for executing commands. It will make sure
/// that the following two constraints are satisfied
/// - `numberOfProcessesUsed <= maxProcesses`
/// - `numberOfBrowserProcessesUsed <= maxBrowserProcesses`
///
/// It provides a [runCommand] method which will complete with a
/// [CommandOutput] object.
///
/// It provides a [cleanup] method to free all the allocated resources.
abstract class CommandExecutor {
Future cleanup();
// TODO(kustermann): The [timeout] parameter should be a property of Command.
Future<CommandOutput> runCommand(
Node<Command> node, Command command, int timeout);
}
class CommandExecutorImpl implements CommandExecutor {
final TestConfiguration globalConfiguration;
final int maxProcesses;
final int maxBrowserProcesses;
AdbDevicePool adbDevicePool;
/// For dart2js and analyzer batch processing,
/// we keep a list of batch processes.
final _batchProcesses = <String, List<BatchRunnerProcess>>{};
/// We keep a BrowserTestRunner for every configuration.
final _browserTestRunners = <TestConfiguration, BrowserTestRunner>{};
bool _finishing = false;
CommandExecutorImpl(
this.globalConfiguration, this.maxProcesses, this.maxBrowserProcesses,
{this.adbDevicePool});
Future cleanup() {
assert(!_finishing);
_finishing = true;
Future _terminateBatchRunners() {
var futures = <Future>[];
for (var runners in _batchProcesses.values) {
futures.addAll(runners.map((runner) => runner.terminate()));
}
return Future.wait(futures);
}
Future _terminateBrowserRunners() {
var futures =
_browserTestRunners.values.map((runner) => runner.terminate());
return Future.wait(futures);
}
return Future.wait([
_terminateBatchRunners(),
_terminateBrowserRunners(),
]);
}
Future<CommandOutput> runCommand(node, Command command, int timeout) {
assert(!_finishing);
Future<CommandOutput> runCommand(int retriesLeft) {
return _runCommand(command, timeout).then((CommandOutput output) {
if (retriesLeft > 0 && shouldRetryCommand(output)) {
DebugLogger.warning("Rerunning Command: ($retriesLeft "
"attempt(s) remains) [cmd: $command]");
return runCommand(retriesLeft - 1);
} else {
return Future.value(output);
}
});
}
return runCommand(command.maxNumRetries);
}
Future<CommandOutput> _runCommand(Command command, int timeout) {
if (command is BrowserTestCommand) {
return _startBrowserControllerTest(command, timeout);
} else if (command is VMKernelCompilationCommand) {
// For now, we always run vm_compile_to_kernel in batch mode.
var name = command.displayName;
assert(name == 'vm_compile_to_kernel');
return _getBatchRunner(name)
.runCommand(name, command, timeout, command.arguments);
} else if (command is CompilationCommand &&
globalConfiguration.batchDart2JS &&
command.displayName == 'dart2js') {
return _getBatchRunner("dart2js")
.runCommand("dart2js", command, timeout, command.arguments);
} else if (command is AnalysisCommand && globalConfiguration.batch) {
return _getBatchRunner(command.displayName)
.runCommand(command.displayName, command, timeout, command.arguments);
} else if (command is CompilationCommand &&
(command.displayName == 'dartdevc' ||
command.displayName == 'dartdevk' ||
command.displayName == 'fasta') &&
globalConfiguration.batch) {
return _getBatchRunner(command.displayName)
.runCommand(command.displayName, command, timeout, command.arguments);
} else if (command is ScriptCommand) {
return command.run();
} else if (command is AdbPrecompilationCommand ||
command is AdbDartkCommand) {
assert(adbDevicePool != null);
return adbDevicePool.acquireDevice().then((AdbDevice device) async {
try {
if (command is AdbPrecompilationCommand) {
return await _runAdbPrecompilationCommand(device, command, timeout);
} else {
return await _runAdbDartkCommand(
device, command as AdbDartkCommand, timeout);
}
} finally {
adbDevicePool.releaseDevice(device);
}
});
} else if (command is VMBatchCommand) {
var name = command.displayName;
return _getBatchRunner(command.displayName + command.dartFile)
.runCommand(name, command, timeout, command.arguments);
} else if (command is CompilationCommand &&
command.displayName == 'babel') {
return RunningProcess(command, timeout,
configuration: globalConfiguration,
outputFile: io.File(command.outputFile))
.run();
} else if (command is ProcessCommand) {
return RunningProcess(command, timeout,
configuration: globalConfiguration)
.run();
} else if (command is RRCommand) {
return command.run(timeout);
} else {
throw ArgumentError("Unknown command type ${command.runtimeType}.");
}
}
List<_StepFunction> _pushLibraries(AdbCommand command, AdbDevice device,
String deviceDir, String deviceTestDir) {
var steps = <_StepFunction>[];
for (var lib in command.extraLibraries) {
var libName = "lib$lib.so";
steps.add(() => device.runAdbCommand([
'push',
'${command.buildPath}/$libName',
'$deviceTestDir/$libName'
]));
}
return steps;
}
Future<CommandOutput> _runAdbPrecompilationCommand(
AdbDevice device, AdbPrecompilationCommand command, int timeout) async {
var buildPath = command.buildPath;
var processTest = command.processTestFilename;
var testdir = command.precompiledTestDirectory;
var arguments = command.arguments;
var devicedir = DartPrecompiledAdbRuntimeConfiguration.deviceDir;
var deviceTestDir = DartPrecompiledAdbRuntimeConfiguration.deviceTestDir;
// We copy all the files which the vm precompiler puts into the test
// directory.
var files = io.Directory(testdir)
.listSync()
.map((file) => file.path)
.map((path) => path.substring(path.lastIndexOf('/') + 1))
.toList();
var timeoutDuration = Duration(seconds: timeout);
var steps = <_StepFunction>[];
steps.add(() => device.runAdbShellCommand(['rm', '-Rf', deviceTestDir]));
steps.add(() => device.runAdbShellCommand(['mkdir', '-p', deviceTestDir]));
steps.add(() => device.pushCachedData('$buildPath/dart_precompiled_runtime',
'$devicedir/dart_precompiled_runtime'));
steps.add(
() => device.pushCachedData(processTest, '$devicedir/process_test'));
steps.add(() => device.runAdbShellCommand([
'chmod',
'777',
'$devicedir/dart_precompiled_runtime $devicedir/process_test'
]));
steps.addAll(_pushLibraries(command, device, devicedir, deviceTestDir));
for (var file in files) {
steps.add(() => device
.runAdbCommand(['push', '$testdir/$file', '$deviceTestDir/$file']));
}
steps.add(() => device.runAdbShellCommand(
[
'export LD_LIBRARY_PATH=\$LD_LIBRARY_PATH:$deviceTestDir;'
'$devicedir/dart_precompiled_runtime',
'--android-log-to-stderr'
]..addAll(arguments),
timeout: timeoutDuration));
var stopwatch = Stopwatch()..start();
var writer = StringBuffer();
await device.waitForBootCompleted();
await device.waitForDevice();
AdbCommandResult result;
for (var i = 0; i < steps.length; i++) {
var fun = steps[i];
var commandStopwatch = Stopwatch()..start();
result = await fun();
writer.writeln("Executing ${result.command}");
if (result.stdout.isNotEmpty) {
writer.writeln("Stdout:\n${result.stdout.trim()}");
}
if (result.stderr.isNotEmpty) {
writer.writeln("Stderr:\n${result.stderr.trim()}");
}
writer.writeln("ExitCode: ${result.exitCode}");
writer.writeln("Time: ${commandStopwatch.elapsed}");
writer.writeln("");
// If one command fails, we stop processing the others and return
// immediately.
if (result.exitCode != 0) break;
}
return command.createOutput(result.exitCode, result.timedOut,
utf8.encode('$writer'), [], stopwatch.elapsed, false);
}
Future<CommandOutput> _runAdbDartkCommand(
AdbDevice device, AdbDartkCommand command, int timeout) async {
var buildPath = command.buildPath;
var hostKernelFile = command.kernelFile;
var arguments = command.arguments;
var devicedir = DartkAdbRuntimeConfiguration.deviceDir;
var deviceTestDir = DartkAdbRuntimeConfiguration.deviceTestDir;
var timeoutDuration = Duration(seconds: timeout);
var steps = <_StepFunction>[];
steps.add(() => device.runAdbShellCommand(['rm', '-Rf', deviceTestDir]));
steps.add(() => device.runAdbShellCommand(['mkdir', '-p', deviceTestDir]));
steps
.add(() => device.pushCachedData("$buildPath/dart", '$devicedir/dart'));
steps.add(() => device
.runAdbCommand(['push', hostKernelFile, '$deviceTestDir/out.dill']));
steps.addAll(_pushLibraries(command, device, devicedir, deviceTestDir));
steps.add(() => device.runAdbShellCommand(
[
'export LD_LIBRARY_PATH=\$LD_LIBRARY_PATH:$deviceTestDir;'
'$devicedir/dart',
'--android-log-to-stderr'
]..addAll(arguments),
timeout: timeoutDuration));
var stopwatch = Stopwatch()..start();
var writer = StringBuffer();
await device.waitForBootCompleted();
await device.waitForDevice();
AdbCommandResult result;
for (var i = 0; i < steps.length; i++) {
var step = steps[i];
var commandStopwatch = Stopwatch()..start();
result = await step();
writer.writeln("Executing ${result.command}");
if (result.stdout.isNotEmpty) {
writer.writeln("Stdout:\n${result.stdout.trim()}");
}
if (result.stderr.isNotEmpty) {
writer.writeln("Stderr:\n${result.stderr.trim()}");
}
writer.writeln("ExitCode: ${result.exitCode}");
writer.writeln("Time: ${commandStopwatch.elapsed}");
writer.writeln("");
// If one command fails, we stop processing the others and return
// immediately.
if (result.exitCode != 0) break;
}
return command.createOutput(result.exitCode, result.timedOut,
utf8.encode('$writer'), [], stopwatch.elapsed, false);
}
BatchRunnerProcess _getBatchRunner(String identifier) {
// Start batch processes if needed.
var runners = _batchProcesses[identifier];
if (runners == null) {
runners = List<BatchRunnerProcess>.filled(maxProcesses, null);
for (var i = 0; i < maxProcesses; i++) {
runners[i] = BatchRunnerProcess(useJson: identifier == "fasta");
}
_batchProcesses[identifier] = runners;
}
for (var runner in runners) {
if (!runner._currentlyRunning) return runner;
}
throw Exception('Unable to find inactive batch runner.');
}
Future<CommandOutput> _startBrowserControllerTest(
BrowserTestCommand browserCommand, int timeout) {
var completer = Completer<CommandOutput>();
callback(BrowserTestOutput output) {
completer.complete(BrowserCommandOutput(browserCommand, output));
}
var browserTest = BrowserTest(browserCommand.url, callback, timeout);
_getBrowserTestRunner(browserCommand.configuration).then((testRunner) {
testRunner.enqueueTest(browserTest);
});
return completer.future;
}
Future<BrowserTestRunner> _getBrowserTestRunner(
TestConfiguration configuration) async {
if (_browserTestRunners[configuration] == null) {
var testRunner = BrowserTestRunner(
configuration, globalConfiguration.localIP, maxBrowserProcesses);
if (globalConfiguration.isVerbose) {
testRunner.logger = DebugLogger.info;
}
_browserTestRunners[configuration] = testRunner;
await testRunner.start();
}
return _browserTestRunners[configuration];
}
}
bool shouldRetryCommand(CommandOutput output) {
if (!output.successful) {
List<String> stdout, stderr;
decodeOutput() {
if (stdout == null && stderr == null) {
stdout = decodeUtf8(output.stderr).split("\n");
stderr = decodeUtf8(output.stderr).split("\n");
}
}
final command = output.command;
// The dartk batch compiler sometimes runs out of memory. In such a case we
// will retry running it.
if (command is VMKernelCompilationCommand) {
if (output.hasCrashed) {
bool containsOutOfMemoryMessage(String line) {
return line.contains('Exhausted heap space, trying to allocat');
}
decodeOutput();
if (stdout.any(containsOutOfMemoryMessage) ||
stderr.any(containsOutOfMemoryMessage)) {
return true;
}
}
}
if (io.Platform.operatingSystem == 'linux') {
decodeOutput();
// No matter which command we ran: If we get failures due to the
// "xvfb-run" issue 7564, try re-running the test.
bool containsFailureMsg(String line) {
return line.contains(_cannotOpenDisplayMessage) ||
line.contains(_failedToRunCommandMessage);
}
if (stdout.any(containsFailureMsg) || stderr.any(containsFailureMsg)) {
return true;
}
}
}
return false;
}
/// [TestCaseCompleter] will listen for
/// NodeState.processing -> NodeState.{successful,failed} state changes and
/// will complete a TestCase if it is finished.
///
/// It provides a stream [finishedTestCases], which will stream all TestCases
/// once they're finished. After all TestCases are done, the stream will be
/// closed.
class TestCaseCompleter {
static const _completedStates = [NodeState.failed, NodeState.successful];
final Graph<Command> _graph;
final TestCaseEnqueuer _enqueuer;
final CommandQueue _commandQueue;
final Map<Command, CommandOutput> _outputs = {};
final StreamController<TestCase> _controller = StreamController();
bool _closed = false;
TestCaseCompleter(this._graph, this._enqueuer, this._commandQueue) {
var finishedRemainingTestCases = false;
// Store all the command outputs -- they will be delivered synchronously
// (i.e. before state changes in the graph)
_commandQueue.completedCommands.listen((CommandOutput output) {
_outputs[output.command] = output;
}, onDone: () {
_completeTestCasesIfPossible(List.from(_enqueuer.remainingTestCases));
finishedRemainingTestCases = true;
assert(_enqueuer.remainingTestCases.isEmpty);
_checkDone();
});
// Listen for NodeState.Processing -> NodeState.{Successful,Failed}
// changes.
_graph.changed.listen((event) {
if (event.from == NodeState.processing && !finishedRemainingTestCases) {
var command = event.node.data;
assert(_completedStates.contains(event.to));
assert(_outputs[command] != null);
_completeTestCasesIfPossible(_enqueuer.command2testCases[command]);
_checkDone();
}
});
// Listen also for GraphSealedEvents. If there is not a single node in the
// graph, we still want to finish after the graph was sealed.
_graph.sealed.listen((_) {
if (!_closed && _enqueuer.remainingTestCases.isEmpty) {
_controller.close();
_closed = true;
}
});
}
Stream<TestCase> get finishedTestCases => _controller.stream;
void _checkDone() {
if (!_closed && _graph.isSealed && _enqueuer.remainingTestCases.isEmpty) {
_controller.close();
_closed = true;
}
}
void _completeTestCasesIfPossible(Iterable<TestCase> testCases) {
// Update TestCases with command outputs.
for (var test in testCases) {
for (var icommand in test.commands) {
var output = _outputs[icommand];
if (output != null) {
test.commandOutputs[icommand] = output;
}
}
}
void completeTestCase(TestCase testCase) {
if (_enqueuer.remainingTestCases.contains(testCase)) {
_controller.add(testCase);
_enqueuer.remainingTestCases.remove(testCase);
} else {
DebugLogger.error("${testCase.displayName} would be finished twice");
}
}
for (var testCase in testCases) {
// Ask the [testCase] if it's done. Note that we assume, that
// [TestCase.isFinished] will return true if all commands were executed
// or if a previous one failed.
if (testCase.isFinished) {
completeTestCase(testCase);
}
}
}
}
class BatchRunnerProcess {
/// When true, the command line is passed to the test runner as a
/// JSON-encoded list of strings.
final bool _useJson;
Completer<CommandOutput> _completer;
ProcessCommand _command;
List<String> _arguments;
String _runnerType;
io.Process _process;
Map<String, String> _processEnvironmentOverrides;
Completer<Null> _stdoutCompleter;
Completer<Null> _stderrCompleter;
StreamSubscription<String> _stdoutSubscription;
StreamSubscription<String> _stderrSubscription;
Function _processExitHandler;
bool _currentlyRunning = false;
OutputLog _testStdout;
OutputLog _testStderr;
String _status;
DateTime _startTime;
Timer _timer;
int _testCount = 0;
BatchRunnerProcess({bool useJson = true}) : _useJson = useJson;
Future<CommandOutput> runCommand(String runnerType, ProcessCommand command,
int timeout, List<String> arguments) {
assert(_completer == null);
assert(!_currentlyRunning);
_completer = Completer();
var sameRunnerType = _runnerType == runnerType &&
_dictEquals(_processEnvironmentOverrides, command.environmentOverrides);
_runnerType = runnerType;
_currentlyRunning = true;
_command = command;
_arguments = arguments;
_processEnvironmentOverrides = command.environmentOverrides;
// TODO(jmesserly): this restarts `dartdevc --batch` to work around a
// memory leak, see https://github.com/dart-lang/sdk/issues/30314.
var clearMemoryLeak = command is CompilationCommand &&
command.displayName == 'dartdevc' &&
++_testCount % 100 == 0;
if (_process == null) {
// Start process if not yet started.
_startProcess(() {
doStartTest(command, timeout);
});
} else if (!sameRunnerType || clearMemoryLeak) {
// Restart this runner with the right executable for this test if needed.
_processExitHandler = (_) {
_startProcess(() {
doStartTest(command, timeout);
});
};
_process.kill();
_stdoutSubscription.cancel();
_stderrSubscription.cancel();
} else {
doStartTest(command, timeout);
}
return _completer.future;
}
Future<bool> terminate() {
if (_process == null) return Future.value(true);
var terminateCompleter = Completer<bool>();
final sigkillTimer = Timer(const Duration(seconds: 5), () {
_process.kill(io.ProcessSignal.sigkill);
});
_processExitHandler = (_) {
sigkillTimer.cancel();
terminateCompleter.complete(true);
};
_process.kill();
_stdoutSubscription.cancel();
_stderrSubscription.cancel();
return terminateCompleter.future;
}
void doStartTest(Command command, int timeout) {
_startTime = DateTime.now();
_testStdout = OutputLog();
_testStderr = OutputLog();
_status = null;
_stdoutCompleter = Completer();
_stderrCompleter = Completer();
_timer = Timer(Duration(seconds: timeout), _timeoutHandler);
var line = _createArgumentsLine(_arguments, timeout);
_process.stdin.write(line);
_stdoutSubscription.resume();
_stderrSubscription.resume();
Future.wait([_stdoutCompleter.future, _stderrCompleter.future])
.then((_) => _reportResult());
}
String _createArgumentsLine(List<String> arguments, int timeout) {
if (_useJson) {
return "${jsonEncode(arguments)}\n";
} else {
return arguments.join(' ') + '\n';
}
}
void _reportResult() {
if (!_currentlyRunning) return;
var outcome = _status.split(" ")[2];
var exitCode = 0;
if (outcome == "CRASH") exitCode = unhandledCompilerExceptionExitCode;
if (outcome == "PARSE_FAIL") exitCode = parseFailExitCode;
if (outcome == "FAIL" || outcome == "TIMEOUT") exitCode = 1;
var output = _command.createOutput(
exitCode,
outcome == "TIMEOUT",
_testStdout.toList(),
_testStderr.toList(),
DateTime.now().difference(_startTime),
false);
assert(_completer != null);
_completer.complete(output);
_completer = null;
_currentlyRunning = false;
}
void Function(int) makeExitHandler(String status) {
return (int exitCode) {
if (_currentlyRunning) {
if (_timer != null) _timer.cancel();
_status = status;
_stdoutSubscription.cancel();
_stderrSubscription.cancel();
_startProcess(_reportResult);
} else {
// No active test case running.
_process = null;
}
};
}
void _timeoutHandler() {
_processExitHandler = makeExitHandler(">>> TEST TIMEOUT");
_process.kill();
}
void _startProcess(void Function() callback) {
assert(_command is ProcessCommand);
var executable = _command.executable;
var arguments = _command.batchArguments.toList();
arguments.add('--batch');
var environment = Map<String, String>.from(io.Platform.environment);
if (_processEnvironmentOverrides != null) {
for (var key in _processEnvironmentOverrides.keys) {
environment[key] = _processEnvironmentOverrides[key];
}
}
var processFuture =
io.Process.start(executable, arguments, environment: environment);
processFuture.then<dynamic>((io.Process p) {
_process = p;
var _stdoutStream = _process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter());
_stdoutSubscription = _stdoutStream.listen((String line) {
if (line.startsWith('>>> TEST')) {
_status = line;
} else if (line.startsWith('>>> BATCH')) {
// ignore
} else if (line.startsWith('>>> ')) {
throw Exception("Unexpected command from batch runner: '$line'.");
} else {
_testStdout.add(encodeUtf8(line));
_testStdout.add("\n".codeUnits);
}
if (_status != null) {
_stdoutSubscription.pause();
_timer.cancel();
_stdoutCompleter.complete(null);
}
});
_stdoutSubscription.pause();
var _stderrStream = _process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter());
_stderrSubscription = _stderrStream.listen((String line) {
if (line.startsWith('>>> EOF STDERR')) {
_stderrSubscription.pause();
_stderrCompleter.complete(null);
} else {
_testStderr.add(encodeUtf8(line));
_testStderr.add("\n".codeUnits);
}
});
_stderrSubscription.pause();
_processExitHandler = makeExitHandler(">>> TEST CRASH");
_process.exitCode.then((exitCode) {
_processExitHandler(exitCode);
});
_process.stdin.done.catchError((err) {
print('Error on batch runner input stream stdin');
print(' Previous test\'s status: $_status');
print(' Error: $err');
throw err;
});
callback();
}).catchError((e) {
// TODO(floitsch): should we try to report the stacktrace?
print("Process error:");
print(" Command: $executable ${arguments.join(' ')} ($_arguments)");
print(" Error: $e");
// If there is an error starting a batch process, chances are that
// it will always fail. So rather than re-trying a 1000+ times, we
// exit.
io.exit(1);
});
}
bool _dictEquals(Map a, Map b) {
if (a == null) return b == null;
if (b == null) return false;
if (a.length != b.length) return false;
for (var key in a.keys) {
if (a[key] != b[key]) return false;
}
return true;
}
}