OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2012, 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 multitest; |
| 6 |
| 7 import "dart:io"; |
| 8 import "package:path/path.dart" as path; |
| 9 import 'package:base_lib/base_lib.dart'; |
| 10 import "utils.dart"; |
| 11 |
| 12 // import "test_suite.dart"; |
| 13 |
| 14 // Multitests are Dart test scripts containing lines of the form |
| 15 // " [some dart code] //# [key]: [error type]" |
| 16 // |
| 17 // To support legacy multi tests we also handle lines of the form |
| 18 // " [some dart code] /// [key]: [error type]" |
| 19 // |
| 20 // For each key in the file, a new test file is made containing all |
| 21 // the normal lines of the file, and all of the multitest lines containing |
| 22 // that key, in the same order as in the source file. The new test is expected |
| 23 // to pass if the error type listed is 'ok', or to fail if there is an error |
| 24 // type of type 'compile-time error', 'runtime error', 'static type warning', or |
| 25 // 'dynamic type error'. The type error tests fail only in checked mode. |
| 26 // There is also a test created from only the untagged lines of the file, |
| 27 // with key "none", which is expected to pass. This library extracts these |
| 28 // tests, writes them into a temporary directory, and passes them to the test |
| 29 // runner. These tests may be referred to in the status files with the |
| 30 // pattern [test name]/[key]. |
| 31 // |
| 32 // For example: file I_am_a_multitest.dart |
| 33 // aaa |
| 34 // bbb //# 02: runtime error |
| 35 // ccc //# 02: continued |
| 36 // ddd //# 07: static type warning |
| 37 // eee //# 10: ok |
| 38 // fff |
| 39 // |
| 40 // should create four tests: |
| 41 // I_am_a_multitest_none.dart |
| 42 // aaa |
| 43 // fff |
| 44 // |
| 45 // I_am_a_multitest_02.dart |
| 46 // aaa |
| 47 // bbb //# 02: runtime error |
| 48 // ccc //# 02: continued |
| 49 // fff |
| 50 // |
| 51 // I_am_a_multitest_07.dart |
| 52 // aaa |
| 53 // ddd //# 07: static type warning |
| 54 // fff |
| 55 // |
| 56 // and I_am_a_multitest_10.dart |
| 57 // aaa |
| 58 // eee //# 10: ok |
| 59 // fff |
| 60 // |
| 61 // Note that it is possible to indicate more than one acceptable outcome |
| 62 // in the case of dynamic and static type warnings |
| 63 // aaa |
| 64 // ddd //# 07: static type warning, dynamic type error |
| 65 // fff |
| 66 |
| 67 /// Until legacy multitests are ported we need to support both /// and //# |
| 68 final _multitestMarker = new RegExp(r"//[/#]"); |
| 69 |
| 70 class ExtractMultitestResult { |
| 71 Map<String, String> tests; |
| 72 Map<String, Set<String>> outcomes; |
| 73 |
| 74 ExtractMultitestResult(this.tests, this.outcomes); |
| 75 } |
| 76 |
| 77 ExtractMultitestResult ExtractTestsFromMultitest( |
| 78 String filePath, Logger logger) { |
| 79 // Read the entire file into a byte buffer and transform it to a |
| 80 // String. This will treat the file as ascii but the only parts |
| 81 // we are interested in will be ascii in any case. |
| 82 |
| 83 Map<String, String> tests = {}; |
| 84 Map<String, Set<String>> outcomes = {}; |
| 85 |
| 86 var bytes = new File(path.absolute(filePath)).readAsBytesSync(); |
| 87 var contents = decodeUtf8(bytes); |
| 88 var firstNewline = contents.indexOf('\n'); |
| 89 var lineSeparator = |
| 90 (firstNewline == 0 || contents[firstNewline - 1] != '\r') ? '\n' : '\r\n'; |
| 91 var lines = contents.split(lineSeparator); |
| 92 if (lines.last == '') lines.removeLast(); |
| 93 bytes = null; |
| 94 contents = null; |
| 95 var validMultitestOutcomes = [ |
| 96 'ok', |
| 97 'compile-time error', |
| 98 'runtime error', |
| 99 'static type warning', |
| 100 'dynamic type error', |
| 101 'checked mode compile-time error' |
| 102 ].toSet(); |
| 103 |
| 104 // Create the set of multitests, which will have a new test added each |
| 105 // time we see a multitest line with a new key. |
| 106 var testsAsLines = <String, List<String>>{}; |
| 107 |
| 108 // Add the default case with key "none". |
| 109 testsAsLines['none'] = <String>[]; |
| 110 outcomes['none'] = new Set<String>(); |
| 111 |
| 112 var lineCount = 0; |
| 113 for (var line in lines) { |
| 114 lineCount++; |
| 115 var annotation = new _Annotation.from(line); |
| 116 if (annotation != null) { |
| 117 testsAsLines.putIfAbsent( |
| 118 annotation.key, () => new List<String>.from(testsAsLines["none"])); |
| 119 // Add line to test with annotation.key as key, empty line to the rest. |
| 120 for (var key in testsAsLines.keys) { |
| 121 testsAsLines[key].add(annotation.key == key ? line : ""); |
| 122 } |
| 123 outcomes.putIfAbsent(annotation.key, () => new Set<String>()); |
| 124 if (annotation.rest != 'continued') { |
| 125 for (String nextOutcome in annotation.outcomesList) { |
| 126 if (validMultitestOutcomes.contains(nextOutcome)) { |
| 127 outcomes[annotation.key].add(nextOutcome); |
| 128 } else { |
| 129 logger.warning( |
| 130 "Warning: Invalid test directive '$nextOutcome' on line " |
| 131 "${lineCount}:\n${annotation.rest} "); |
| 132 } |
| 133 } |
| 134 } |
| 135 } else { |
| 136 for (var test in testsAsLines.values) test.add(line); |
| 137 } |
| 138 } |
| 139 // End marker, has a final line separator so we don't need to add it after |
| 140 // joining the lines. |
| 141 var marker = |
| 142 '// Test created from multitest named ${path.absolute(filePath)}.' |
| 143 '$lineSeparator'; |
| 144 for (var test in testsAsLines.values) test.add(marker); |
| 145 |
| 146 var keysToDelete = <String>[]; |
| 147 // Check that every key (other than the none case) has at least one outcome |
| 148 for (var outcomeKey in outcomes.keys) { |
| 149 if (outcomeKey != 'none' && outcomes[outcomeKey].isEmpty) { |
| 150 logger.warning( |
| 151 "Warning: Test ${outcomeKey} has no valid annotated outcomes.\n" |
| 152 "Expected one of: ${validMultitestOutcomes.toString()}"); |
| 153 // If this multitest doesn't have an outcome, mark the multitest for |
| 154 // deletion. |
| 155 keysToDelete.add(outcomeKey); |
| 156 } |
| 157 } |
| 158 // If a key/multitest was marked for deletion, do the necessary cleanup. |
| 159 keysToDelete.forEach(outcomes.remove); |
| 160 keysToDelete.forEach(testsAsLines.remove); |
| 161 |
| 162 // Copy all the tests into the output map tests, as multiline strings. |
| 163 for (var key in testsAsLines.keys) { |
| 164 tests[key] = testsAsLines[key].join(lineSeparator); |
| 165 } |
| 166 |
| 167 return new ExtractMultitestResult(tests, outcomes); |
| 168 } |
| 169 |
| 170 // Represents a mutlitest annotation in the special //# comment. |
| 171 class _Annotation { |
| 172 String key; |
| 173 String rest; |
| 174 List<String> outcomesList; |
| 175 _Annotation() {} |
| 176 factory _Annotation.from(String line) { |
| 177 // Do an early return with "null" if this is not a valid multitest |
| 178 // annotation. |
| 179 if (!line.contains(_multitestMarker)) { |
| 180 return null; |
| 181 } |
| 182 var parts = line |
| 183 .split(_multitestMarker)[1] |
| 184 .split(':') |
| 185 .map((s) => s.trim()) |
| 186 .where((s) => s.length > 0) |
| 187 .toList(); |
| 188 if (parts.length <= 1) { |
| 189 return null; |
| 190 } |
| 191 |
| 192 var annotation = new _Annotation(); |
| 193 annotation.key = parts[0]; |
| 194 annotation.rest = parts[1]; |
| 195 annotation.outcomesList = |
| 196 annotation.rest.split(',').map((s) => s.trim()).toList(); |
| 197 return annotation; |
| 198 } |
| 199 } |
| 200 |
| 201 // // Find all relative imports and copy them into the dir that contains |
| 202 // // the generated tests. |
| 203 // Set<String> _findAllRelativeImports(String topLibrary) { |
| 204 // var toSearch = [topLibrary].toSet(); |
| 205 // var foundImports = new Set<String>(); |
| 206 // var libraryDir = topLibrary.directoryPath; |
| 207 // var relativeImportRegExp = new RegExp( |
| 208 // '^(?:@.*\\s+)?' // Allow for a meta-data annotation. |
| 209 // '(import|part)' |
| 210 // '\\s+["\']' |
| 211 // '(?!(dart:|dart-ext:|data:|package:|/))' // Look-ahead: not in package. |
| 212 // '([^"\']*)' // The path to the imported file. |
| 213 // '["\']'); |
| 214 // while (!toSearch.isEmpty) { |
| 215 // var thisPass = toSearch; |
| 216 // toSearch = new Set<String>(); |
| 217 // for (String filename in thisPass) { |
| 218 // File f = new File(path.absolute(filename)); |
| 219 // for (String line in f.readAsLinesSync()) { |
| 220 // Match match = relativeImportRegExp.firstMatch(line); |
| 221 // if (match != null) { |
| 222 // Path relativePath = new Path(match.group(3)); |
| 223 // if (foundImports.contains(relativePath.toString())) { |
| 224 // continue; |
| 225 // } |
| 226 // if (relativePath.toString().contains('..')) { |
| 227 // // This is just for safety reasons, we don't want |
| 228 // // to unintentionally clobber files relative to the destination |
| 229 // // dir when copying them ove. |
| 230 // print("relative paths containing .. are not allowed."); |
| 231 // exit(1); |
| 232 // } |
| 233 // foundImports.add(relativePath.toString()); |
| 234 // toSearch.add(libraryDir.join(relativePath)); |
| 235 // } |
| 236 // } |
| 237 // } |
| 238 // } |
| 239 // return foundImports; |
| 240 // } |
| 241 |
| 242 // Future doMultitest(Path filePath, String outputDir, Path suiteDir, |
| 243 // CreateTest doTest, bool hotReload) { |
| 244 // void writeFile(String filepath, String content) { |
| 245 // final File file = new File(filepath); |
| 246 |
| 247 // if (file.existsSync()) { |
| 248 // var oldContent = file.readAsStringSync(); |
| 249 // if (oldContent == content) { |
| 250 // // Don't write to the file if the content is the same |
| 251 // return; |
| 252 // } |
| 253 // } |
| 254 // file.writeAsStringSync(content); |
| 255 // } |
| 256 |
| 257 // // Each new test is a single String value in the Map tests. |
| 258 // Map<String, String> tests = new Map<String, String>(); |
| 259 // Map<String, Set<String>> outcomes = new Map<String, Set<String>>(); |
| 260 // ExtractTestsFromMultitest(filePath, tests, outcomes); |
| 261 |
| 262 // Path sourceDir = filePath.directoryPath; |
| 263 // Path targetDir = createMultitestDirectory(outputDir, suiteDir, sourceDir); |
| 264 // assert(targetDir != null); |
| 265 |
| 266 // // Copy all the relative imports of the multitest. |
| 267 // Set<String> importsToCopy = _findAllRelativeImports(filePath); |
| 268 // List<Future> futureCopies = []; |
| 269 // for (String relativeImport in importsToCopy) { |
| 270 // Path importPath = new Path(relativeImport); |
| 271 // // Make sure the target directory exists. |
| 272 // Path importDir = importPath.directoryPath; |
| 273 // if (!importDir.isEmpty) { |
| 274 // TestUtils.mkdirRecursive(targetDir, importDir); |
| 275 // } |
| 276 // // Copy file. |
| 277 // futureCopies.add(TestUtils.copyFile( |
| 278 // sourceDir.join(importPath), targetDir.join(importPath))); |
| 279 // } |
| 280 |
| 281 // // Wait until all imports are copied before scheduling test cases. |
| 282 // return Future.wait(futureCopies).then((_) { |
| 283 // String baseFilename = filePath.filenameWithoutExtension; |
| 284 // for (String key in tests.keys) { |
| 285 // final Path multitestFilename = |
| 286 // targetDir.append('${baseFilename}_$key.dart'); |
| 287 // writeFile(multitestFilename.toNativePath(), tests[key]); |
| 288 // Set<String> outcome = outcomes[key]; |
| 289 // bool hasStaticWarning = outcome.contains('static type warning'); |
| 290 // bool hasRuntimeErrors = outcome.contains('runtime error'); |
| 291 // bool hasCompileError = outcome.contains('compile-time error'); |
| 292 // bool isNegativeIfChecked = outcome.contains('dynamic type error'); |
| 293 // bool hasCompileErrorIfChecked = |
| 294 // outcome.contains('checked mode compile-time error'); |
| 295 // if (hotReload) { |
| 296 // if (hasCompileError || hasCompileErrorIfChecked) { |
| 297 // // Running a test that expects a compilation error with hot reloadi
ng |
| 298 // // is redundant with a regular run of the test. |
| 299 // continue; |
| 300 // } |
| 301 // } |
| 302 // doTest(multitestFilename, filePath, hasCompileError, hasRuntimeErrors, |
| 303 // isNegativeIfChecked: isNegativeIfChecked, |
| 304 // hasCompileErrorIfChecked: hasCompileErrorIfChecked, |
| 305 // hasStaticWarning: hasStaticWarning, |
| 306 // multitestKey: key); |
| 307 // } |
| 308 |
| 309 // return null; |
| 310 // }); |
| 311 // } |
| 312 |
| 313 // String suiteNameFromPath(Path suiteDir) { |
| 314 // var split = suiteDir.segments(); |
| 315 // // co19 test suite is at tests/co19/src. |
| 316 // if (split.last == 'src') { |
| 317 // split.removeLast(); |
| 318 // } |
| 319 // return split.last; |
| 320 // } |
| 321 |
| 322 // Path createMultitestDirectory(String outputDir, Path suiteDir, Path sourceDir
) { |
| 323 // Path relative = sourceDir.relativeTo(suiteDir); |
| 324 // Path path = new Path(outputDir) |
| 325 // .append('generated_tests') |
| 326 // .append(suiteNameFromPath(suiteDir)) |
| 327 // .join(relative); |
| 328 // TestUtils.mkdirRecursive(TestUtils.currentWorkingDirectory, path); |
| 329 // return new Path(new File(path.toNativePath()).absolute.path); |
| 330 // } |
OLD | NEW |