diff --git a/pkg/test_runner/lib/src/command.dart b/pkg/test_runner/lib/src/command.dart index 348ae039730..27ca02d528c 100644 --- a/pkg/test_runner/lib/src/command.dart +++ b/pkg/test_runner/lib/src/command.dart @@ -10,6 +10,7 @@ import 'dart:io' as io; import 'command_output.dart'; import 'configuration.dart'; import 'path.dart'; +import 'test_case.dart'; import 'utils.dart'; /// A command executed as a step in a test case. @@ -531,6 +532,75 @@ class VMBatchCommand extends ProcessCommand implements VMCommand { } } +// Run a VM test under RR, and copy the trace if it crashes. Using a helper +// script like the precompiler does not work because the RR traces are large +// and we must diligently erase them for non-crashes even if the test times +// out and would be killed by the harness, so the copying and cleanup logic +// must be in the harness. +class RRCommand extends Command { + VMCommand originalCommand; + VMCommand wrappedCommand; + io.Directory recordingDir; + io.Directory savedDir; + + RRCommand(this.originalCommand) + : super._("rr", index: originalCommand.index) { + final suffix = "/rr-trace-" + originalCommand.hashCode.toString(); + recordingDir = io.Directory(io.Directory.systemTemp.path + suffix); + savedDir = io.Directory("out" + suffix); + final executable = "rr"; + final arguments = [ + "record", + "--chaos", + "--output-trace-dir=" + recordingDir.path, + ]; + arguments.add(originalCommand.executable); + arguments.addAll(originalCommand.arguments); + wrappedCommand = VMCommand( + executable, arguments, originalCommand.environmentOverrides, + index: originalCommand.index); + } + + RRCommand indexedCopy(int index) => + RRCommand(originalCommand.indexedCopy(index)); + + Future run(int timeout) async { + // rr will fail if the output trace directory already exists. Delete any + // that might be leftover from interrupting the harness. + if (await recordingDir.exists()) { + await recordingDir.delete(recursive: true); + } + final output = await RunningProcess(wrappedCommand, timeout).run(); + if (output.hasCrashed) { + if (await savedDir.exists()) { + await savedDir.delete(recursive: true); + } + await recordingDir.rename(savedDir.path); + await io.File(savedDir.path + "/command.txt") + .writeAsString(wrappedCommand.reproductionCommand); + await io.File(savedDir.path + "/stdout.txt").writeAsBytes(output.stdout); + await io.File(savedDir.path + "/stderr.txt").writeAsBytes(output.stderr); + } else { + await recordingDir.delete(recursive: true); + } + + return VMCommandOutput(this, output.exitCode, output.hasTimedOut, + output.stdout, output.stderr, output.time, output.pid); + } + + String get reproductionCommand => + wrappedCommand.reproductionCommand + " (rr replay " + savedDir.path + ")"; + + void _buildHashCode(HashCodeBuilder builder) { + originalCommand._buildHashCode(builder); + builder.add(42); + } + + bool _equal(RRCommand other) => + hashCode == other.hashCode && + originalCommand._equal(other.originalCommand); +} + abstract class AdbCommand { String get buildPath; List get extraLibraries; diff --git a/pkg/test_runner/lib/src/configuration.dart b/pkg/test_runner/lib/src/configuration.dart index c5d61b02685..2392b63710e 100644 --- a/pkg/test_runner/lib/src/configuration.dart +++ b/pkg/test_runner/lib/src/configuration.dart @@ -32,6 +32,7 @@ class TestConfiguration { this.batch, this.batchDart2JS, this.copyCoreDumps, + this.rr, this.isVerbose, this.listTests, this.listStatusFiles, @@ -82,6 +83,7 @@ class TestConfiguration { final bool batch; final bool batchDart2JS; final bool copyCoreDumps; + final bool rr; final bool fastTestsOnly; final bool isVerbose; final bool listTests; diff --git a/pkg/test_runner/lib/src/options.dart b/pkg/test_runner/lib/src/options.dart index e0c78fc8e87..7f3aed5ce5b 100644 --- a/pkg/test_runner/lib/src/options.dart +++ b/pkg/test_runner/lib/src/options.dart @@ -293,6 +293,8 @@ settings.''', '''If we see a crash that we did not expect, copy the core dumps to "/tmp".''', hide: true), + _Option.bool('rr', '''Run VM tests under rr and save traces from crashes''', + hide: true), _Option( 'local_ip', '''IP address the HTTP servers should listen on. This address is also @@ -735,6 +737,7 @@ compiler.''', batch: !(data["noBatch"] as bool), batchDart2JS: data["dart2js_batch"] as bool, copyCoreDumps: data["copy_coredumps"] as bool, + rr: data["rr"] as bool, isVerbose: data["verbose"] as bool, listTests: data["list"] as bool, listStatusFiles: data["list_status_files"] as bool, diff --git a/pkg/test_runner/lib/src/process_queue.dart b/pkg/test_runner/lib/src/process_queue.dart index 6d7a58c9118..947666f61e7 100644 --- a/pkg/test_runner/lib/src/process_queue.dart +++ b/pkg/test_runner/lib/src/process_queue.dart @@ -614,6 +614,8 @@ class CommandExecutorImpl implements CommandExecutor { return RunningProcess(command, timeout, configuration: globalConfiguration) .run(); + } else if (command is RRCommand) { + return command.run(timeout); } else { throw ArgumentError("Unknown command type ${command.runtimeType}."); } diff --git a/pkg/test_runner/lib/src/runtime_configuration.dart b/pkg/test_runner/lib/src/runtime_configuration.dart index cb2c9c49c3e..2cb95c70f87 100644 --- a/pkg/test_runner/lib/src/runtime_configuration.dart +++ b/pkg/test_runner/lib/src/runtime_configuration.dart @@ -263,6 +263,9 @@ class DartVmRuntimeConfiguration extends RuntimeConfiguration { if (_configuration.sanitizer != Sanitizer.none) { multiplier *= 2; } + if (_configuration.rr) { + multiplier *= 2; + } return multiplier; } } @@ -297,7 +300,11 @@ class StandaloneDartRuntimeConfiguration extends DartVmRuntimeConfiguration { arguments.insertAll(0, config.arguments); executable = config.executable; } - return [VMCommand(executable, arguments, environmentOverrides)]; + var command = VMCommand(executable, arguments, environmentOverrides); + if (_configuration.rr && !isCrashExpected) { + return [RRCommand(command)]; + } + return [command]; } } @@ -326,7 +333,11 @@ class DartPrecompiledRuntimeConfiguration extends DartVmRuntimeConfiguration { executable = config.executable; } - return [VMCommand(executable, arguments, environmentOverrides)]; + var command = VMCommand(executable, arguments, environmentOverrides); + if (_configuration.rr && !isCrashExpected) { + return [RRCommand(command)]; + } + return [command]; } }