| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2014, 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 status_clean; |
| 6 |
| 7 import "dart:async"; |
| 8 import "dart:convert" show JSON, UTF8; |
| 9 import "dart:io"; |
| 10 import "testing/dart/multitest.dart"; |
| 11 import "testing/dart/status_file_parser.dart"; |
| 12 import "testing/dart/test_suite.dart" |
| 13 show multiHtmlTestGroupRegExp, multiTestRegExp, multiHtmlTestRegExp, |
| 14 TestUtils; |
| 15 import "testing/dart/utils.dart" show Path; |
| 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 ["pkgbuild", ".", "pkg/pkgbuild.status"], |
| 31 ["utils", "tests/utils", "tests/utils/utils.status"], |
| 32 ["samples", "samples", "samples/samples.status"], |
| 33 ["analyze_library", "sdk", "tests/lib/analyzer/analyze_library.status"], |
| 34 ["dart2js_extra", "tests/compiler/dart2js_extra", |
| 35 "tests/compiler/dart2js_extra/dart2js_extra.status"], |
| 36 ["dart2js_native", "tests/compiler/dart2js_native", |
| 37 "tests/compiler/dart2js_native/dart2js_native.status"], |
| 38 ["dart2js", "tests/compiler/dart2js", |
| 39 "tests/compiler/dart2js/dart2js.status"], |
| 40 ["pub", "sdk/lib/_internal/pub", "sdk/lib/_internal/pub/pub.status"], |
| 41 ["benchmark_smoke", "tests/benchmark_smoke", |
| 42 "tests/benchmark_smoke/benchmark_smoke.status"], |
| 43 ["co19", "tests/co19/src", "tests/co19/co19-analyzer2.status"], |
| 44 ["co19", "tests/co19/src", "tests/co19/co19-analyzer.status"], |
| 45 ["co19", "tests/co19/src", "tests/co19/co19-dart2dart.status"], |
| 46 ["co19", "tests/co19/src", "tests/co19/co19-dart2js.status"], |
| 47 ["co19", "tests/co19/src", "tests/co19/co19-co19.status"], |
| 48 ["co19", "tests/co19/src", "tests/co19/co19-dartium.status"], |
| 49 ["co19", "tests/co19/src", "tests/co19/co19-runtime.status"], |
| 50 ]; |
| 51 |
| 52 void main(List<String> args) { |
| 53 usage() { |
| 54 print("Usage: ${Platform.executable} <deflake|remove-nonexistent-tests>"); |
| 55 exit(1); |
| 56 } |
| 57 |
| 58 if (args.length == 0) usage(); |
| 59 |
| 60 if (args[0] == 'deflake') { |
| 61 run(new StatusFileDeflaker()); |
| 62 } else if (args[0] == 'remove-nonexistent-tests') { |
| 63 run(new StatusFileNonExistentTestRemover()); |
| 64 } else { |
| 65 usage(); |
| 66 } |
| 67 } |
| 68 |
| 69 run(StatusFileProcessor processor) { |
| 70 Future.forEach(STATUS_TUPLES, (List tuple) { |
| 71 String suiteName = tuple[0]; |
| 72 String directory = tuple[1]; |
| 73 String filePath = tuple[2]; |
| 74 print("Processing $filePath"); |
| 75 return processor.run(suiteName, directory, filePath); |
| 76 }); |
| 77 } |
| 78 |
| 79 abstract class StatusFileProcessor { |
| 80 Future run(String suiteName, String directory, String filePath); |
| 81 |
| 82 Future<List<Section>> _readSections(String filePath) { |
| 83 File file = new File(filePath); |
| 84 |
| 85 if (file.existsSync()) { |
| 86 var completer = new Completer(); |
| 87 List<Section> sections = new List<Section>(); |
| 88 |
| 89 ReadConfigurationInto(new Path(file.path), sections, () { |
| 90 completer.complete(sections); |
| 91 }); |
| 92 return completer.future; |
| 93 } |
| 94 return new Future.value([]); |
| 95 } |
| 96 } |
| 97 |
| 98 class StatusFileNonExistentTestRemover extends StatusFileProcessor { |
| 99 final MultiTestDetector multiTestDetector = new MultiTestDetector(); |
| 100 final TestFileLister testFileLister = new TestFileLister(); |
| 101 |
| 102 Future run(String suiteName, String directory, String filePath) { |
| 103 return _readSections(filePath).then((List<Section> sections) { |
| 104 Set<int> invalidLines = _analyzeStatusFile(directory, filePath, sections); |
| 105 if (invalidLines.length > 0) { |
| 106 return _writeFixedStatusFile(filePath, invalidLines); |
| 107 } |
| 108 return new Future.value(); |
| 109 }); |
| 110 } |
| 111 |
| 112 bool _testExists(String filePath, |
| 113 List<String> testFiles, |
| 114 String directory, |
| 115 TestRule rule) { |
| 116 // TODO: Unify this regular expression matching with status_file_parser.dart |
| 117 List<RegExp> getRuleRegex(String name) { |
| 118 return name.split("/") |
| 119 .map((name) => new RegExp(name.replaceAll('*', '.*'))) |
| 120 .toList(); |
| 121 } |
| 122 bool matchRegexp(List<RegExp> patterns, String str) { |
| 123 var parts = str.split("/"); |
| 124 if (patterns.length > parts.length) { |
| 125 return false; |
| 126 } |
| 127 // NOTE: patterns.length <= parts.length |
| 128 for (var i = 0; i < patterns.length; i++) { |
| 129 if (!patterns[i].hasMatch(parts[i])) { |
| 130 return false; |
| 131 } |
| 132 } |
| 133 return true; |
| 134 } |
| 135 |
| 136 var rulePattern = getRuleRegex(rule.name); |
| 137 return testFiles.any((String file) { |
| 138 // TODO: Use test_suite.dart's [buildTestCaseDisplayName] instead. |
| 139 var filePath = new Path(file).relativeTo(new Path(directory)); |
| 140 String baseTestName = _concat("${filePath.directoryPath}", |
| 141 "${filePath.filenameWithoutExtension}"); |
| 142 |
| 143 List<String> testNames = []; |
| 144 for (var name in multiTestDetector.getMultitestNames(file)) { |
| 145 testNames.add(_concat(baseTestName, name)); |
| 146 } |
| 147 |
| 148 // If it is not a multitest the testname is [baseTestName] |
| 149 if (testNames.isEmpty) { |
| 150 testNames.add(baseTestName); |
| 151 } |
| 152 |
| 153 return testNames.any( |
| 154 (String testName) => matchRegexp(rulePattern, testName)); |
| 155 }); |
| 156 } |
| 157 |
| 158 Set<int> _analyzeStatusFile(String directory, |
| 159 String filePath, |
| 160 List<Section> sections) { |
| 161 var invalidLines = new Set<int>(); |
| 162 var dartFiles = testFileLister.listTestFiles(directory); |
| 163 for (var section in sections) { |
| 164 for (var rule in section.testRules) { |
| 165 if (!_testExists(filePath, dartFiles, directory, rule)) { |
| 166 print("Invalid rule: ${rule.name} in file " |
| 167 "$filePath:${rule.lineNumber}"); |
| 168 invalidLines.add(rule.lineNumber); |
| 169 } |
| 170 } |
| 171 } |
| 172 return invalidLines; |
| 173 } |
| 174 |
| 175 _writeFixedStatusFile(String statusFilePath, Set<int> invalidLines) { |
| 176 var lines = new File(statusFilePath).readAsLinesSync(); |
| 177 var outputLines = <String>[]; |
| 178 for (int i = 0; i < lines.length; i++) { |
| 179 // The status file parser numbers lines starting with 1, not 0. |
| 180 if (!invalidLines.contains(i + 1)) { |
| 181 outputLines.add(lines[i]); |
| 182 } |
| 183 } |
| 184 var outputFile = new File("$statusFilePath.fixed"); |
| 185 outputFile.writeAsStringSync(outputLines.join("\n")); |
| 186 } |
| 187 |
| 188 String _concat(String base, String part) { |
| 189 if (base == "") return part; |
| 190 if (part == "") return base; |
| 191 return "$base/$part"; |
| 192 } |
| 193 } |
| 194 |
| 195 class StatusFileDeflaker extends StatusFileProcessor { |
| 196 TestOutcomeFetcher _testOutcomeFetcher = new TestOutcomeFetcher(); |
| 197 |
| 198 Future run(String suiteName, String directory, String filePath) { |
| 199 return _readSections(filePath).then((List<Section> sections) { |
| 200 return _generatedDeflakedLines(suiteName, sections) |
| 201 .then((Map<int, String> fixedLines) { |
| 202 if (fixedLines.length > 0) { |
| 203 return _writeFixedStatusFile(filePath, fixedLines); |
| 204 } |
| 205 }); |
| 206 }); |
| 207 } |
| 208 |
| 209 Future _generatedDeflakedLines(String suiteName, |
| 210 List<Section> sections) { |
| 211 var fixedLines = new Map<int, String>(); |
| 212 return Future.forEach(sections, (Section section) { |
| 213 return Future.forEach(section.testRules, (rule) { |
| 214 return _maybeFixStatusfileLine(suiteName, section, rule, fixedLines); |
| 215 }); |
| 216 }).then((_) => fixedLines); |
| 217 } |
| 218 |
| 219 Future _maybeFixStatusfileLine(String suiteName, |
| 220 Section section, |
| 221 TestRule rule, |
| 222 Map<int, String> fixedLines) { |
| 223 print("Processing ${section.statusFile.location}: ${rule.lineNumber}"); |
| 224 // None of our status file lines have expressions, so we pass {} here. |
| 225 var notedOutcomes = rule.expression |
| 226 .evaluate({}) |
| 227 .map((name) => Expectation.byName(name)) |
| 228 .where((Expectation expectation) => !expectation.isMetaExpectation) |
| 229 .toSet(); |
| 230 |
| 231 if (notedOutcomes.isEmpty) return new Future.value(); |
| 232 |
| 233 // TODO: [rule.name] is actually a pattern not just a testname. We should |
| 234 // find all possible testnames this rule matches against and unify the |
| 235 // outcomes of these tests. |
| 236 return _testOutcomeFetcher.outcomesOf(suiteName, section, rule.name) |
| 237 .then((Set<Expectation> actualOutcomes) { |
| 238 |
| 239 var outcomesThatNeverHappened = new Set<Expectation>(); |
| 240 for (Expectation notedOutcome in notedOutcomes) { |
| 241 bool found = false; |
| 242 for (Expectation actualOutcome in actualOutcomes) { |
| 243 if (actualOutcome.canBeOutcomeOf(notedOutcome)) { |
| 244 found = true; |
| 245 break; |
| 246 } |
| 247 } |
| 248 if (!found) { |
| 249 outcomesThatNeverHappened.add(notedOutcome); |
| 250 } |
| 251 } |
| 252 |
| 253 if (outcomesThatNeverHappened.length > 0 && actualOutcomes.length > 0) { |
| 254 // Print the change to stdout. |
| 255 print("${rule.name} " |
| 256 "(${section.statusFile.location}:${rule.lineNumber}):"); |
| 257 print(" Actual outcomes: ${actualOutcomes.toList()}"); |
| 258 print(" Outcomes in status file: ${notedOutcomes.toList()}"); |
| 259 print(" Outcomes in status file that never happened : " |
| 260 "${outcomesThatNeverHappened.toList()}\n"); |
| 261 |
| 262 // Build the fixed status file line. |
| 263 fixedLines[rule.lineNumber] = |
| 264 '${rule.name}: ${actualOutcomes.join(', ')} ' |
| 265 '# before: ${notedOutcomes.join(', ')} / ' |
| 266 'never happened: ${outcomesThatNeverHappened.join(', ')}'; |
| 267 } |
| 268 }); |
| 269 } |
| 270 |
| 271 _writeFixedStatusFile(String filePath, Map<int, String> fixedLines) { |
| 272 var lines = new File(filePath).readAsLinesSync(); |
| 273 var outputLines = <String>[]; |
| 274 for (int i = 0; i < lines.length; i++) { |
| 275 if (fixedLines.containsKey(i + 1)) { |
| 276 outputLines.add(fixedLines[i + 1]); |
| 277 } else { |
| 278 outputLines.add(lines[i]); |
| 279 } |
| 280 } |
| 281 var output = outputLines.join("\n"); |
| 282 var outputFile = new File("$filePath.deflaked"); |
| 283 outputFile.writeAsStringSync(output); |
| 284 } |
| 285 } |
| 286 |
| 287 class MultiTestDetector { |
| 288 final multiTestsCache = new Map<String,List<String>>(); |
| 289 final multiHtmlTestsCache = new Map<String,List<String>>(); |
| 290 |
| 291 |
| 292 List<String> getMultitestNames(String file) { |
| 293 List<String> names = []; |
| 294 names.addAll(getStandardMultitestNames(file)); |
| 295 names.addAll(getHtmlMultitestNames(file)); |
| 296 return names; |
| 297 } |
| 298 |
| 299 List<String> getStandardMultitestNames(String file) { |
| 300 return multiTestsCache.putIfAbsent(file, () { |
| 301 try { |
| 302 var tests = new Map<String, String>(); |
| 303 var outcomes = new Map<String, Set<String>>(); |
| 304 if (multiTestRegExp.hasMatch(new File(file).readAsStringSync())) { |
| 305 ExtractTestsFromMultitest(new Path(file), tests, outcomes); |
| 306 } |
| 307 return tests.keys.toList(); |
| 308 } catch (error) { |
| 309 print("WARNING: Couldn't determine multitests in file ${file}: $error"); |
| 310 return []; |
| 311 } |
| 312 }); |
| 313 } |
| 314 |
| 315 List<String> getHtmlMultitestNames(String file) { |
| 316 return multiHtmlTestsCache.putIfAbsent(file, () { |
| 317 try { |
| 318 List<String> subtestNames = []; |
| 319 var content = new File(file).readAsStringSync(); |
| 320 |
| 321 if (multiHtmlTestRegExp.hasMatch(content)) { |
| 322 var matchesIter = multiHtmlTestGroupRegExp.allMatches(content).iterato
r; |
| 323 while(matchesIter.moveNext()) { |
| 324 String fullMatch = matchesIter.current.group(0); |
| 325 subtestNames.add(fullMatch.substring(fullMatch.indexOf("'") + 1)); |
| 326 } |
| 327 } |
| 328 return subtestNames; |
| 329 } catch (error) { |
| 330 print("WARNING: Couldn't determine multitests in file ${file}: $error"); |
| 331 } |
| 332 return []; |
| 333 }); |
| 334 } |
| 335 } |
| 336 |
| 337 class TestFileLister { |
| 338 final Map<String, List<String>> _filesCache = {}; |
| 339 |
| 340 List<String> listTestFiles(String directory) { |
| 341 return _filesCache.putIfAbsent(directory, () { |
| 342 var dir = new Directory(directory); |
| 343 // Cannot test for _test.dart because co19 tests don't have that ending. |
| 344 var dartFiles = dir.listSync(recursive: true) |
| 345 .where((fe) => fe is File) |
| 346 .where((file) => file.path.endsWith(".dart") || |
| 347 file.path.endsWith("_test.html")) |
| 348 .map((file) => file.path) |
| 349 .toList(); |
| 350 return dartFiles; |
| 351 }); |
| 352 } |
| 353 } |
| 354 |
| 355 |
| 356 /* |
| 357 * [TestOutcomeFetcher] will fetch test results from a server using a REST-like |
| 358 * interface. |
| 359 */ |
| 360 class TestOutcomeFetcher { |
| 361 static String SERVER = '108.170.219.8'; |
| 362 static int PORT = 4540; |
| 363 |
| 364 HttpClient _client = new HttpClient(); |
| 365 |
| 366 Future<Set<Expectation>> outcomesOf( |
| 367 String suiteName, Section section, String testName) { |
| 368 var pathComponents = ['json', 'test-outcomes', 'outcomes', |
| 369 Uri.encodeComponent("$suiteName/$testName")]; |
| 370 var path = pathComponents.join('/') + '/'; |
| 371 var url = new Uri(scheme: 'http', host: SERVER, port: PORT, path: path); |
| 372 |
| 373 return _client.getUrl(url) |
| 374 .then((HttpClientRequest request) => request.close()) |
| 375 .then((HttpClientResponse response) { |
| 376 return response.transform(UTF8.decoder).transform(JSON.decoder).first |
| 377 .then((List testResults) { |
| 378 var setOfActualOutcomes = new Set<Expectation>(); |
| 379 |
| 380 try { |
| 381 for (var result in testResults) { |
| 382 var config = result['configuration']; |
| 383 var testResult = result['test_result']; |
| 384 var outcome = testResult['outcome']; |
| 385 |
| 386 // These variables are derived variables and will be set in |
| 387 // tools/testing/dart/test_options.dart. |
| 388 // [Mostly due to the fact that we don't have an unary ! |
| 389 // operator in status file expressions.] |
| 390 config['unchecked'] = !config['checked']; |
| 391 config['unminified'] = !config['minified']; |
| 392 config['nocsp'] = !config['csp']; |
| 393 config['browser'] = |
| 394 TestUtils.isBrowserRuntime(config['runtime']); |
| 395 config['analyzer'] = |
| 396 TestUtils.isCommandLineAnalyzer(config['compiler']); |
| 397 config['jscl'] = |
| 398 TestUtils.isJsCommandLineRuntime(config['runtime']); |
| 399 |
| 400 if (section.condition == null || |
| 401 section.condition.evaluate(config)) { |
| 402 setOfActualOutcomes.add(Expectation.byName(outcome)); |
| 403 } |
| 404 } |
| 405 return setOfActualOutcomes; |
| 406 } catch (error) { |
| 407 print("Warning: Error occured while processing testoutcomes" |
| 408 ": $error"); |
| 409 return []; |
| 410 } |
| 411 }).catchError((error) { |
| 412 print("Warning: Error occured while fetching testoutcomes: $error"
); |
| 413 return []; |
| 414 }); |
| 415 }); |
| 416 } |
| 417 } |
| OLD | NEW |