Skip to content

Commit 71bfbdc

Browse files
committed
Add RunnerSuite.{on,is}Debugging properties.
These is useful for #335. It will allow the test runner to detect when it can pause normal command-line output and present an interface for re-running tests. [email protected] Review URL: https://codereview.chromium.org//1422963004 .
1 parent e4f82b3 commit 71bfbdc

File tree

5 files changed

+263
-104
lines changed

5 files changed

+263
-104
lines changed

lib/src/runner/browser/browser_manager.dart

Lines changed: 10 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,23 @@ import 'package:async/async.dart';
1111
import 'package:http_parser/http_parser.dart';
1212
import 'package:pool/pool.dart';
1313

14-
import '../../backend/group.dart';
1514
import '../../backend/metadata.dart';
16-
import '../../backend/test.dart';
1715
import '../../backend/test_platform.dart';
1816
import '../../util/multi_channel.dart';
19-
import '../../util/remote_exception.dart';
2017
import '../../util/stack_trace_mapper.dart';
2118
import '../../utils.dart';
2219
import '../application_exception.dart';
2320
import '../environment.dart';
24-
import '../load_exception.dart';
2521
import '../runner_suite.dart';
2622
import 'browser.dart';
2723
import 'chrome.dart';
2824
import 'content_shell.dart';
2925
import 'dartium.dart';
3026
import 'firefox.dart';
31-
import 'iframe_test.dart';
3227
import 'internet_explorer.dart';
3328
import 'phantom_js.dart';
3429
import 'safari.dart';
30+
import 'suite.dart';
3531

3632
/// A class that manages the connection to a single running browser.
3733
///
@@ -180,104 +176,32 @@ class BrowserManager {
180176
// prematurely (e.g. via Control-C).
181177
var suiteVirtualChannel = _channel.virtualChannel();
182178
var suiteId = _suiteId++;
183-
var suiteChannel;
184179

185180
closeIframe() {
186181
if (_closed) return;
187-
suiteChannel.sink.close();
188182
_channel.sink.add({
189183
"command": "closeSuite",
190184
"id": suiteId
191185
});
192186
}
193187

194-
var response = await _pool.withResource(() {
188+
return await _pool.withResource(() async {
195189
_channel.sink.add({
196190
"command": "loadSuite",
197191
"url": url.toString(),
198192
"id": suiteId,
199193
"channel": suiteVirtualChannel.id
200194
});
201195

202-
// Create a nested MultiChannel because the iframe will be using a channel
203-
// wrapped within the host's channel.
204-
suiteChannel = new MultiChannel(
205-
suiteVirtualChannel.stream, suiteVirtualChannel.sink);
206-
207-
var completer = new Completer();
208-
suiteChannel.stream.listen((response) {
209-
if (response["type"] == "print") {
210-
print(response["line"]);
211-
} else {
212-
completer.complete(response);
213-
}
214-
}, onDone: () {
215-
if (!completer.isCompleted) completer.complete();
216-
});
217-
218-
return completer.future.timeout(new Duration(minutes: 1), onTimeout: () {
219-
throw new LoadException(
220-
path,
221-
"Timed out waiting for the test suite to connect on "
222-
"${_platform.name}.");
223-
});
224-
});
225-
226-
if (response == null) {
227-
closeIframe();
228-
throw new LoadException(
229-
path, "Connection closed before test suite loaded.");
230-
}
231-
232-
if (response["type"] == "loadException") {
233-
closeIframe();
234-
throw new LoadException(path, response["message"]);
235-
}
236-
237-
if (response["type"] == "error") {
238-
closeIframe();
239-
var asyncError = RemoteException.deserialize(response["error"]);
240-
await new Future.error(
241-
new LoadException(path, asyncError.error),
242-
asyncError.stackTrace);
243-
}
244-
245-
return new RunnerSuite(
246-
await _environment,
247-
_deserializeGroup(suiteChannel, mapper, response["root"]),
248-
platform: _platform,
249-
path: path,
250-
onClose: () => closeIframe());
251-
}
252-
253-
/// Deserializes [group] into a concrete [Group] class.
254-
Group _deserializeGroup(MultiChannel suiteChannel,
255-
StackTraceMapper mapper, Map group) {
256-
var metadata = new Metadata.deserialize(group['metadata']);
257-
return new Group(group['name'], group['entries'].map((entry) {
258-
if (entry['type'] == 'group') {
259-
return _deserializeGroup(suiteChannel, mapper, entry);
196+
try {
197+
return await loadBrowserSuite(
198+
suiteVirtualChannel, await _environment, path,
199+
mapper: mapper, platform: _platform, onClose: () => closeIframe());
200+
} catch (_) {
201+
closeIframe();
202+
rethrow;
260203
}
261-
262-
return _deserializeTest(suiteChannel, mapper, entry);
263-
}),
264-
metadata: metadata,
265-
setUpAll: _deserializeTest(suiteChannel, mapper, group['setUpAll']),
266-
tearDownAll:
267-
_deserializeTest(suiteChannel, mapper, group['tearDownAll']));
268-
}
269-
270-
/// Deserializes [test] into a concrete [Test] class.
271-
///
272-
/// Returns `null` if [test] is `null`.
273-
Test _deserializeTest(MultiChannel suiteChannel, StackTraceMapper mapper,
274-
Map test) {
275-
if (test == null) return null;
276-
277-
var metadata = new Metadata.deserialize(test['metadata']);
278-
var testChannel = suiteChannel.virtualChannel(test['channel']);
279-
return new IframeTest(test['name'], metadata, testChannel,
280-
mapper: mapper);
204+
});
281205
}
282206

283207
/// An implementation of [Environment.displayPause].

lib/src/runner/browser/iframe_listener.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ class IframeListener {
3939
static Future start(Function getMain()) async {
4040
var channel = _postMessageChannel();
4141

42+
// Send periodic pings to the test runner so it can know when the suite is
43+
// paused for debugging.
44+
new Timer.periodic(new Duration(seconds: 1),
45+
(_) => channel.sink.add({"type": "ping"}));
46+
4247
var main;
4348
try {
4449
main = getMain();

lib/src/runner/browser/suite.dart

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
library test.runner.browser.suite;
6+
7+
import 'dart:async';
8+
9+
import 'package:async/async.dart';
10+
11+
import '../../backend/group.dart';
12+
import '../../backend/metadata.dart';
13+
import '../../backend/test.dart';
14+
import '../../backend/test_platform.dart';
15+
import '../../util/multi_channel.dart';
16+
import '../../util/remote_exception.dart';
17+
import '../../util/stack_trace_mapper.dart';
18+
import '../../util/stream_channel.dart';
19+
import '../../utils.dart';
20+
import '../environment.dart';
21+
import '../load_exception.dart';
22+
import '../runner_suite.dart';
23+
import 'iframe_test.dart';
24+
25+
/// Loads a [RunnerSuite] for a browser.
26+
///
27+
/// [channel] should connect to the iframe containing the suite, which should
28+
/// eventually emit a message containing the suite's test information.
29+
/// [environment], [path], [platform], and [onClose] are passed to the
30+
/// [RunnerSuite]. If passed, [mapper] is used to reformat the test's stack
31+
/// traces.
32+
Future<RunnerSuite> loadBrowserSuite(StreamChannel channel,
33+
Environment environment, String path, {StackTraceMapper mapper,
34+
TestPlatform platform, AsyncFunction onClose}) async {
35+
// The controller for the returned suite. This is set once we've loaded the
36+
// information about the tests in the suite.
37+
var controller;
38+
39+
// A timer that's reset whenever we receive a message from the browser.
40+
// Because the browser stops running code when the user is actively debugging,
41+
// this lets us detect whether they're debugging reasonably accurately.
42+
//
43+
// The duration should be short enough that the debugging console is open as
44+
// soon as the user is done setting breakpoints, but long enough that a test
45+
// doing a lot of synchronous work doesn't trigger a false positive.
46+
//
47+
// Start this canceled because we don't want it to start ticking until we get
48+
// some response from the iframe.
49+
var timer = new RestartableTimer(new Duration(seconds: 3), () {
50+
controller.setDebugging(true);
51+
})..cancel();
52+
53+
// Even though [channel] is probably a [MultiChannel] already, create a
54+
// nested MultiChannel because the iframe will be using a channel wrapped
55+
// within the host's channel.
56+
var suiteChannel = new MultiChannel(channel.stream.map((message) {
57+
// Whenever we get a message, no matter which child channel it's for, we the
58+
// browser is still running code which means the using isn't debugging.
59+
if (controller != null) {
60+
timer.reset();
61+
controller.setDebugging(false);
62+
}
63+
64+
return message;
65+
}), channel.sink);
66+
67+
var response = await _getResponse(suiteChannel.stream)
68+
.timeout(new Duration(minutes: 1), onTimeout: () {
69+
suiteChannel.sink.close();
70+
throw new LoadException(
71+
path,
72+
"Timed out waiting for the test suite to connect.");
73+
});
74+
75+
try {
76+
_validateResponse(path, response);
77+
} catch (_) {
78+
suiteChannel.sink.close();
79+
rethrow;
80+
}
81+
82+
controller = new RunnerSuiteController(environment,
83+
_deserializeGroup(suiteChannel, response["root"], mapper),
84+
platform: platform, path: path,
85+
onClose: () {
86+
suiteChannel.sink.close();
87+
timer.cancel();
88+
return onClose == null ? null : onClose();
89+
});
90+
91+
// Start the debugging timer counting down.
92+
timer.reset();
93+
return controller.suite;
94+
}
95+
96+
/// Listens for responses from the iframe on [stream].
97+
///
98+
/// Returns the serialized representation of the the root group for the suite,
99+
/// or a response indicating that an error occurred.
100+
Future<Map> _getResponse(Stream stream) {
101+
var completer = new Completer();
102+
stream.listen((response) {
103+
if (response["type"] == "print") {
104+
print(response["line"]);
105+
} else if (response["type"] != "ping") {
106+
completer.complete(response);
107+
}
108+
}, onDone: () {
109+
if (!completer.isCompleted) completer.complete();
110+
});
111+
112+
return completer.future;
113+
}
114+
115+
/// Throws an error encoded in [response], if there is one.
116+
///
117+
/// [path] is used for the error's metadata.
118+
Future _validateResponse(String path, Map response) {
119+
if (response == null) {
120+
throw new LoadException(
121+
path, "Connection closed before test suite loaded.");
122+
}
123+
124+
if (response["type"] == "loadException") {
125+
throw new LoadException(path, response["message"]);
126+
}
127+
128+
if (response["type"] == "error") {
129+
var asyncError = RemoteException.deserialize(response["error"]);
130+
return new Future.error(
131+
new LoadException(path, asyncError.error),
132+
asyncError.stackTrace);
133+
}
134+
135+
return new Future.value();
136+
}
137+
138+
/// Deserializes [group] into a concrete [Group] class.
139+
Group _deserializeGroup(MultiChannel suiteChannel, Map group,
140+
[StackTraceMapper mapper]) {
141+
var metadata = new Metadata.deserialize(group['metadata']);
142+
return new Group(group['name'], group['entries'].map((entry) {
143+
if (entry['type'] == 'group') {
144+
return _deserializeGroup(suiteChannel, entry, mapper);
145+
}
146+
147+
return _deserializeTest(suiteChannel, entry, mapper);
148+
}),
149+
metadata: metadata,
150+
setUpAll: _deserializeTest(suiteChannel, group['setUpAll'], mapper),
151+
tearDownAll:
152+
_deserializeTest(suiteChannel, group['tearDownAll'], mapper));
153+
}
154+
155+
/// Deserializes [test] into a concrete [Test] class.
156+
///
157+
/// Returns `null` if [test] is `null`.
158+
Test _deserializeTest(MultiChannel suiteChannel, Map test,
159+
[StackTraceMapper mapper]) {
160+
if (test == null) return null;
161+
162+
var metadata = new Metadata.deserialize(test['metadata']);
163+
var testChannel = suiteChannel.virtualChannel(test['channel']);
164+
return new IframeTest(test['name'], metadata, testChannel,
165+
mapper: mapper);
166+
}

lib/src/runner/load_suite.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../../test.dart';
1212
import '../backend/group.dart';
1313
import '../backend/invoker.dart';
1414
import '../backend/metadata.dart';
15+
import '../backend/suite.dart';
1516
import '../backend/test.dart';
1617
import '../backend/test_platform.dart';
1718
import '../utils.dart';
@@ -33,7 +34,11 @@ import 'vm/environment.dart';
3334
/// a normal test body, this logic isn't run until [LiveTest.run] is called. The
3435
/// suite itself is returned by [suite] once it's avaialble, but any errors or
3536
/// prints will be emitted through the running [LiveTest].
36-
class LoadSuite extends RunnerSuite {
37+
class LoadSuite extends Suite implements RunnerSuite {
38+
final environment = const VMEnvironment();
39+
final isDebugging = false;
40+
final onDebugging = new StreamController<bool>().stream;
41+
3742
/// A future that completes to the loaded suite once the suite's test has been
3843
/// run and completed successfully.
3944
///
@@ -108,15 +113,15 @@ class LoadSuite extends RunnerSuite {
108113
}
109114

110115
LoadSuite._(String name, void body(), this.suite, {TestPlatform platform})
111-
: super(const VMEnvironment(), new Group.root([
116+
: super(new Group.root([
112117
new LocalTest(name,
113118
new Metadata(timeout: new Timeout(new Duration(minutes: 5))),
114119
body)
115120
]), platform: platform);
116121

117122
/// A constructor used by [changeSuite].
118123
LoadSuite._changeSuite(LoadSuite old, Future<RunnerSuite> this.suite)
119-
: super(const VMEnvironment(), old.group, platform: old.platform);
124+
: super(old.group, platform: old.platform);
120125

121126
/// Creates a new [LoadSuite] that's identical to this one, but that
122127
/// transforms [suite] once it's loaded.
@@ -144,4 +149,6 @@ class LoadSuite extends RunnerSuite {
144149
await new Future.error(error.error, error.stackTrace);
145150
throw 'unreachable';
146151
}
152+
153+
Future close() async {}
147154
}

0 commit comments

Comments
 (0)