Skip to content

Commit 1325b69

Browse files
authored
Merge pull request #483 from dart-lang/json-debug
Add debugging support for the JSON reporter.
2 parents c4370e3 + 367d23d commit 1325b69

17 files changed

+1546
-827
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.12.16
2+
3+
* Allow tools to interact with browser debuggers using the JSON reporter.
4+
15
## 0.12.15+12
26

37
* Fix a race condition that could cause the runner to stall for up to three

doc/json_reporter.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,36 @@ A suite event is emitted before any `GroupEvent`s for groups in a given test
132132
suite. This is the only event that contains the full metadata about a suite;
133133
future events will refer to the suite by its opaque ID.
134134

135+
### DebugEvent
136+
137+
```
138+
class DebugEvent extends Event {
139+
String type = "debug";
140+
141+
/// The suite for which debug information is reported.
142+
int suiteID;
143+
144+
/// The HTTP URL for the Dart Observatory, or `null` if the Observatory isn't
145+
/// available for this suite.
146+
String observatory;
147+
148+
/// The HTTP URL for the remote debugger for this suite's host page, or `null`
149+
/// if no remote debugger is available for this suite.
150+
String remoteDebugger;
151+
}
152+
```
153+
154+
A debug event is emitted after (although not necessarily directly after) a
155+
`SuiteEvent`, and includes information about how to debug that suite. It's only
156+
emitted if the `--pause-after-load` flag is passed to the test runner.
157+
158+
Note that the `remoteDebugger` URL refers to a remote debugger whose protocol
159+
may differ based on the browser the suite is running on. You can tell which
160+
protocol is in use by the `Suite.platform` field for the suite with the given
161+
ID. Since the same browser instance is used for multiple suites, different
162+
suites may have the same `host` URL, although only one suite at a time will be
163+
active when `--pause-after-load` is passed.
164+
135165
### GroupEvent
136166

137167
```
@@ -410,3 +440,33 @@ class Metadata {
410440
```
411441

412442
The metadata class is deprecated and should not be used.
443+
444+
## Remote Debugger APIs
445+
446+
When running browser tests with `--pause-after-load`, the test package embeds a
447+
few APIs in the JavaScript context of the host page. These allow tools to
448+
control the debugging process in the same way a user might do from the command
449+
line. They can be accessed by connecting to the remote debugger using the
450+
[`DebugEvent.remoteDebugger`](#DebugEvent) URL.
451+
452+
All APIs are defined as methods on the top-level `dartTest` object. The
453+
following methods are available:
454+
455+
### `resume()`
456+
457+
Calling `resume()` when the test runner is paused causes it to resume running
458+
tests. If the test runner is not paused, it won't do anything. When
459+
`--pause-after-load` is passed, the test runner will pause after loading each
460+
suite but before any tests are run.
461+
462+
This gives external tools a chance to use the remote debugger protocol to set
463+
breakpoints before tests have begun executing. They can start the test runner
464+
with `--pause-after-load`, connect to the remote debugger using the
465+
[`DebugEvent.remoteDebugger`](#DebugEvent) URL, set breakpoints, then call
466+
`dartTest.resume()` in the host frame when they're finished.
467+
468+
### `restartCurrent()`
469+
470+
Calling `restartCurrent()` when the test runner is running a test causes it to
471+
re-run that test once it completes its current run. It's intended to be called
472+
when the browser is paused, as at a breakpoint.

json_reporter.schema.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,22 @@
128128
"required": ["suite"],
129129
"properties": {
130130
"type": {"enum": ["suite"]},
131-
"group": {"$ref": "#/definitions/Suite"}
131+
"suite": {"$ref": "#/definitions/Suite"}
132+
}
133+
},
134+
135+
{
136+
"title": "DebugEvent",
137+
"required": ["suiteID"],
138+
"properties": {
139+
"type": {"enum": ["debug"]},
140+
"suiteID": {"type": "integer", "minimum": 0},
141+
"observatory": {
142+
"oneOf": [{"type": "string", "format": "uri"}, {"type": "null"}]
143+
},
144+
"remoteDebugger": {
145+
"oneOf": [{"type": "string", "format": "uri"}, {"type": "null"}]
146+
}
132147
}
133148
},
134149

@@ -194,7 +209,7 @@
194209
"not": {
195210
"enum": [
196211
"start", "testStart", "allSuites", "suite", "group", "print",
197-
"error", "testDone", "done"
212+
"error", "testDone", "done", "debug"
198213
]
199214
}
200215
}

lib/src/runner/browser/browser.dart

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,15 @@ abstract class Browser {
2525

2626
/// The Observatory URL for this browser.
2727
///
28-
/// This will throw an [UnsupportedError] for browsers that aren't running the
29-
/// Dart VM, and return `null` if the Observatory URL can't be found.
30-
Future<Uri> get observatoryUrl =>
31-
throw new UnsupportedError("$name doesn't support Observatory.");
28+
/// This will return `null` for browsers that aren't running the Dart VM, or
29+
/// if the Observatory URL can't be found.
30+
Future<Uri> get observatoryUrl => null;
3231

3332
/// The remote debugger URL for this browser.
3433
///
35-
/// This will throw an [UnsupportedError] for browsers that don't support
36-
/// remote debugging, and return `null` if the remote debugging URL can't be
37-
/// found.
38-
Future<Uri> get remoteDebuggerUrl =>
39-
throw new UnsupportedError("$name doesn't support remote debugging.");
34+
/// This will return `null` for browsers that don't support remote debugging,
35+
/// or if the remote debugging URL can't be found.
36+
Future<Uri> get remoteDebuggerUrl => null;
4037

4138
/// The underlying process.
4239
///

lib/src/runner/browser/browser_manager.dart

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ class BrowserManager {
6868
/// screen.
6969
CancelableCompleter _pauseCompleter;
7070

71+
/// The controller for [_BrowserEnvironment.onRestart].
72+
final _onRestartController = new StreamController();
73+
7174
/// The environment to attach to each suite.
7275
Future<_BrowserEnvironment> _environment;
7376

@@ -133,7 +136,7 @@ class BrowserManager {
133136
case TestPlatform.dartium: return new Dartium(url, debug: debug);
134137
case TestPlatform.contentShell:
135138
return new ContentShell(url, debug: debug);
136-
case TestPlatform.chrome: return new Chrome(url);
139+
case TestPlatform.chrome: return new Chrome(url, debug: debug);
137140
case TestPlatform.phantomJS: return new PhantomJS(url, debug: debug);
138141
case TestPlatform.firefox: return new Firefox(url);
139142
case TestPlatform.safari: return new Safari(url);
@@ -178,15 +181,8 @@ class BrowserManager {
178181

179182
/// Loads [_BrowserEnvironment].
180183
Future<_BrowserEnvironment> _loadBrowserEnvironment() async {
181-
var observatoryUrl;
182-
if (_platform.isDartVM) observatoryUrl = await _browser.observatoryUrl;
183-
184-
var remoteDebuggerUrl;
185-
if (_platform.isHeadless) {
186-
remoteDebuggerUrl = await _browser.remoteDebuggerUrl;
187-
}
188-
189-
return new _BrowserEnvironment(this, observatoryUrl, remoteDebuggerUrl);
184+
return new _BrowserEnvironment(this, await _browser.observatoryUrl,
185+
await _browser.remoteDebuggerUrl, _onRestartController.stream);
190186
}
191187

192188
/// Tells the browser the load a test suite from the URL [url].
@@ -266,11 +262,22 @@ class BrowserManager {
266262

267263
/// The callback for handling messages received from the host page.
268264
void _onMessage(Map message) {
269-
if (message["command"] == "ping") return;
265+
switch (message["command"]) {
266+
case "ping": break;
267+
268+
case "restart":
269+
_onRestartController.add(null);
270+
break;
270271

271-
assert(message["command"] == "resume");
272-
if (_pauseCompleter == null) return;
273-
_pauseCompleter.complete();
272+
case "resume":
273+
if (_pauseCompleter != null) _pauseCompleter.complete();
274+
break;
275+
276+
default:
277+
// Unreachable.
278+
assert(false);
279+
break;
280+
}
274281
}
275282

276283
/// Closes the manager and releases any resources it owns, including closing
@@ -298,8 +305,10 @@ class _BrowserEnvironment implements Environment {
298305

299306
final Uri remoteDebuggerUrl;
300307

308+
final Stream onRestart;
309+
301310
_BrowserEnvironment(this._manager, this.observatoryUrl,
302-
this.remoteDebuggerUrl);
311+
this.remoteDebuggerUrl, this.onRestart);
303312

304313
CancelableOperation displayPause() => _manager._displayPause();
305314
}

lib/src/runner/browser/chrome.dart

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'dart:io';
88
import 'package:path/path.dart' as p;
99

1010
import '../../util/io.dart';
11+
import '../../utils.dart';
1112
import 'browser.dart';
1213

1314
// TODO(nweiz): move this into its own package?
@@ -21,36 +22,77 @@ import 'browser.dart';
2122
class Chrome extends Browser {
2223
final name = "Chrome";
2324

25+
final Future<Uri> remoteDebuggerUrl;
26+
2427
/// Starts a new instance of Chrome open to the given [url], which may be a
2528
/// [Uri] or a [String].
2629
///
2730
/// If [executable] is passed, it's used as the Chrome executable. Otherwise
2831
/// the default executable name for the current OS will be used.
29-
Chrome(url, {String executable})
30-
: super(() => _startBrowser(url, executable));
31-
32-
static Future<Process> _startBrowser(url, [String executable]) async {
33-
if (executable == null) executable = _defaultExecutable();
34-
35-
var dir = createTempDir();
36-
var process = await Process.start(executable, [
37-
"--user-data-dir=$dir",
38-
url.toString(),
39-
"--disable-extensions",
40-
"--disable-popup-blocking",
41-
"--bwsi",
42-
"--no-first-run",
43-
"--no-default-browser-check",
44-
"--disable-default-apps",
45-
"--disable-translate"
46-
]);
47-
48-
process.exitCode
49-
.then((_) => new Directory(dir).deleteSync(recursive: true));
50-
51-
return process;
32+
factory Chrome(url, {String executable, bool debug: false}) {
33+
var remoteDebuggerCompleter = new Completer<Uri>.sync();
34+
return new Chrome._(() async {
35+
if (executable == null) executable = _defaultExecutable();
36+
37+
var tryPort = ([int port]) async {
38+
var dir = createTempDir();
39+
var args = [
40+
"--user-data-dir=$dir", url.toString(), "--disable-extensions",
41+
"--disable-popup-blocking", "--bwsi", "--no-first-run",
42+
"--no-default-browser-check", "--disable-default-apps",
43+
"--disable-translate",
44+
];
45+
46+
if (port != null) {
47+
args.add("--remote-debugging-port=$port");
48+
// These flags cause Chrome to print a consistent line of output after
49+
// its internal call to `bind()` has succeeded or failed. We wait for
50+
// that output to determine whether the port we chose worked.
51+
args.add("--enable-logging=stderr");
52+
args.add("--vmodule=startup_browser_creator_impl=1");
53+
}
54+
55+
var process = await Process.start(executable, args);
56+
57+
if (port != null) {
58+
var stderr = new StreamIterator(lineSplitter.bind(process.stderr));
59+
60+
// Before we can consider Chrome to have started successfully, we have
61+
// to make sure the remote debugging port worked. Any errors from this
62+
// will always come before the "startup_browser_creater_impl" message.
63+
while (await stderr.moveNext() &&
64+
!stderr.current.contains("startup_browser_creator_impl")) {
65+
if (stderr.current.contains("bind() returned an error")) {
66+
// If we failed to bind to the port, return null to tell
67+
// getUnusedPort to try another one.
68+
stderr.cancel();
69+
process.kill();
70+
return null;
71+
}
72+
}
73+
}
74+
75+
if (port != null) {
76+
remoteDebuggerCompleter.complete(
77+
getRemoteDebuggerUrl(Uri.parse("http://localhost:$port")));
78+
} else {
79+
remoteDebuggerCompleter.complete(null);
80+
}
81+
82+
process.exitCode
83+
.then((_) => new Directory(dir).deleteSync(recursive: true));
84+
85+
return process;
86+
};
87+
88+
if (!debug) return tryPort();
89+
return getUnusedPort/*<Future<Process>>*/(tryPort);
90+
}, remoteDebuggerCompleter.future);
5291
}
5392

93+
Chrome._(Future<Process> startBrowser(), this.remoteDebuggerUrl)
94+
: super(startBrowser);
95+
5496
/// Return the default executable for the current operating system.
5597
static String _defaultExecutable() {
5698
if (Platform.isMacOS) {

lib/src/runner/browser/content_shell.dart

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6-
import 'dart:convert';
76
import 'dart:io';
87

98
import '../../util/io.dart';
@@ -56,7 +55,7 @@ class ContentShell extends Browser {
5655
// Before we can consider content_shell started successfully, we have to
5756
// make sure it's not expired and that the remote debugging port worked.
5857
// Any errors from this will always come before the "Running without
59-
// renderer sanxbox" message.
58+
// renderer sandbox" message.
6059
while (await stderr.moveNext() &&
6160
!stderr.current.endsWith("Running without renderer sandbox")) {
6261
if (stderr.current == "[dartToStderr]: Dartium build has expired") {
@@ -81,7 +80,7 @@ class ContentShell extends Browser {
8180

8281
if (port != null) {
8382
remoteDebuggerCompleter.complete(
84-
_getRemoteDebuggerUrl(Uri.parse("http://localhost:$port")));
83+
getRemoteDebuggerUrl(Uri.parse("http://localhost:$port")));
8584
} else {
8685
remoteDebuggerCompleter.complete(null);
8786
}
@@ -95,25 +94,6 @@ class ContentShell extends Browser {
9594
}, observatoryCompleter.future, remoteDebuggerCompleter.future);
9695
}
9796

98-
/// Returns the full URL of the remote debugger for the host page.
99-
///
100-
/// This takes the base remote debugger URL (which points to a browser-wide
101-
/// page) and uses its JSON API to find the resolved URL for debugging the
102-
/// host page.
103-
static Future<Uri> _getRemoteDebuggerUrl(Uri base) async {
104-
try {
105-
var client = new HttpClient();
106-
var request = await client.getUrl(base.resolve("/json/list"));
107-
var response = await request.close();
108-
var json = await JSON.fuse(UTF8).decoder.bind(response).single as List;
109-
return base.resolve(json.first["devtoolsFrontendUrl"]);
110-
} catch (_) {
111-
// If we fail to talk to the remote debugger protocol, give up and return
112-
// the raw URL rather than crashing.
113-
return base;
114-
}
115-
}
116-
11797
ContentShell._(Future<Process> startBrowser(), this.observatoryUrl,
11898
this.remoteDebuggerUrl)
11999
: super(startBrowser);

0 commit comments

Comments
 (0)