| // Copyright (c) 2016, 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 'package:async/async.dart'; |
| |
| import 'package:test_api/src/utils.dart'; // ignore: implementation_imports |
| |
| import '../util/io.dart'; |
| import 'runner_suite.dart'; |
| import 'configuration.dart'; |
| import 'console.dart'; |
| import 'engine.dart'; |
| import 'load_suite.dart'; |
| import 'reporter.dart'; |
| |
| /// Runs [loadSuite] in debugging mode. |
| /// |
| /// Runs the suite's tests using [engine]. The [reporter] should already be |
| /// watching [engine], and the [config] should contain the user configuration |
| /// for the test runner. |
| /// |
| /// Returns a [CancelableOperation] that will complete once the suite has |
| /// finished running. If the operation is canceled, the debugger will clean up |
| /// any resources it allocated. |
| CancelableOperation debug( |
| Engine engine, Reporter reporter, LoadSuite loadSuite) { |
| _Debugger debugger; |
| var canceled = false; |
| return CancelableOperation.fromFuture(() async { |
| // Make the underlying suite null so that the engine doesn't start running |
| // it immediately. |
| engine.suiteSink.add(loadSuite.changeSuite((runnerSuite) { |
| engine.pause(); |
| return runnerSuite; |
| })); |
| |
| var suite = await loadSuite.suite; |
| if (canceled || suite == null) return; |
| |
| debugger = _Debugger(engine, reporter, suite); |
| await debugger.run(); |
| }(), onCancel: () { |
| canceled = true; |
| // Make sure the load test finishes so the engine can close. |
| engine.resume(); |
| if (debugger != null) debugger.close(); |
| }); |
| } |
| |
| // TODO(nweiz): Test using the console and restarting a test once sdk#25369 is |
| // fixed and the VM service client is released |
| /// A debugger for a single test suite. |
| class _Debugger { |
| /// The test runner configuration. |
| final _config = Configuration.current; |
| |
| /// The engine that will run the suite. |
| final Engine _engine; |
| |
| /// The reporter that's reporting [_engine]'s progress. |
| final Reporter _reporter; |
| |
| /// The suite to run. |
| final RunnerSuite _suite; |
| |
| /// The console through which the user can control the debugger. |
| /// |
| /// This is only visible when the test environment is paused, so as not to |
| /// overlap with the reporter's reporting. |
| final Console _console; |
| |
| /// A completer that's used to manually unpause the test if the debugger is |
| /// closed. |
| final _pauseCompleter = CancelableCompleter(); |
| |
| /// The subscription to [_suite.onDebugging]. |
| StreamSubscription<bool> _onDebuggingSubscription; |
| |
| /// The subscription to [_suite.environment.onRestart]. |
| StreamSubscription _onRestartSubscription; |
| |
| /// Whether [close] has been called. |
| bool _closed = false; |
| |
| bool get _json => _config.reporter == 'json'; |
| |
| _Debugger(this._engine, this._reporter, this._suite) |
| : _console = Console(color: Configuration.current.color) { |
| _console.registerCommand("restart", |
| "Restart the current test after it finishes running.", _restartTest); |
| |
| _onRestartSubscription = _suite.environment.onRestart.listen((_) { |
| _restartTest(); |
| }); |
| } |
| |
| /// Runs the debugger. |
| /// |
| /// This prints information about the suite's debugger, then once the user has |
| /// had a chance to set breakpoints, runs the suite's tests. |
| Future run() async { |
| try { |
| await _pause(); |
| if (_closed) return; |
| |
| _onDebuggingSubscription = _suite.onDebugging.listen((debugging) { |
| if (debugging) { |
| _onDebugging(); |
| } else { |
| _onNotDebugging(); |
| } |
| }); |
| |
| _engine.resume(); |
| await _engine.onIdle.first; |
| } finally { |
| close(); |
| } |
| } |
| |
| /// Prints URLs for the [_suite]'s debugger and waits for the user to tell the |
| /// suite to run. |
| Future _pause() async { |
| if (_suite.platform == null) return; |
| if (!_suite.environment.supportsDebugging) return; |
| |
| try { |
| if (!_json) { |
| _reporter.pause(); |
| |
| var bold = _config.color ? '\u001b[1m' : ''; |
| var yellow = _config.color ? '\u001b[33m' : ''; |
| var noColor = _config.color ? '\u001b[0m' : ''; |
| print(''); |
| |
| var runtime = _suite.platform.runtime; |
| if (runtime.isDartVM) { |
| var url = _suite.environment.observatoryUrl; |
| if (url == null) { |
| print("${yellow}Observatory URL not found. Make sure you're using " |
| "${runtime.name} 1.11 or later.$noColor"); |
| } else { |
| print("Observatory URL: $bold$url$noColor"); |
| } |
| } |
| |
| if (runtime.isHeadless && !runtime.isDartVM) { |
| var url = _suite.environment.remoteDebuggerUrl; |
| if (url == null) { |
| print("${yellow}Remote debugger URL not found.$noColor"); |
| } else { |
| print("Remote debugger URL: $bold$url$noColor"); |
| } |
| } |
| |
| var buffer = |
| StringBuffer("${bold}The test runner is paused.${noColor} "); |
| if (runtime.isDartVM) { |
| buffer.write("Open the Observatory "); |
| } else { |
| if (!runtime.isHeadless) { |
| buffer.write("Open the dev console in $runtime "); |
| } else { |
| buffer.write("Open the remote debugger "); |
| } |
| } |
| |
| buffer.write("and set breakpoints. Once you're finished, return to " |
| "this terminal and press Enter."); |
| |
| print(wordWrap(buffer.toString())); |
| } |
| |
| await inCompletionOrder([ |
| _suite.environment.displayPause(), |
| stdinLines.cancelable((queue) => queue.next), |
| _pauseCompleter.operation |
| ]).first; |
| } finally { |
| if (!_json) _reporter.resume(); |
| } |
| } |
| |
| /// Handles the environment pausing to debug. |
| /// |
| /// This starts the interactive console. |
| void _onDebugging() { |
| if (!_json) _reporter.pause(); |
| |
| if (!_json) { |
| print('\nEntering debugging console. Type "help" for help.'); |
| } |
| _console.start(); |
| } |
| |
| /// Handles the environment starting up again. |
| /// |
| /// This closes the interactive console. |
| void _onNotDebugging() { |
| if (!_json) _reporter.resume(); |
| _console.stop(); |
| } |
| |
| /// Restarts the current test. |
| void _restartTest() { |
| if (_engine.active.isEmpty) return; |
| var liveTest = _engine.active.single; |
| _engine.restartTest(liveTest); |
| if (!_json) { |
| print(wordWrap( |
| 'Will restart "${liveTest.test.name}" once it finishes running.')); |
| } |
| } |
| |
| /// Closes the debugger and releases its resources. |
| void close() { |
| _pauseCompleter.complete(); |
| _closed = true; |
| _onDebuggingSubscription?.cancel(); |
| _onRestartSubscription.cancel(); |
| _console.stop(); |
| } |
| } |