// 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_2", "tests/corelib_2", "tests/corelib_2/corelib_2.status"], ["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"], ["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 args) { TestUtils.setDartDirUri(Platform.script.resolve('..')); usage() { print("Usage: ${Platform.executable} "); 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> _readSections(String filePath) { File file = new File(filePath); if (file.existsSync()) { var completer = new Completer(); List
sections = new List
(); 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
sections) { Set invalidLines = _analyzeStatusFile(directory, filePath, sections); if (invalidLines.length > 0) { return _writeFixedStatusFile(filePath, invalidLines); } return new Future.value(); }); } bool _testExists(String filePath, List testFiles, String directory, TestRule rule) { // TODO: Unify this regular expression matching with status_file_parser.dart List getRuleRegex(String name) { return name .split("/") .map((name) => new RegExp(name.replaceAll('*', '.*'))) .toList(); } bool matchRegexp(List 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 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 _analyzeStatusFile( String directory, String filePath, List
sections) { var invalidLines = new Set(); 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 invalidLines) { var lines = new File(statusFilePath).readAsLinesSync(); var outputLines = []; 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
sections) { return _generatedDeflakedLines(suiteName, sections) .then((Map fixedLines) { if (fixedLines.length > 0) { return _writeFixedStatusFile(filePath, fixedLines); } }); }); } Future _generatedDeflakedLines(String suiteName, List
sections) { var fixedLines = new Map(); 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 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 actualOutcomes) { var outcomesThatNeverHappened = new Set(); 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 fixedLines) { var lines = new File(filePath).readAsLinesSync(); var outputLines = []; 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>(); final multiHtmlTestsCache = new Map>(); List getMultitestNames(String file) { List names = []; names.addAll(getStandardMultitestNames(file)); names.addAll(getHtmlMultitestNames(file)); return names; } List getStandardMultitestNames(String file) { return multiTestsCache.putIfAbsent(file, () { try { var tests = new Map(); var outcomes = new Map>(); 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 getHtmlMultitestNames(String file) { return multiHtmlTestsCache.putIfAbsent(file, () { try { List 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> _filesCache = {}; List 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> 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(); 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 []; }); }); } }