| // Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file | 
 | // for details. All rights reserved. Use of this source code is governed by a | 
 | // BSD-style license that can be found in the LICENSE file. | 
 |  | 
 | library status_clean; | 
 |  | 
 | import "dart:async"; | 
 | import "dart:convert" show JSON, UTF8; | 
 | import "dart:io"; | 
 | import "testing/dart/multitest.dart"; | 
 | import "testing/dart/status_file_parser.dart"; | 
 | import "testing/dart/test_suite.dart" | 
 |     show multiHtmlTestGroupRegExp, multiTestRegExp, multiHtmlTestRegExp, | 
 |          TestUtils; | 
 | import "testing/dart/utils.dart" show Path; | 
 |  | 
 | // [STATUS_TUPLES] is a list of (suite-name, directory, status-file)-tuples. | 
 | final STATUS_TUPLES = [ | 
 |     ["corelib", "tests/corelib", "tests/corelib/corelib.status"], | 
 |     ["html", "tests/html", "tests/html/html.status"], | 
 |     ["isolate", "tests/isolate", "tests/isolate/isolate.status"], | 
 |     ["language", "tests/language", "tests/language/language.status"], | 
 |     ["language", "tests/language", "tests/language/language_analyzer2.status"], | 
 |     ["language","tests/language", "tests/language/language_analyzer.status"], | 
 |     ["language","tests/language", "tests/language/language_dart2js.status"], | 
 |     ["lib", "tests/lib", "tests/lib/lib.status"], | 
 |     ["standalone", "tests/standalone", "tests/standalone/standalone.status"], | 
 |     ["pkg", "pkg", "pkg/pkg.status"], | 
 |     ["pkgbuild", ".", "pkg/pkgbuild.status"], | 
 |     ["utils", "tests/utils", "tests/utils/utils.status"], | 
 |     ["samples", "samples", "samples/samples.status"], | 
 |     ["analyze_library", "sdk", "tests/lib/analyzer/analyze_library.status"], | 
 |     ["dart2js_extra", "tests/compiler/dart2js_extra", | 
 |      "tests/compiler/dart2js_extra/dart2js_extra.status"], | 
 |     ["dart2js_native", "tests/compiler/dart2js_native", | 
 |      "tests/compiler/dart2js_native/dart2js_native.status"], | 
 |     ["dart2js", "tests/compiler/dart2js", | 
 |      "tests/compiler/dart2js/dart2js.status"], | 
 |     ["benchmark_smoke", "tests/benchmark_smoke", | 
 |      "tests/benchmark_smoke/benchmark_smoke.status"], | 
 |     ["co19", "tests/co19/src", "tests/co19/co19-analyzer2.status"], | 
 |     ["co19", "tests/co19/src", "tests/co19/co19-analyzer.status"], | 
 |     ["co19", "tests/co19/src", "tests/co19/co19-dart2js.status"], | 
 |     ["co19", "tests/co19/src", "tests/co19/co19-co19.status"], | 
 |     ["co19", "tests/co19/src", "tests/co19/co19-dartium.status"], | 
 |     ["co19", "tests/co19/src", "tests/co19/co19-runtime.status"], | 
 | ]; | 
 |  | 
 | void main(List<String> args) { | 
 |   TestUtils.setDartDirUri(Platform.script.resolve('..')); | 
 |   usage() { | 
 |     print("Usage: ${Platform.executable} <deflake|remove-nonexistent-tests>"); | 
 |     exit(1); | 
 |   } | 
 |  | 
 |   if (args.length == 0) usage(); | 
 |  | 
 |   if (args[0] == 'deflake') { | 
 |     run(new StatusFileDeflaker()); | 
 |   } else if (args[0] == 'remove-nonexistent-tests') { | 
 |     run(new StatusFileNonExistentTestRemover()); | 
 |   } else { | 
 |     usage(); | 
 |   } | 
 | } | 
 |  | 
 | run(StatusFileProcessor processor) { | 
 |   Future.forEach(STATUS_TUPLES, (List tuple) { | 
 |     String suiteName = tuple[0]; | 
 |     String directory = tuple[1]; | 
 |     String filePath = tuple[2]; | 
 |     print("Processing $filePath"); | 
 |     return processor.run(suiteName, directory, filePath); | 
 |   }); | 
 | } | 
 |  | 
 | abstract class StatusFileProcessor { | 
 |   Future run(String suiteName, String directory, String filePath); | 
 |  | 
 |   Future<List<Section>> _readSections(String filePath) { | 
 |     File file = new File(filePath); | 
 |  | 
 |     if (file.existsSync()) { | 
 |       var completer = new Completer(); | 
 |       List<Section> sections = new List<Section>(); | 
 |  | 
 |       ReadConfigurationInto(new Path(file.path), sections, () { | 
 |         completer.complete(sections); | 
 |       }); | 
 |       return completer.future; | 
 |     } | 
 |     return new Future.value([]); | 
 |   } | 
 | } | 
 |  | 
 | class StatusFileNonExistentTestRemover extends StatusFileProcessor { | 
 |   final MultiTestDetector multiTestDetector = new MultiTestDetector(); | 
 |   final TestFileLister testFileLister = new TestFileLister(); | 
 |  | 
 |   Future run(String suiteName, String directory, String filePath) { | 
 |     return _readSections(filePath).then((List<Section> sections) { | 
 |       Set<int> invalidLines = _analyzeStatusFile(directory, filePath, sections); | 
 |       if (invalidLines.length > 0) { | 
 |         return _writeFixedStatusFile(filePath, invalidLines); | 
 |       } | 
 |       return new Future.value(); | 
 |     }); | 
 |   } | 
 |  | 
 |   bool _testExists(String filePath, | 
 |                    List<String> testFiles, | 
 |                    String directory, | 
 |                    TestRule rule) { | 
 |     // TODO: Unify this regular expression matching with status_file_parser.dart | 
 |     List<RegExp> getRuleRegex(String name) { | 
 |       return name.split("/") | 
 |           .map((name) => new RegExp(name.replaceAll('*', '.*'))) | 
 |           .toList(); | 
 |     } | 
 |     bool matchRegexp(List<RegExp> patterns, String str) { | 
 |       var parts = str.split("/"); | 
 |       if (patterns.length > parts.length) { | 
 |         return false; | 
 |       } | 
 |       // NOTE: patterns.length <= parts.length | 
 |       for (var i = 0; i < patterns.length; i++) { | 
 |         if (!patterns[i].hasMatch(parts[i])) { | 
 |           return false; | 
 |         } | 
 |       } | 
 |       return true; | 
 |     } | 
 |  | 
 |     var rulePattern = getRuleRegex(rule.name); | 
 |     return testFiles.any((String file) { | 
 |       // TODO: Use test_suite.dart's [buildTestCaseDisplayName] instead. | 
 |       var filePath = new Path(file).relativeTo(new Path(directory)); | 
 |       String baseTestName = _concat("${filePath.directoryPath}", | 
 |                                     "${filePath.filenameWithoutExtension}"); | 
 |  | 
 |       List<String> testNames = []; | 
 |       for (var name in multiTestDetector.getMultitestNames(file)) { | 
 |         testNames.add(_concat(baseTestName, name)); | 
 |       } | 
 |  | 
 |       // If it is not a multitest the testname is [baseTestName] | 
 |       if (testNames.isEmpty) { | 
 |         testNames.add(baseTestName); | 
 |       } | 
 |  | 
 |       return testNames.any( | 
 |           (String testName) => matchRegexp(rulePattern, testName)); | 
 |     }); | 
 |   } | 
 |  | 
 |   Set<int> _analyzeStatusFile(String directory, | 
 |                               String filePath, | 
 |                               List<Section> sections) { | 
 |     var invalidLines = new Set<int>(); | 
 |     var dartFiles = testFileLister.listTestFiles(directory); | 
 |     for (var section in sections) { | 
 |       for (var rule in section.testRules) { | 
 |         if (!_testExists(filePath, dartFiles, directory, rule)) { | 
 |           print("Invalid rule: ${rule.name} in file " | 
 |                 "$filePath:${rule.lineNumber}"); | 
 |           invalidLines.add(rule.lineNumber); | 
 |         } | 
 |       } | 
 |     } | 
 |     return invalidLines; | 
 |   } | 
 |  | 
 |   _writeFixedStatusFile(String statusFilePath, Set<int> invalidLines) { | 
 |     var lines = new File(statusFilePath).readAsLinesSync(); | 
 |     var outputLines = <String>[]; | 
 |     for (int i = 0; i < lines.length; i++) { | 
 |       // The status file parser numbers lines starting with 1, not 0. | 
 |       if (!invalidLines.contains(i + 1)) { | 
 |         outputLines.add(lines[i]); | 
 |       } | 
 |     } | 
 |     var outputFile = new File("$statusFilePath.fixed"); | 
 |     outputFile.writeAsStringSync(outputLines.join("\n")); | 
 |  } | 
 |  | 
 |   String _concat(String base, String part) { | 
 |     if (base == "") return part; | 
 |     if (part == "") return base; | 
 |     return "$base/$part"; | 
 |   } | 
 | } | 
 |  | 
 | class StatusFileDeflaker extends StatusFileProcessor { | 
 |   TestOutcomeFetcher _testOutcomeFetcher = new TestOutcomeFetcher(); | 
 |  | 
 |   Future run(String suiteName, String directory, String filePath) { | 
 |     return _readSections(filePath).then((List<Section> sections) { | 
 |       return _generatedDeflakedLines(suiteName, sections) | 
 |           .then((Map<int, String> fixedLines) { | 
 |             if (fixedLines.length > 0) { | 
 |               return _writeFixedStatusFile(filePath, fixedLines); | 
 |             } | 
 |       }); | 
 |     }); | 
 |   } | 
 |  | 
 |   Future _generatedDeflakedLines(String suiteName, | 
 |                                  List<Section> sections) { | 
 |     var fixedLines = new Map<int, String>(); | 
 |     return Future.forEach(sections, (Section section) { | 
 |       return Future.forEach(section.testRules, (rule) { | 
 |         return _maybeFixStatusfileLine(suiteName, section, rule, fixedLines); | 
 |       }); | 
 |     }).then((_) => fixedLines); | 
 |   } | 
 |  | 
 |   Future _maybeFixStatusfileLine(String suiteName, | 
 |                                  Section section, | 
 |                                  TestRule rule, | 
 |                                  Map<int, String> fixedLines) { | 
 |     print("Processing ${section.statusFile.location}: ${rule.lineNumber}"); | 
 |     // None of our status file lines have expressions, so we pass {} here. | 
 |     var notedOutcomes = rule.expression | 
 |         .evaluate({}) | 
 |         .map((name) => Expectation.byName(name)) | 
 |         .where((Expectation expectation) => !expectation.isMetaExpectation) | 
 |         .toSet(); | 
 |  | 
 |     if (notedOutcomes.isEmpty) return new Future.value(); | 
 |  | 
 |     // TODO: [rule.name] is actually a pattern not just a testname. We should | 
 |     // find all possible testnames this rule matches against and unify the | 
 |     // outcomes of these tests. | 
 |     return _testOutcomeFetcher.outcomesOf(suiteName, section, rule.name) | 
 |       .then((Set<Expectation> actualOutcomes) { | 
 |  | 
 |       var outcomesThatNeverHappened = new Set<Expectation>(); | 
 |       for (Expectation notedOutcome in notedOutcomes) { | 
 |         bool found = false; | 
 |         for (Expectation actualOutcome in actualOutcomes) { | 
 |           if (actualOutcome.canBeOutcomeOf(notedOutcome)) { | 
 |             found = true; | 
 |             break; | 
 |           } | 
 |         } | 
 |         if (!found) { | 
 |           outcomesThatNeverHappened.add(notedOutcome); | 
 |         } | 
 |       } | 
 |  | 
 |       if (outcomesThatNeverHappened.length > 0 && actualOutcomes.length > 0) { | 
 |         // Print the change to stdout. | 
 |         print("${rule.name} " | 
 |               "(${section.statusFile.location}:${rule.lineNumber}):"); | 
 |         print("   Actual outcomes:         ${actualOutcomes.toList()}"); | 
 |         print("   Outcomes in status file: ${notedOutcomes.toList()}"); | 
 |         print("   Outcomes in status file that never happened : " | 
 |               "${outcomesThatNeverHappened.toList()}\n"); | 
 |  | 
 |         // Build the fixed status file line. | 
 |         fixedLines[rule.lineNumber] = | 
 |             '${rule.name}: ${actualOutcomes.join(', ')} ' | 
 |             '# before: ${notedOutcomes.join(', ')} / ' | 
 |             'never happened:  ${outcomesThatNeverHappened.join(', ')}'; | 
 |       } | 
 |     }); | 
 |   } | 
 |  | 
 |   _writeFixedStatusFile(String filePath, Map<int, String> fixedLines) { | 
 |     var lines = new File(filePath).readAsLinesSync(); | 
 |     var outputLines = <String>[]; | 
 |     for (int i = 0; i < lines.length; i++) { | 
 |       if (fixedLines.containsKey(i + 1)) { | 
 |         outputLines.add(fixedLines[i + 1]); | 
 |       } else { | 
 |         outputLines.add(lines[i]); | 
 |       } | 
 |     } | 
 |     var output = outputLines.join("\n"); | 
 |     var outputFile = new File("$filePath.deflaked"); | 
 |     outputFile.writeAsStringSync(output); | 
 |  } | 
 | } | 
 |  | 
 | class MultiTestDetector { | 
 |   final multiTestsCache = new Map<String,List<String>>(); | 
 |   final multiHtmlTestsCache = new Map<String,List<String>>(); | 
 |  | 
 |  | 
 |   List<String> getMultitestNames(String file) { | 
 |     List<String> names = []; | 
 |     names.addAll(getStandardMultitestNames(file)); | 
 |     names.addAll(getHtmlMultitestNames(file)); | 
 |     return names; | 
 |   } | 
 |  | 
 |   List<String> getStandardMultitestNames(String file) { | 
 |     return multiTestsCache.putIfAbsent(file, () { | 
 |       try { | 
 |         var tests = new Map<String, String>(); | 
 |         var outcomes = new Map<String, Set<String>>(); | 
 |         if (multiTestRegExp.hasMatch(new File(file).readAsStringSync())) { | 
 |           ExtractTestsFromMultitest(new Path(file), tests, outcomes); | 
 |         } | 
 |         return tests.keys.toList(); | 
 |       } catch (error) { | 
 |         print("WARNING: Couldn't determine multitests in file ${file}: $error"); | 
 |         return []; | 
 |       } | 
 |     }); | 
 |   } | 
 |  | 
 |   List<String> getHtmlMultitestNames(String file) { | 
 |     return multiHtmlTestsCache.putIfAbsent(file, () { | 
 |       try { | 
 |         List<String> subtestNames = []; | 
 |         var content = new File(file).readAsStringSync(); | 
 |  | 
 |         if (multiHtmlTestRegExp.hasMatch(content)) { | 
 |           var matchesIter = multiHtmlTestGroupRegExp.allMatches(content).iterator; | 
 |           while(matchesIter.moveNext()) { | 
 |             String fullMatch = matchesIter.current.group(0); | 
 |             subtestNames.add(fullMatch.substring(fullMatch.indexOf("'") + 1)); | 
 |           } | 
 |         } | 
 |         return subtestNames; | 
 |       } catch (error) { | 
 |         print("WARNING: Couldn't determine multitests in file ${file}: $error"); | 
 |       } | 
 |       return []; | 
 |     }); | 
 |   } | 
 | } | 
 |  | 
 | class TestFileLister { | 
 |   final Map<String, List<String>> _filesCache = {}; | 
 |  | 
 |   List<String> listTestFiles(String directory) { | 
 |     return _filesCache.putIfAbsent(directory, () { | 
 |       var dir = new Directory(directory); | 
 |       // Cannot test for _test.dart because co19 tests don't have that ending. | 
 |       var dartFiles = dir.listSync(recursive: true) | 
 |           .where((fe) => fe is File) | 
 |           .where((file) => file.path.endsWith(".dart") || | 
 |                            file.path.endsWith("_test.html")) | 
 |           .map((file) => file.path) | 
 |           .toList(); | 
 |       return dartFiles; | 
 |     }); | 
 |   } | 
 | } | 
 |  | 
 |  | 
 | /* | 
 |  * [TestOutcomeFetcher] will fetch test results from a server using a REST-like | 
 |  * interface. | 
 |  */ | 
 | class TestOutcomeFetcher { | 
 |   static String SERVER = '108.170.219.8'; | 
 |   static int PORT = 4540; | 
 |  | 
 |   HttpClient _client = new HttpClient(); | 
 |  | 
 |   Future<Set<Expectation>> outcomesOf( | 
 |       String suiteName, Section section, String testName) { | 
 |     var pathComponents = ['json', 'test-outcomes', 'outcomes', | 
 |                           Uri.encodeComponent("$suiteName/$testName")]; | 
 |     var path = pathComponents.join('/') + '/'; | 
 |     var url = new Uri(scheme: 'http', host: SERVER, port: PORT, path: path); | 
 |  | 
 |     return _client.getUrl(url) | 
 |       .then((HttpClientRequest request) => request.close()) | 
 |       .then((HttpClientResponse response) { | 
 |         return response.transform(UTF8.decoder).transform(JSON.decoder).first | 
 |             .then((List testResults) { | 
 |               var setOfActualOutcomes = new Set<Expectation>(); | 
 |  | 
 |               try { | 
 |                 for (var result in testResults) { | 
 |                   var config = result['configuration']; | 
 |                   var testResult = result['test_result']; | 
 |                   var outcome = testResult['outcome']; | 
 |  | 
 |                   // These variables are derived variables and will be set in | 
 |                   // tools/testing/dart/test_options.dart. | 
 |                   // [Mostly due to the fact that we don't have an unary ! | 
 |                   //  operator in status file expressions.] | 
 |                   config['unchecked'] = !config['checked']; | 
 |                   config['unminified'] = !config['minified']; | 
 |                   config['nocsp'] = !config['csp']; | 
 |                   config['browser'] = | 
 |                       TestUtils.isBrowserRuntime(config['runtime']); | 
 |                   config['analyzer'] = | 
 |                       TestUtils.isCommandLineAnalyzer(config['compiler']); | 
 |                   config['jscl'] = | 
 |                       TestUtils.isJsCommandLineRuntime(config['runtime']); | 
 |  | 
 |                   if (section.condition == null || | 
 |                       section.condition.evaluate(config)) { | 
 |                     setOfActualOutcomes.add(Expectation.byName(outcome)); | 
 |                   } | 
 |                 } | 
 |                 return setOfActualOutcomes; | 
 |               } catch (error) { | 
 |                 print("Warning: Error occured while processing testoutcomes" | 
 |                       ": $error"); | 
 |                 return []; | 
 |               } | 
 |             }).catchError((error) { | 
 |               print("Warning: Error occured while fetching testoutcomes: $error"); | 
 |               return []; | 
 |             }); | 
 |     }); | 
 |   } | 
 | } |