Expose a WebSocket debug controller. Still needs tests.
diff --git a/doc/json_reporter.md b/doc/json_reporter.md index a3a557b..bb9a001 100644 --- a/doc/json_reporter.md +++ b/doc/json_reporter.md
@@ -453,11 +453,15 @@ 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 @@ 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 20f4cdb..8678741 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart
@@ -157,7 +157,7 @@ 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 4dd0faf..29ced0c 100644 --- a/lib/src/runner.dart +++ b/lib/src/runner.dart
@@ -46,6 +46,10 @@ /// 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 @@ 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 @@ 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 @@ }); } - 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 @@ /// 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 0b89e7d..9216be2 100644 --- a/lib/src/runner/browser/browser_manager.dart +++ b/lib/src/runner/browser/browser_manager.dart
@@ -305,7 +305,7 @@ 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 11b424f..90bae04 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 @@ /// 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 @@ 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 @@ /// 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 @@ /// 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 bfa4b75..a37389b 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 @@ 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 @@ _emit("start", { "protocolVersion": "0.1.0", - "runnerVersion": testVersion + "runnerVersion": testVersion, + "controllerUrl": controllerUrl?.toString() }); }