Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 #library("test_suite"); | 5 #library("test_suite"); |
| 6 | 6 |
| 7 #import("status_file_parser.dart"); | 7 #import("status_file_parser.dart"); |
| 8 #import("test_runner.dart"); | 8 #import("test_runner.dart"); |
| 9 #import("multitest.dart"); | 9 #import("multitest.dart"); |
| 10 | 10 |
| 11 | 11 |
| 12 interface TestSuite { | 12 interface TestSuite { |
| 13 void forEachTest(Function onTest, [Function onDone]); | 13 void forEachTest(Function onTest, [Function onDone]); |
|
Bill Hesse
2011/12/09 13:20:15
Why isn't this function signature changed? Does t
Mads Ager (google)
2011/12/09 13:21:58
Whoops. Thanks! Done.
| |
| 14 } | 14 } |
| 15 | 15 |
| 16 | 16 |
| 17 class CCTestListerIsolate extends Isolate { | 17 class CCTestListerIsolate extends Isolate { |
| 18 CCTestListerIsolate() : super.heavy(); | 18 CCTestListerIsolate() : super.heavy(); |
| 19 | 19 |
| 20 void main() { | 20 void main() { |
| 21 port.receive((String runnerPath, SendPort replyTo) { | 21 port.receive((String runnerPath, SendPort replyTo) { |
| 22 var p = new Process(runnerPath, ["--list"]); | 22 var p = new Process(runnerPath, ["--list"]); |
| 23 StringInputStream stdoutStream = new StringInputStream(p.stdout); | 23 StringInputStream stdoutStream = new StringInputStream(p.stdout); |
| (...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 92 runnerPath, | 92 runnerPath, |
| 93 args, | 93 args, |
| 94 configuration, | 94 configuration, |
| 95 completeHandler, | 95 completeHandler, |
| 96 expectations)); | 96 expectations)); |
| 97 | 97 |
| 98 receiveTestName.receive(testNameHandler); | 98 receiveTestName.receive(testNameHandler); |
| 99 } | 99 } |
| 100 } | 100 } |
| 101 | 101 |
| 102 void forEachTest(Function onTest, [Function onDone]) { | 102 void forEachTest(Function onTest, Map testCache, [Function onDone]) { |
| 103 doTest = onTest; | 103 doTest = onTest; |
| 104 doDone = (ignore) => (onDone != null) ? onDone() : null; | 104 doDone = (ignore) => (onDone != null) ? onDone() : null; |
| 105 | 105 |
| 106 testExpectations = | 106 testExpectations = |
| 107 new TestExpectations(complexMatching: complexStatusMatching()); | 107 new TestExpectations(complexMatching: complexStatusMatching()); |
| 108 for (var statusFilePath in statusFilePaths) { | 108 for (var statusFilePath in statusFilePaths) { |
| 109 ReadTestExpectationsInto(testExpectations, | 109 ReadTestExpectationsInto(testExpectations, |
| 110 statusFilePath, | 110 statusFilePath, |
| 111 configuration); | 111 configuration); |
| 112 } | 112 } |
| 113 | 113 |
| 114 receiveTestName = new ReceivePort(); | 114 receiveTestName = new ReceivePort(); |
| 115 new CCTestListerIsolate().spawn().then((port) { | 115 new CCTestListerIsolate().spawn().then((port) { |
| 116 port.send(runnerPath, receiveTestName.toSendPort()); | 116 port.send(runnerPath, receiveTestName.toSendPort()); |
| 117 receiveTestName.receive(testNameHandler); | 117 receiveTestName.receive(testNameHandler); |
| 118 }); | 118 }); |
| 119 } | 119 } |
| 120 | 120 |
| 121 void completeHandler(TestCase testCase) { | 121 void completeHandler(TestCase testCase) { |
| 122 } | 122 } |
| 123 } | 123 } |
| 124 | 124 |
| 125 | 125 |
| 126 class TestInformation { | |
| 127 String filename; | |
| 128 Map optionsFromFile; | |
| 129 bool isNegative; | |
| 130 bool isNegativeIfChecked; | |
| 131 bool hasFatalTypeErrors; | |
| 132 | |
| 133 TestInformation(this.filename, this.optionsFromFile, this.isNegative, | |
| 134 this.isNegativeIfChecked, this.hasFatalTypeErrors); | |
| 135 } | |
| 136 | |
| 137 | |
| 126 class StandardTestSuite implements TestSuite { | 138 class StandardTestSuite implements TestSuite { |
| 127 Map configuration; | 139 Map configuration; |
| 128 String suiteName; | 140 String suiteName; |
| 129 String directoryPath; | 141 String directoryPath; |
| 130 List<String> statusFilePaths; | 142 List<String> statusFilePaths; |
| 131 Function doTest; | 143 Function doTest; |
| 132 Function doDone; | 144 Function doDone; |
| 133 int activeTestGenerators = 0; | 145 int activeTestGenerators = 0; |
| 134 bool listingDone = false; | 146 bool listingDone = false; |
| 135 TestExpectations testExpectations; | 147 TestExpectations testExpectations; |
| 148 List<TestInformation> cachedTests; | |
| 136 | 149 |
| 137 StandardTestSuite(Map this.configuration, | 150 StandardTestSuite(Map this.configuration, |
| 138 String this.suiteName, | 151 String this.suiteName, |
| 139 String this.directoryPath, | 152 String this.directoryPath, |
| 140 List<String> this.statusFilePaths); | 153 List<String> this.statusFilePaths); |
| 141 | 154 |
| 142 void isTestFile(String filename) => filename.endsWith("Test.dart"); | 155 void isTestFile(String filename) => filename.endsWith("Test.dart"); |
| 143 | 156 |
| 144 void listRecursively() => false; | 157 void listRecursively() => false; |
| 145 | 158 |
| 146 void complexStatusMatching() => false; | 159 void complexStatusMatching() => false; |
| 147 | 160 |
| 148 String shellPath() => TestUtils.dartShellFileName(configuration); | 161 String shellPath() => TestUtils.dartShellFileName(configuration); |
| 149 | 162 |
| 150 List<String> additionalOptions() => []; | 163 List<String> additionalOptions() => []; |
| 151 | 164 |
| 152 void forEachTest(Function onTest, [Function onDone = null]) { | 165 void forEachTest(Function onTest, Map testCache, [Function onDone = null]) { |
| 153 doTest = onTest; | 166 doTest = onTest; |
| 154 doDone = (onDone != null) ? onDone : (() => null); | 167 doDone = (onDone != null) ? onDone : (() => null); |
| 155 | 168 |
| 156 // Read test expectations from status files. | 169 // Read test expectations from status files. |
| 157 testExpectations = | 170 testExpectations = |
| 158 new TestExpectations(complexMatching: complexStatusMatching()); | 171 new TestExpectations(complexMatching: complexStatusMatching()); |
| 159 for (var statusFilePath in statusFilePaths) { | 172 for (var statusFilePath in statusFilePaths) { |
| 160 ReadTestExpectationsInto(testExpectations, | 173 ReadTestExpectationsInto(testExpectations, |
| 161 statusFilePath, | 174 statusFilePath, |
| 162 configuration); | 175 configuration); |
| 163 } | 176 } |
| 164 | 177 |
| 165 processDirectory(); | 178 // Checked if we have already found and generated the tests for |
| 179 // this suite. | |
| 180 if (!testCache.containsKey(suiteName)) { | |
| 181 cachedTests = testCache[suiteName] = []; | |
| 182 processDirectory(); | |
| 183 } else { | |
| 184 // We rely on enqueueing completing asynchroneously so use a | |
|
Bill Hesse
2011/12/09 13:20:15
asynchronously
Mads Ager (google)
2011/12/09 13:21:58
Done.
| |
| 185 // timer to make it so. | |
| 186 void enqueueCachedTests(Timer ignore) { | |
| 187 for (var info in testCache[suiteName]) { | |
| 188 enqueueTestCaseFromTestInformation(info); | |
| 189 } | |
| 190 doDone(); | |
| 191 } | |
| 192 new Timer(enqueueCachedTests, 0, false); | |
| 193 } | |
| 166 } | 194 } |
| 167 | 195 |
| 168 void processDirectory() { | 196 void processDirectory() { |
| 169 directoryPath = getDirname(directoryPath); | 197 directoryPath = getDirname(directoryPath); |
| 170 Directory dir = new Directory(directoryPath); | 198 Directory dir = new Directory(directoryPath); |
| 171 dir.errorHandler = (s) { | 199 dir.errorHandler = (s) { |
| 172 throw s; | 200 throw s; |
| 173 }; | 201 }; |
| 174 dir.fileHandler = processFile; | 202 dir.fileHandler = processFile; |
| 175 dir.doneHandler = directoryListingDone; | 203 dir.doneHandler = directoryListingDone; |
| 176 dir.list(recursive: listRecursively()); | 204 dir.list(recursive: listRecursively()); |
| 177 } | 205 } |
| 178 | 206 |
| 179 Function makeTestCaseCreator(Map optionsFromFile, Map configuration) { | 207 void enqueueTestCaseFromTestInformation(TestInformation info) { |
| 208 var filename = info.filename; | |
| 209 var optionsFromFile = info.optionsFromFile; | |
| 210 var isNegative = info.isNegative; | |
| 211 | |
| 212 // Look up expectations in status files using a modified file path. | |
| 213 String pathSeparator = new Platform().pathSeparator(); | |
| 214 String testName; | |
| 215 int start = filename.lastIndexOf('src' + pathSeparator); | |
| 216 if (start != -1) { | |
| 217 testName = filename.substring(start + 4, filename.length - 5); | |
| 218 } else if (optionsFromFile['isMultitest']) { | |
| 219 start = filename.lastIndexOf(pathSeparator); | |
| 220 int middle = filename.lastIndexOf('_'); | |
| 221 testName = filename.substring(start + 1, middle) + pathSeparator + | |
| 222 filename.substring(middle + 1, filename.length - 5); | |
| 223 } else { | |
| 224 // This case is hit by the dartc client compilation | |
| 225 // tests. These tests are pretty broken compared to the | |
| 226 // rest. They use the .dart suffix in the status files. They | |
| 227 // find tests in weird ways (testing that they contain "#"). | |
| 228 // They need to be redone. | |
| 229 start = filename.indexOf(directoryPath); | |
| 230 testName = filename.substring(start + directoryPath.length + 1, | |
| 231 filename.length); | |
| 232 } | |
| 233 Set<String> expectations = testExpectations.expectations(testName); | |
| 234 if (configuration["report"]) { | |
| 235 // Tests with multiple VMOptions are counted more than once. | |
| 236 for (var dummy in optionsFromFile["vmOptions"]) { | |
| 237 SummaryReport.add(expectations); | |
| 238 } | |
| 239 } | |
| 240 if (expectations.contains(SKIP)) return; | |
| 241 | |
| 242 // Only dartc supports fatal type errors. Enable fatal type | |
| 243 // errors with a flag and treat tests that have fatal type | |
| 244 // errors as negetive. | |
|
Bill Hesse
2011/12/09 13:20:15
negative
Mads Ager (google)
2011/12/09 13:21:58
Done.
| |
| 245 var enableFatalTypeErrors = | |
| 246 (info.hasFatalTypeErrors && configuration['component'] == 'dartc'); | |
| 247 var argumentLists = argumentListsFromFile(filename, | |
| 248 optionsFromFile, | |
| 249 enableFatalTypeErrors); | |
| 250 isNegative = isNegative || | |
| 251 (configuration['checked'] && info.isNegativeIfChecked) || | |
| 252 enableFatalTypeErrors; | |
| 253 | |
| 254 for (var args in argumentLists) { | |
| 255 doTest(new TestCase('$suiteName/$testName', | |
| 256 shellPath(), | |
| 257 args, | |
| 258 configuration, | |
| 259 completeHandler, | |
| 260 expectations, | |
| 261 isNegative)); | |
| 262 } | |
| 263 } | |
| 264 | |
| 265 Function makeTestCaseCreator(Map optionsFromFile) { | |
| 180 return (String filename, | 266 return (String filename, |
| 181 bool isNegative, | 267 bool isNegative, |
| 182 [bool isNegativeIfChecked = false, | 268 [bool isNegativeIfChecked = false, |
| 183 bool enableFatalTypeErrors = false]) { | 269 bool hasFatalTypeErrors = false]) { |
| 184 // Look up expectations in status files using a modified file path. | 270 // Cache the test information for each test case. |
| 185 String pathSeparator = new Platform().pathSeparator(); | 271 var info = new TestInformation(filename, |
| 186 String testName; | 272 optionsFromFile, |
| 187 int start = filename.lastIndexOf('src' + pathSeparator); | 273 isNegative, |
| 188 if (start != -1) { | 274 isNegativeIfChecked, |
| 189 testName = filename.substring(start + 4, filename.length - 5); | 275 hasFatalTypeErrors); |
| 190 } else if (optionsFromFile['isMultitest']) { | 276 cachedTests.add(info); |
| 191 start = filename.lastIndexOf(pathSeparator); | 277 enqueueTestCaseFromTestInformation(info); |
| 192 int middle = filename.lastIndexOf('_'); | |
| 193 testName = filename.substring(start + 1, middle) + pathSeparator + | |
| 194 filename.substring(middle + 1, filename.length - 5); | |
| 195 } else { | |
| 196 // This case is hit by the dartc client compilation | |
| 197 // tests. These tests are pretty broken compared to the | |
| 198 // rest. They use the .dart suffix in the status files. They | |
| 199 // find tests in weird ways (testing that they contain "#"). | |
| 200 // They need to be redone. | |
| 201 start = filename.indexOf(directoryPath); | |
| 202 testName = filename.substring(start + directoryPath.length + 1, | |
| 203 filename.length); | |
| 204 } | |
| 205 Set<String> expectations = testExpectations.expectations(testName); | |
| 206 if (configuration["report"]) { | |
| 207 // Tests with multiple VMOptions are counted more than once. | |
| 208 for (var dummy in optionsFromFile["vmOptions"]) { | |
| 209 SummaryReport.add(expectations); | |
| 210 } | |
| 211 } | |
| 212 if (expectations.contains(SKIP)) return; | |
| 213 | |
| 214 isNegative = isNegative || | |
| 215 (configuration['checked'] && isNegativeIfChecked); | |
| 216 var argumentLists = argumentListsFromFile(filename, | |
| 217 optionsFromFile, | |
| 218 enableFatalTypeErrors); | |
| 219 for (var args in argumentLists) { | |
| 220 doTest(new TestCase('$suiteName/$testName', | |
| 221 shellPath(), | |
| 222 args, | |
| 223 configuration, | |
| 224 completeHandler, | |
| 225 expectations, | |
| 226 isNegative)); | |
| 227 } | |
| 228 }; | 278 }; |
| 229 } | 279 } |
| 230 | 280 |
| 231 void processFile(String filename) { | 281 void processFile(String filename) { |
| 232 if (!isTestFile(filename)) return; | 282 if (!isTestFile(filename)) return; |
| 233 | 283 |
| 234 // Only run the tests that match the pattern. | 284 // Only run the tests that match the pattern. |
| 235 RegExp pattern = configuration['selectors'][suiteName]; | 285 RegExp pattern = configuration['selectors'][suiteName]; |
| 236 if (!pattern.hasMatch(filename)) return; | 286 if (!pattern.hasMatch(filename)) return; |
| 237 | 287 |
| 238 var optionsFromFile = optionsFromFile(filename); | 288 var optionsFromFile = optionsFromFile(filename); |
| 239 Function createTestCase = | 289 Function createTestCase = makeTestCaseCreator(optionsFromFile); |
| 240 makeTestCaseCreator(optionsFromFile, configuration); | |
| 241 | 290 |
| 242 if (optionsFromFile['isMultitest']) { | 291 if (optionsFromFile['isMultitest']) { |
| 243 bool supportsFatalTypeErrors = (configuration['component'] == 'dartc'); | |
| 244 testGeneratorStarted(); | 292 testGeneratorStarted(); |
| 245 DoMultitest(filename, | 293 DoMultitest(filename, |
| 246 TestUtils.buildDir(configuration), | 294 TestUtils.outputDir(configuration), |
| 247 directoryPath, | 295 directoryPath, |
| 248 supportsFatalTypeErrors, | |
| 249 createTestCase, | 296 createTestCase, |
| 250 testGeneratorDone); | 297 testGeneratorDone); |
| 251 } else { | 298 } else { |
| 252 createTestCase(filename, optionsFromFile['isNegative']); | 299 createTestCase(filename, optionsFromFile['isNegative']); |
| 253 } | 300 } |
| 254 } | 301 } |
| 255 | 302 |
| 256 void testGeneratorStarted() { | 303 void testGeneratorStarted() { |
| 257 ++activeTestGenerators; | 304 ++activeTestGenerators; |
| 258 } | 305 } |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 272 } | 319 } |
| 273 | 320 |
| 274 void completeHandler(TestCase testCase) { | 321 void completeHandler(TestCase testCase) { |
| 275 } | 322 } |
| 276 | 323 |
| 277 List<List<String>> argumentListsFromFile(String filename, | 324 List<List<String>> argumentListsFromFile(String filename, |
| 278 Map optionsFromFile, | 325 Map optionsFromFile, |
| 279 bool enableFatalTypeErrors) { | 326 bool enableFatalTypeErrors) { |
| 280 List args = TestUtils.standardOptions(configuration); | 327 List args = TestUtils.standardOptions(configuration); |
| 281 args.addAll(additionalOptions()); | 328 args.addAll(additionalOptions()); |
| 282 if (enableFatalTypeErrors) args.add('--fatal-type-errors'); | 329 if (enableFatalTypeErrors && configuration['component'] == 'dartc') { |
| 330 args.add('--fatal-type-errors'); | |
| 331 } | |
| 283 | 332 |
| 284 bool isMultitest = optionsFromFile["isMultitest"]; | 333 bool isMultitest = optionsFromFile["isMultitest"]; |
| 285 List<String> dartOptions = optionsFromFile["dartOptions"]; | 334 List<String> dartOptions = optionsFromFile["dartOptions"]; |
| 286 List<List<String>> vmOptionsList = optionsFromFile["vmOptions"]; | 335 List<List<String>> vmOptionsList = optionsFromFile["vmOptions"]; |
| 287 Expect.isTrue(!isMultitest || dartOptions == null); | 336 Expect.isTrue(!isMultitest || dartOptions == null); |
| 288 if (dartOptions == null) { | 337 if (dartOptions == null) { |
| 289 args.add(filename); | 338 args.add(filename); |
| 290 } else { | 339 } else { |
| 291 var filename = dartOptions[0]; | 340 var filename = dartOptions[0]; |
| 292 // TODO(ager): Get rid of this hack when the runtime checkout goes away. | 341 // TODO(ager): Get rid of this hack when the runtime checkout goes away. |
| 293 var file = new File(filename); | 342 var file = new File(filename); |
| 294 if (!file.existsSync()) { | 343 if (!file.existsSync()) { |
| 295 filename = '../$filename'; | 344 filename = '../$filename'; |
| 296 Expect.isTrue(new File(filename).existsSync()); | 345 Expect.isTrue(new File(filename).existsSync()); |
| 297 dartOptions[0] = filename; | 346 dartOptions[0] = filename; |
| 298 } | 347 } |
| 299 args.addAll(dartOptions); | 348 args.addAll(dartOptions); |
| 300 } | 349 } |
| 301 | 350 |
| 302 var result = new List<List<String>>(); | 351 var result = new List<List<String>>(); |
| 303 Expect.isFalse(vmOptionsList.isEmpty(), "empty vmOptionsList"); | 352 Expect.isFalse(vmOptionsList.isEmpty(), "empty vmOptionsList"); |
| 304 for (var vmOptions in vmOptionsList) { | 353 for (var vmOptions in vmOptionsList) { |
| 305 if (isMultitest) { | 354 var options = new List<String>.from(vmOptions); |
| 306 // Make copy of vmOptions, since we will modify it at each iteration. | 355 options.addAll(args); |
| 307 vmOptions = new List<String>.from(vmOptions); | 356 result.add(options); |
| 308 } | |
| 309 vmOptions.addAll(args); | |
| 310 result.add(vmOptions); | |
| 311 } | 357 } |
| 312 | 358 |
| 313 return result; | 359 return result; |
| 314 } | 360 } |
| 315 | 361 |
| 316 Map optionsFromFile(String filename) { | 362 Map optionsFromFile(String filename) { |
| 317 RegExp testOptionsRegExp = const RegExp(@"// VMOptions=(.*)"); | 363 RegExp testOptionsRegExp = const RegExp(@"// VMOptions=(.*)"); |
| 318 RegExp dartOptionsRegExp = const RegExp(@"// DartOptions=(.*)"); | 364 RegExp dartOptionsRegExp = const RegExp(@"// DartOptions=(.*)"); |
| 319 RegExp multiTestRegExp = const RegExp(@"/// [0-9][0-9]:(.*)"); | 365 RegExp multiTestRegExp = const RegExp(@"/// [0-9][0-9]:(.*)"); |
| 320 RegExp leadingHashRegExp = const RegExp(@"^#", multiLine: true); | 366 RegExp leadingHashRegExp = const RegExp(@"^#", multiLine: true); |
| (...skipping 137 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 458 } | 504 } |
| 459 | 505 |
| 460 static String dartcCompilationShellPath(Map configuration) { | 506 static String dartcCompilationShellPath(Map configuration) { |
| 461 var name = buildDir(configuration) + 'compiler/bin/dartc'; | 507 var name = buildDir(configuration) + 'compiler/bin/dartc'; |
| 462 if (!(new File(name)).existsSync()) { | 508 if (!(new File(name)).existsSync()) { |
| 463 throw "Executable '$name' does not exist"; | 509 throw "Executable '$name' does not exist"; |
| 464 } | 510 } |
| 465 return name; | 511 return name; |
| 466 } | 512 } |
| 467 | 513 |
| 468 static String buildDir(Map configuration) { | 514 static String outputDir(Map configuration) { |
| 469 var buildDir = ''; | 515 var outputDir = ''; |
| 470 var system = configuration['system']; | 516 var system = configuration['system']; |
| 471 if (system == 'linux') { | 517 if (system == 'linux') { |
| 472 buildDir = 'out/'; | 518 outputDir = 'out/'; |
| 473 } else if (system == 'macos') { | 519 } else if (system == 'macos') { |
| 474 buildDir = 'xcodebuild/'; | 520 outputDir = 'xcodebuild/'; |
| 475 } | 521 } |
| 522 return outputDir; | |
| 523 } | |
| 524 | |
| 525 static String buildDir(Map configuration) { | |
| 526 var buildDir = outputDir(configuration); | |
| 476 buildDir += (configuration['mode'] == 'debug') ? 'Debug_' : 'Release_'; | 527 buildDir += (configuration['mode'] == 'debug') ? 'Debug_' : 'Release_'; |
| 477 buildDir += configuration['arch'] + '/'; | 528 buildDir += configuration['arch'] + '/'; |
| 478 return buildDir; | 529 return buildDir; |
| 479 } | 530 } |
| 480 | 531 |
| 481 static List<String> standardOptions(Map configuration) { | 532 static List<String> standardOptions(Map configuration) { |
| 482 List args = ["--ignore-unrecognized-flags"]; | 533 List args = ["--ignore-unrecognized-flags"]; |
| 483 if (configuration["checked"]) { | 534 if (configuration["checked"]) { |
| 484 args.add('--enable_asserts'); | 535 args.add('--enable_asserts'); |
| 485 args.add("--enable_type_checks"); | 536 args.add("--enable_type_checks"); |
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 541 * $noCrash tests are expected to be flaky but not crash | 592 * $noCrash tests are expected to be flaky but not crash |
| 542 * $pass tests are expected to pass | 593 * $pass tests are expected to pass |
| 543 * $failOk tests are expected to fail that we won't fix | 594 * $failOk tests are expected to fail that we won't fix |
| 544 * $fail tests are expected to fail that we should fix | 595 * $fail tests are expected to fail that we should fix |
| 545 * $crash tests are expected to crash that we should fix | 596 * $crash tests are expected to crash that we should fix |
| 546 * $timeout tests are allowed to timeout\ | 597 * $timeout tests are allowed to timeout\ |
| 547 """; | 598 """; |
| 548 print(report); | 599 print(report); |
| 549 } | 600 } |
| 550 } | 601 } |
| OLD | NEW |