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