OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library test.runner; | 5 library test.runner; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:io'; | 8 import 'dart:io'; |
9 | 9 |
10 import 'package:async/async.dart'; | 10 import 'package:async/async.dart'; |
11 | 11 |
| 12 import 'backend/group.dart'; |
| 13 import 'backend/group_entry.dart'; |
| 14 import 'backend/suite.dart'; |
| 15 import 'backend/test.dart'; |
12 import 'backend/test_platform.dart'; | 16 import 'backend/test_platform.dart'; |
13 import 'runner/application_exception.dart'; | 17 import 'runner/application_exception.dart'; |
14 import 'runner/configuration.dart'; | 18 import 'runner/configuration.dart'; |
15 import 'runner/engine.dart'; | 19 import 'runner/engine.dart'; |
16 import 'runner/load_exception.dart'; | 20 import 'runner/load_exception.dart'; |
17 import 'runner/load_suite.dart'; | 21 import 'runner/load_suite.dart'; |
18 import 'runner/loader.dart'; | 22 import 'runner/loader.dart'; |
19 import 'runner/reporter.dart'; | 23 import 'runner/reporter.dart'; |
20 import 'runner/reporter/compact.dart'; | 24 import 'runner/reporter/compact.dart'; |
21 import 'runner/reporter/expanded.dart'; | 25 import 'runner/reporter/expanded.dart'; |
(...skipping 16 matching lines...) Expand all Loading... |
38 | 42 |
39 /// The engine that runs the test suites. | 43 /// The engine that runs the test suites. |
40 final Engine _engine; | 44 final Engine _engine; |
41 | 45 |
42 /// The reporter that's emitting the test runner's results. | 46 /// The reporter that's emitting the test runner's results. |
43 final Reporter _reporter; | 47 final Reporter _reporter; |
44 | 48 |
45 /// The subscription to the stream returned by [_loadSuites]. | 49 /// The subscription to the stream returned by [_loadSuites]. |
46 StreamSubscription _suiteSubscription; | 50 StreamSubscription _suiteSubscription; |
47 | 51 |
| 52 /// The set of suite paths for which [_warnForUnknownTags] has already been |
| 53 /// called. |
| 54 /// |
| 55 /// This is used to avoid printing duplicate warnings when a suite is loaded |
| 56 /// on multiple platforms. |
| 57 final _tagWarningSuites = new Set<String>(); |
| 58 |
48 /// The memoizer for ensuring [close] only runs once. | 59 /// The memoizer for ensuring [close] only runs once. |
49 final _closeMemo = new AsyncMemoizer(); | 60 final _closeMemo = new AsyncMemoizer(); |
50 bool get _closed => _closeMemo.hasRun; | 61 bool get _closed => _closeMemo.hasRun; |
51 | 62 |
52 /// Creates a new runner based on [configuration]. | 63 /// Creates a new runner based on [configuration]. |
53 factory Runner(Configuration config) { | 64 factory Runner(Configuration config) { |
54 var loader = new Loader(config); | 65 var loader = new Loader(config); |
55 var engine = new Engine(concurrency: config.concurrency); | 66 var engine = new Engine(concurrency: config.concurrency); |
56 | 67 |
57 var reporter; | 68 var reporter; |
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
163 return mergeStreams(_config.paths.map((path) { | 174 return mergeStreams(_config.paths.map((path) { |
164 if (new Directory(path).existsSync()) return _loader.loadDir(path); | 175 if (new Directory(path).existsSync()) return _loader.loadDir(path); |
165 if (new File(path).existsSync()) return _loader.loadFile(path); | 176 if (new File(path).existsSync()) return _loader.loadFile(path); |
166 | 177 |
167 return new Stream.fromIterable([ | 178 return new Stream.fromIterable([ |
168 new LoadSuite("loading $path", () => | 179 new LoadSuite("loading $path", () => |
169 throw new LoadException(path, 'Does not exist.')) | 180 throw new LoadException(path, 'Does not exist.')) |
170 ]); | 181 ]); |
171 })).map((loadSuite) { | 182 })).map((loadSuite) { |
172 return loadSuite.changeSuite((suite) { | 183 return loadSuite.changeSuite((suite) { |
| 184 _warnForUnknownTags(suite); |
| 185 |
173 return suite.filter((test) { | 186 return suite.filter((test) { |
174 // Warn if any test has tags that don't appear on the command line. | |
175 // | |
176 // TODO(nweiz): Only print this once per test, even if it's run on | |
177 // multiple runners. | |
178 // | |
179 // TODO(nweiz): If groups or suites are tagged, don't print this for | |
180 // every test they contain. | |
181 // | |
182 // TODO(nweiz): Print this as part of the test's output so it's easy | |
183 // to associate with the correct test. | |
184 var specifiedTags = _config.tags.union(_config.excludeTags); | |
185 var unrecognizedTags = test.metadata.tags.difference(specifiedTags); | |
186 if (unrecognizedTags.isNotEmpty) { | |
187 // Pause the reporter while we print to ensure that we don't | |
188 // interfere with its output. | |
189 _reporter.pause(); | |
190 warn( | |
191 'Unknown ${pluralize('tag', unrecognizedTags.length)} ' | |
192 '${toSentence(unrecognizedTags)} in test "${test.name}".', | |
193 color: _config.color); | |
194 _reporter.resume(); | |
195 } | |
196 | |
197 // Skip any tests that don't match the given pattern. | 187 // Skip any tests that don't match the given pattern. |
198 if (_config.pattern != null && !test.name.contains(_config.pattern)) { | 188 if (_config.pattern != null && !test.name.contains(_config.pattern)) { |
199 return false; | 189 return false; |
200 } | 190 } |
201 | 191 |
202 // If the user provided tags, skip tests that don't match all of them. | 192 // If the user provided tags, skip tests that don't match all of them. |
203 if (!_config.tags.isEmpty && | 193 if (!_config.tags.isEmpty && |
204 !test.metadata.tags.containsAll(_config.tags)) { | 194 !test.metadata.tags.containsAll(_config.tags)) { |
205 return false; | 195 return false; |
206 } | 196 } |
207 | 197 |
208 // Skip tests that do match any tags the user wants to exclude. | 198 // Skip tests that do match any tags the user wants to exclude. |
209 if (_config.excludeTags.intersection(test.metadata.tags).isNotEmpty) { | 199 if (_config.excludeTags.intersection(test.metadata.tags).isNotEmpty) { |
210 return false; | 200 return false; |
211 } | 201 } |
212 | 202 |
213 return true; | 203 return true; |
214 }); | 204 }); |
215 }); | 205 }); |
216 }); | 206 }); |
217 } | 207 } |
218 | 208 |
| 209 /// Prints a warning for any unknown tags referenced in [suite] or its |
| 210 /// children. |
| 211 void _warnForUnknownTags(Suite suite) { |
| 212 if (_tagWarningSuites.contains(suite.path)) return; |
| 213 _tagWarningSuites.add(suite.path); |
| 214 |
| 215 var unknownTags = _collectUnknownTags(suite); |
| 216 if (unknownTags.isEmpty) return; |
| 217 |
| 218 var yellow = _config.color ? '\u001b[33m' : ''; |
| 219 var bold = _config.color ? '\u001b[1m' : ''; |
| 220 var noColor = _config.color ? '\u001b[0m' : ''; |
| 221 |
| 222 var buffer = new StringBuffer() |
| 223 ..write("${yellow}Warning:$noColor ") |
| 224 ..write(unknownTags.length == 1 ? "A tag was " : "Tags were ") |
| 225 ..write("used that ") |
| 226 ..write(unknownTags.length == 1 ? "wasn't " : "weren't ") |
| 227 ..writeln("specified on the command line."); |
| 228 |
| 229 unknownTags.forEach((tag, entries) { |
| 230 buffer.write(" $bold$tag$noColor was used in"); |
| 231 |
| 232 if (entries.length == 1) { |
| 233 buffer.writeln(" ${_entryDescription(entries.single)}"); |
| 234 return; |
| 235 } |
| 236 |
| 237 buffer.write(":"); |
| 238 for (var entry in entries) { |
| 239 buffer.write("\n ${_entryDescription(entry)}"); |
| 240 } |
| 241 buffer.writeln(); |
| 242 }); |
| 243 |
| 244 print(buffer.toString()); |
| 245 } |
| 246 |
| 247 /// Collects all tags used by [suite] or its children that aren't also passed |
| 248 /// on the command line. |
| 249 /// |
| 250 /// This returns a map from tag names to lists of entries that use those tags. |
| 251 Map<String, List<GroupEntry>> _collectUnknownTags(Suite suite) { |
| 252 var knownTags = _config.tags.union(_config.excludeTags); |
| 253 var unknownTags = {}; |
| 254 var currentTags = new Set(); |
| 255 |
| 256 collect(entry) { |
| 257 var newTags = new Set(); |
| 258 for (var unknownTag in entry.metadata.tags.difference(knownTags)) { |
| 259 if (currentTags.contains(unknownTag)) continue; |
| 260 unknownTags.putIfAbsent(unknownTag, () => []).add(entry); |
| 261 newTags.add(unknownTag); |
| 262 } |
| 263 |
| 264 if (entry is! Group) return; |
| 265 |
| 266 currentTags.addAll(newTags); |
| 267 for (var child in entry.entries) { |
| 268 collect(child); |
| 269 } |
| 270 currentTags.removeAll(newTags); |
| 271 } |
| 272 |
| 273 collect(suite.group); |
| 274 return unknownTags; |
| 275 } |
| 276 |
| 277 /// Returns a human-readable description of [entry], including its type. |
| 278 String _entryDescription(GroupEntry entry) { |
| 279 if (entry is Test) return 'the test "${entry.name}"'; |
| 280 if (entry.name != null) return 'the group "${entry.name}"'; |
| 281 return 'the suite itself'; |
| 282 } |
| 283 |
219 /// Loads each suite in [suites] in order, pausing after load for platforms | 284 /// Loads each suite in [suites] in order, pausing after load for platforms |
220 /// that support debugging. | 285 /// that support debugging. |
221 Future<bool> _loadThenPause(Stream<LoadSuite> suites) async { | 286 Future<bool> _loadThenPause(Stream<LoadSuite> suites) async { |
222 if (_config.platforms.contains(TestPlatform.vm)) { | 287 if (_config.platforms.contains(TestPlatform.vm)) { |
223 warn("Debugging is currently unsupported on the Dart VM.", | 288 warn("Debugging is currently unsupported on the Dart VM.", |
224 color: _config.color); | 289 color: _config.color); |
225 } | 290 } |
226 | 291 |
227 _suiteSubscription = suites.asyncMap((loadSuite) async { | 292 _suiteSubscription = suites.asyncMap((loadSuite) async { |
228 // Make the underlying suite null so that the engine doesn't start running | 293 // Make the underlying suite null so that the engine doesn't start running |
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
298 | 363 |
299 await inCompletionOrder([ | 364 await inCompletionOrder([ |
300 suite.environment.displayPause(), | 365 suite.environment.displayPause(), |
301 cancelableNext(stdinLines) | 366 cancelableNext(stdinLines) |
302 ]).first; | 367 ]).first; |
303 } finally { | 368 } finally { |
304 _reporter.resume(); | 369 _reporter.resume(); |
305 } | 370 } |
306 } | 371 } |
307 } | 372 } |
OLD | NEW |