Skip to content

Commit 0baae3a

Browse files
committed
Expose the Observatory URL when debugging.
This also refactors BrowserManager so that it has full control over the Browser instances it manages. Closes #295 [email protected] Review URL: https://codereview.chromium.org//1258363003 .
1 parent baad78b commit 0baae3a

11 files changed

+441
-184
lines changed

lib/src/backend/test_platform.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class TestPlatform {
2121
/// Dartium content shell.
2222
static const TestPlatform contentShell = const TestPlatform._(
2323
"Dartium Content Shell", "content-shell",
24-
isBrowser: true, isBlink: true, isDartVM: true);
24+
isBrowser: true, isBlink: true, isDartVM: true, isHeadless: true);
2525

2626
/// Google Chrome.
2727
static const TestPlatform chrome = const TestPlatform._("Chrome", "chrome",
@@ -30,7 +30,7 @@ class TestPlatform {
3030
/// PhantomJS.
3131
static const TestPlatform phantomJS = const TestPlatform._(
3232
"PhantomJS", "phantomjs",
33-
isBrowser: true, isJS: true, isBlink: true);
33+
isBrowser: true, isJS: true, isBlink: true, isHeadless: true);
3434

3535
/// Mozilla Firefox.
3636
static const TestPlatform firefox = const TestPlatform._("Firefox", "firefox",
@@ -82,8 +82,12 @@ class TestPlatform {
8282
/// Whether this platform uses the Blink rendering engine.
8383
final bool isBlink;
8484

85+
/// Whether this platform has no visible window.
86+
final bool isHeadless;
87+
8588
const TestPlatform._(this.name, this.identifier, {this.isDartVM: false,
86-
this.isBrowser: false, this.isJS: false, this.isBlink: false});
89+
this.isBrowser: false, this.isJS: false, this.isBlink: false,
90+
this.isHeadless: false});
8791

8892
String toString() => name;
8993
}

lib/src/runner.dart

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import 'utils.dart';
2727

2828
/// The set of platforms for which debug flags are (currently) not supported.
2929
final _debugUnsupportedPlatforms = new Set.from(
30-
[TestPlatform.vm, TestPlatform.phantomJS, TestPlatform.contentShell]);
30+
[TestPlatform.vm, TestPlatform.phantomJS]);
3131

3232
/// A class that loads and runs tests based on a [Configuration].
3333
///
@@ -231,17 +231,39 @@ class Runner {
231231
_reporter.pause();
232232

233233
var bold = _configuration.color ? '\u001b[1m' : '';
234+
var yellow = _configuration.color ? '\u001b[33m' : '';
234235
var noColor = _configuration.color ? '\u001b[0m' : '';
235236
print('');
236-
print(wordWrap(
237-
"${bold}The test runner is paused.${noColor} Open the dev console in "
238-
"${suite.platform} and set breakpoints. Once you're finished, "
239-
"return to this terminal and press Enter."));
240237

241-
await race([
238+
if (suite.platform.isDartVM) {
239+
var url = suite.environment.observatoryUrl;
240+
if (url == null) {
241+
print("${yellow}Observatory URL not found. Make sure you're using "
242+
"${suite.platform.name} 1.11 or later.$noColor");
243+
} else {
244+
print("Observatory URL: $bold$url$noColor");
245+
}
246+
}
247+
248+
var buffer = new StringBuffer(
249+
"${bold}The test runner is paused.${noColor} ");
250+
if (!suite.platform.isHeadless) {
251+
buffer.write("Open the dev console in ${suite.platform} ");
252+
if (suite.platform.isDartVM) buffer.write("or ");
253+
} else {
254+
buffer.write("Open ");
255+
}
256+
if (suite.platform.isDartVM) buffer.write("the Observatory ");
257+
258+
buffer.write("and set breakpoints. Once you're finished, return to this "
259+
"terminal and press Enter.");
260+
261+
print(wordWrap(buffer.toString()));
262+
263+
await inCompletionOrder([
242264
suite.environment.displayPause(),
243265
cancelableNext(stdinLines)
244-
]);
266+
]).first;
245267
} finally {
246268
_reporter.resume();
247269
}

lib/src/runner/browser/browser.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ typedef Future<Process> StartBrowserFn();
2525
abstract class Browser {
2626
String get name;
2727

28+
/// The Observatory URL for this browser.
29+
///
30+
/// This will throw an [UnsupportedError] for browsers that aren't running the
31+
/// Dart VM, and return `null` if the Observatory URL can't be found.
32+
Future<Uri> get observatoryUrl =>
33+
throw new UnsupportedError("$name doesn't support Observatory.");
34+
2835
/// The underlying process.
2936
///
3037
/// This will fire once the process has started successfully.

lib/src/runner/browser/browser_manager.dart

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ library test.runner.browser.browser_manager;
77
import 'dart:async';
88
import 'dart:convert';
99

10+
import 'package:async/async.dart';
1011
import 'package:http_parser/http_parser.dart';
1112
import 'package:pool/pool.dart';
1213

@@ -17,18 +18,32 @@ import '../../util/multi_channel.dart';
1718
import '../../util/remote_exception.dart';
1819
import '../../util/stack_trace_mapper.dart';
1920
import '../../utils.dart';
21+
import '../application_exception.dart';
2022
import '../environment.dart';
2123
import '../load_exception.dart';
2224
import '../runner_suite.dart';
25+
import 'browser.dart';
26+
import 'chrome.dart';
27+
import 'content_shell.dart';
28+
import 'dartium.dart';
29+
import 'firefox.dart';
2330
import 'iframe_test.dart';
31+
import 'internet_explorer.dart';
32+
import 'phantom_js.dart';
33+
import 'safari.dart';
2434

2535
/// A class that manages the connection to a single running browser.
2636
///
2737
/// This is in charge of telling the browser which test suites to load and
2838
/// converting its responses into [Suite] objects.
2939
class BrowserManager {
30-
/// The browser that this is managing.
31-
final TestPlatform browser;
40+
/// The browser instance that this is connected to via [_channel].
41+
final Browser _browser;
42+
43+
// TODO(nweiz): Consider removing the duplication between this and
44+
// [_browser.name].
45+
/// The [TestPlatform] for [_browser].
46+
final TestPlatform _platform;
3247

3348
/// The channel used to communicate with the browser.
3449
///
@@ -60,16 +75,78 @@ class BrowserManager {
6075
CancelableCompleter _pauseCompleter;
6176

6277
/// The environment to attach to each suite.
63-
_BrowserEnvironment _environment;
78+
Future<_BrowserEnvironment> _environment;
79+
80+
/// Starts the browser identified by [platform] and has it connect to [url].
81+
///
82+
/// [url] should serve a page that establishes a WebSocket connection with
83+
/// this process. That connection, once established, should be emitted via
84+
/// [future].
85+
///
86+
/// Returns the browser manager, or throws an [ApplicationException] if a
87+
/// connection fails to be established.
88+
static Future<BrowserManager> start(TestPlatform platform, Uri url,
89+
Future<CompatibleWebSocket> future) {
90+
var browser = _newBrowser(url, platform);
91+
92+
var completer = new Completer();
93+
94+
// TODO(nweiz): Gracefully handle the browser being killed before the
95+
// tests complete.
96+
browser.onExit.then((_) {
97+
throw new ApplicationException(
98+
"${platform.name} exited before connecting.");
99+
}).catchError((error, stackTrace) {
100+
if (completer.isCompleted) return;
101+
completer.completeError(error, stackTrace);
102+
});
103+
104+
future.then((webSocket) {
105+
if (completer.isCompleted) return;
106+
completer.complete(new BrowserManager._(browser, platform, webSocket));
107+
}).catchError((error, stackTrace) {
108+
browser.close();
109+
if (completer.isCompleted) return;
110+
completer.completeError(error, stackTrace);
111+
});
112+
113+
return completer.future.timeout(new Duration(seconds: 30), onTimeout: () {
114+
browser.close();
115+
throw new ApplicationException(
116+
"Timed out waiting for ${platform.name} to connect.");
117+
});
118+
}
119+
120+
/// Starts the browser identified by [browser] and has it load [url].
121+
static Browser _newBrowser(Uri url, TestPlatform browser) {
122+
switch (browser) {
123+
case TestPlatform.dartium: return new Dartium(url);
124+
case TestPlatform.contentShell: return new ContentShell(url);
125+
case TestPlatform.chrome: return new Chrome(url);
126+
case TestPlatform.phantomJS: return new PhantomJS(url);
127+
case TestPlatform.firefox: return new Firefox(url);
128+
case TestPlatform.safari: return new Safari(url);
129+
case TestPlatform.internetExplorer: return new InternetExplorer(url);
130+
default:
131+
throw new ArgumentError("$browser is not a browser.");
132+
}
133+
}
64134

65135
/// Creates a new BrowserManager that communicates with [browser] over
66136
/// [webSocket].
67-
BrowserManager(this.browser, CompatibleWebSocket webSocket)
137+
BrowserManager._(this._browser, this._platform, CompatibleWebSocket webSocket)
68138
: _channel = new MultiChannel(
69139
webSocket.map(JSON.decode),
70140
mapSink(webSocket, JSON.encode)) {
71-
_environment = new _BrowserEnvironment(this);
72-
_channel.stream.listen(_onMessage, onDone: _onDone);
141+
_environment = _loadBrowserEnvironment();
142+
_channel.stream.listen(_onMessage, onDone: close);
143+
}
144+
145+
/// Loads [_BrowserEnvironment].
146+
Future<_BrowserEnvironment> _loadBrowserEnvironment() async {
147+
var observatoryUrl;
148+
if (_platform.isDartVM) observatoryUrl = await _browser.observatoryUrl;
149+
return new _BrowserEnvironment(this, observatoryUrl);
73150
}
74151

75152
/// Tells the browser the load a test suite from the URL [url].
@@ -84,7 +161,7 @@ class BrowserManager {
84161
{StackTraceMapper mapper}) async {
85162
url = url.replace(fragment: Uri.encodeFull(JSON.encode({
86163
"metadata": metadata.serialize(),
87-
"browser": browser.identifier
164+
"browser": _platform.identifier
88165
})));
89166

90167
// The stream may close before emitting a value if the browser is killed
@@ -130,13 +207,14 @@ class BrowserManager {
130207
throw new LoadException(
131208
path,
132209
"Timed out waiting for the test suite to connect on "
133-
"${browser.name}.");
210+
"${_platform.name}.");
134211
});
135212
});
136213

137214
if (response == null) {
138215
closeIframe();
139-
return null;
216+
throw new LoadException(
217+
path, "Connection closed before test suite loaded.");
140218
}
141219

142220
if (response["type"] == "loadException") {
@@ -152,12 +230,12 @@ class BrowserManager {
152230
asyncError.stackTrace);
153231
}
154232

155-
return new RunnerSuite(_environment, response["tests"].map((test) {
233+
return new RunnerSuite(await _environment, response["tests"].map((test) {
156234
var testMetadata = new Metadata.deserialize(test['metadata']);
157235
var testChannel = suiteChannel.virtualChannel(test['channel']);
158236
return new IframeTest(test['name'], testMetadata, testChannel,
159237
mapper: mapper);
160-
}), platform: browser, metadata: metadata, path: path,
238+
}), platform: _platform, metadata: metadata, path: path,
161239
onClose: () => closeIframe());
162240
}
163241

@@ -183,12 +261,15 @@ class BrowserManager {
183261
_pauseCompleter.complete();
184262
}
185263

186-
/// The callback called when the WebSocket is closed.
187-
void _onDone() {
264+
/// Closes the manager and releases any resources it owns, including closing
265+
/// the browser.
266+
Future close() => _closeMemoizer.runOnce(() {
188267
_closed = true;
189268
if (_pauseCompleter != null) _pauseCompleter.complete();
190269
_pauseCompleter = null;
191-
}
270+
return _browser.close();
271+
});
272+
final _closeMemoizer = new AsyncMemoizer();
192273
}
193274

194275
/// An implementation of [Environment] for the browser.
@@ -197,7 +278,9 @@ class BrowserManager {
197278
class _BrowserEnvironment implements Environment {
198279
final BrowserManager _manager;
199280

200-
_BrowserEnvironment(this._manager);
281+
final Uri observatoryUrl;
282+
283+
_BrowserEnvironment(this._manager, this.observatoryUrl);
201284

202285
CancelableFuture displayPause() => _manager._displayPause();
203286
}

lib/src/runner/browser/content_shell.dart

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import '../../utils.dart';
1111
import '../application_exception.dart';
1212
import 'browser.dart';
1313

14+
final _observatoryRegExp = new RegExp(r"^Observatory listening on ([^ ]+)");
15+
1416
/// A class for running an instance of the Dartium content shell.
1517
///
1618
/// Most of the communication with the browser is expected to happen via HTTP,
@@ -21,37 +23,45 @@ import 'browser.dart';
2123
class ContentShell extends Browser {
2224
final name = "Content Shell";
2325

24-
ContentShell(url, {String executable})
25-
: super(() => _startBrowser(url, executable));
26-
27-
/// Starts a new instance of content shell open to the given [url], which may
28-
/// be a [Uri] or a [String].
29-
///
30-
/// If [executable] is passed, it's used as the content shell executable.
31-
/// Otherwise the default executable name for the current OS will be used.
32-
static Future<Process> _startBrowser(url, [String executable]) async {
33-
if (executable == null) executable = _defaultExecutable();
34-
35-
var process = await Process.start(
36-
executable, ["--dump-render-tree", url.toString()],
37-
environment: {"DART_FLAGS": "--checked"});
38-
39-
lineSplitter.bind(process.stderr).listen((line) {
40-
if (line != "[dartToStderr]: Dartium build has expired") return;
41-
42-
// TODO(nweiz): link to dartlang.org once it has download links for
43-
// content shell
44-
// (https://github.com/dart-lang/www.dartlang.org/issues/1164).
45-
throw new ApplicationException(
46-
"You're using an expired content_shell. Upgrade to the latest "
47-
"version:\n"
48-
"http://gsdview.appspot.com/dart-archive/channels/stable/release/"
49-
"latest/dartium/");
50-
});
51-
52-
return process;
26+
final Future<Uri> observatoryUrl;
27+
28+
factory ContentShell(url, {String executable}) {
29+
var completer = new Completer.sync();
30+
return new ContentShell._(() async {
31+
if (executable == null) executable = _defaultExecutable();
32+
33+
var process = await Process.start(
34+
executable, ["--dump-render-tree", url.toString()],
35+
environment: {"DART_FLAGS": "--checked"});
36+
37+
// The first observatory URL emitted is for the empty start page; the
38+
// second is actually for the host page.
39+
completer.complete(lineSplitter.bind(process.stdout).map((line) {
40+
var match = _observatoryRegExp.firstMatch(line);
41+
if (match == null) return null;
42+
return Uri.parse(match[1]);
43+
}).where((uri) => uri != null).first);
44+
45+
lineSplitter.bind(process.stderr).listen((line) {
46+
if (line != "[dartToStderr]: Dartium build has expired") return;
47+
48+
// TODO(nweiz): link to dartlang.org once it has download links for
49+
// content shell
50+
// (https://github.com/dart-lang/www.dartlang.org/issues/1164).
51+
throw new ApplicationException(
52+
"You're using an expired content_shell. Upgrade to the latest "
53+
"version:\n"
54+
"http://gsdview.appspot.com/dart-archive/channels/stable/release/"
55+
"latest/dartium/");
56+
});
57+
58+
return process;
59+
}, completer.future);
5360
}
5461

62+
ContentShell._(Future<Process> startBrowser(), this.observatoryUrl)
63+
: super(startBrowser);
64+
5565
/// Return the default executable for the current operating system.
5666
static String _defaultExecutable() =>
5767
Platform.isWindows ? "content_shell.exe" : "content_shell";

0 commit comments

Comments
 (0)