Skip to content

Commit cd82d2b

Browse files
Add option to run browser tests using pub serve in coverage/test tasks
1 parent cf73294 commit cd82d2b

23 files changed

+474
-52
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pubspec.lock
1111

1212
# generated assets in test fixtures
1313
/test/fixtures/coverage/browser/coverage/
14+
/test/fixtures/coverage/browser_needs_pub_serve/coverage/
1415
/test/fixtures/coverage/non_test_file/coverage/
1516
/test/fixtures/coverage/vm/coverage/
1617
/test/fixtures/docs/docs/doc/api/

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,12 @@ configuration from the `config.test` object.
302302
<td><code>['lib/']</code></td>
303303
<td>List of paths to include in the generated coverage report (LCOV and HTML).</td>
304304
</tr>
305+
<tr>
306+
<td><code>pubServe</code></td>
307+
<td><code>bool</code></td>
308+
<td><code>false</code></td>
309+
<td>Whether or not to serve browser tests using a Pub server.</td>
310+
</tr>
305311
</tbody>
306312
</table>
307313

@@ -413,6 +419,12 @@ object.
413419
<td><code>['test/']</code></td>
414420
<td>Unit test locations. Items in this list can be directories and/or files.</td>
415421
</tr>
422+
<tr>
423+
<td><code>pubServe</code></td>
424+
<td><code>bool</code></td>
425+
<td><code>false</code></td>
426+
<td>Whether or not to serve browser tests using a Pub server.</td>
427+
</tr>
416428
</tbody>
417429
</table>
418430
* Individual test files can be executed by appending their path to the end of the command.

lib/src/reporter.dart

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,31 @@ class Reporter {
4444
_log(stdout, message, shout: shout);
4545
}
4646

47+
static String _indent(String lines) {
48+
return ' ' + lines.replaceAll('\n', '\n ');
49+
}
50+
4751
void logGroup(String title,
4852
{String output,
53+
String error,
4954
Stream<String> outputStream,
5055
Stream<String> errorStream}) {
51-
log(colorBlue('\n::: $title'));
52-
if (output != null) {
53-
log('${output.split('\n').join('\n ')}');
54-
return;
55-
}
56+
var formattedTitle = colorBlue('\n::: $title');
5657

57-
if (outputStream != null) {
58-
outputStream.listen((line) {
59-
log(' $line');
58+
if (output != null) {
59+
log(formattedTitle);
60+
log(_indent(output));
61+
} else if (error != null) {
62+
warning(formattedTitle);
63+
warning(_indent(error));
64+
} else {
65+
log(formattedTitle);
66+
67+
outputStream?.listen((line) {
68+
log(_indent(line));
6069
});
61-
}
62-
if (errorStream != null) {
63-
errorStream.listen((line) {
64-
warning(' $line');
70+
errorStream?.listen((line) {
71+
warning(_indent(line));
6572
});
6673
}
6774
}

lib/src/tasks/coverage/api.dart

Lines changed: 103 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import 'package:path/path.dart' as path;
2424
import 'package:dart_dev/src/platform_util/api.dart' as platform_util;
2525
import 'package:dart_dev/src/tasks/coverage/config.dart';
2626
import 'package:dart_dev/src/tasks/coverage/exceptions.dart';
27+
import 'package:dart_dev/src/tasks/serve/api.dart';
2728
import 'package:dart_dev/src/tasks/task.dart';
29+
import 'package:dart_dev/src/tasks/test/config.dart';
2830

2931
const String _dartFilePattern = '.dart';
3032
const String _testFilePattern = '_test.dart';
@@ -85,14 +87,18 @@ class CoverageTask extends Task {
8587
/// [tests] will be searched (recursively) for all files ending in
8688
/// "_test.dart" and all matching files will be run as tests.
8789
///
90+
/// If [pubServe] is true, a Pub server will be automatically started and
91+
/// used to run any browser tests.
92+
///
8893
/// If [html] is true, `genhtml` will be used to generate an HTML report of
8994
/// the collected coverage and the report will be opened.
9095
static CoverageTask start(List<String> tests,
9196
{bool html: defaultHtml,
97+
bool pubServe: defaultPubServe,
9298
String output: defaultOutput,
9399
List<String> reportOn: defaultReportOn}) {
94-
CoverageTask coverage =
95-
new CoverageTask._(tests, reportOn, html: html, output: output);
100+
CoverageTask coverage = new CoverageTask._(tests, reportOn,
101+
pubServe: pubServe, html: html, output: output);
96102
coverage._run();
97103
return coverage;
98104
}
@@ -120,6 +126,10 @@ class CoverageTask extends Task {
120126
/// Whether or not to generate the HTML report.
121127
bool _html = defaultHtml;
122128

129+
/// Whether to automatically start and use a Pub server when running
130+
/// browser tests.
131+
final bool pubServe;
132+
123133
/// File created to run the test in a browser. Need to store it so it can be
124134
/// cleaned up after the test finishes.
125135
File _lastHtmlFile;
@@ -135,7 +145,9 @@ class CoverageTask extends Task {
135145
List<String> _reportOn;
136146

137147
CoverageTask._(List<String> tests, List<String> reportOn,
138-
{bool html: defaultHtml, String output: defaultOutput})
148+
{bool html: defaultHtml,
149+
bool this.pubServe: defaultPubServe,
150+
String output: defaultOutput})
139151
: _html = html,
140152
_outputDirectory = new Directory(output),
141153
_reportOn = reportOn {
@@ -407,45 +419,98 @@ class CoverageTask extends Task {
407419
String _testsPassedPattern = 'All tests passed!';
408420

409421
if (isBrowserTest) {
410-
// Run the test in content-shell.
411-
String executable = 'content_shell';
412-
List args = [htmlFile.path];
413-
_coverageOutput.add('');
414-
_coverageOutput.add('Running test suite ${file.path}');
415-
_coverageOutput.add('$executable ${args.join(' ')}\n');
416-
TaskProcess process =
417-
_lastTestProcess = new TaskProcess('content_shell', args);
422+
PubServeTask pubServeTask;
418423

419-
// Content-shell dumps render tree to stderr, which is where the test
420-
// results will be. The observatory port should be output to stderr as
421-
// well, but it is sometimes malformed. In those cases, the correct
422-
// observatory port is output to stdout. So we listen to both.
423-
int observatoryPort;
424-
process.stdout.listen((line) {
425-
_coverageOutput.add(' $line');
426-
if (line.contains(_observatoryPortPattern)) {
427-
Match m = _observatoryPortPattern.firstMatch(line);
428-
observatoryPort = int.parse(m.group(2));
429-
}
430-
});
431-
await for (String line in process.stderr) {
432-
_coverageOutput.add(' $line');
433-
if (line.contains(_observatoryFailPattern)) {
434-
throw new CoverageTestSuiteException(file.path);
435-
}
436-
if (line.contains(_observatoryPortPattern)) {
437-
Match m = _observatoryPortPattern.firstMatch(line);
438-
observatoryPort = int.parse(m.group(2));
439-
}
440-
if (line.contains(_testsFailedPattern)) {
441-
throw new CoverageTestSuiteException(file.path);
424+
try {
425+
String testPath;
426+
if (pubServe) {
427+
_coverageOutput.add('Starting Pub server...');
428+
429+
// Start `pub serve` on the `test` directory.
430+
pubServeTask = startPubServe(additionalArgs: ['test']);
431+
432+
_coverageOutput.add('::: ${pubServeTask.command}');
433+
String indentLine(String line) => ' $line';
434+
435+
var startupLogFinished = new Completer();
436+
pubServeTask.stdOut
437+
.transform(until(startupLogFinished.future))
438+
.map(indentLine)
439+
.listen(_coverageOutput.add);
440+
pubServeTask.stdErr
441+
.transform(until(startupLogFinished.future))
442+
.map(indentLine)
443+
.listen(_coverageErrorOutput.add);
444+
445+
PubServeInfo serveInfo = await pubServeTask.serveInfos.first;
446+
if (!path.isWithin(serveInfo.directory, htmlFile.path)) {
447+
throw '`pub serve` directory does not contain test file: ${htmlFile.path}';
448+
}
449+
450+
var relativeHtmlPath =
451+
path.relative(htmlFile.path, from: serveInfo.directory);
452+
testPath = 'http://localhost:${serveInfo.port}/$relativeHtmlPath';
453+
454+
startupLogFinished.complete();
455+
pubServeTask.stdOut.map(indentLine).join('\n').then((stdOut) {
456+
if (stdOut.isNotEmpty) {
457+
_coverageOutput
458+
.add('`${pubServeTask.command}` (buffered stdout)');
459+
_coverageOutput.add(stdOut);
460+
}
461+
});
462+
pubServeTask.stdErr.map(indentLine).join('\n').then((stdErr) {
463+
if (stdErr.isNotEmpty) {
464+
_coverageOutput
465+
.add('`${pubServeTask.command}` (buffered stdout)');
466+
_coverageOutput.add(stdErr);
467+
}
468+
});
469+
} else {
470+
testPath = htmlFile.path;
442471
}
443-
if (line.contains(_testsPassedPattern)) {
444-
break;
472+
473+
// Run the test in content-shell.
474+
String executable = 'content_shell';
475+
List args = [testPath];
476+
_coverageOutput.add('');
477+
_coverageOutput.add('Running test suite ${file.path}');
478+
_coverageOutput.add('$executable ${args.join(' ')}\n');
479+
TaskProcess process =
480+
_lastTestProcess = new TaskProcess('content_shell', args);
481+
482+
// Content-shell dumps render tree to stderr, which is where the test
483+
// results will be. The observatory port should be output to stderr as
484+
// well, but it is sometimes malformed. In those cases, the correct
485+
// observatory port is output to stdout. So we listen to both.
486+
int observatoryPort;
487+
process.stdout.listen((line) {
488+
_coverageOutput.add(' $line');
489+
if (line.contains(_observatoryPortPattern)) {
490+
Match m = _observatoryPortPattern.firstMatch(line);
491+
observatoryPort = int.parse(m.group(2));
492+
}
493+
});
494+
await for (String line in process.stderr) {
495+
_coverageOutput.add(' $line');
496+
if (line.contains(_observatoryFailPattern)) {
497+
throw new CoverageTestSuiteException(file.path);
498+
}
499+
if (line.contains(_observatoryPortPattern)) {
500+
Match m = _observatoryPortPattern.firstMatch(line);
501+
observatoryPort = int.parse(m.group(2));
502+
}
503+
if (line.contains(_testsFailedPattern)) {
504+
throw new CoverageTestSuiteException(file.path);
505+
}
506+
if (line.contains(_testsPassedPattern)) {
507+
break;
508+
}
445509
}
510+
return observatoryPort;
511+
} finally {
512+
pubServeTask?.stop();
446513
}
447-
448-
return observatoryPort;
449514
} else {
450515
// Find an open port to observe the Dart VM on.
451516
int port = await getOpenPort();

lib/src/tasks/coverage/cli.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ class CoverageCli extends TaskCli {
4040
negatable: true,
4141
defaultsTo: defaultHtml,
4242
help: 'Generate and open an HTML report.')
43+
..addFlag('pub-serve',
44+
negatable: true,
45+
defaultsTo: defaultPubServe,
46+
help: 'Serves browser tests using a Pub server.')
4347
..addFlag('open',
4448
negatable: true,
4549
defaultsTo: true,
@@ -61,6 +65,8 @@ class CoverageCli extends TaskCli {
6165
}
6266

6367
bool html = TaskCli.valueOf('html', parsedArgs, config.coverage.html);
68+
bool pubServe =
69+
TaskCli.valueOf('pub-serve', parsedArgs, config.coverage.pubServe);
6470
bool open = TaskCli.valueOf('open', parsedArgs, true);
6571

6672
List<String> tests = [];
@@ -85,6 +91,7 @@ class CoverageCli extends TaskCli {
8591
try {
8692
CoverageTask task = CoverageTask.start(tests,
8793
html: html,
94+
pubServe: pubServe,
8895
output: config.coverage.output,
8996
reportOn: config.coverage.reportOn);
9097
reporter.logGroup('Collecting coverage',

lib/src/tasks/coverage/config.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
library dart_dev.src.tasks.coverage.config;
1616

1717
import 'package:dart_dev/src/tasks/config.dart';
18+
import 'package:dart_dev/src/tasks/test/config.dart';
1819

1920
const bool defaultHtml = true;
2021
const String defaultOutput = 'coverage/';
2122
const List<String> defaultReportOn = const ['lib/'];
2223

2324
class CoverageConfig extends TaskConfig {
2425
bool html = defaultHtml;
26+
bool pubServe = defaultPubServe;
2527
String output = defaultOutput;
2628
List<String> reportOn = defaultReportOn;
2729
}

0 commit comments

Comments
 (0)