| Index: tools/gardening_tools/multitest/lib/src/multitest.dart
|
| diff --git a/tools/gardening_tools/multitest/lib/src/multitest.dart b/tools/gardening_tools/multitest/lib/src/multitest.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..078715bbb7b620813a92acae92cb85339d0921d3
|
| --- /dev/null
|
| +++ b/tools/gardening_tools/multitest/lib/src/multitest.dart
|
| @@ -0,0 +1,330 @@
|
| +// Copyright (c) 2012, 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 multitest;
|
| +
|
| +import "dart:io";
|
| +import "package:path/path.dart" as path;
|
| +import 'package:base_lib/base_lib.dart';
|
| +import "utils.dart";
|
| +
|
| +// import "test_suite.dart";
|
| +
|
| +// Multitests are Dart test scripts containing lines of the form
|
| +// " [some dart code] //# [key]: [error type]"
|
| +//
|
| +// To support legacy multi tests we also handle lines of the form
|
| +// " [some dart code] /// [key]: [error type]"
|
| +//
|
| +// For each key in the file, a new test file is made containing all
|
| +// the normal lines of the file, and all of the multitest lines containing
|
| +// that key, in the same order as in the source file. The new test is expected
|
| +// to pass if the error type listed is 'ok', or to fail if there is an error
|
| +// type of type 'compile-time error', 'runtime error', 'static type warning', or
|
| +// 'dynamic type error'. The type error tests fail only in checked mode.
|
| +// There is also a test created from only the untagged lines of the file,
|
| +// with key "none", which is expected to pass. This library extracts these
|
| +// tests, writes them into a temporary directory, and passes them to the test
|
| +// runner. These tests may be referred to in the status files with the
|
| +// pattern [test name]/[key].
|
| +//
|
| +// For example: file I_am_a_multitest.dart
|
| +// aaa
|
| +// bbb //# 02: runtime error
|
| +// ccc //# 02: continued
|
| +// ddd //# 07: static type warning
|
| +// eee //# 10: ok
|
| +// fff
|
| +//
|
| +// should create four tests:
|
| +// I_am_a_multitest_none.dart
|
| +// aaa
|
| +// fff
|
| +//
|
| +// I_am_a_multitest_02.dart
|
| +// aaa
|
| +// bbb //# 02: runtime error
|
| +// ccc //# 02: continued
|
| +// fff
|
| +//
|
| +// I_am_a_multitest_07.dart
|
| +// aaa
|
| +// ddd //# 07: static type warning
|
| +// fff
|
| +//
|
| +// and I_am_a_multitest_10.dart
|
| +// aaa
|
| +// eee //# 10: ok
|
| +// fff
|
| +//
|
| +// Note that it is possible to indicate more than one acceptable outcome
|
| +// in the case of dynamic and static type warnings
|
| +// aaa
|
| +// ddd //# 07: static type warning, dynamic type error
|
| +// fff
|
| +
|
| +/// Until legacy multitests are ported we need to support both /// and //#
|
| +final _multitestMarker = new RegExp(r"//[/#]");
|
| +
|
| +class ExtractMultitestResult {
|
| + Map<String, String> tests;
|
| + Map<String, Set<String>> outcomes;
|
| +
|
| + ExtractMultitestResult(this.tests, this.outcomes);
|
| +}
|
| +
|
| +ExtractMultitestResult ExtractTestsFromMultitest(
|
| + String filePath, Logger logger) {
|
| + // Read the entire file into a byte buffer and transform it to a
|
| + // String. This will treat the file as ascii but the only parts
|
| + // we are interested in will be ascii in any case.
|
| +
|
| + Map<String, String> tests = {};
|
| + Map<String, Set<String>> outcomes = {};
|
| +
|
| + var bytes = new File(path.absolute(filePath)).readAsBytesSync();
|
| + var contents = decodeUtf8(bytes);
|
| + var firstNewline = contents.indexOf('\n');
|
| + var lineSeparator =
|
| + (firstNewline == 0 || contents[firstNewline - 1] != '\r') ? '\n' : '\r\n';
|
| + var lines = contents.split(lineSeparator);
|
| + if (lines.last == '') lines.removeLast();
|
| + bytes = null;
|
| + contents = null;
|
| + var validMultitestOutcomes = [
|
| + 'ok',
|
| + 'compile-time error',
|
| + 'runtime error',
|
| + 'static type warning',
|
| + 'dynamic type error',
|
| + 'checked mode compile-time error'
|
| + ].toSet();
|
| +
|
| + // Create the set of multitests, which will have a new test added each
|
| + // time we see a multitest line with a new key.
|
| + var testsAsLines = <String, List<String>>{};
|
| +
|
| + // Add the default case with key "none".
|
| + testsAsLines['none'] = <String>[];
|
| + outcomes['none'] = new Set<String>();
|
| +
|
| + var lineCount = 0;
|
| + for (var line in lines) {
|
| + lineCount++;
|
| + var annotation = new _Annotation.from(line);
|
| + if (annotation != null) {
|
| + testsAsLines.putIfAbsent(
|
| + annotation.key, () => new List<String>.from(testsAsLines["none"]));
|
| + // Add line to test with annotation.key as key, empty line to the rest.
|
| + for (var key in testsAsLines.keys) {
|
| + testsAsLines[key].add(annotation.key == key ? line : "");
|
| + }
|
| + outcomes.putIfAbsent(annotation.key, () => new Set<String>());
|
| + if (annotation.rest != 'continued') {
|
| + for (String nextOutcome in annotation.outcomesList) {
|
| + if (validMultitestOutcomes.contains(nextOutcome)) {
|
| + outcomes[annotation.key].add(nextOutcome);
|
| + } else {
|
| + logger.warning(
|
| + "Warning: Invalid test directive '$nextOutcome' on line "
|
| + "${lineCount}:\n${annotation.rest} ");
|
| + }
|
| + }
|
| + }
|
| + } else {
|
| + for (var test in testsAsLines.values) test.add(line);
|
| + }
|
| + }
|
| + // End marker, has a final line separator so we don't need to add it after
|
| + // joining the lines.
|
| + var marker =
|
| + '// Test created from multitest named ${path.absolute(filePath)}.'
|
| + '$lineSeparator';
|
| + for (var test in testsAsLines.values) test.add(marker);
|
| +
|
| + var keysToDelete = <String>[];
|
| + // Check that every key (other than the none case) has at least one outcome
|
| + for (var outcomeKey in outcomes.keys) {
|
| + if (outcomeKey != 'none' && outcomes[outcomeKey].isEmpty) {
|
| + logger.warning(
|
| + "Warning: Test ${outcomeKey} has no valid annotated outcomes.\n"
|
| + "Expected one of: ${validMultitestOutcomes.toString()}");
|
| + // If this multitest doesn't have an outcome, mark the multitest for
|
| + // deletion.
|
| + keysToDelete.add(outcomeKey);
|
| + }
|
| + }
|
| + // If a key/multitest was marked for deletion, do the necessary cleanup.
|
| + keysToDelete.forEach(outcomes.remove);
|
| + keysToDelete.forEach(testsAsLines.remove);
|
| +
|
| + // Copy all the tests into the output map tests, as multiline strings.
|
| + for (var key in testsAsLines.keys) {
|
| + tests[key] = testsAsLines[key].join(lineSeparator);
|
| + }
|
| +
|
| + return new ExtractMultitestResult(tests, outcomes);
|
| +}
|
| +
|
| +// Represents a mutlitest annotation in the special //# comment.
|
| +class _Annotation {
|
| + String key;
|
| + String rest;
|
| + List<String> outcomesList;
|
| + _Annotation() {}
|
| + factory _Annotation.from(String line) {
|
| + // Do an early return with "null" if this is not a valid multitest
|
| + // annotation.
|
| + if (!line.contains(_multitestMarker)) {
|
| + return null;
|
| + }
|
| + var parts = line
|
| + .split(_multitestMarker)[1]
|
| + .split(':')
|
| + .map((s) => s.trim())
|
| + .where((s) => s.length > 0)
|
| + .toList();
|
| + if (parts.length <= 1) {
|
| + return null;
|
| + }
|
| +
|
| + var annotation = new _Annotation();
|
| + annotation.key = parts[0];
|
| + annotation.rest = parts[1];
|
| + annotation.outcomesList =
|
| + annotation.rest.split(',').map((s) => s.trim()).toList();
|
| + return annotation;
|
| + }
|
| +}
|
| +
|
| +// // Find all relative imports and copy them into the dir that contains
|
| +// // the generated tests.
|
| +// Set<String> _findAllRelativeImports(String topLibrary) {
|
| +// var toSearch = [topLibrary].toSet();
|
| +// var foundImports = new Set<String>();
|
| +// var libraryDir = topLibrary.directoryPath;
|
| +// var relativeImportRegExp = new RegExp(
|
| +// '^(?:@.*\\s+)?' // Allow for a meta-data annotation.
|
| +// '(import|part)'
|
| +// '\\s+["\']'
|
| +// '(?!(dart:|dart-ext:|data:|package:|/))' // Look-ahead: not in package.
|
| +// '([^"\']*)' // The path to the imported file.
|
| +// '["\']');
|
| +// while (!toSearch.isEmpty) {
|
| +// var thisPass = toSearch;
|
| +// toSearch = new Set<String>();
|
| +// for (String filename in thisPass) {
|
| +// File f = new File(path.absolute(filename));
|
| +// for (String line in f.readAsLinesSync()) {
|
| +// Match match = relativeImportRegExp.firstMatch(line);
|
| +// if (match != null) {
|
| +// Path relativePath = new Path(match.group(3));
|
| +// if (foundImports.contains(relativePath.toString())) {
|
| +// continue;
|
| +// }
|
| +// if (relativePath.toString().contains('..')) {
|
| +// // This is just for safety reasons, we don't want
|
| +// // to unintentionally clobber files relative to the destination
|
| +// // dir when copying them ove.
|
| +// print("relative paths containing .. are not allowed.");
|
| +// exit(1);
|
| +// }
|
| +// foundImports.add(relativePath.toString());
|
| +// toSearch.add(libraryDir.join(relativePath));
|
| +// }
|
| +// }
|
| +// }
|
| +// }
|
| +// return foundImports;
|
| +// }
|
| +
|
| +// Future doMultitest(Path filePath, String outputDir, Path suiteDir,
|
| +// CreateTest doTest, bool hotReload) {
|
| +// void writeFile(String filepath, String content) {
|
| +// final File file = new File(filepath);
|
| +
|
| +// if (file.existsSync()) {
|
| +// var oldContent = file.readAsStringSync();
|
| +// if (oldContent == content) {
|
| +// // Don't write to the file if the content is the same
|
| +// return;
|
| +// }
|
| +// }
|
| +// file.writeAsStringSync(content);
|
| +// }
|
| +
|
| +// // Each new test is a single String value in the Map tests.
|
| +// Map<String, String> tests = new Map<String, String>();
|
| +// Map<String, Set<String>> outcomes = new Map<String, Set<String>>();
|
| +// ExtractTestsFromMultitest(filePath, tests, outcomes);
|
| +
|
| +// Path sourceDir = filePath.directoryPath;
|
| +// Path targetDir = createMultitestDirectory(outputDir, suiteDir, sourceDir);
|
| +// assert(targetDir != null);
|
| +
|
| +// // Copy all the relative imports of the multitest.
|
| +// Set<String> importsToCopy = _findAllRelativeImports(filePath);
|
| +// List<Future> futureCopies = [];
|
| +// for (String relativeImport in importsToCopy) {
|
| +// Path importPath = new Path(relativeImport);
|
| +// // Make sure the target directory exists.
|
| +// Path importDir = importPath.directoryPath;
|
| +// if (!importDir.isEmpty) {
|
| +// TestUtils.mkdirRecursive(targetDir, importDir);
|
| +// }
|
| +// // Copy file.
|
| +// futureCopies.add(TestUtils.copyFile(
|
| +// sourceDir.join(importPath), targetDir.join(importPath)));
|
| +// }
|
| +
|
| +// // Wait until all imports are copied before scheduling test cases.
|
| +// return Future.wait(futureCopies).then((_) {
|
| +// String baseFilename = filePath.filenameWithoutExtension;
|
| +// for (String key in tests.keys) {
|
| +// final Path multitestFilename =
|
| +// targetDir.append('${baseFilename}_$key.dart');
|
| +// writeFile(multitestFilename.toNativePath(), tests[key]);
|
| +// Set<String> outcome = outcomes[key];
|
| +// bool hasStaticWarning = outcome.contains('static type warning');
|
| +// bool hasRuntimeErrors = outcome.contains('runtime error');
|
| +// bool hasCompileError = outcome.contains('compile-time error');
|
| +// bool isNegativeIfChecked = outcome.contains('dynamic type error');
|
| +// bool hasCompileErrorIfChecked =
|
| +// outcome.contains('checked mode compile-time error');
|
| +// if (hotReload) {
|
| +// if (hasCompileError || hasCompileErrorIfChecked) {
|
| +// // Running a test that expects a compilation error with hot reloading
|
| +// // is redundant with a regular run of the test.
|
| +// continue;
|
| +// }
|
| +// }
|
| +// doTest(multitestFilename, filePath, hasCompileError, hasRuntimeErrors,
|
| +// isNegativeIfChecked: isNegativeIfChecked,
|
| +// hasCompileErrorIfChecked: hasCompileErrorIfChecked,
|
| +// hasStaticWarning: hasStaticWarning,
|
| +// multitestKey: key);
|
| +// }
|
| +
|
| +// return null;
|
| +// });
|
| +// }
|
| +
|
| +// String suiteNameFromPath(Path suiteDir) {
|
| +// var split = suiteDir.segments();
|
| +// // co19 test suite is at tests/co19/src.
|
| +// if (split.last == 'src') {
|
| +// split.removeLast();
|
| +// }
|
| +// return split.last;
|
| +// }
|
| +
|
| +// Path createMultitestDirectory(String outputDir, Path suiteDir, Path sourceDir) {
|
| +// Path relative = sourceDir.relativeTo(suiteDir);
|
| +// Path path = new Path(outputDir)
|
| +// .append('generated_tests')
|
| +// .append(suiteNameFromPath(suiteDir))
|
| +// .join(relative);
|
| +// TestUtils.mkdirRecursive(TestUtils.currentWorkingDirectory, path);
|
| +// return new Path(new File(path.toNativePath()).absolute.path);
|
| +// }
|
|
|