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()
});
}