Skip to content

Commit a67318c

Browse files
committed
Support re-running tests while debugging.
When the browser is paused at a breakpoint, the runner will now open a command-line console with a "restart" command that will restart the current test. This currently has no tests, due to dart-lang/sdk#25369 and the lack of a programmatic API for interacting with Observatory. I've tested it reasonably thoroughly by hand, but it's more likely than the average feature to have lurking bugs. Closes #335 [email protected] Review URL: https://codereview.chromium.org//1561073003 .
1 parent a99ce0f commit a67318c

File tree

10 files changed

+404
-82
lines changed

10 files changed

+404
-82
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## 0.12.7
22

3+
* Add the ability to re-run tests while debugging. When the browser is paused at
4+
a breakpoint, the test runner will open an interactive console on the command
5+
line that can be used to restart the test.
6+
37
* Add the ability to tag tests. Tests with specific tags may be run by passing
48
the `--tags` command-line argument, or excluded by passing the
59
`--exclude-tags` parameter.
@@ -11,7 +15,7 @@
1115
[focusing]: http://jasmine.github.io/2.1/focused_specs.html
1216
[issue 16]: https://github.com/dart-lang/test/issues/16
1317

14-
## 0.12.6+1
18+
## 0.12.6+2
1519

1620
* Declare compatibility with `http_parser` 2.0.0.
1721

lib/src/backend/live_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ abstract class LiveTest {
115115
return test.name.substring(group.name.length + 1);
116116
}
117117

118+
/// Loads a copy of this [LiveTest] that's able to be run again.
119+
LiveTest copy() => test.load(suite, groups: groups);
120+
118121
/// Signals that this test should start running as soon as possible.
119122
///
120123
/// A test may not start running immediately for various reasons specific to

lib/src/runner.dart

Lines changed: 10 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'backend/test.dart';
1616
import 'backend/test_platform.dart';
1717
import 'runner/application_exception.dart';
1818
import 'runner/configuration.dart';
19+
import 'runner/debugger.dart';
1920
import 'runner/engine.dart';
2021
import 'runner/load_exception.dart';
2122
import 'runner/load_suite.dart';
@@ -24,7 +25,6 @@ import 'runner/reporter.dart';
2425
import 'runner/reporter/compact.dart';
2526
import 'runner/reporter/expanded.dart';
2627
import 'runner/reporter/json.dart';
27-
import 'runner/runner_suite.dart';
2828
import 'util/io.dart';
2929
import 'utils.dart';
3030

@@ -56,6 +56,11 @@ class Runner {
5656
/// on multiple platforms.
5757
final _tagWarningSuites = new Set<String>();
5858

59+
/// The current debug operation, if any.
60+
///
61+
/// This is stored so that we can cancel it when the runner is closed.
62+
CancelableOperation _debugOperation;
63+
5964
/// The memoizer for ensuring [close] only runs once.
6065
final _closeMemo = new AsyncMemoizer();
6166
bool get _closed => _closeMemo.hasRun;
@@ -156,6 +161,8 @@ class Runner {
156161
});
157162
}
158163

164+
if (_debugOperation != null) await _debugOperation.cancel();
165+
159166
if (_suiteSubscription != null) _suiteSubscription.cancel();
160167
_suiteSubscription = null;
161168

@@ -290,18 +297,8 @@ class Runner {
290297
}
291298

292299
_suiteSubscription = suites.asyncMap((loadSuite) async {
293-
// Make the underlying suite null so that the engine doesn't start running
294-
// it immediately.
295-
_engine.suiteSink.add(loadSuite.changeSuite((_) => null));
296-
297-
var suite = await loadSuite.suite;
298-
if (suite == null) return;
299-
300-
await _pause(suite);
301-
if (_closed) return;
302-
303-
_engine.suiteSink.add(suite);
304-
await _engine.onIdle.first;
300+
_debugOperation = debug(_config, _engine, _reporter, loadSuite);
301+
await _debugOperation.valueOrCancellation();
305302
}).listen(null);
306303

307304
var results = await Future.wait([
@@ -310,63 +307,4 @@ class Runner {
310307
]);
311308
return results.last;
312309
}
313-
314-
/// Pauses the engine and the reporter so that the user can set breakpoints as
315-
/// necessary.
316-
///
317-
/// This is a no-op for test suites that aren't on platforms where debugging
318-
/// is supported.
319-
Future _pause(RunnerSuite suite) async {
320-
if (suite.platform == null) return;
321-
if (suite.platform == TestPlatform.vm) return;
322-
323-
try {
324-
_reporter.pause();
325-
326-
var bold = _config.color ? '\u001b[1m' : '';
327-
var yellow = _config.color ? '\u001b[33m' : '';
328-
var noColor = _config.color ? '\u001b[0m' : '';
329-
print('');
330-
331-
if (suite.platform.isDartVM) {
332-
var url = suite.environment.observatoryUrl;
333-
if (url == null) {
334-
print("${yellow}Observatory URL not found. Make sure you're using "
335-
"${suite.platform.name} 1.11 or later.$noColor");
336-
} else {
337-
print("Observatory URL: $bold$url$noColor");
338-
}
339-
}
340-
341-
if (suite.platform.isHeadless) {
342-
var url = suite.environment.remoteDebuggerUrl;
343-
if (url == null) {
344-
print("${yellow}Remote debugger URL not found.$noColor");
345-
} else {
346-
print("Remote debugger URL: $bold$url$noColor");
347-
}
348-
}
349-
350-
var buffer = new StringBuffer(
351-
"${bold}The test runner is paused.${noColor} ");
352-
if (!suite.platform.isHeadless) {
353-
buffer.write("Open the dev console in ${suite.platform} ");
354-
} else {
355-
buffer.write("Open the remote debugger ");
356-
}
357-
if (suite.platform.isDartVM) buffer.write("or the Observatory ");
358-
359-
buffer.write("and set breakpoints. Once you're finished, return to this "
360-
"terminal and press Enter.");
361-
362-
print(wordWrap(buffer.toString()));
363-
364-
await inCompletionOrder([
365-
suite.environment.displayPause(),
366-
cancelableNext(stdinLines)
367-
]).first;
368-
} finally {
369-
_reporter.resume();
370-
}
371-
}
372310
}

lib/src/runner/browser/iframe_test.dart

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import '../../backend/state.dart';
1313
import '../../backend/suite.dart';
1414
import '../../backend/test.dart';
1515
import '../../backend/test_platform.dart';
16+
import '../../utils.dart';
1617
import '../../util/multi_channel.dart';
1718
import '../../util/remote_exception.dart';
1819
import '../../util/stack_trace_mapper.dart';
@@ -63,13 +64,27 @@ class IframeTest extends Test {
6364
assert(message['type'] == 'complete');
6465
controller.completer.complete();
6566
}
67+
}, onDone: () {
68+
// When the test channel closes—presumably becuase the browser
69+
// closed—mark the test as complete no matter what.
70+
if (controller.completer.isCompleted) return;
71+
controller.completer.complete();
6672
});
6773
}, () {
68-
// Ignore all future messages from the test and complete it immediately.
69-
// We don't need to tell it to run its tear-down because there's nothing a
70-
// browser test needs to clean up on the file system anyway.
71-
testChannel.sink.close();
72-
if (!controller.completer.isCompleted) controller.completer.complete();
74+
// If the test has finished running, just disconnect the channel.
75+
if (controller.completer.isCompleted) {
76+
testChannel.sink.close();
77+
return;
78+
}
79+
80+
invoke(() async {
81+
// If the test is still running, send it a message telling it to shut
82+
// down ASAP. This causes the [Invoker] to eagerly throw exceptions
83+
// whenever the test touches it.
84+
testChannel.sink.add({'command': 'close'});
85+
await controller.completer.future;
86+
testChannel.sink.close();
87+
});
7388
}, groups: groups);
7489
return controller.liveTest;
7590
}

lib/src/runner/console.dart

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) 2016, 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.console;
6+
7+
import 'dart:io';
8+
import 'dart:math' as math;
9+
10+
import 'package:async/async.dart';
11+
12+
import '../util/io.dart';
13+
import '../utils.dart';
14+
15+
/// An interactive console for taking user commands.
16+
class Console {
17+
/// The registered commands.
18+
final _commands = <String, _Command>{};
19+
20+
/// The pending next line of standard input, if we're waiting on one.
21+
CancelableOperation _nextLine;
22+
23+
/// Whether the console is currently running.
24+
bool _running = false;
25+
26+
/// The terminal escape for red text, or the empty string if this is Windows
27+
/// or not outputting to a terminal.
28+
final String _red;
29+
30+
/// The terminal escape for bold text, or the empty string if this is
31+
/// Windows or not outputting to a terminal.
32+
final String _bold;
33+
34+
/// The terminal escape for removing test coloring, or the empty string if
35+
/// this is Windows or not outputting to a terminal.
36+
final String _noColor;
37+
38+
/// Creates a new [Console].
39+
///
40+
/// If [color] is true, this uses Unix terminal colors.
41+
Console({bool color: true})
42+
: _red = color ? '\u001b[31m' : '',
43+
_bold = color ? '\u001b[1m' : '',
44+
_noColor = color ? '\u001b[0m' : '' {
45+
registerCommand("help", "Displays this help information.", _displayHelp);
46+
}
47+
48+
/// Registers a command to be run whenever the user types [name].
49+
///
50+
/// The [description] should be a one-line description of the command to print
51+
/// in the help output. The [body] callback will be called when the user types
52+
/// the command, and may return a [Future].
53+
void registerCommand(String name, String description, body()) {
54+
if (_commands.containsKey(name)) {
55+
throw new ArgumentError(
56+
'The console already has a command named "$name".');
57+
}
58+
59+
_commands[name] = new _Command(name, description, body);
60+
}
61+
62+
/// Starts running the console.
63+
///
64+
/// This prints the initial prompt and loops while waiting for user input.
65+
void start() {
66+
_running = true;
67+
invoke(() async {
68+
while (_running) {
69+
stdout.write("> ");
70+
_nextLine = cancelableNext(stdinLines);
71+
var commandName = await _nextLine.value;
72+
_nextLine = null;
73+
74+
var command = _commands[commandName];
75+
if (command == null) {
76+
stderr.writeln(
77+
"${_red}Unknown command $_bold$commandName$_noColor$_red."
78+
"$_noColor");
79+
} else {
80+
await command.body();
81+
}
82+
}
83+
});
84+
}
85+
86+
/// Stops the console running.
87+
void stop() {
88+
_running = false;
89+
if (_nextLine != null) {
90+
stdout.writeln();
91+
_nextLine.cancel();
92+
}
93+
}
94+
95+
/// Displays the help info for the console commands.
96+
void _displayHelp() {
97+
var maxCommandLength = _commands.values
98+
.map((command) => command.name.length)
99+
.reduce(math.max);
100+
101+
for (var command in _commands.values) {
102+
var name = command.name.padRight(maxCommandLength + 4);
103+
print("$_bold$name$_noColor${command.description}");
104+
}
105+
}
106+
}
107+
108+
/// An individual console command.
109+
class _Command {
110+
/// The name of the command.
111+
final String name;
112+
113+
/// The single-line description of the command.
114+
final String description;
115+
116+
/// The callback to run when the command is invoked.
117+
final Function body;
118+
119+
_Command(this.name, this.description, this.body);
120+
}

0 commit comments

Comments
 (0)