diff --git a/doc/json_reporter.md b/doc/json_reporter.md index a3a557b4a138baa6dfa7bc0fb7ff4edd998cd950..bb9a0016b4b279243c6b6daba586d0ccd54710e8 100644 --- a/doc/json_reporter.md +++ b/doc/json_reporter.md @@ -453,11 +453,15 @@ During debugging, users of the JSON API need a way to communicate with the test runner to tell it things like "all the breakpoints are set and the test should begin running". This is done through a [JSON-RPC 2.0][] API over a WebSocket connection. The WebSocket URL is available in -[`StartEvent.controllerUrl`](#StartEvent). The following RPCs are available: +[`StartEvent.controllerUrl`](#StartEvent). [JSON-RPC 2.0][]: http://www.jsonrpc.org/specification -### `null resume()` +Each RPC will return a success response with a null result once the request has +been handled. If there's no test suite currently being debugged, they'll return +an error response with error code 1. The following RPCs are available: + +### `resume()` Calling `resume()` when the test runner is paused causes it to resume running tests. If the test runner is not paused, it won't do anything. When @@ -469,9 +473,9 @@ breakpoints before tests have begun executing. They can start the test runner with `--pause-after-load`, connect to the controller protocol using [`StartEvent.controllerUrl`](#StartEvent), set breakpoints, then call `resume()` in when they're finished. +L +#### `restartTest()` -#### `restartCurrent()` - -Calling `restartCurrent()` when the test runner is running a test causes it to +Calling `restartTest()` when the test runner is running a test causes it to re-run that test once it completes its current run. It's intended to be called when the browser is paused, as at a breakpoint. diff --git a/lib/src/executable.dart b/lib/src/executable.dart index 20f4cdb6d822f04612c343293a0e0dac04b2cf2f..867874104ca54f92aba595289fd782f172c81b14 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -157,7 +157,7 @@ transformers: return; } - var runner = new Runner(configuration); + var runner = await Runner.start(configuration); var signalSubscription; close() async { diff --git a/lib/src/runner.dart b/lib/src/runner.dart index 4dd0faf30aeb3cdc91f02376c3a093ea81985484..29ced0c02281709067e8aad3deb5431888ad9ab3 100644 --- a/lib/src/runner.dart +++ b/lib/src/runner.dart @@ -46,6 +46,10 @@ class Runner { /// The reporter that's emitting the test runner's results. final Reporter _reporter; + /// The controller that controls suite debugging, or `null` if we aren't in + /// debug mode or we aren't using the JSON reporter. + final DebugController _debugController; + /// The subscription to the stream returned by [_loadSuites]. StreamSubscription _suiteSubscription; @@ -66,10 +70,12 @@ class Runner { bool get _closed => _closeMemo.hasRun; /// Creates a new runner based on [configuration]. - factory Runner(Configuration config) => config.asCurrent(() { + static Future<Runner> start(Configuration config) => + config.asCurrent(() async { var engine = new Engine(concurrency: config.concurrency); - var reporter; + Reporter reporter; + DebugController controller; switch (config.reporter) { case "expanded": reporter = ExpandedReporter.watch( @@ -85,14 +91,15 @@ class Runner { break; case "json": - reporter = JsonReporter.watch(engine); + if (config.pauseAfterLoad) controller = await DebugController.start(); + reporter = JsonReporter.watch(engine, controller?.url); break; } - return new Runner._(engine, reporter); + return new Runner._(engine, reporter, controller); }); - Runner._(this._engine, this._reporter); + Runner._(this._engine, this._reporter, this._debugController); /// Starts the runner. /// @@ -207,9 +214,11 @@ class Runner { }); } - if (_debugOperation != null) await _debugOperation.cancel(); + print("in close"); + await _debugOperation?.cancel(); + await _debugController?.close(); - if (_suiteSubscription != null) _suiteSubscription.cancel(); + _suiteSubscription?.cancel(); _suiteSubscription = null; // Make sure we close the engine *before* the loader. Otherwise, @@ -372,7 +381,7 @@ class Runner { /// that support debugging. Future<bool> _loadThenPause(Stream<LoadSuite> suites) async { _suiteSubscription = suites.asyncMap((loadSuite) async { - _debugOperation = debug(_engine, _reporter, loadSuite); + _debugOperation = debug(_engine, _reporter, loadSuite, _debugController); await _debugOperation.valueOrCancellation(); }).listen(null); diff --git a/lib/src/runner/browser/browser_manager.dart b/lib/src/runner/browser/browser_manager.dart index 0b89e7d5aa0dd12fdf776715489053e1ed52d20e..9216be2470a9b884712330b8b21730b36f77a773 100644 --- a/lib/src/runner/browser/browser_manager.dart +++ b/lib/src/runner/browser/browser_manager.dart @@ -305,7 +305,7 @@ class _BrowserEnvironment implements Environment { final Uri remoteDebuggerUrl; - final String isolateID => null; + final String isolateID = null; final Stream onRestart; diff --git a/lib/src/runner/debugger.dart b/lib/src/runner/debugger.dart index 11b424f695d1ac30471aed69391b30f683b482dc..90bae0401fd96516c2ad0d6c7549a930872cecee 100644 --- a/lib/src/runner/debugger.dart +++ b/lib/src/runner/debugger.dart @@ -3,8 +3,13 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:async/async.dart'; +import 'package:http_multi_server/http_multi_server.dart'; +import 'package:json_rpc_2/json_rpc_2.dart' as rpc; +import 'package:shelf_web_socket/shelf_web_socket.dart'; +import 'package:shelf/shelf_io.dart' as io; import '../backend/test_platform.dart'; import '../util/io.dart'; @@ -22,11 +27,14 @@ import 'runner_suite.dart'; /// watching [engine], and the [config] should contain the user configuration /// for the test runner. /// +/// If [controller] is passed, the debugger will hook into it to allow debugging +/// to be controlled via WebSocket. +/// /// 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) { + LoadSuite loadSuite, [DebugController controller]) { var debugger; var canceled = false; return new CancelableOperation.fromFuture(() async { @@ -41,7 +49,9 @@ CancelableOperation debug(Engine engine, Reporter reporter, if (canceled || suite == null) return; debugger = new _Debugger(engine, reporter, suite); + controller?._debugger = debugger; await debugger.run(); + controller?._debugger = null; }(), onCancel: () { canceled = true; // Make sure the load test finishes so the engine can close. @@ -73,8 +83,7 @@ class _Debugger { /// overlap with the reporter's reporting. final Console _console; - /// A completer that's used to manually unpause the test if the debugger is - /// closed. + /// A completer that's used to manually unpause the test. final _pauseCompleter = new CancelableCompleter(); /// The subscription to [_suite.onDebugging]. @@ -221,10 +230,48 @@ class _Debugger { /// Closes the debugger and releases its resources. void close() { - _pauseCompleter.complete(); + if (!_pauseCompleter.isCompleted) _pauseCompleter.complete(); _closed = true; _onDebuggingSubscription?.cancel(); _onRestartSubscription.cancel(); _console.stop(); } } + +/// A singleton WebSocket server with a JSON-RPC 2.0 protocol that services RPCs +/// that allow IDEs using the JSON reporter to control the debugger. +class DebugController { + final HttpServer _server; + + _Debugger _debugger; + + Uri get url => Uri.parse("ws://localhost:${_server.port}"); + + static Future<DebugController> start() async => + new DebugController._(await HttpMultiServer.loopback(0)); + + DebugController._(this._server) { + io.serveRequests(_server, webSocketHandler((webSocket) { + var server = new rpc.Server(webSocket); + server.registerMethod("resume", () { + _assertDebugger(); + _debugger._pauseCompleter.complete(); + }); + + server.registerMethod("restartTest", () { + _assertDebugger(); + _debugger._restartTest(); + }); + })); + } + + void _assertDebugger() { + if (_debugger != null) return; + throw new rpc.RpcException(1, "No suite is being debugged."); + } + + Future close() { + print("closing debug controller"); + return _server.close(); + } +} diff --git a/lib/src/runner/reporter/json.dart b/lib/src/runner/reporter/json.dart index bfa4b75a6bc0c25b4082841ab5f89242e5e0b138..a37389bd691cb73a584133605c09f0ad35c2bcc7 100644 --- a/lib/src/runner/reporter/json.dart +++ b/lib/src/runner/reporter/json.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:developer'; import '../../backend/group.dart'; import '../../backend/group_entry.dart'; @@ -59,9 +58,14 @@ class JsonReporter implements Reporter { var _nextID = 0; /// Watches the tests run by [engine] and prints their results as JSON. - static JsonReporter watch(Engine engine) => new JsonReporter._(engine); + /// + /// If [controllerUrl] is passed, it's emitted as the URL for IDE clients to + /// use to control the debugger. + static JsonReporter watch(Engine engine, [Uri controllerUrl]) => + new JsonReporter._(engine, controllerUrl); - JsonReporter._(this._engine) : _config = Configuration.current { + JsonReporter._(this._engine, Uri controllerUrl) + : _config = Configuration.current { _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted)); /// Convert the future to a stream so that the subscription can be paused or @@ -76,7 +80,8 @@ class JsonReporter implements Reporter { _emit("start", { "protocolVersion": "0.1.0", - "runnerVersion": testVersion + "runnerVersion": testVersion, + "controllerUrl": controllerUrl?.toString() }); }