Index: tools/testing/dart/fletch_session_command.dart |
diff --git a/tools/testing/dart/fletch_session_command.dart b/tools/testing/dart/fletch_session_command.dart |
deleted file mode 100644 |
index f385006fe2b180abcf75760518b3c82cf9faf60d..0000000000000000000000000000000000000000 |
--- a/tools/testing/dart/fletch_session_command.dart |
+++ /dev/null |
@@ -1,688 +0,0 @@ |
-// Copyright (c) 2015, the Dartino 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.md file. |
- |
-/// Provides a [Command] interface for interacting with a Fletch driver session. |
-/// |
-/// Normally, this is used by test.dart, but is also has a [main] method that |
-/// makes it possible to run a test outside test.dart. |
-library test.fletch_session_command; |
- |
-import 'dart:async' show |
- Completer, |
- EventSink, |
- Future, |
- Stream, |
- StreamController, |
- StreamTransformer, |
- Timer; |
- |
-import 'dart:collection' show |
- Queue; |
- |
-import 'dart:convert' show |
- UTF8, |
- LineSplitter; |
- |
-import 'dart:io' show |
- BytesBuilder, |
- Platform, |
- Process, |
- ProcessSignal; |
- |
-import 'test_runner.dart' show |
- Command, |
- CommandOutputImpl; |
- |
-import 'decode_exit_code.dart' show |
- DecodeExitCode; |
- |
-import '../../../pkg/fletchc/lib/src/hub/exit_codes.dart' show |
- COMPILER_EXITCODE_CONNECTION_ERROR, |
- COMPILER_EXITCODE_CRASH, |
- DART_VM_EXITCODE_COMPILE_TIME_ERROR, |
- DART_VM_EXITCODE_UNCAUGHT_EXCEPTION; |
- |
-import '../../../pkg/fletchc/lib/fletch_vm.dart' show |
- FletchVm; |
- |
-const String settingsFileNameFlag = "test.fletch_settings_file_name"; |
-const String settingsFileName = |
- const String.fromEnvironment(settingsFileNameFlag); |
- |
-/// Default timeout value (in seconds) used for running commands that are |
-/// assumed to complete fast. |
-// TODO(ahe): Lower this to 5 seconds. |
-const int defaultTimeout = 20; |
- |
-final Queue<FletchSessionMirror> sessions = new Queue<FletchSessionMirror>(); |
- |
-int sessionCount = 0; |
- |
-/// Return an available [FletchSessionMirror] or construct a new. |
-FletchSessionMirror getAvailableSession() { |
- if (sessions.isEmpty) { |
- return new FletchSessionMirror(sessionCount++); |
- } else { |
- return sessions.removeFirst(); |
- } |
-} |
- |
-void returnSession(FletchSessionMirror session) { |
- sessions.addLast(session); |
-} |
- |
-String explainExitCode(int code) { |
- String exit_message; |
- if (code == null) { |
- exit_message = "no exit code"; |
- } else if (code == 0) { |
- exit_message = "(success exit code)"; |
- } else if (code > 0) { |
- switch (code) { |
- case COMPILER_EXITCODE_CONNECTION_ERROR: |
- exit_message = "(connection error)"; |
- break; |
- case COMPILER_EXITCODE_CRASH: |
- exit_message = "(compiler crash)"; |
- break; |
- case DART_VM_EXITCODE_COMPILE_TIME_ERROR: |
- exit_message = "(compile-time error)"; |
- break; |
- case DART_VM_EXITCODE_UNCAUGHT_EXCEPTION: |
- exit_message = "(uncaught exception)"; |
- break; |
- default: |
- exit_message = "(error exit code)"; |
- break; |
- } |
- } else { |
- exit_message = "(signal ${-code})"; |
- if (code == -15 || code == -9) { |
- exit_message += " (killed by external signal - timeout?)"; |
- } else if (code == -7 || code == -11 || code == -4) { |
- // SIGBUS, SIGSEGV, SIGILL |
- exit_message += " (internal error)"; |
- } else if (code == -2) { |
- exit_message += " (control-C)"; |
- } else { |
- exit_message += " (see man 7 signal)"; |
- } |
- } |
- return exit_message; |
-} |
- |
-class FletchSessionCommand implements Command { |
- final String executable; |
- final String script; |
- final List<String> arguments; |
- final Map<String, String> environmentOverrides; |
- final String snapshotFileName; |
- final String settingsFileName; |
- |
- FletchSessionCommand( |
- this.executable, |
- this.script, |
- this.arguments, |
- this.environmentOverrides, |
- {this.snapshotFileName, |
- this.settingsFileName: ".fletch-settings"}); |
- |
- String get displayName => "fletch_session"; |
- |
- int get maxNumRetries => 0; |
- |
- String get reproductionCommand { |
- var dartVm = Uri.parse(executable).resolve('dart'); |
- String fletchPath = Uri.parse(executable).resolve('fletch-vm').toString(); |
- String versionFlag = '-Dfletch.version=`$fletchPath --version`'; |
- String settingsFileFlag = "-D$settingsFileNameFlag=$settingsFileName"; |
- |
- return """ |
- |
- |
- |
-There are three ways to reproduce this error: |
- |
- 1. Run the test exactly as in this test framework. This is the hardest to |
- debug using gdb: |
- |
- ${Platform.executable} -c $settingsFileFlag \\ |
- $versionFlag \\ |
- tools/testing/dart/fletch_session_command.dart $executable \\ |
- ${arguments.join(' ')} |
- |
- |
- 2. Run the helper program `tests/fletchc/run.dart` under `gdb` using |
- `set follow-fork-mode child`. This can be confusing, but makes it |
- easy to run a reproduction command in a loop: |
- |
- gdb -ex 'set follow-fork-mode child' -ex run --args \\ |
- $dartVm $settingsFileFlag \\ |
- $versionFlag \\ |
- -c tests/fletchc/run.dart $script |
- |
- 3. Run the `fletch-vm` in gdb and attach to it via the helper program. This |
- is the easiest way to debug using both gdb and lldb. You need to start two |
- processes, each in their own terminal window: |
- |
- gdb -ex run --args $executable-vm --port=54321 |
- |
- $dartVm $settingsFileFlag \\ |
- $versionFlag \\ |
- -c -DattachToVm=54321 tests/fletchc/run.dart $script |
- |
- |
-"""; |
- } |
- |
- Future<FletchTestCommandOutput> run( |
- int timeout, |
- bool verbose, |
- {bool superVerbose: false}) async { |
- if (arguments.length > 1) { |
- String options = arguments |
- .where((String argument) => argument != script) |
- .join(' '); |
- // TODO(ahe): Passing options to the incremental compiler isn't |
- // trivial. We don't want to reset the compiler each time an option |
- // changes. For example, when changing the package root, the compiler |
- // should refresh all package files to see if they have changed. |
- return compilerFail("Compiler options not implemented: $options"); |
- } |
- |
- FletchSessionHelper fletch = |
- new FletchSessionHelper( |
- getAvailableSession(), executable, environmentOverrides, |
- verbose, superVerbose); |
- |
- fletch.sessionMirror.printLoggedCommands(fletch.stdout, executable); |
- |
- Stopwatch sw = new Stopwatch()..start(); |
- int exitCode; |
- bool endedSession = false; |
- try { |
- Future vmTerminationFuture; |
- try { |
- await fletch.createSession(settingsFileName); |
- |
- // Now that the session is created, start a Fletch VM. |
- String vmSocketAddress = await fletch.spawnVm(); |
- // Timeout of the VM is implemented by shutting down the Fletch VM |
- // after [timeout] seconds. This ensures that compilation+runtime never |
- // exceed [timeout] seconds (plus whatever time is spent in setting up |
- // the session above). |
- vmTerminationFuture = fletch.shutdownVm(timeout); |
- await fletch.runInSession(["attach", "tcp_socket", vmSocketAddress]); |
- if (snapshotFileName != null) { |
- exitCode = await fletch.runInSession( |
- ["export", script, 'to', 'file', snapshotFileName], |
- checkExitCode: false, timeout: timeout); |
- } else { |
- exitCode = await fletch.runInSession(["compile", script], |
- checkExitCode: false, timeout: timeout); |
- fletch.stderr.writeln("Compilation took: ${sw.elapsed}"); |
- if (exitCode == 0) { |
- exitCode = await fletch.runInSession( |
- ["run", "--terminate-debugger"], |
- checkExitCode: false, timeout: timeout); |
- } |
- } |
- } finally { |
- if (exitCode == COMPILER_EXITCODE_CRASH) { |
- // If the compiler crashes, chances are that it didn't close the |
- // connection to the Fletch VM. So we kill it. |
- fletch.killVmProcess(ProcessSignal.SIGTERM); |
- } |
- int vmExitCode = await vmTerminationFuture; |
- fletch.stdout.writeln("Fletch VM exitcode is $vmExitCode " |
- "${explainExitCode(vmExitCode)}\n" |
- "Exit code reported by ${fletch.executable} is $exitCode " |
- "${explainExitCode(exitCode)}\n"); |
- if (exitCode == COMPILER_EXITCODE_CONNECTION_ERROR) { |
- fletch.stderr.writeln("Connection error from compiler"); |
- exitCode = vmExitCode; |
- } else if (exitCode != vmExitCode) { |
- if (!fletch.killedVmProcess || vmExitCode == null || |
- vmExitCode >= 0) { |
- throw new UnexpectedExitCode( |
- vmExitCode, "${fletch.executable}-vm", <String>[]); |
- } |
- } |
- } |
- } on UnexpectedExitCode catch (error) { |
- fletch.stderr.writeln("$error"); |
- exitCode = combineExitCodes(exitCode, error.exitCode); |
- try { |
- if (!endedSession) { |
- // TODO(ahe): Only end if there's a crash. |
- endedSession = true; |
- await fletch.run(["x-end", "session", fletch.sessionName]); |
- } |
- } on UnexpectedExitCode catch (error) { |
- fletch.stderr.writeln("$error"); |
- // TODO(ahe): Error ignored, long term we should be able to guarantee |
- // that shutting down a session never leads to an error. |
- } |
- } |
- |
- if (exitCode == null) { |
- exitCode = COMPILER_EXITCODE_CRASH; |
- fletch.stdout.writeln( |
- '**test.py** could not determine a good exitcode, using $exitCode.'); |
- } |
- |
- if (endedSession) { |
- returnSession(new FletchSessionMirror(fletch.sessionMirror.id)); |
- } else { |
- returnSession(fletch.sessionMirror); |
- } |
- |
- return new FletchTestCommandOutput( |
- this, exitCode, fletch.hasTimedOut, |
- fletch.combinedStdout, fletch.combinedStderr, sw.elapsed, -1); |
- } |
- |
- FletchTestCommandOutput compilerFail(String message) { |
- return new FletchTestCommandOutput( |
- this, DART_VM_EXITCODE_COMPILE_TIME_ERROR, false, <int>[], |
- UTF8.encode(message), const Duration(seconds: 0), -1); |
- } |
- |
- String toString() => reproductionCommand; |
- |
- set displayName(_) => throw "not supported"; |
- |
- get commandLine => throw "not supported"; |
- set commandLine(_) => throw "not supported"; |
- |
- get outputIsUpToDate => throw "not supported"; |
-} |
- |
-/// [compiler] is assumed to be coming from `fletch` in which case |
-/// [COMPILER_EXITCODE_CRASH], [DART_VM_EXITCODE_COMPILE_TIME_ERROR], and |
-/// [DART_VM_EXITCODE_UNCAUGHT_EXCEPTION] all represent a compiler crash. |
-/// |
-/// [runtime] is assumed to be coming from `fletch-vm` in which case which case |
-/// [DART_VM_EXITCODE_COMPILE_TIME_ERROR], and |
-/// [DART_VM_EXITCODE_UNCAUGHT_EXCEPTION] is just the result of running a test |
-/// that has an error (not a crash). |
-int combineExitCodes(int compiler, int runtime) { |
- if (compiler == null) return runtime; |
- |
- if (runtime == null) return compiler; |
- |
- switch (compiler) { |
- case COMPILER_EXITCODE_CRASH: |
- case DART_VM_EXITCODE_COMPILE_TIME_ERROR: |
- case DART_VM_EXITCODE_UNCAUGHT_EXCEPTION: |
- // If the compiler exits with any of those values above, it crashed. It |
- // should never crash. |
- return COMPILER_EXITCODE_CRASH; |
- |
- default: |
- break; |
- } |
- |
- if (compiler < 0) { |
- // Normally, this would be a timeout. However, it can also signify that the |
- // Dart VM crashed, with, for example, SIGABRT or SIGSEGV. |
- return compiler; |
- } |
- |
- return runtime; |
-} |
- |
-class UnexpectedExitCode extends Error { |
- final int exitCode; |
- final String executable; |
- final List<String> arguments; |
- |
- UnexpectedExitCode(this.exitCode, this.executable, this.arguments); |
- |
- String toString() { |
- return "Non-zero exit code ($exitCode) from: " |
- "$executable ${arguments.join(' ')}"; |
- } |
-} |
- |
-class FletchTestCommandOutput extends CommandOutputImpl with DecodeExitCode { |
- FletchTestCommandOutput( |
- Command command, |
- int exitCode, |
- bool timedOut, |
- List<int> stdout, |
- List<int> stderr, |
- Duration time, |
- int pid) |
- : super(command, exitCode, timedOut, stdout, stderr, time, false, pid); |
-} |
- |
-Stream<List<int>> addPrefixWhenNotEmpty( |
- Stream<List<int>> input, |
- String prefix) async* { |
- bool isFirst = true; |
- await for (List<int> bytes in input) { |
- if (isFirst) { |
- yield UTF8.encode("$prefix\n"); |
- isFirst = false; |
- } |
- yield bytes; |
- } |
-} |
- |
-class BytesOutputSink implements Sink<List<int>> { |
- final BytesBuilder bytesBuilder = new BytesBuilder(); |
- |
- final Sink<List<int>> verboseSink; |
- |
- factory BytesOutputSink(bool isVerbose) { |
- StreamController<List<int>> verboseController = |
- new StreamController<List<int>>(); |
- Stream<List<int>> verboseStream = verboseController.stream; |
- if (isVerbose) { |
- verboseStream.transform(UTF8.decoder).transform(new LineSplitter()) |
- .listen(print); |
- } else { |
- verboseStream.listen(null); |
- } |
- return new BytesOutputSink.internal(verboseController); |
- } |
- |
- BytesOutputSink.internal(this.verboseSink); |
- |
- void add(List<int> data) { |
- verboseSink.add(data); |
- bytesBuilder.add(data); |
- } |
- |
- void writeln(String text) { |
- writeText("$text\n"); |
- } |
- |
- void writeText(String text) { |
- add(UTF8.encode(text)); |
- } |
- |
- void close() { |
- verboseSink.close(); |
- } |
-} |
- |
-class FletchSessionHelper { |
- final String executable; |
- |
- final FletchSessionMirror sessionMirror; |
- |
- final String sessionName; |
- |
- final Map<String, String> environmentOverrides; |
- |
- final bool isVerbose; |
- |
- final BytesOutputSink stdout; |
- |
- final BytesOutputSink stderr; |
- |
- final BytesOutputSink vmStdout; |
- |
- final BytesOutputSink vmStderr; |
- |
- Process vmProcess; |
- |
- Future<int> vmExitCodeFuture; |
- |
- bool killedVmProcess = false; |
- |
- bool hasTimedOut = false; |
- |
- FletchSessionHelper( |
- FletchSessionMirror sessionMirror, |
- this.executable, |
- this.environmentOverrides, |
- this.isVerbose, |
- bool superVerbose) |
- : sessionMirror = sessionMirror, |
- sessionName = sessionMirror.makeSessionName(), |
- stdout = new BytesOutputSink(superVerbose), |
- stderr = new BytesOutputSink(superVerbose), |
- vmStdout = new BytesOutputSink(superVerbose), |
- vmStderr = new BytesOutputSink(superVerbose); |
- |
- List<int> get combinedStdout { |
- stdout.close(); |
- vmStdout.close(); |
- BytesBuilder combined = new BytesBuilder() |
- ..add(stdout.bytesBuilder.takeBytes()) |
- ..add(vmStdout.bytesBuilder.takeBytes()); |
- return combined.takeBytes(); |
- } |
- |
- List<int> get combinedStderr { |
- stderr.close(); |
- vmStderr.close(); |
- BytesBuilder combined = new BytesBuilder() |
- ..add(stderr.bytesBuilder.takeBytes()) |
- ..add(vmStderr.bytesBuilder.takeBytes()); |
- return combined.takeBytes(); |
- } |
- |
- /// Run [executable] with arguments and wait for it to exit. This method |
- /// uses [exitCodeWithTimeout] to ensure the process exits within [timeout] |
- /// seconds. |
- /// |
- /// If the process times out, UnexpectedExitCode is thrown. |
- /// |
- /// If [checkExitCode] is true (the default), UnexpectedExitCode is thrown |
- /// unless the process' exit code is 0. |
- Future<int> run( |
- List<String> arguments, |
- {bool checkExitCode: true, |
- int timeout: defaultTimeout}) async { |
- sessionMirror.logCommand(arguments); |
- Process process = await Process.start( |
- "$executable", arguments, environment: environmentOverrides); |
- String commandDescription = "$executable ${arguments.join(' ')}"; |
- if (isVerbose) { |
- print("Running $commandDescription"); |
- } |
- String commandDescriptionForLog = "\$ $commandDescription"; |
- stdout.writeln(commandDescriptionForLog); |
- Future stdoutFuture = process.stdout.listen(stdout.add).asFuture(); |
- Future stderrFuture = |
- addPrefixWhenNotEmpty(process.stderr, commandDescriptionForLog) |
- .listen(stderr.add) |
- .asFuture(); |
- await process.stdin.close(); |
- |
- // Can't reuse [hasTimedOut] as we don't want to throw when calling 'x-end' |
- // in the finally block above. |
- bool thisCommandTimedout = false; |
- |
- int exitCode = await exitCodeWithTimeout(process, timeout, () { |
- stdout.writeln( |
- "\n=> Reached command timeout (sent SIGTERM to fletch-vm)"); |
- thisCommandTimedout = true; |
- hasTimedOut = true; |
- if (vmProcess != null) { |
- killedVmProcess = vmProcess.kill(ProcessSignal.SIGTERM); |
- } |
- }); |
- await stdoutFuture; |
- await stderrFuture; |
- |
- stdout.writeln("\n => $exitCode ${explainExitCode(exitCode)}\n"); |
- if (checkExitCode && (thisCommandTimedout || exitCode != 0)) { |
- throw new UnexpectedExitCode(exitCode, executable, arguments); |
- } |
- return exitCode; |
- } |
- |
- /// Same as [run], except that the arguments "in session $sessionName" are |
- /// implied. |
- Future<int> runInSession( |
- List<String> arguments, |
- {bool checkExitCode: true, |
- int timeout: defaultTimeout}) { |
- return run( |
- []..addAll(arguments)..addAll(["in", "session", sessionName]), |
- checkExitCode: checkExitCode, timeout: timeout); |
- } |
- |
- Future<int> createSession(String settingsFileName) async { |
- if (sessionMirror.isCreated) { |
- return 0; |
- } else { |
- sessionMirror.isCreated = true; |
- return await run( |
- ["create", "session", sessionName, "with", settingsFileName]); |
- } |
- } |
- |
- Future<String> spawnVm() async { |
- FletchVm fletchVm = await FletchVm.start( |
- "$executable-vm", environment: environmentOverrides); |
- vmProcess = fletchVm.process; |
- String commandDescription = "$executable-vm"; |
- if (isVerbose) { |
- print("Running $commandDescription"); |
- } |
- String commandDescriptionForLog = "\$ $commandDescription"; |
- vmStdout.writeln(commandDescriptionForLog); |
- stdout.writeln('$commandDescriptionForLog &'); |
- |
- Future stdoutFuture = |
- fletchVm.stdoutLines.listen(vmStdout.writeln).asFuture(); |
- bool isFirstStderrLine = true; |
- Future stderrFuture = |
- fletchVm.stderrLines.listen( |
- (String line) { |
- if (isFirstStderrLine) { |
- vmStdout.writeln(commandDescriptionForLog); |
- isFirstStderrLine = false; |
- } |
- vmStdout.writeln(line); |
- }) |
- .asFuture(); |
- |
- vmExitCodeFuture = fletchVm.exitCode.then((int exitCode) async { |
- if (isVerbose) print("Exiting Fletch VM with exit code $exitCode."); |
- await stdoutFuture; |
- if (isVerbose) print("Stdout of Fletch VM process closed."); |
- await stderrFuture; |
- if (isVerbose) print("Stderr of Fletch VM process closed."); |
- return exitCode; |
- }); |
- |
- return "${fletchVm.host}:${fletchVm.port}"; |
- } |
- |
- /// Returns a future that completes when the fletch VM exits using |
- /// [exitCodeWithTimeout] to ensure termination within [timeout] seconds. |
- Future<int> shutdownVm(int timeout) async { |
- await exitCodeWithTimeout(vmProcess, timeout, () { |
- stdout.writeln( |
- "\n**fletch-vm** Reached total timeout (sent SIGTERM to fletch-vm)"); |
- killedVmProcess = true; |
- hasTimedOut = true; |
- }); |
- return vmExitCodeFuture; |
- } |
- |
- void killVmProcess(ProcessSignal signal) { |
- if (vmProcess == null) return; |
- killedVmProcess = vmProcess.kill(ProcessSignal.SIGTERM); |
- } |
-} |
- |
-/// Helper method for implementing timing out while waiting for [process] to |
-/// exit. [timeout] is in seconds. If the process times out, it will be killed |
-/// using SIGTERM and onTimeout will be called. |
-/// |
-/// After SIGTERM, the process has 5 seconds to exit or it will be killed with |
-/// SIGKILL. |
-/// |
-/// Note: We treat SIGKILL as a crash, not a timeout. The process is supposed |
-/// to exit quickly and gracefully after receiving SIGTERM. See |
-/// [DecodeExitCode] in `decode_exit_code.dart`. |
-Future<int> exitCodeWithTimeout( |
- Process process, |
- int timeout, |
- void onTimeout()) async { |
- if (process == null) return 0; |
- |
- bool done = false; |
- Timer timer; |
- |
- void secondTimeout() { |
- if (done) return; |
- process.kill(ProcessSignal.SIGKILL); |
- } |
- |
- void firstTimeout() { |
- if (done) return; |
- if (process.kill(ProcessSignal.SIGTERM)) { |
- timer = new Timer(const Duration(seconds: 5), secondTimeout); |
- onTimeout(); |
- } |
- } |
- |
- timer = new Timer(new Duration(seconds: timeout), firstTimeout); |
- |
- int exitCode = await process.exitCode; |
- done = true; |
- timer.cancel(); |
- return exitCode; |
-} |
- |
-/// Represents a session in the persistent Fletch client process. |
-class FletchSessionMirror { |
- static const int RINGBUFFER_SIZE = 15; |
- |
- final int id; |
- |
- final Queue<List<String>> internalLoggedCommands = new Queue<List<String>>(); |
- |
- bool isCreated = false; |
- |
- FletchSessionMirror(this.id); |
- |
- void logCommand(List<String> command) { |
- internalLoggedCommands.add(command); |
- if (internalLoggedCommands.length >= RINGBUFFER_SIZE) { |
- internalLoggedCommands.removeFirst(); |
- } |
- } |
- |
- void printLoggedCommands(BytesOutputSink sink, String executable) { |
- sink.writeln("Previous commands in this session:"); |
- for (List<String> command in internalLoggedCommands) { |
- sink.writeText(executable); |
- for (String argument in command) { |
- sink.writeText(" "); |
- sink.writeText(argument); |
- } |
- sink.writeln(""); |
- } |
- sink.writeln(""); |
- } |
- |
- String makeSessionName() => '$id'; |
-} |
- |
-Future<Null> main(List<String> arguments) async { |
- // Setting [sessionCount] to the current time in milliseconds ensures that it |
- // is highly unlikely that reproduction commands conflicts with an existing |
- // session in a persistent process that wasn't killed. |
- sessionCount = new DateTime.now().millisecondsSinceEpoch; |
- String executable = arguments.first; |
- String script = arguments[1]; |
- arguments = arguments.skip(2).toList(); |
- Map<String, String> environmentOverrides = <String, String>{}; |
- FletchSessionCommand command = new FletchSessionCommand( |
- executable, script, arguments, environmentOverrides, |
- settingsFileName: settingsFileName); |
- FletchTestCommandOutput output = |
- await command.run(0, true, superVerbose: true); |
- print("Test outcome: ${output.decodeExitCode()}"); |
-} |