Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
|
ricow1
2014/02/11 14:33:52
2014
kustermann
2014/02/14 11:52:07
Done.
| |
| 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 StatusFileParserTest; | |
|
ricow1
2014/02/11 14:33:52
I find that name a little odd
kustermann
2014/02/14 11:52:07
Done.
| |
| 6 | |
| 7 import "dart:async"; | |
| 8 import "dart:convert" show JSON, UTF8; | |
| 9 import "dart:io"; | |
| 10 import "testing/dart/status_file_parser.dart"; | |
| 11 import "testing/dart/multitest.dart"; | |
|
ricow1
2014/02/11 14:33:52
m<s
kustermann
2014/02/14 11:52:07
Done.
| |
| 12 import "testing/dart/utils.dart" show Path; | |
| 13 import "testing/dart/test_suite.dart" | |
|
ricow1
2014/02/11 14:33:52
t<u
kustermann
2014/02/14 11:52:07
Done.
| |
| 14 show multiHtmlTestGroupRegExp, multiTestRegExp, multiHtmlTestRegExp, | |
| 15 TestUtils; | |
| 16 | |
| 17 // [STATUS_TUPLES] is a list of (suite-name, directory, status-file)-tuples. | |
| 18 final STATUS_TUPLES = [ | |
| 19 ["corelib", "tests/corelib", "tests/corelib/corelib.status"], | |
| 20 ["html", "tests/html", "tests/html/html.status"], | |
| 21 ["isolate", "tests/isolate", "tests/isolate/isolate.status"], | |
| 22 ["json", "tests/json", "tests/json/json.status"], | |
| 23 ["language", "tests/language", "tests/language/language.status"], | |
| 24 ["language", "tests/language", "tests/language/language_analyzer2.status"], | |
| 25 ["language","tests/language", "tests/language/language_analyzer.status"], | |
| 26 ["language","tests/language", "tests/language/language_dart2js.status"], | |
| 27 ["lib", "tests/lib", "tests/lib/lib.status"], | |
| 28 ["standalone", "tests/standalone", "tests/standalone/standalone.status"], | |
| 29 ["pkg", "pkg", "pkg/pkg.status"], | |
| 30 | |
| 31 ["pkgbuild", ".", "pkg/pkgbuild.status"], | |
| 32 ["utils", "tests/utils", "tests/utils/utils.status"], | |
| 33 ["samples", "samples", "samples/samples.status"], | |
| 34 ["analyze_library", "sdk", "tests/lib/analyzer/analyze_library.status"], | |
| 35 | |
| 36 ["dart2js_extra", "tests/compiler/dart2js_extra", | |
| 37 "tests/compiler/dart2js_extra/dart2js_extra.status"], | |
| 38 ["dart2js_native", "tests/compiler/dart2js_native", | |
| 39 "tests/compiler/dart2js_native/dart2js_native.status"], | |
| 40 ["dart2js", "tests/compiler/dart2js", | |
| 41 "tests/compiler/dart2js/dart2js.status"], | |
| 42 | |
| 43 ["pub", "sdk/lib/_internal/pub", "sdk/lib/_internal/pub/pub.status"], | |
| 44 ["benchmark_smoke", "tests/benchmark_smoke", | |
| 45 "tests/benchmark_smoke/benchmark_smoke.status"], | |
| 46 | |
| 47 ["co19", "tests/co19/src", "tests/co19/co19-analyzer2.status"], | |
| 48 ["co19", "tests/co19/src", "tests/co19/co19-analyzer.status"], | |
| 49 ["co19", "tests/co19/src", "tests/co19/co19-dart2dart.status"], | |
| 50 ["co19", "tests/co19/src", "tests/co19/co19-dart2js.status"], | |
| 51 ["co19", "tests/co19/src", "tests/co19/co19-co19.status"], | |
| 52 ["co19", "tests/co19/src", "tests/co19/co19-dartium.status"], | |
| 53 ["co19", "tests/co19/src", "tests/co19/co19-runtime.status"], | |
| 54 ]; | |
|
ricow1
2014/02/11 14:33:52
grouping (i.e., blank lines in the above seems ran
kustermann
2014/02/14 11:52:07
The really bad thing about this is, that this info
| |
| 55 | |
| 56 void main(List<String> args) { | |
| 57 usage() { | |
| 58 print("Usage: ${Platform.executable} <deflake|fix>"); | |
|
ricow1
2014/02/11 14:33:52
fix is not a very descriptive word here
kustermann
2014/02/14 11:52:07
I'll change it to "remove-nonexistent-tests. Feel
| |
| 59 exit(1); | |
| 60 } | |
| 61 | |
| 62 if (args.length == 0) usage(); | |
| 63 | |
|
Bill Hesse
2014/02/13 17:05:08
You could extract the function to run (statusFileD
kustermann
2014/02/14 11:52:07
I thought about it and left it like that because i
| |
| 64 if (args[0] == 'deflake') { | |
| 65 var statusFileDeflaker = new StatusFileDeflaker(); | |
| 66 Future.forEach(STATUS_TUPLES, (List tuple) { | |
| 67 String suiteName = tuple[0]; | |
| 68 String filePath = tuple[2]; | |
| 69 print("Processing $filePath"); | |
| 70 return statusFileDeflaker.deflakeStatusFile(suiteName, filePath); | |
| 71 }); | |
| 72 } else if (args[0] == 'fix') { | |
| 73 var invalidTestFixer = new StatusFileNonExistentTestRemover(); | |
| 74 Future.forEach(STATUS_TUPLES, (List tuple) { | |
| 75 String directory = tuple[1]; | |
| 76 String filePath = tuple[2]; | |
| 77 print("Processing $filePath"); | |
| 78 return invalidTestFixer | |
| 79 .removeNonExistentTestsFromStatusFile(directory, filePath); | |
| 80 }); | |
| 81 } else { | |
| 82 usage(); | |
| 83 } | |
| 84 } | |
| 85 | |
| 86 abstract class StatusFileProcessor { | |
| 87 Future<List<Section>> _readSections(String filePath) { | |
| 88 File file = new File(filePath); | |
| 89 | |
| 90 if (file.existsSync()) { | |
| 91 var completer = new Completer(); | |
| 92 List<Section> sections = new List<Section>(); | |
| 93 | |
| 94 ReadConfigurationInto(new Path(file.path), sections, () { | |
| 95 completer.complete(sections); | |
| 96 }); | |
| 97 return completer.future; | |
| 98 } | |
| 99 return new Future.value([]); | |
| 100 } | |
| 101 } | |
| 102 | |
| 103 class StatusFileNonExistentTestRemover extends StatusFileProcessor { | |
| 104 final MultiTestDetector multiTestDetector = new MultiTestDetector(); | |
| 105 final TestFileLister testFileLister = new TestFileLister(); | |
| 106 | |
| 107 Future removeNonExistentTestsFromStatusFile(String directory, | |
| 108 String filePath) { | |
| 109 return _readSections(filePath).then((List<Section> sections) { | |
| 110 Set<int> invalidLines = _analyzeStatusFile(directory, filePath, sections); | |
| 111 if (invalidLines.length > 0) { | |
| 112 return _writeFixedStatusFile(filePath, invalidLines); | |
| 113 } | |
| 114 return new Future.value(); | |
| 115 }); | |
| 116 } | |
| 117 | |
| 118 bool _doesTestExist(String filePath, | |
|
Bill Hesse
2014/02/13 17:05:08
_testExists is a shorter name.
kustermann
2014/02/14 11:52:07
Done.
| |
| 119 List<String> dartFiles, | |
| 120 String directory, | |
| 121 TestRule rule) { | |
| 122 List<RegExp> getRuleRegex(String name) { | |
| 123 return name.split("/") | |
| 124 .map((name) => new RegExp(name.replaceAll('*', '.*'))) | |
| 125 .toList(); | |
| 126 } | |
| 127 bool matchRegexp(List<RegExp> patterns, String str) { | |
| 128 var parts = str.split("/"); | |
| 129 if (patterns.length > parts.length) { | |
| 130 return false; | |
| 131 } | |
| 132 // NOTE: patterns.length <= parts.length | |
| 133 for (var i = 0; i < patterns.length; i++) { | |
| 134 if (!patterns[i].hasMatch(parts[i])) { | |
| 135 return false; | |
| 136 } | |
| 137 } | |
| 138 return true; | |
| 139 } | |
| 140 | |
| 141 if (rule.name.contains("packages") && filePath.contains("pkg.status")) { | |
|
Bill Hesse
2014/02/13 17:05:08
Add a comment, that fix option does nothing to pkg
kustermann
2014/02/14 11:52:07
Well, that's not what it's doing. It's processing
| |
| 142 return true; | |
| 143 } | |
| 144 | |
| 145 var rulePattern = getRuleRegex(rule.name); | |
| 146 return dartFiles.any((String file) { | |
| 147 var relative = new Path(file).relativeTo(new Path(directory)).toString(); | |
| 148 for (int splitIndex = 0; splitIndex < rulePattern.length; splitIndex++) { | |
| 149 // Construct a pattern for the file name. | |
| 150 var filePattern = new List(); | |
| 151 for (var i = 0; i <= splitIndex; i++) { | |
| 152 filePattern.add(rulePattern[i]); | |
| 153 } | |
| 154 | |
| 155 // Construct a pattern for the multitest name. | |
| 156 var multitestPattern = new List(); | |
| 157 for (var i = splitIndex + 1; i < rulePattern.length; i++) { | |
| 158 multitestPattern.add(rulePattern[i]); | |
| 159 } | |
| 160 | |
| 161 if (matchRegexp(filePattern, relative)) { | |
|
Bill Hesse
2014/02/13 17:05:08
There is a strange semantics here, that filePatter
| |
| 162 // Could be a normal test | |
| 163 if (multitestPattern.length == 0) { | |
| 164 return true; | |
| 165 } | |
| 166 | |
| 167 // Could be a real multitest. | |
| 168 if (multiTestDetector.getMultitestNamesFromFile(file).any( | |
| 169 (name) => matchRegexp(multitestPattern, name))) { | |
| 170 return true; | |
| 171 } | |
| 172 | |
| 173 // Could be a multi html test. | |
| 174 if (multiTestDetector.getMultiHtmlTests(file).any( | |
| 175 (name) => matchRegexp(multitestPattern, name))) { | |
| 176 return true; | |
| 177 } | |
| 178 } | |
| 179 } | |
| 180 return false; | |
| 181 }); | |
| 182 } | |
| 183 | |
| 184 Set<int> _analyzeStatusFile(String directory, | |
| 185 String filePath, | |
| 186 List<Section> sections) { | |
| 187 var invalidLines = new Set<int>(); | |
| 188 var dartFiles = testFileLister.listTestFiles(directory); | |
| 189 for (var section in sections) { | |
| 190 for (var rule in section.testRules) { | |
| 191 if (!_doesTestExist(filePath, dartFiles, directory, rule)) { | |
| 192 print("Invalid rule: ${rule.name} in file $filePath:${rule.lineNr}"); | |
| 193 invalidLines.add(rule.lineNr); | |
| 194 } | |
| 195 } | |
| 196 } | |
| 197 return invalidLines; | |
| 198 } | |
| 199 | |
| 200 _writeFixedStatusFile(String filePath, Set<int> invalidLines) { | |
|
Bill Hesse
2014/02/13 17:05:08
statusFilePath
kustermann
2014/02/14 11:52:07
Done.
| |
| 201 var lines = new File(filePath).readAsLinesSync(); | |
| 202 var outputLines = <String>[]; | |
| 203 for (int i = 0; i < lines.length; i++) { | |
| 204 if (!invalidLines.contains(i + 1)) { | |
|
Bill Hesse
2014/02/13 17:05:08
// The status file parser numbers lines starting w
kustermann
2014/02/14 11:52:07
Done.
| |
| 205 outputLines.add(lines[i]); | |
| 206 } | |
| 207 } | |
| 208 var outputFile = new File("$filePath.fixed"); | |
| 209 outputFile.writeAsStringSync(outputLines.join("\n")); | |
|
Bill Hesse
2014/02/13 17:05:08
Writing the output can happen asynchronously, whil
kustermann
2014/02/14 11:52:07
But then there is a future and nobody waits for it
| |
| 210 } | |
| 211 } | |
| 212 | |
| 213 class StatusFileDeflaker extends StatusFileProcessor { | |
| 214 TestOutcomeFetcher _testOutcomeFetcher = new TestOutcomeFetcher(); | |
| 215 | |
| 216 Future deflakeStatusFile(String suiteName, String filePath) { | |
| 217 return _readSections(filePath).then((List<Section> sections) { | |
| 218 var fixedLines = new Map<int, String>(); | |
|
Bill Hesse
2014/02/13 17:05:08
Dead variable, hidden by parameter name in closure
kustermann
2014/02/14 11:52:07
Dead and left-over variable :)
[I moved it into _g
| |
| 219 return _generatedDeflakedLines(suiteName, sections) | |
| 220 .then((Map<int, String> fixedLines) { | |
| 221 if (fixedLines.length > 0) { | |
| 222 return _writeFixedStatusFile(filePath, fixedLines); | |
| 223 } | |
| 224 }); | |
| 225 }); | |
| 226 } | |
| 227 | |
| 228 Future _generatedDeflakedLines(String suiteName, | |
| 229 List<Section> sections) { | |
| 230 var completer = new Completer(); | |
|
Bill Hesse
2014/02/13 17:05:08
Completer not needed. Just
return Future.forEac
kustermann
2014/02/14 11:52:07
Done.
| |
| 231 var fixedLines = new Map<int, String>(); | |
| 232 Future.forEach(sections, (Section section) { | |
| 233 return Future.forEach(section.testRules, (rule) { | |
| 234 return _maybeFixStatusfileLine(suiteName, section, rule, fixedLines); | |
| 235 }); | |
| 236 }).then((_) => completer.complete(fixedLines)); | |
| 237 return completer.future; | |
| 238 } | |
| 239 | |
| 240 Future _maybeFixStatusfileLine(String suiteName, | |
| 241 Section section, | |
| 242 TestRule rule, | |
| 243 Map<int, String> fixedLines) { | |
| 244 print("Processing ${section.statusFile.location}: ${rule.lineNr}"); | |
| 245 var notedOutcomes = rule.expression | |
| 246 .evaluate({}) | |
|
Bill Hesse
2014/02/13 17:05:08
Comment that none of our status files have express
kustermann
2014/02/14 11:52:07
Done.
| |
| 247 .map((name) => Expectation.byName(name)) | |
| 248 .where((Expectation expectation) => !expectation.isMetaExpectation) | |
| 249 .toSet(); | |
| 250 | |
| 251 if (notedOutcomes.isEmpty) return new Future.value(); | |
| 252 | |
| 253 return _testOutcomeFetcher.outcomesOf(suiteName, section, rule.name) | |
| 254 .then((Set<Expectation> actualOutcomes) { | |
| 255 | |
| 256 var outcomesThatNeverHappend = new Set<Expectation>(); | |
|
Bill Hesse
2014/02/13 17:05:08
Happened
kustermann
2014/02/14 11:52:07
Done.
| |
| 257 for (Expectation notedOutcome in notedOutcomes) { | |
| 258 bool found = false; | |
| 259 for (Expectation actualOutcome in actualOutcomes) { | |
| 260 if (actualOutcome.canBeOutcomeOf(notedOutcome)) { | |
| 261 found = true; | |
| 262 break; | |
| 263 } | |
| 264 } | |
| 265 if (!found) { | |
| 266 outcomesThatNeverHappend.add(notedOutcome); | |
| 267 } | |
| 268 } | |
| 269 | |
| 270 if (outcomesThatNeverHappend.length > 0 && actualOutcomes.length > 0) { | |
| 271 // Print the change to stdout. | |
| 272 print("${rule.name} (${section.statusFile.location}:${rule.lineNr}):"); | |
| 273 print(" Actual outcomes: ${actualOutcomes.toList()}"); | |
| 274 print(" Outcomes in status file: ${notedOutcomes.toList()}"); | |
| 275 print(" Outcomes in status file that never happened : " | |
| 276 "${outcomesThatNeverHappend.toList()}\n"); | |
| 277 | |
| 278 // Build the fixed status file line. | |
| 279 fixedLines[rule.lineNr] = | |
| 280 '${rule.name}: ${actualOutcomes.join(', ')} ' | |
| 281 '# before: ${notedOutcomes.join(', ')} / ' | |
| 282 'never happened: ${outcomesThatNeverHappend.join(', ')}'; | |
| 283 } | |
| 284 }); | |
| 285 } | |
| 286 | |
| 287 _writeFixedStatusFile(String filePath, Map<int, String> fixedLines) { | |
| 288 var lines = new File(filePath).readAsLinesSync(); | |
| 289 var outputLines = <String>[]; | |
| 290 for (int i = 0; i < lines.length; i++) { | |
| 291 if (fixedLines.containsKey(i + 1)) { | |
| 292 outputLines.add(fixedLines[i + 1]); | |
| 293 } else { | |
| 294 outputLines.add(lines[i]); | |
| 295 } | |
| 296 } | |
| 297 var output = outputLines.join("\n"); | |
| 298 var outputFile = new File("$filePath.deflaked"); | |
| 299 outputFile.writeAsStringSync(output); | |
| 300 } | |
| 301 } | |
| 302 | |
| 303 class MultiTestDetector { | |
| 304 final multiTestsCache = new Map<String,List<String>>(); | |
| 305 final multiHtmlTestsCache = new Map<String,List<String>>(); | |
| 306 | |
| 307 List<String> getMultitestNamesFromFile(String file) { | |
| 308 if (multiTestsCache.containsKey(file)) return multiTestsCache[file]; | |
| 309 | |
| 310 var tests = new Map<String, String>(); | |
| 311 var outcomes = new Map<String, Set<String>>(); | |
| 312 if (multiTestRegExp.hasMatch(new File(file).readAsStringSync())) { | |
| 313 ExtractTestsFromMultitest(new Path(file), tests, outcomes); | |
| 314 } | |
| 315 multiTestsCache[file] = tests.keys.toList(); | |
| 316 return multiTestsCache[file]; | |
| 317 } | |
| 318 | |
| 319 List<String> getMultiHtmlTests(String file) { | |
| 320 if (multiHtmlTestsCache.containsKey(file)) return multiHtmlTestsCache[file]; | |
| 321 | |
|
Bill Hesse
2014/02/13 17:05:08
return multiHtmlTestsCache.putIfAbsent(file, () {
kustermann
2014/02/14 11:52:07
Done.
| |
| 322 try { | |
| 323 List<String> subtestNames = []; | |
| 324 var content = new File(file).readAsStringSync(); | |
| 325 | |
| 326 if (multiHtmlTestRegExp.hasMatch(content)) { | |
| 327 var matchesIter = multiHtmlTestGroupRegExp.allMatches(content).iterator; | |
| 328 while(matchesIter.moveNext()) { | |
| 329 String fullMatch = matchesIter.current.group(0); | |
| 330 subtestNames.add(fullMatch.substring(fullMatch.indexOf("'") + 1)); | |
| 331 } | |
| 332 } | |
| 333 multiHtmlTestsCache[file] = subtestNames; | |
| 334 return subtestNames; | |
| 335 } catch (e) { | |
| 336 print("WARNING: couldn't determine html multitests in file ${file}"); | |
| 337 } | |
| 338 return []; | |
| 339 } | |
| 340 } | |
| 341 | |
| 342 class TestFileLister { | |
| 343 final Map<String, List<String>> _filesCache = {}; | |
| 344 | |
|
Bill Hesse
2014/02/13 17:05:08
putIfAbsent
kustermann
2014/02/14 11:52:07
Done.
| |
| 345 List<String> listTestFiles(String directory) { | |
| 346 if (_filesCache.containsKey(directory)) { | |
| 347 return _filesCache[directory]; | |
| 348 } | |
| 349 | |
| 350 var dir = new Directory(directory); | |
| 351 // Cannot test for _test.dart because co19 tests don't have that ending. | |
| 352 var dartFiles = dir.listSync(recursive: true) | |
| 353 .where((fe) => fe is File) | |
| 354 .where((file) => file.path.endsWith(".dart") || | |
| 355 file.path.endsWith("_test.html")) | |
| 356 .map((file) => file.path) | |
| 357 .toList(); | |
| 358 _filesCache[directory] = dartFiles; | |
| 359 return dartFiles; | |
| 360 } | |
| 361 } | |
| 362 | |
| 363 | |
| 364 /* | |
| 365 * [TestOutcomeFetcher] will fetch test results from a server using a REST-like | |
| 366 * interface. | |
| 367 */ | |
| 368 class TestOutcomeFetcher { | |
| 369 static String SERVER = '108.170.219.8'; | |
|
Bill Hesse
2014/02/13 17:05:08
Is there anything better than hardcoding these val
kustermann
2014/02/14 11:52:07
No. We don't have a DNS name for it. I could make
| |
| 370 static int PORT = 4540; | |
| 371 | |
| 372 HttpClient _client = new HttpClient(); | |
| 373 | |
| 374 Future<Set<Expectation>> outcomesOf( | |
| 375 String suiteName, Section section, String testName) { | |
| 376 var completer = new Completer(); | |
| 377 var pathComponents = ['json', 'test-outcomes', 'outcomes', | |
| 378 Uri.encodeComponent("$suiteName/$testName")]; | |
| 379 var path = pathComponents.join('/') + '/'; | |
| 380 var url = new Uri(scheme: 'http', host: SERVER, port: PORT, path: path); | |
| 381 | |
| 382 _client.getUrl(url) | |
| 383 .then((HttpClientRequest request) => request.close()) | |
| 384 .then((HttpClientResponse response) { | |
| 385 response | |
| 386 .transform(UTF8.decoder) | |
| 387 .transform(JSON.decoder).listen((List testResults) { | |
|
Bill Hesse
2014/02/13 17:05:08
Is this .listen really a .first? If so, then you
kustermann
2014/02/14 11:52:07
Good point.
| |
| 388 var setOfActualOutcomes = new Set<Expectation>(); | |
| 389 | |
| 390 try { | |
| 391 for (var result in testResults) { | |
| 392 var config = result['configuration']; | |
| 393 var testResult = result['test_result']; | |
| 394 var outcome = testResult['outcome']; | |
| 395 | |
| 396 config['unchecked'] = !config['checked']; | |
|
Bill Hesse
2014/02/13 17:05:08
Is there a way to keep these in sync with what the
kustermann
2014/02/14 11:52:07
Added comment. I don't want to make this CL bigger
| |
| 397 config['unminified'] = !config['minified']; | |
| 398 config['nocsp'] = !config['csp']; | |
| 399 config['browser'] = | |
| 400 TestUtils.isBrowserRuntime(config['runtime']); | |
| 401 config['analyzer'] = | |
| 402 TestUtils.isCommandLineAnalyzer(config['compiler']); | |
| 403 config['jscl'] = | |
| 404 TestUtils.isJsCommandLineRuntime(config['runtime']); | |
| 405 | |
| 406 if (section.condition == null || | |
| 407 section.condition.evaluate(config)) { | |
| 408 setOfActualOutcomes.add(Expectation.byName(outcome)); | |
| 409 } | |
| 410 } | |
| 411 completer.complete(setOfActualOutcomes); | |
| 412 } catch (error) { | |
| 413 print("Warning: Error occured while processing testoutcomes" | |
| 414 ": $error"); | |
| 415 completer.complete([]); | |
| 416 } | |
| 417 }, onError: (error) { | |
| 418 print("Warning: Error occured while fetching testoutcomes: $error"); | |
| 419 completer.complete([]); | |
| 420 }, cancelOnError: true); | |
| 421 }); | |
| 422 return completer.future; | |
| 423 } | |
| 424 } | |
| OLD | NEW |