Index: lib/src/runner.dart |
diff --git a/lib/src/runner.dart b/lib/src/runner.dart |
index 7ab303211157a41ec2d73cd53f4c15da15a55ce9..7ff651d13cfcc49cb1e5b1c8507f51bb423bfca8 100644 |
--- a/lib/src/runner.dart |
+++ b/lib/src/runner.dart |
@@ -9,6 +9,10 @@ import 'dart:io'; |
import 'package:async/async.dart'; |
+import 'backend/group.dart'; |
+import 'backend/group_entry.dart'; |
+import 'backend/suite.dart'; |
+import 'backend/test.dart'; |
import 'backend/test_platform.dart'; |
import 'runner/application_exception.dart'; |
import 'runner/configuration.dart'; |
@@ -45,6 +49,13 @@ class Runner { |
/// The subscription to the stream returned by [_loadSuites]. |
StreamSubscription _suiteSubscription; |
+ /// The set of suite paths for which [_warnForUnknownTags] has already been |
+ /// called. |
+ /// |
+ /// This is used to avoid printing duplicate warnings when a suite is loaded |
+ /// on multiple platforms. |
+ final _tagWarningSuites = new Set<String>(); |
+ |
/// The memoizer for ensuring [close] only runs once. |
final _closeMemo = new AsyncMemoizer(); |
bool get _closed => _closeMemo.hasRun; |
@@ -170,30 +181,9 @@ class Runner { |
]); |
})).map((loadSuite) { |
return loadSuite.changeSuite((suite) { |
- return suite.filter((test) { |
- // Warn if any test has tags that don't appear on the command line. |
- // |
- // TODO(nweiz): Only print this once per test, even if it's run on |
- // multiple runners. |
- // |
- // TODO(nweiz): If groups or suites are tagged, don't print this for |
- // every test they contain. |
- // |
- // TODO(nweiz): Print this as part of the test's output so it's easy |
- // to associate with the correct test. |
- var specifiedTags = _config.tags.union(_config.excludeTags); |
- var unrecognizedTags = test.metadata.tags.difference(specifiedTags); |
- if (unrecognizedTags.isNotEmpty) { |
- // Pause the reporter while we print to ensure that we don't |
- // interfere with its output. |
- _reporter.pause(); |
- warn( |
- 'Unknown ${pluralize('tag', unrecognizedTags.length)} ' |
- '${toSentence(unrecognizedTags)} in test "${test.name}".', |
- color: _config.color); |
- _reporter.resume(); |
- } |
+ _warnForUnknownTags(suite); |
+ return suite.filter((test) { |
// Skip any tests that don't match the given pattern. |
if (_config.pattern != null && !test.name.contains(_config.pattern)) { |
return false; |
@@ -216,6 +206,81 @@ class Runner { |
}); |
} |
+ /// Prints a warning for any unknown tags referenced in [suite] or its |
+ /// children. |
+ void _warnForUnknownTags(Suite suite) { |
+ if (_tagWarningSuites.contains(suite.path)) return; |
+ _tagWarningSuites.add(suite.path); |
+ |
+ var unknownTags = _collectUnknownTags(suite); |
+ if (unknownTags.isEmpty) return; |
+ |
+ var yellow = _config.color ? '\u001b[33m' : ''; |
+ var bold = _config.color ? '\u001b[1m' : ''; |
+ var noColor = _config.color ? '\u001b[0m' : ''; |
+ |
+ var buffer = new StringBuffer() |
+ ..write("${yellow}Warning:$noColor ") |
+ ..write(unknownTags.length == 1 ? "A tag was " : "Tags were ") |
+ ..write("used that ") |
+ ..write(unknownTags.length == 1 ? "wasn't " : "weren't ") |
+ ..writeln("specified on the command line."); |
+ |
+ unknownTags.forEach((tag, entries) { |
+ buffer.write(" $bold$tag$noColor was used in"); |
+ |
+ if (entries.length == 1) { |
+ buffer.writeln(" ${_entryDescription(entries.single)}"); |
+ return; |
+ } |
+ |
+ buffer.write(":"); |
+ for (var entry in entries) { |
+ buffer.write("\n ${_entryDescription(entry)}"); |
+ } |
+ buffer.writeln(); |
+ }); |
+ |
+ print(buffer.toString()); |
+ } |
+ |
+ /// Collects all tags used by [suite] or its children that aren't also passed |
+ /// on the command line. |
+ /// |
+ /// This returns a map from tag names to lists of entries that use those tags. |
+ Map<String, List<GroupEntry>> _collectUnknownTags(Suite suite) { |
+ var knownTags = _config.tags.union(_config.excludeTags); |
+ var unknownTags = {}; |
+ var currentTags = new Set(); |
+ |
+ collect(entry) { |
+ var newTags = new Set(); |
+ for (var unknownTag in entry.metadata.tags.difference(knownTags)) { |
+ if (currentTags.contains(unknownTag)) continue; |
+ unknownTags.putIfAbsent(unknownTag, () => []).add(entry); |
+ newTags.add(unknownTag); |
+ } |
+ |
+ if (entry is! Group) return; |
+ |
+ currentTags.addAll(newTags); |
+ for (var child in entry.entries) { |
+ collect(child); |
+ } |
+ currentTags.removeAll(newTags); |
+ } |
+ |
+ collect(suite.group); |
+ return unknownTags; |
+ } |
+ |
+ /// Returns a human-readable description of [entry], including its type. |
+ String _entryDescription(GroupEntry entry) { |
+ if (entry is Test) return 'the test "${entry.name}"'; |
+ if (entry.name != null) return 'the group "${entry.name}"'; |
+ return 'the suite itself'; |
+ } |
+ |
/// Loads each suite in [suites] in order, pausing after load for platforms |
/// that support debugging. |
Future<bool> _loadThenPause(Stream<LoadSuite> suites) async { |