| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 library command; |
| 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 | 2 |
| 5 /** | 3 import 'dart:async'; |
| 6 * Classes and methods for executing tests. | 4 import 'dart:convert' show LineSplitter, UTF8, JSON; |
| 7 * | |
| 8 * This module includes: | |
| 9 * - Managing parallel execution of tests, including timeout checks. | |
| 10 * - Evaluating the output of each test as pass/fail/crash/timeout. | |
| 11 */ | |
| 12 library test_runner; | |
| 13 | |
| 14 import "dart:async"; | |
| 15 import "dart:collection" show Queue; | |
| 16 import "dart:convert" show LineSplitter, UTF8, JSON; | |
| 17 // We need to use the 'io' prefix here, otherwise io.exitCode will shadow | 5 // We need to use the 'io' prefix here, otherwise io.exitCode will shadow |
| 18 // CommandOutput.exitCode in subclasses of CommandOutput. | 6 // CommandOutput.exitCode in subclasses of CommandOutput. |
| 19 import "dart:io" as io; | 7 import 'dart:io' as io; |
| 20 import "dart:math" as math; | |
| 21 import 'dependency_graph.dart' as dgraph; | |
| 22 import "browser_controller.dart"; | |
| 23 import "path.dart"; | |
| 24 import "status_file_parser.dart"; | |
| 25 import "test_progress.dart"; | |
| 26 import "test_suite.dart"; | |
| 27 import "utils.dart"; | |
| 28 import 'record_and_replay.dart'; | |
| 29 | 8 |
| 30 const int CRASHING_BROWSER_EXITCODE = -10; | 9 import 'browser_controller.dart'; |
| 31 const int SLOW_TIMEOUT_MULTIPLIER = 4; | 10 import 'path.dart'; |
| 11 import 'status_file_parser.dart'; |
| 12 import 'test_case.dart'; |
| 13 import 'test_utils.dart'; |
| 14 import 'utils.dart'; |
| 15 |
| 16 const CRASHING_BROWSER_EXITCODE = -10; |
| 32 | 17 |
| 33 const MESSAGE_CANNOT_OPEN_DISPLAY = 'Gtk-WARNING **: cannot open display'; | 18 const MESSAGE_CANNOT_OPEN_DISPLAY = 'Gtk-WARNING **: cannot open display'; |
| 34 const MESSAGE_FAILED_TO_RUN_COMMAND = 'Failed to run command. return code=1'; | 19 const MESSAGE_FAILED_TO_RUN_COMMAND = 'Failed to run command. return code=1'; |
| 35 | 20 |
| 36 typedef void TestCaseEvent(TestCase testCase); | 21 CommandOutput createCommandOutput(Command command, |
| 37 typedef void ExitCodeEvent(int exitCode); | 22 int exitCode, |
| 38 typedef void EnqueueMoreWork(ProcessQueue queue); | 23 bool timedOut, |
| 24 List<int> stdout, |
| 25 List<int> stderr, |
| 26 Duration time, |
| 27 bool compilationSkipped, |
| 28 [int pid = 0]) { |
| 29 if (command is ContentShellCommand) { |
| 30 return new BrowserCommandOutputImpl( |
| 31 command, exitCode, timedOut, stdout, stderr, |
| 32 time, compilationSkipped); |
| 33 } else if (command is BrowserTestCommand) { |
| 34 return new HTMLBrowserCommandOutputImpl( |
| 35 command, exitCode, timedOut, stdout, stderr, |
| 36 time, compilationSkipped); |
| 37 } else if (command is AnalysisCommand) { |
| 38 return new AnalysisCommandOutputImpl( |
| 39 command, exitCode, timedOut, stdout, stderr, |
| 40 time, compilationSkipped); |
| 41 } else if (command is VmCommand) { |
| 42 return new VmCommandOutputImpl( |
| 43 command, exitCode, timedOut, stdout, stderr, time, pid); |
| 44 } else if (command is CompilationCommand) { |
| 45 return new CompilationCommandOutputImpl( |
| 46 command, exitCode, timedOut, stdout, stderr, time, compilationSkipped); |
| 47 } else if (command is JSCommandlineCommand) { |
| 48 return new JsCommandlineOutputImpl( |
| 49 command, exitCode, timedOut, stdout, stderr, time); |
| 50 } else if (command is PubCommand) { |
| 51 return new PubCommandOutputImpl( |
| 52 command, exitCode, timedOut, stdout, stderr, time); |
| 53 } |
| 39 | 54 |
| 40 // Some IO tests use these variables and get confused if the host environment | 55 return new CommandOutputImpl( |
| 41 // variables are inherited so they are excluded. | 56 command, exitCode, timedOut, stdout, stderr, |
| 42 const List<String> EXCLUDED_ENVIRONMENT_VARIABLES = | 57 time, compilationSkipped, pid); |
| 43 const ['http_proxy', 'https_proxy', 'no_proxy', | 58 } |
| 44 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY']; | |
| 45 | 59 |
| 60 bool shouldRetryCommand(CommandOutput output) { |
| 61 var command = output.command; |
| 62 // We rerun tests on Safari because 6.2 and 7.1 are flaky. Issue 21434. |
| 63 if (command is BrowserTestCommand && |
| 64 command.retry && |
| 65 command.browser == 'safari' && |
| 66 output is BrowserControllerTestOutcome && |
| 67 output._rawOutcome != Expectation.PASS) { |
| 68 return true; |
| 69 } |
| 70 |
| 71 if (!output.successful) { |
| 72 List<String> stdout, stderr; |
| 73 |
| 74 decodeOutput() { |
| 75 if (stdout == null && stderr == null) { |
| 76 stdout = decodeUtf8(output.stderr).split("\n"); |
| 77 stderr = decodeUtf8(output.stderr).split("\n"); |
| 78 } |
| 79 } |
| 80 |
| 81 if (io.Platform.operatingSystem == 'linux') { |
| 82 decodeOutput(); |
| 83 // No matter which command we ran: If we get failures due to the |
| 84 // "xvfb-run" issue 7564, try re-running the test. |
| 85 bool containsFailureMsg(String line) { |
| 86 return line.contains(MESSAGE_CANNOT_OPEN_DISPLAY) || |
| 87 line.contains(MESSAGE_FAILED_TO_RUN_COMMAND); |
| 88 } |
| 89 if (stdout.any(containsFailureMsg) || stderr.any(containsFailureMsg)) { |
| 90 return true; |
| 91 } |
| 92 } |
| 93 |
| 94 // We currently rerun dartium tests, see issue 14074. |
| 95 if (command is BrowserTestCommand && |
| 96 command.retry && |
| 97 command.browser == 'dartium') { |
| 98 return true; |
| 99 } |
| 100 } |
| 101 return false; |
| 102 } |
| 103 |
| 104 /// This is just a Pair(String, Map) class with hashCode and operator == |
| 105 class AddFlagsKey { |
| 106 final String flags; |
| 107 final Map env; |
| 108 AddFlagsKey(this.flags, this.env); |
| 109 // Just use object identity for environment map |
| 110 bool operator ==(other) => |
| 111 other is AddFlagsKey && flags == other.flags && env == other.env; |
| 112 int get hashCode => flags.hashCode ^ env.hashCode; |
| 113 } |
| 46 | 114 |
| 47 /** A command executed as a step in a test case. */ | 115 /** A command executed as a step in a test case. */ |
| 48 class Command { | 116 class Command { |
| 49 /** A descriptive name for this command. */ | 117 /** A descriptive name for this command. */ |
| 50 String displayName; | 118 String displayName; |
| 51 | 119 |
| 52 /** Number of times this command *can* be retried */ | 120 /** Number of times this command *can* be retried */ |
| 53 int get maxNumRetries => 2; | 121 int get maxNumRetries => 2; |
| 54 | 122 |
| 55 /** Reproduction command */ | 123 /** Reproduction command */ |
| (...skipping 145 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 201 builder.addJson(_bootstrapDependencies); | 269 builder.addJson(_bootstrapDependencies); |
| 202 } | 270 } |
| 203 | 271 |
| 204 bool _equal(CompilationCommand other) => | 272 bool _equal(CompilationCommand other) => |
| 205 super._equal(other) && | 273 super._equal(other) && |
| 206 _outputFile == other._outputFile && | 274 _outputFile == other._outputFile && |
| 207 _neverSkipCompilation == other._neverSkipCompilation && | 275 _neverSkipCompilation == other._neverSkipCompilation && |
| 208 deepJsonCompare(_bootstrapDependencies, other._bootstrapDependencies); | 276 deepJsonCompare(_bootstrapDependencies, other._bootstrapDependencies); |
| 209 } | 277 } |
| 210 | 278 |
| 211 /// This is just a Pair(String, Map) class with hashCode and operator == | 279 class CommandBuilder { |
| 212 class AddFlagsKey { | 280 static final CommandBuilder instance = new CommandBuilder._(); |
| 213 final String flags; | 281 |
| 214 final Map env; | 282 bool _cleared = false; |
| 215 AddFlagsKey(this.flags, this.env); | 283 final _cachedCommands = new Map<Command, Command>(); |
| 216 // Just use object identity for environment map | 284 |
| 217 bool operator ==(other) => | 285 CommandBuilder._(); |
| 218 other is AddFlagsKey && flags == other.flags && env == other.env; | 286 |
| 219 int get hashCode => flags.hashCode ^ env.hashCode; | 287 void clearCommandCache() { |
| 288 _cachedCommands.clear(); |
| 289 _cleared = true; |
| 290 } |
| 291 |
| 292 ContentShellCommand getContentShellCommand(String executable, |
| 293 String htmlFile, |
| 294 List<String> options, |
| 295 List<String> dartFlags, |
| 296 Map<String, String> environment) { |
| 297 ContentShellCommand command = new ContentShellCommand._( |
| 298 executable, htmlFile, options, dartFlags, environment); |
| 299 return _getUniqueCommand(command); |
| 300 } |
| 301 |
| 302 BrowserTestCommand getBrowserTestCommand(String browser, |
| 303 String url, |
| 304 Map configuration, |
| 305 bool retry) { |
| 306 var command = new BrowserTestCommand._(browser, url, configuration, retry); |
| 307 return _getUniqueCommand(command); |
| 308 } |
| 309 |
| 310 BrowserHtmlTestCommand getBrowserHtmlTestCommand(String browser, |
| 311 String url, |
| 312 Map configuration, |
| 313 List<String> expectedMessages, |
| 314 bool retry) { |
| 315 var command = new BrowserHtmlTestCommand._( |
| 316 browser, url, configuration, expectedMessages, retry); |
| 317 return _getUniqueCommand(command); |
| 318 } |
| 319 |
| 320 CompilationCommand getCompilationCommand(String displayName, |
| 321 outputFile, |
| 322 neverSkipCompilation, |
| 323 List<Uri> bootstrapDependencies, |
| 324 String executable, |
| 325 List<String> arguments, |
| 326 Map<String, String> environment) { |
| 327 var command = |
| 328 new CompilationCommand._( |
| 329 displayName, outputFile, neverSkipCompilation, |
| 330 bootstrapDependencies, executable, arguments, environment); |
| 331 return _getUniqueCommand(command); |
| 332 } |
| 333 |
| 334 AnalysisCommand getAnalysisCommand( |
| 335 String displayName, executable, arguments, environmentOverrides, |
| 336 {String flavor: 'dartanalyzer'}) { |
| 337 var command = new AnalysisCommand._( |
| 338 flavor, displayName, executable, arguments, environmentOverrides); |
| 339 return _getUniqueCommand(command); |
| 340 } |
| 341 |
| 342 VmCommand getVmCommand(String executable, |
| 343 List<String> arguments, |
| 344 Map<String, String> environmentOverrides) { |
| 345 var command = new VmCommand._(executable, arguments, environmentOverrides); |
| 346 return _getUniqueCommand(command); |
| 347 } |
| 348 |
| 349 Command getJSCommandlineCommand(String displayName, executable, arguments, |
| 350 [environment = null]) { |
| 351 var command = new JSCommandlineCommand._(displayName, executable, arguments, |
| 352 environment); |
| 353 return _getUniqueCommand(command); |
| 354 } |
| 355 |
| 356 Command getProcessCommand(String displayName, executable, arguments, |
| 357 [environment = null, workingDirectory = null]) { |
| 358 var command = new ProcessCommand._(displayName, executable, arguments, |
| 359 environment, workingDirectory); |
| 360 return _getUniqueCommand(command); |
| 361 } |
| 362 |
| 363 Command getCopyCommand(String sourceDirectory, String destinationDirectory) { |
| 364 var command = new CleanDirectoryCopyCommand._(sourceDirectory, |
| 365 destinationDirectory); |
| 366 return _getUniqueCommand(command); |
| 367 } |
| 368 |
| 369 Command getPubCommand(String pubCommand, |
| 370 String pubExecutable, |
| 371 String pubspecYamlDirectory, |
| 372 String pubCacheDirectory) { |
| 373 var command = new PubCommand._(pubCommand, |
| 374 pubExecutable, |
| 375 pubspecYamlDirectory, |
| 376 pubCacheDirectory); |
| 377 return _getUniqueCommand(command); |
| 378 } |
| 379 |
| 380 Command getMakeSymlinkCommand(String link, String target) { |
| 381 return _getUniqueCommand(new MakeSymlinkCommand._(link, target)); |
| 382 } |
| 383 |
| 384 Command getModifyPubspecCommand(String pubspecYamlFile, Map depsOverrides, |
| 385 {String destinationFile: null}) { |
| 386 if (destinationFile == null) destinationFile = pubspecYamlFile; |
| 387 return _getUniqueCommand(new ModifyPubspecYamlCommand._( |
| 388 pubspecYamlFile, destinationFile, depsOverrides)); |
| 389 } |
| 390 |
| 391 Command _getUniqueCommand(Command command) { |
| 392 // All Command classes implement hashCode and operator==. |
| 393 // We check if this command has already been built. |
| 394 // If so, we return the cached one. Otherwise we |
| 395 // store the one given as [command] argument. |
| 396 if (_cleared) { |
| 397 throw new Exception( |
| 398 "CommandBuilder.get[type]Command called after cache cleared"); |
| 399 } |
| 400 var cachedCommand = _cachedCommands[command]; |
| 401 if (cachedCommand != null) { |
| 402 return cachedCommand; |
| 403 } |
| 404 _cachedCommands[command] = command; |
| 405 return command; |
| 406 } |
| 220 } | 407 } |
| 221 | 408 |
| 222 class ContentShellCommand extends ProcessCommand { | 409 class ContentShellCommand extends ProcessCommand { |
| 223 ContentShellCommand._(String executable, | 410 ContentShellCommand._(String executable, |
| 224 String htmlFile, | 411 String htmlFile, |
| 225 List<String> options, | 412 List<String> options, |
| 226 List<String> dartFlags, | 413 List<String> dartFlags, |
| 227 Map<String, String> environmentOverrides) | 414 Map<String, String> environmentOverrides) |
| 228 : super._("content_shell", | 415 : super._("content_shell", |
| 229 executable, | 416 executable, |
| (...skipping 311 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 541 builder.addJson(_link); | 728 builder.addJson(_link); |
| 542 builder.addJson(_target); | 729 builder.addJson(_target); |
| 543 } | 730 } |
| 544 | 731 |
| 545 bool _equal(MakeSymlinkCommand other) => | 732 bool _equal(MakeSymlinkCommand other) => |
| 546 super._equal(other) && | 733 super._equal(other) && |
| 547 _link == other._link && | 734 _link == other._link && |
| 548 _target == other._target; | 735 _target == other._target; |
| 549 } | 736 } |
| 550 | 737 |
| 551 class CommandBuilder { | |
| 552 static final CommandBuilder instance = new CommandBuilder._(); | |
| 553 | |
| 554 bool _cleared = false; | |
| 555 final _cachedCommands = new Map<Command, Command>(); | |
| 556 | |
| 557 CommandBuilder._(); | |
| 558 | |
| 559 void clearCommandCache() { | |
| 560 _cachedCommands.clear(); | |
| 561 _cleared = true; | |
| 562 } | |
| 563 | |
| 564 ContentShellCommand getContentShellCommand(String executable, | |
| 565 String htmlFile, | |
| 566 List<String> options, | |
| 567 List<String> dartFlags, | |
| 568 Map<String, String> environment) { | |
| 569 ContentShellCommand command = new ContentShellCommand._( | |
| 570 executable, htmlFile, options, dartFlags, environment); | |
| 571 return _getUniqueCommand(command); | |
| 572 } | |
| 573 | |
| 574 BrowserTestCommand getBrowserTestCommand(String browser, | |
| 575 String url, | |
| 576 Map configuration, | |
| 577 bool retry) { | |
| 578 var command = new BrowserTestCommand._(browser, url, configuration, retry); | |
| 579 return _getUniqueCommand(command); | |
| 580 } | |
| 581 | |
| 582 BrowserHtmlTestCommand getBrowserHtmlTestCommand(String browser, | |
| 583 String url, | |
| 584 Map configuration, | |
| 585 List<String> expectedMessages, | |
| 586 bool retry) { | |
| 587 var command = new BrowserHtmlTestCommand._( | |
| 588 browser, url, configuration, expectedMessages, retry); | |
| 589 return _getUniqueCommand(command); | |
| 590 } | |
| 591 | |
| 592 CompilationCommand getCompilationCommand(String displayName, | |
| 593 outputFile, | |
| 594 neverSkipCompilation, | |
| 595 List<Uri> bootstrapDependencies, | |
| 596 String executable, | |
| 597 List<String> arguments, | |
| 598 Map<String, String> environment) { | |
| 599 var command = | |
| 600 new CompilationCommand._( | |
| 601 displayName, outputFile, neverSkipCompilation, | |
| 602 bootstrapDependencies, executable, arguments, environment); | |
| 603 return _getUniqueCommand(command); | |
| 604 } | |
| 605 | |
| 606 AnalysisCommand getAnalysisCommand( | |
| 607 String displayName, executable, arguments, environmentOverrides, | |
| 608 {String flavor: 'dartanalyzer'}) { | |
| 609 var command = new AnalysisCommand._( | |
| 610 flavor, displayName, executable, arguments, environmentOverrides); | |
| 611 return _getUniqueCommand(command); | |
| 612 } | |
| 613 | |
| 614 VmCommand getVmCommand(String executable, | |
| 615 List<String> arguments, | |
| 616 Map<String, String> environmentOverrides) { | |
| 617 var command = new VmCommand._(executable, arguments, environmentOverrides); | |
| 618 return _getUniqueCommand(command); | |
| 619 } | |
| 620 | |
| 621 Command getJSCommandlineCommand(String displayName, executable, arguments, | |
| 622 [environment = null]) { | |
| 623 var command = new JSCommandlineCommand._(displayName, executable, arguments, | |
| 624 environment); | |
| 625 return _getUniqueCommand(command); | |
| 626 } | |
| 627 | |
| 628 Command getProcessCommand(String displayName, executable, arguments, | |
| 629 [environment = null, workingDirectory = null]) { | |
| 630 var command = new ProcessCommand._(displayName, executable, arguments, | |
| 631 environment, workingDirectory); | |
| 632 return _getUniqueCommand(command); | |
| 633 } | |
| 634 | |
| 635 Command getCopyCommand(String sourceDirectory, String destinationDirectory) { | |
| 636 var command = new CleanDirectoryCopyCommand._(sourceDirectory, | |
| 637 destinationDirectory); | |
| 638 return _getUniqueCommand(command); | |
| 639 } | |
| 640 | |
| 641 Command getPubCommand(String pubCommand, | |
| 642 String pubExecutable, | |
| 643 String pubspecYamlDirectory, | |
| 644 String pubCacheDirectory) { | |
| 645 var command = new PubCommand._(pubCommand, | |
| 646 pubExecutable, | |
| 647 pubspecYamlDirectory, | |
| 648 pubCacheDirectory); | |
| 649 return _getUniqueCommand(command); | |
| 650 } | |
| 651 | |
| 652 Command getMakeSymlinkCommand(String link, String target) { | |
| 653 return _getUniqueCommand(new MakeSymlinkCommand._(link, target)); | |
| 654 } | |
| 655 | |
| 656 Command getModifyPubspecCommand(String pubspecYamlFile, Map depsOverrides, | |
| 657 {String destinationFile: null}) { | |
| 658 if (destinationFile == null) destinationFile = pubspecYamlFile; | |
| 659 return _getUniqueCommand(new ModifyPubspecYamlCommand._( | |
| 660 pubspecYamlFile, destinationFile, depsOverrides)); | |
| 661 } | |
| 662 | |
| 663 Command _getUniqueCommand(Command command) { | |
| 664 // All Command classes implement hashCode and operator==. | |
| 665 // We check if this command has already been built. | |
| 666 // If so, we return the cached one. Otherwise we | |
| 667 // store the one given as [command] argument. | |
| 668 if (_cleared) { | |
| 669 throw new Exception( | |
| 670 "CommandBuilder.get[type]Command called after cache cleared"); | |
| 671 } | |
| 672 var cachedCommand = _cachedCommands[command]; | |
| 673 if (cachedCommand != null) { | |
| 674 return cachedCommand; | |
| 675 } | |
| 676 _cachedCommands[command] = command; | |
| 677 return command; | |
| 678 } | |
| 679 } | |
| 680 | |
| 681 /** | |
| 682 * TestCase contains all the information needed to run a test and evaluate | |
| 683 * its output. Running a test involves starting a separate process, with | |
| 684 * the executable and arguments given by the TestCase, and recording its | |
| 685 * stdout and stderr output streams, and its exit code. TestCase only | |
| 686 * contains static information about the test; actually running the test is | |
| 687 * performed by [ProcessQueue] using a [RunningProcess] object. | |
| 688 * | |
| 689 * The output information is stored in a [CommandOutput] instance contained | |
| 690 * in TestCase.commandOutputs. The last CommandOutput instance is responsible | |
| 691 * for evaluating if the test has passed, failed, crashed, or timed out, and the | |
| 692 * TestCase has information about what the expected result of the test should | |
| 693 * be. | |
| 694 * | |
| 695 * The TestCase has a callback function, [completedHandler], that is run when | |
| 696 * the test is completed. | |
| 697 */ | |
| 698 class TestCase extends UniqueObject { | |
| 699 // Flags set in _expectations from the optional argument info. | |
| 700 static final int IS_NEGATIVE = 1 << 0; | |
| 701 static final int HAS_RUNTIME_ERROR = 1 << 1; | |
| 702 static final int HAS_STATIC_WARNING = 1 << 2; | |
| 703 static final int IS_NEGATIVE_IF_CHECKED = 1 << 3; | |
| 704 static final int HAS_COMPILE_ERROR = 1 << 4; | |
| 705 static final int HAS_COMPILE_ERROR_IF_CHECKED = 1 << 5; | |
| 706 static final int EXPECT_COMPILE_ERROR = 1 << 6; | |
| 707 /** | |
| 708 * A list of commands to execute. Most test cases have a single command. | |
| 709 * Dart2js tests have two commands, one to compile the source and another | |
| 710 * to execute it. Some isolate tests might even have three, if they require | |
| 711 * compiling multiple sources that are run in isolation. | |
| 712 */ | |
| 713 List<Command> commands; | |
| 714 Map<Command, CommandOutput> commandOutputs = new Map<Command,CommandOutput>(); | |
| 715 | |
| 716 Map configuration; | |
| 717 String displayName; | |
| 718 int _expectations = 0; | |
| 719 int hash = 0; | |
| 720 Set<Expectation> expectedOutcomes; | |
| 721 | |
| 722 TestCase(this.displayName, | |
| 723 this.commands, | |
| 724 this.configuration, | |
| 725 this.expectedOutcomes, | |
| 726 {isNegative: false, | |
| 727 TestInformation info: null}) { | |
| 728 if (isNegative || displayName.contains("negative_test")) { | |
| 729 _expectations |= IS_NEGATIVE; | |
| 730 } | |
| 731 if (info != null) { | |
| 732 _setExpectations(info); | |
| 733 hash = info.originTestPath.relativeTo(TestUtils.dartDir) | |
| 734 .toString().hashCode; | |
| 735 } | |
| 736 } | |
| 737 | |
| 738 void _setExpectations(TestInformation info) { | |
| 739 // We don't want to keep the entire (large) TestInformation structure, | |
| 740 // so we copy the needed bools into flags set in a single integer. | |
| 741 if (info.hasRuntimeError) _expectations |= HAS_RUNTIME_ERROR; | |
| 742 if (info.hasStaticWarning) _expectations |= HAS_STATIC_WARNING; | |
| 743 if (info.isNegativeIfChecked) _expectations |= IS_NEGATIVE_IF_CHECKED; | |
| 744 if (info.hasCompileError) _expectations |= HAS_COMPILE_ERROR; | |
| 745 if (info.hasCompileErrorIfChecked) { | |
| 746 _expectations |= HAS_COMPILE_ERROR_IF_CHECKED; | |
| 747 } | |
| 748 if (info.hasCompileError || | |
| 749 (configuration['checked'] && info.hasCompileErrorIfChecked)) { | |
| 750 _expectations |= EXPECT_COMPILE_ERROR; | |
| 751 } | |
| 752 } | |
| 753 | |
| 754 bool get isNegative => _expectations & IS_NEGATIVE != 0; | |
| 755 bool get hasRuntimeError => _expectations & HAS_RUNTIME_ERROR != 0; | |
| 756 bool get hasStaticWarning => _expectations & HAS_STATIC_WARNING != 0; | |
| 757 bool get isNegativeIfChecked => _expectations & IS_NEGATIVE_IF_CHECKED != 0; | |
| 758 bool get hasCompileError => _expectations & HAS_COMPILE_ERROR != 0; | |
| 759 bool get hasCompileErrorIfChecked => | |
| 760 _expectations & HAS_COMPILE_ERROR_IF_CHECKED != 0; | |
| 761 bool get expectCompileError => _expectations & EXPECT_COMPILE_ERROR != 0; | |
| 762 | |
| 763 bool get unexpectedOutput { | |
| 764 var outcome = lastCommandOutput.result(this); | |
| 765 return !expectedOutcomes.any((expectation) { | |
| 766 return outcome.canBeOutcomeOf(expectation); | |
| 767 }); | |
| 768 } | |
| 769 | |
| 770 Expectation get result => lastCommandOutput.result(this); | |
| 771 | |
| 772 CommandOutput get lastCommandOutput { | |
| 773 if (commandOutputs.length == 0) { | |
| 774 throw new Exception("CommandOutputs is empty, maybe no command was run? (" | |
| 775 "displayName: '$displayName', " | |
| 776 "configurationString: '$configurationString')"); | |
| 777 } | |
| 778 return commandOutputs[commands[commandOutputs.length - 1]]; | |
| 779 } | |
| 780 | |
| 781 Command get lastCommandExecuted { | |
| 782 if (commandOutputs.length == 0) { | |
| 783 throw new Exception("CommandOutputs is empty, maybe no command was run? (" | |
| 784 "displayName: '$displayName', " | |
| 785 "configurationString: '$configurationString')"); | |
| 786 } | |
| 787 return commands[commandOutputs.length - 1]; | |
| 788 } | |
| 789 | |
| 790 int get timeout { | |
| 791 if (expectedOutcomes.contains(Expectation.SLOW)) { | |
| 792 return configuration['timeout'] * SLOW_TIMEOUT_MULTIPLIER; | |
| 793 } else { | |
| 794 return configuration['timeout']; | |
| 795 } | |
| 796 } | |
| 797 | |
| 798 String get configurationString { | |
| 799 final compiler = configuration['compiler']; | |
| 800 final runtime = configuration['runtime']; | |
| 801 final mode = configuration['mode']; | |
| 802 final arch = configuration['arch']; | |
| 803 final checked = configuration['checked'] ? '-checked' : ''; | |
| 804 return "$compiler-$runtime$checked ${mode}_$arch"; | |
| 805 } | |
| 806 | |
| 807 List<String> get batchTestArguments { | |
| 808 assert(commands.last is ProcessCommand); | |
| 809 return (commands.last as ProcessCommand).arguments; | |
| 810 } | |
| 811 | |
| 812 bool get isFlaky { | |
| 813 if (expectedOutcomes.contains(Expectation.SKIP) || | |
| 814 expectedOutcomes.contains(Expectation.SKIP_BY_DESIGN)) { | |
| 815 return false; | |
| 816 } | |
| 817 | |
| 818 return expectedOutcomes | |
| 819 .where((expectation) => !expectation.isMetaExpectation).length > 1; | |
| 820 } | |
| 821 | |
| 822 bool get isFinished { | |
| 823 return commandOutputs.length > 0 && | |
| 824 (!lastCommandOutput.successful || | |
| 825 commands.length == commandOutputs.length); | |
| 826 } | |
| 827 } | |
| 828 | |
| 829 | |
| 830 /** | |
| 831 * BrowserTestCase has an extra compilation command that is run in a separate | |
| 832 * process, before the regular test is run as in the base class [TestCase]. | |
| 833 * If the compilation command fails, then the rest of the test is not run. | |
| 834 */ | |
| 835 class BrowserTestCase extends TestCase { | |
| 836 | |
| 837 BrowserTestCase(displayName, commands, configuration, | |
| 838 expectedOutcomes, info, isNegative, this._testingUrl) | |
| 839 : super(displayName, commands, configuration, | |
| 840 expectedOutcomes, isNegative: isNegative, info: info); | |
| 841 | |
| 842 String _testingUrl; | |
| 843 | |
| 844 String get testingUrl => _testingUrl; | |
| 845 } | |
| 846 | |
| 847 class UnittestSuiteMessagesMixin { | |
| 848 bool _isAsyncTest(String testOutput) { | |
| 849 return testOutput.contains("unittest-suite-wait-for-done"); | |
| 850 } | |
| 851 | |
| 852 bool _isAsyncTestSuccessful(String testOutput) { | |
| 853 return testOutput.contains("unittest-suite-success"); | |
| 854 } | |
| 855 | |
| 856 Expectation _negateOutcomeIfIncompleteAsyncTest(Expectation outcome, | |
| 857 String testOutput) { | |
| 858 // If this is an asynchronous test and the asynchronous operation didn't | |
| 859 // complete successfully, it's outcome is Expectation.FAIL. | |
| 860 // TODO: maybe we should introduce a AsyncIncomplete marker or so | |
| 861 if (outcome == Expectation.PASS) { | |
| 862 if (_isAsyncTest(testOutput) && | |
| 863 !_isAsyncTestSuccessful(testOutput)) { | |
| 864 return Expectation.FAIL; | |
| 865 } | |
| 866 } | |
| 867 return outcome; | |
| 868 } | |
| 869 } | |
| 870 | 738 |
| 871 /** | 739 /** |
| 872 * CommandOutput records the output of a completed command: the process's exit | 740 * CommandOutput records the output of a completed command: the process's exit |
| 873 * code, the standard output and standard error, whether the process timed out, | 741 * code, the standard output and standard error, whether the process timed out, |
| 874 * and the time the process took to run. It also contains a pointer to the | 742 * and the time the process took to run. It also contains a pointer to the |
| 875 * [TestCase] this is the output of. | 743 * [TestCase] this is the output of. |
| 876 */ | 744 */ |
| 877 abstract class CommandOutput { | 745 abstract class CommandOutput { |
| 878 Command get command; | 746 Command get command; |
| 879 | 747 |
| (...skipping 288 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1168 | 1036 |
| 1169 | 1037 |
| 1170 bool get _browserTestFailure { | 1038 bool get _browserTestFailure { |
| 1171 // We should not need to convert back and forward. | 1039 // We should not need to convert back and forward. |
| 1172 var output = decodeUtf8(super.stdout); | 1040 var output = decodeUtf8(super.stdout); |
| 1173 if (output.contains("FAIL")) return true; | 1041 if (output.contains("FAIL")) return true; |
| 1174 return !output.contains("PASS"); | 1042 return !output.contains("PASS"); |
| 1175 } | 1043 } |
| 1176 } | 1044 } |
| 1177 | 1045 |
| 1178 class BrowserTestJsonResult { | |
| 1179 static const ALLOWED_TYPES = | |
| 1180 const ['sync_exception', 'window_onerror', 'script_onerror', | |
| 1181 'window_compilationerror', 'print', 'message_received', 'dom', | |
| 1182 'debug']; | |
| 1183 | |
| 1184 final Expectation outcome; | |
| 1185 final String htmlDom; | |
| 1186 final List events; | |
| 1187 | |
| 1188 BrowserTestJsonResult(this.outcome, this.htmlDom, this.events); | |
| 1189 | |
| 1190 static BrowserTestJsonResult parseFromString(String content) { | |
| 1191 void validate(String assertion, bool value) { | |
| 1192 if (!value) { | |
| 1193 throw "InvalidFormat sent from browser driving page: $assertion:\n\n" | |
| 1194 "$content"; | |
| 1195 } | |
| 1196 } | |
| 1197 | |
| 1198 var events; | |
| 1199 try { | |
| 1200 events = JSON.decode(content); | |
| 1201 if (events != null) { | |
| 1202 validate("Message must be a List", events is List); | |
| 1203 | |
| 1204 Map<String, List<String>> messagesByType = {}; | |
| 1205 ALLOWED_TYPES.forEach((type) => messagesByType[type] = <String>[]); | |
| 1206 | |
| 1207 for (var entry in events) { | |
| 1208 validate("An entry must be a Map", entry is Map); | |
| 1209 | |
| 1210 var type = entry['type']; | |
| 1211 var value = entry['value']; | |
| 1212 var timestamp = entry['timestamp']; | |
| 1213 | |
| 1214 validate("'type' of an entry must be a String", | |
| 1215 type is String); | |
| 1216 validate("'type' has to be in $ALLOWED_TYPES.", | |
| 1217 ALLOWED_TYPES.contains(type)); | |
| 1218 validate("'timestamp' of an entry must be a number", | |
| 1219 timestamp is num); | |
| 1220 | |
| 1221 messagesByType[type].add(value); | |
| 1222 } | |
| 1223 validate("The message must have exactly one 'dom' entry.", | |
| 1224 messagesByType['dom'].length == 1); | |
| 1225 | |
| 1226 var dom = messagesByType['dom'][0]; | |
| 1227 if (dom.endsWith('\n')) { | |
| 1228 dom = '$dom\n'; | |
| 1229 } | |
| 1230 | |
| 1231 return new BrowserTestJsonResult( | |
| 1232 _getOutcome(messagesByType), dom, events); | |
| 1233 } | |
| 1234 } catch(error) { | |
| 1235 // If something goes wrong, we know the content was not in the correct | |
| 1236 // JSON format. So we can't parse it. | |
| 1237 // The caller is responsible for falling back to the old way of | |
| 1238 // determining if a test failed. | |
| 1239 } | |
| 1240 | |
| 1241 return null; | |
| 1242 } | |
| 1243 | |
| 1244 static Expectation _getOutcome(Map<String, List<String>> messagesByType) { | |
| 1245 occured(type) => messagesByType[type].length > 0; | |
| 1246 searchForMsg(types, message) { | |
| 1247 return types.any((type) => messagesByType[type].contains(message)); | |
| 1248 } | |
| 1249 | |
| 1250 // FIXME(kustermann,ricow): I think this functionality doesn't work in | |
| 1251 // test_controller.js: So far I haven't seen anything being reported on | |
| 1252 // "window.compilationerror" | |
| 1253 if (occured('window_compilationerror')) { | |
| 1254 return Expectation.COMPILETIME_ERROR; | |
| 1255 } | |
| 1256 | |
| 1257 if (occured('sync_exception') || | |
| 1258 occured('window_onerror') || | |
| 1259 occured('script_onerror')) { | |
| 1260 return Expectation.RUNTIME_ERROR; | |
| 1261 } | |
| 1262 | |
| 1263 if (messagesByType['dom'][0].contains('FAIL')) { | |
| 1264 return Expectation.RUNTIME_ERROR; | |
| 1265 } | |
| 1266 | |
| 1267 // We search for these messages in 'print' and 'message_received' because | |
| 1268 // the unittest implementation posts these messages using | |
| 1269 // "window.postMessage()" instead of the normal "print()" them. | |
| 1270 | |
| 1271 var isAsyncTest = searchForMsg(['print', 'message_received'], | |
| 1272 'unittest-suite-wait-for-done'); | |
| 1273 var isAsyncSuccess = | |
| 1274 searchForMsg(['print', 'message_received'], 'unittest-suite-success') || | |
| 1275 searchForMsg(['print', 'message_received'], 'unittest-suite-done'); | |
| 1276 | |
| 1277 if (isAsyncTest) { | |
| 1278 if (isAsyncSuccess) { | |
| 1279 return Expectation.PASS; | |
| 1280 } | |
| 1281 return Expectation.RUNTIME_ERROR; | |
| 1282 } | |
| 1283 | |
| 1284 var mainStarted = | |
| 1285 searchForMsg(['print', 'message_received'], 'dart-calling-main'); | |
| 1286 var mainDone = | |
| 1287 searchForMsg(['print', 'message_received'], 'dart-main-done'); | |
| 1288 | |
| 1289 if (mainStarted && mainDone) { | |
| 1290 return Expectation.PASS; | |
| 1291 } | |
| 1292 return Expectation.FAIL; | |
| 1293 } | |
| 1294 } | |
| 1295 | 1046 |
| 1296 class BrowserControllerTestOutcome extends CommandOutputImpl | 1047 class BrowserControllerTestOutcome extends CommandOutputImpl |
| 1297 with UnittestSuiteMessagesMixin { | 1048 with UnittestSuiteMessagesMixin { |
| 1298 BrowserTestOutput _result; | 1049 BrowserTestOutput _result; |
| 1299 Expectation _rawOutcome; | 1050 Expectation _rawOutcome; |
| 1300 | 1051 |
| 1301 factory BrowserControllerTestOutcome(Command command, | 1052 factory BrowserControllerTestOutcome(Command command, |
| 1302 BrowserTestOutput result) { | 1053 BrowserTestOutput result) { |
| 1303 void validate(String assertion, bool value) { | 1054 void validate(String assertion, bool value) { |
| 1304 if (!value) { | 1055 if (!value) { |
| (...skipping 336 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1641 } | 1392 } |
| 1642 | 1393 |
| 1643 Expectation result(TestCase testCase) => _result; | 1394 Expectation result(TestCase testCase) => _result; |
| 1644 | 1395 |
| 1645 bool get canRunDependendCommands => _result == Expectation.PASS; | 1396 bool get canRunDependendCommands => _result == Expectation.PASS; |
| 1646 | 1397 |
| 1647 bool get successful => _result == Expectation.PASS; | 1398 bool get successful => _result == Expectation.PASS; |
| 1648 | 1399 |
| 1649 } | 1400 } |
| 1650 | 1401 |
| 1651 CommandOutput createCommandOutput(Command command, | 1402 |
| 1652 int exitCode, | 1403 class UnittestSuiteMessagesMixin { |
| 1653 bool timedOut, | 1404 bool _isAsyncTest(String testOutput) { |
| 1654 List<int> stdout, | 1405 return testOutput.contains("unittest-suite-wait-for-done"); |
| 1655 List<int> stderr, | |
| 1656 Duration time, | |
| 1657 bool compilationSkipped, | |
| 1658 [int pid = 0]) { | |
| 1659 if (command is ContentShellCommand) { | |
| 1660 return new BrowserCommandOutputImpl( | |
| 1661 command, exitCode, timedOut, stdout, stderr, | |
| 1662 time, compilationSkipped); | |
| 1663 } else if (command is BrowserTestCommand) { | |
| 1664 return new HTMLBrowserCommandOutputImpl( | |
| 1665 command, exitCode, timedOut, stdout, stderr, | |
| 1666 time, compilationSkipped); | |
| 1667 } else if (command is AnalysisCommand) { | |
| 1668 return new AnalysisCommandOutputImpl( | |
| 1669 command, exitCode, timedOut, stdout, stderr, | |
| 1670 time, compilationSkipped); | |
| 1671 } else if (command is VmCommand) { | |
| 1672 return new VmCommandOutputImpl( | |
| 1673 command, exitCode, timedOut, stdout, stderr, time, pid); | |
| 1674 } else if (command is CompilationCommand) { | |
| 1675 return new CompilationCommandOutputImpl( | |
| 1676 command, exitCode, timedOut, stdout, stderr, time, compilationSkipped); | |
| 1677 } else if (command is JSCommandlineCommand) { | |
| 1678 return new JsCommandlineOutputImpl( | |
| 1679 command, exitCode, timedOut, stdout, stderr, time); | |
| 1680 } else if (command is PubCommand) { | |
| 1681 return new PubCommandOutputImpl( | |
| 1682 command, exitCode, timedOut, stdout, stderr, time); | |
| 1683 } | 1406 } |
| 1684 | 1407 |
| 1685 return new CommandOutputImpl( | 1408 bool _isAsyncTestSuccessful(String testOutput) { |
| 1686 command, exitCode, timedOut, stdout, stderr, | 1409 return testOutput.contains("unittest-suite-success"); |
| 1687 time, compilationSkipped, pid); | |
| 1688 } | |
| 1689 | |
| 1690 | |
| 1691 /** | |
| 1692 * An OutputLog records the output from a test, but truncates it if | |
| 1693 * it is longer than MAX_HEAD characters, and just keeps the head and | |
| 1694 * the last TAIL_LENGTH characters of the output. | |
| 1695 */ | |
| 1696 class OutputLog { | |
| 1697 static const int MAX_HEAD = 100 * 1024; | |
| 1698 static const int TAIL_LENGTH = 10 * 1024; | |
| 1699 List<int> head = <int>[]; | |
| 1700 List<int> tail; | |
| 1701 List<int> complete; | |
| 1702 bool dataDropped = false; | |
| 1703 | |
| 1704 OutputLog(); | |
| 1705 | |
| 1706 void add(List<int> data) { | |
| 1707 if (complete != null) { | |
| 1708 throw new StateError("Cannot add to OutputLog after calling toList"); | |
| 1709 } | |
| 1710 if (tail == null) { | |
| 1711 head.addAll(data); | |
| 1712 if (head.length > MAX_HEAD) { | |
| 1713 tail = head.sublist(MAX_HEAD); | |
| 1714 head.length = MAX_HEAD; | |
| 1715 } | |
| 1716 } else { | |
| 1717 tail.addAll(data); | |
| 1718 } | |
| 1719 if (tail != null && tail.length > 2 * TAIL_LENGTH) { | |
| 1720 tail = _truncatedTail(); | |
| 1721 dataDropped = true; | |
| 1722 } | |
| 1723 } | 1410 } |
| 1724 | 1411 |
| 1725 List<int> _truncatedTail() => | 1412 Expectation _negateOutcomeIfIncompleteAsyncTest(Expectation outcome, |
| 1726 tail.length > TAIL_LENGTH ? | 1413 String testOutput) { |
| 1727 tail.sublist(tail.length - TAIL_LENGTH) : | 1414 // If this is an asynchronous test and the asynchronous operation didn't |
| 1728 tail; | 1415 // complete successfully, it's outcome is Expectation.FAIL. |
| 1729 | 1416 // TODO: maybe we should introduce a AsyncIncomplete marker or so |
| 1730 List<int> toList() { | 1417 if (outcome == Expectation.PASS) { |
| 1731 if (complete == null) { | 1418 if (_isAsyncTest(testOutput) && |
| 1732 complete = head; | 1419 !_isAsyncTestSuccessful(testOutput)) { |
| 1733 if (dataDropped) { | 1420 return Expectation.FAIL; |
| 1734 complete.addAll(""" | |
| 1735 | |
| 1736 ***************************************************************************** | |
| 1737 | |
| 1738 Data removed due to excessive length | |
| 1739 | |
| 1740 ***************************************************************************** | |
| 1741 | |
| 1742 """.codeUnits); | |
| 1743 complete.addAll(_truncatedTail()); | |
| 1744 } else if (tail != null) { | |
| 1745 complete.addAll(tail); | |
| 1746 } | 1421 } |
| 1747 head = null; | |
| 1748 tail = null; | |
| 1749 } | 1422 } |
| 1750 return complete; | 1423 return outcome; |
| 1751 } | 1424 } |
| 1752 } | 1425 } |
| 1753 | 1426 |
| 1754 /** | 1427 class BrowserTestJsonResult { |
| 1755 * A RunningProcess actually runs a test, getting the command lines from | 1428 static const ALLOWED_TYPES = |
| 1756 * its [TestCase], starting the test process (and first, a compilation | 1429 const ['sync_exception', 'window_onerror', 'script_onerror', |
| 1757 * process if the TestCase is a [BrowserTestCase]), creating a timeout | 1430 'window_compilationerror', 'print', 'message_received', 'dom', |
| 1758 * timer, and recording the results in a new [CommandOutput] object, which it | 1431 'debug']; |
| 1759 * attaches to the TestCase. The lifetime of the RunningProcess is limited | |
| 1760 * to the time it takes to start the process, run the process, and record | |
| 1761 * the result; there are no pointers to it, so it should be available to | |
| 1762 * be garbage collected as soon as it is done. | |
| 1763 */ | |
| 1764 class RunningProcess { | |
| 1765 ProcessCommand command; | |
| 1766 int timeout; | |
| 1767 bool timedOut = false; | |
| 1768 DateTime startTime; | |
| 1769 Timer timeoutTimer; | |
| 1770 int pid; | |
| 1771 OutputLog stdout = new OutputLog(); | |
| 1772 OutputLog stderr = new OutputLog(); | |
| 1773 bool compilationSkipped = false; | |
| 1774 Completer<CommandOutput> completer; | |
| 1775 | 1432 |
| 1776 RunningProcess(this.command, this.timeout); | 1433 final Expectation outcome; |
| 1434 final String htmlDom; |
| 1435 final List events; |
| 1777 | 1436 |
| 1778 Future<CommandOutput> run() { | 1437 BrowserTestJsonResult(this.outcome, this.htmlDom, this.events); |
| 1779 completer = new Completer<CommandOutput>(); | 1438 |
| 1780 startTime = new DateTime.now(); | 1439 static BrowserTestJsonResult parseFromString(String content) { |
| 1781 _runCommand(); | 1440 void validate(String assertion, bool value) { |
| 1782 return completer.future; | 1441 if (!value) { |
| 1442 throw "InvalidFormat sent from browser driving page: $assertion:\n\n" |
| 1443 "$content"; |
| 1444 } |
| 1445 } |
| 1446 |
| 1447 var events; |
| 1448 try { |
| 1449 events = JSON.decode(content); |
| 1450 if (events != null) { |
| 1451 validate("Message must be a List", events is List); |
| 1452 |
| 1453 Map<String, List<String>> messagesByType = {}; |
| 1454 ALLOWED_TYPES.forEach((type) => messagesByType[type] = <String>[]); |
| 1455 |
| 1456 for (var entry in events) { |
| 1457 validate("An entry must be a Map", entry is Map); |
| 1458 |
| 1459 var type = entry['type']; |
| 1460 var value = entry['value']; |
| 1461 var timestamp = entry['timestamp']; |
| 1462 |
| 1463 validate("'type' of an entry must be a String", |
| 1464 type is String); |
| 1465 validate("'type' has to be in $ALLOWED_TYPES.", |
| 1466 ALLOWED_TYPES.contains(type)); |
| 1467 validate("'timestamp' of an entry must be a number", |
| 1468 timestamp is num); |
| 1469 |
| 1470 messagesByType[type].add(value); |
| 1471 } |
| 1472 validate("The message must have exactly one 'dom' entry.", |
| 1473 messagesByType['dom'].length == 1); |
| 1474 |
| 1475 var dom = messagesByType['dom'][0]; |
| 1476 if (dom.endsWith('\n')) { |
| 1477 dom = '$dom\n'; |
| 1478 } |
| 1479 |
| 1480 return new BrowserTestJsonResult( |
| 1481 _getOutcome(messagesByType), dom, events); |
| 1482 } |
| 1483 } catch(error) { |
| 1484 // If something goes wrong, we know the content was not in the correct |
| 1485 // JSON format. So we can't parse it. |
| 1486 // The caller is responsible for falling back to the old way of |
| 1487 // determining if a test failed. |
| 1488 } |
| 1489 |
| 1490 return null; |
| 1783 } | 1491 } |
| 1784 | 1492 |
| 1785 void _runCommand() { | 1493 static Expectation _getOutcome(Map<String, List<String>> messagesByType) { |
| 1786 command.outputIsUpToDate.then((bool isUpToDate) { | 1494 occured(type) => messagesByType[type].length > 0; |
| 1787 if (isUpToDate) { | 1495 searchForMsg(types, message) { |
| 1788 compilationSkipped = true; | 1496 return types.any((type) => messagesByType[type].contains(message)); |
| 1789 _commandComplete(0); | |
| 1790 } else { | |
| 1791 var processEnvironment = _createProcessEnvironment(); | |
| 1792 Future processFuture = | |
| 1793 io.Process.start(command.executable, | |
| 1794 command.arguments, | |
| 1795 environment: processEnvironment, | |
| 1796 workingDirectory: command.workingDirectory); | |
| 1797 processFuture.then((io.Process process) { | |
| 1798 StreamSubscription stdoutSubscription = | |
| 1799 _drainStream(process.stdout, stdout); | |
| 1800 StreamSubscription stderrSubscription = | |
| 1801 _drainStream(process.stderr, stderr); | |
| 1802 | |
| 1803 var stdoutCompleter = new Completer(); | |
| 1804 var stderrCompleter = new Completer(); | |
| 1805 | |
| 1806 bool stdoutDone = false; | |
| 1807 bool stderrDone = false; | |
| 1808 pid = process.pid; | |
| 1809 | |
| 1810 // This timer is used to close stdio to the subprocess once we got | |
| 1811 // the exitCode. Sometimes descendants of the subprocess keep stdio | |
| 1812 // handles alive even though the direct subprocess is dead. | |
| 1813 Timer watchdogTimer; | |
| 1814 | |
| 1815 closeStdout([_]) { | |
| 1816 if (!stdoutDone) { | |
| 1817 stdoutCompleter.complete(); | |
| 1818 stdoutDone = true; | |
| 1819 if (stderrDone && watchdogTimer != null) { | |
| 1820 watchdogTimer.cancel(); | |
| 1821 } | |
| 1822 } | |
| 1823 } | |
| 1824 closeStderr([_]) { | |
| 1825 if (!stderrDone) { | |
| 1826 stderrCompleter.complete(); | |
| 1827 stderrDone = true; | |
| 1828 | |
| 1829 if (stdoutDone && watchdogTimer != null) { | |
| 1830 watchdogTimer.cancel(); | |
| 1831 } | |
| 1832 } | |
| 1833 } | |
| 1834 | |
| 1835 // Close stdin so that tests that try to block on input will fail. | |
| 1836 process.stdin.close(); | |
| 1837 void timeoutHandler() { | |
| 1838 timedOut = true; | |
| 1839 if (process != null) { | |
| 1840 if (!process.kill()) { | |
| 1841 DebugLogger.error("Unable to kill ${process.pid}"); | |
| 1842 } | |
| 1843 } | |
| 1844 } | |
| 1845 | |
| 1846 stdoutSubscription.asFuture().then(closeStdout); | |
| 1847 stderrSubscription.asFuture().then(closeStderr); | |
| 1848 | |
| 1849 process.exitCode.then((exitCode) { | |
| 1850 if (!stdoutDone || !stderrDone) { | |
| 1851 watchdogTimer = new Timer(MAX_STDIO_DELAY, () { | |
| 1852 DebugLogger.warning( | |
| 1853 "$MAX_STDIO_DELAY_PASSED_MESSAGE (command: $command)"); | |
| 1854 watchdogTimer = null; | |
| 1855 stdoutSubscription.cancel(); | |
| 1856 stderrSubscription.cancel(); | |
| 1857 closeStdout(); | |
| 1858 closeStderr(); | |
| 1859 }); | |
| 1860 } | |
| 1861 | |
| 1862 Future.wait([stdoutCompleter.future, | |
| 1863 stderrCompleter.future]).then((_) { | |
| 1864 _commandComplete(exitCode); | |
| 1865 }); | |
| 1866 }); | |
| 1867 | |
| 1868 timeoutTimer = new Timer(new Duration(seconds: timeout), | |
| 1869 timeoutHandler); | |
| 1870 }).catchError((e) { | |
| 1871 // TODO(floitsch): should we try to report the stacktrace? | |
| 1872 print("Process error:"); | |
| 1873 print(" Command: $command"); | |
| 1874 print(" Error: $e"); | |
| 1875 _commandComplete(-1); | |
| 1876 return true; | |
| 1877 }); | |
| 1878 } | |
| 1879 }); | |
| 1880 } | |
| 1881 | |
| 1882 void _commandComplete(int exitCode) { | |
| 1883 if (timeoutTimer != null) { | |
| 1884 timeoutTimer.cancel(); | |
| 1885 } | |
| 1886 var commandOutput = _createCommandOutput(command, exitCode); | |
| 1887 completer.complete(commandOutput); | |
| 1888 } | |
| 1889 | |
| 1890 CommandOutput _createCommandOutput(ProcessCommand command, int exitCode) { | |
| 1891 var commandOutput = createCommandOutput( | |
| 1892 command, | |
| 1893 exitCode, | |
| 1894 timedOut, | |
| 1895 stdout.toList(), | |
| 1896 stderr.toList(), | |
| 1897 new DateTime.now().difference(startTime), | |
| 1898 compilationSkipped, | |
| 1899 pid); | |
| 1900 return commandOutput; | |
| 1901 } | |
| 1902 | |
| 1903 StreamSubscription _drainStream(Stream<List<int>> source, | |
| 1904 OutputLog destination) { | |
| 1905 return source.listen(destination.add); | |
| 1906 } | |
| 1907 | |
| 1908 Map<String, String> _createProcessEnvironment() { | |
| 1909 var environment = new Map.from(io.Platform.environment); | |
| 1910 | |
| 1911 if (command.environmentOverrides != null) { | |
| 1912 for (var key in command.environmentOverrides.keys) { | |
| 1913 environment[key] = command.environmentOverrides[key]; | |
| 1914 } | |
| 1915 } | |
| 1916 for (var excludedEnvironmentVariable in EXCLUDED_ENVIRONMENT_VARIABLES) { | |
| 1917 environment.remove(excludedEnvironmentVariable); | |
| 1918 } | 1497 } |
| 1919 | 1498 |
| 1920 return environment; | 1499 // FIXME(kustermann,ricow): I think this functionality doesn't work in |
| 1500 // test_controller.js: So far I haven't seen anything being reported on |
| 1501 // "window.compilationerror" |
| 1502 if (occured('window_compilationerror')) { |
| 1503 return Expectation.COMPILETIME_ERROR; |
| 1504 } |
| 1505 |
| 1506 if (occured('sync_exception') || |
| 1507 occured('window_onerror') || |
| 1508 occured('script_onerror')) { |
| 1509 return Expectation.RUNTIME_ERROR; |
| 1510 } |
| 1511 |
| 1512 if (messagesByType['dom'][0].contains('FAIL')) { |
| 1513 return Expectation.RUNTIME_ERROR; |
| 1514 } |
| 1515 |
| 1516 // We search for these messages in 'print' and 'message_received' because |
| 1517 // the unittest implementation posts these messages using |
| 1518 // "window.postMessage()" instead of the normal "print()" them. |
| 1519 |
| 1520 var isAsyncTest = searchForMsg(['print', 'message_received'], |
| 1521 'unittest-suite-wait-for-done'); |
| 1522 var isAsyncSuccess = |
| 1523 searchForMsg(['print', 'message_received'], 'unittest-suite-success') || |
| 1524 searchForMsg(['print', 'message_received'], 'unittest-suite-done'); |
| 1525 |
| 1526 if (isAsyncTest) { |
| 1527 if (isAsyncSuccess) { |
| 1528 return Expectation.PASS; |
| 1529 } |
| 1530 return Expectation.RUNTIME_ERROR; |
| 1531 } |
| 1532 |
| 1533 var mainStarted = |
| 1534 searchForMsg(['print', 'message_received'], 'dart-calling-main'); |
| 1535 var mainDone = |
| 1536 searchForMsg(['print', 'message_received'], 'dart-main-done'); |
| 1537 |
| 1538 if (mainStarted && mainDone) { |
| 1539 return Expectation.PASS; |
| 1540 } |
| 1541 return Expectation.FAIL; |
| 1921 } | 1542 } |
| 1922 } | 1543 } |
| 1923 | 1544 |
| 1924 class BatchRunnerProcess { | |
| 1925 Completer<CommandOutput> _completer; | |
| 1926 ProcessCommand _command; | |
| 1927 List<String> _arguments; | |
| 1928 String _runnerType; | |
| 1929 | |
| 1930 io.Process _process; | |
| 1931 Map _processEnvironmentOverrides; | |
| 1932 Completer _stdoutCompleter; | |
| 1933 Completer _stderrCompleter; | |
| 1934 StreamSubscription<String> _stdoutSubscription; | |
| 1935 StreamSubscription<String> _stderrSubscription; | |
| 1936 Function _processExitHandler; | |
| 1937 | |
| 1938 bool _currentlyRunning = false; | |
| 1939 OutputLog _testStdout; | |
| 1940 OutputLog _testStderr; | |
| 1941 String _status; | |
| 1942 DateTime _startTime; | |
| 1943 Timer _timer; | |
| 1944 | |
| 1945 BatchRunnerProcess(); | |
| 1946 | |
| 1947 Future<CommandOutput> runCommand(String runnerType, ProcessCommand command, | |
| 1948 int timeout, List<String> arguments) { | |
| 1949 assert(_completer == null); | |
| 1950 assert(!_currentlyRunning); | |
| 1951 | |
| 1952 _completer = new Completer<CommandOutput>(); | |
| 1953 bool sameRunnerType = _runnerType == runnerType && | |
| 1954 _dictEquals(_processEnvironmentOverrides, command.environmentOverrides); | |
| 1955 _runnerType = runnerType; | |
| 1956 _currentlyRunning = true; | |
| 1957 _command = command; | |
| 1958 _arguments = arguments; | |
| 1959 _processEnvironmentOverrides = command.environmentOverrides; | |
| 1960 | |
| 1961 if (_process == null) { | |
| 1962 // Start process if not yet started. | |
| 1963 _startProcess(() { | |
| 1964 doStartTest(command, timeout); | |
| 1965 }); | |
| 1966 } else if (!sameRunnerType) { | |
| 1967 // Restart this runner with the right executable for this test if needed. | |
| 1968 _processExitHandler = (_) { | |
| 1969 _startProcess(() { | |
| 1970 doStartTest(command, timeout); | |
| 1971 }); | |
| 1972 }; | |
| 1973 _process.kill(); | |
| 1974 _stdoutSubscription.cancel(); | |
| 1975 _stderrSubscription.cancel(); | |
| 1976 } else { | |
| 1977 doStartTest(command, timeout); | |
| 1978 } | |
| 1979 return _completer.future; | |
| 1980 } | |
| 1981 | |
| 1982 Future terminate() { | |
| 1983 if (_process == null) return new Future.value(true); | |
| 1984 Completer terminateCompleter = new Completer(); | |
| 1985 _processExitHandler = (_) { | |
| 1986 terminateCompleter.complete(true); | |
| 1987 }; | |
| 1988 _process.kill(); | |
| 1989 _stdoutSubscription.cancel(); | |
| 1990 _stderrSubscription.cancel(); | |
| 1991 | |
| 1992 return terminateCompleter.future; | |
| 1993 } | |
| 1994 | |
| 1995 void doStartTest(Command command, int timeout) { | |
| 1996 _startTime = new DateTime.now(); | |
| 1997 _testStdout = new OutputLog(); | |
| 1998 _testStderr = new OutputLog(); | |
| 1999 _status = null; | |
| 2000 _stdoutCompleter = new Completer(); | |
| 2001 _stderrCompleter = new Completer(); | |
| 2002 _timer = new Timer(new Duration(seconds: timeout), | |
| 2003 _timeoutHandler); | |
| 2004 | |
| 2005 var line = _createArgumentsLine(_arguments, timeout); | |
| 2006 _process.stdin.write(line); | |
| 2007 _stdoutSubscription.resume(); | |
| 2008 _stderrSubscription.resume(); | |
| 2009 Future.wait([_stdoutCompleter.future, | |
| 2010 _stderrCompleter.future]).then((_) => _reportResult()); | |
| 2011 } | |
| 2012 | |
| 2013 String _createArgumentsLine(List<String> arguments, int timeout) { | |
| 2014 return arguments.join(' ') + '\n'; | |
| 2015 } | |
| 2016 | |
| 2017 void _reportResult() { | |
| 2018 if (!_currentlyRunning) return; | |
| 2019 // _status == '>>> TEST {PASS, FAIL, OK, CRASH, FAIL, TIMEOUT}' | |
| 2020 | |
| 2021 var outcome = _status.split(" ")[2]; | |
| 2022 var exitCode = 0; | |
| 2023 if (outcome == "CRASH") exitCode = CRASHING_BROWSER_EXITCODE; | |
| 2024 if (outcome == "FAIL" || outcome == "TIMEOUT") exitCode = 1; | |
| 2025 var output = createCommandOutput(_command, | |
| 2026 exitCode, | |
| 2027 (outcome == "TIMEOUT"), | |
| 2028 _testStdout.toList(), | |
| 2029 _testStderr.toList(), | |
| 2030 new DateTime.now().difference(_startTime), | |
| 2031 false); | |
| 2032 assert(_completer != null); | |
| 2033 _completer.complete(output); | |
| 2034 _completer = null; | |
| 2035 _currentlyRunning = false; | |
| 2036 } | |
| 2037 | |
| 2038 ExitCodeEvent makeExitHandler(String status) { | |
| 2039 void handler(int exitCode) { | |
| 2040 if (_currentlyRunning) { | |
| 2041 if (_timer != null) _timer.cancel(); | |
| 2042 _status = status; | |
| 2043 _stdoutSubscription.cancel(); | |
| 2044 _stderrSubscription.cancel(); | |
| 2045 _startProcess(_reportResult); | |
| 2046 } else { // No active test case running. | |
| 2047 _process = null; | |
| 2048 } | |
| 2049 } | |
| 2050 return handler; | |
| 2051 } | |
| 2052 | |
| 2053 void _timeoutHandler() { | |
| 2054 _processExitHandler = makeExitHandler(">>> TEST TIMEOUT"); | |
| 2055 _process.kill(); | |
| 2056 } | |
| 2057 | |
| 2058 _startProcess(callback) { | |
| 2059 assert(_command is ProcessCommand); | |
| 2060 var executable = _command.executable; | |
| 2061 var arguments = ['--batch']; | |
| 2062 var environment = new Map.from(io.Platform.environment); | |
| 2063 if (_processEnvironmentOverrides != null) { | |
| 2064 for (var key in _processEnvironmentOverrides.keys) { | |
| 2065 environment[key] = _processEnvironmentOverrides[key]; | |
| 2066 } | |
| 2067 } | |
| 2068 Future processFuture = io.Process.start(executable, | |
| 2069 arguments, | |
| 2070 environment: environment); | |
| 2071 processFuture.then((io.Process p) { | |
| 2072 _process = p; | |
| 2073 | |
| 2074 var _stdoutStream = | |
| 2075 _process.stdout | |
| 2076 .transform(UTF8.decoder) | |
| 2077 .transform(new LineSplitter()); | |
| 2078 _stdoutSubscription = _stdoutStream.listen((String line) { | |
| 2079 if (line.startsWith('>>> TEST')) { | |
| 2080 _status = line; | |
| 2081 } else if (line.startsWith('>>> BATCH')) { | |
| 2082 // ignore | |
| 2083 } else if (line.startsWith('>>> ')) { | |
| 2084 throw new Exception("Unexpected command from batch runner: '$line'."); | |
| 2085 } else { | |
| 2086 _testStdout.add(encodeUtf8(line)); | |
| 2087 _testStdout.add("\n".codeUnits); | |
| 2088 } | |
| 2089 if (_status != null) { | |
| 2090 _stdoutSubscription.pause(); | |
| 2091 _timer.cancel(); | |
| 2092 _stdoutCompleter.complete(null); | |
| 2093 } | |
| 2094 }); | |
| 2095 _stdoutSubscription.pause(); | |
| 2096 | |
| 2097 var _stderrStream = | |
| 2098 _process.stderr | |
| 2099 .transform(UTF8.decoder) | |
| 2100 .transform(new LineSplitter()); | |
| 2101 _stderrSubscription = _stderrStream.listen((String line) { | |
| 2102 if (line.startsWith('>>> EOF STDERR')) { | |
| 2103 _stderrSubscription.pause(); | |
| 2104 _stderrCompleter.complete(null); | |
| 2105 } else { | |
| 2106 _testStderr.add(encodeUtf8(line)); | |
| 2107 _testStderr.add("\n".codeUnits); | |
| 2108 } | |
| 2109 }); | |
| 2110 _stderrSubscription.pause(); | |
| 2111 | |
| 2112 _processExitHandler = makeExitHandler(">>> TEST CRASH"); | |
| 2113 _process.exitCode.then((exitCode) { | |
| 2114 _processExitHandler(exitCode); | |
| 2115 }); | |
| 2116 | |
| 2117 _process.stdin.done.catchError((err) { | |
| 2118 print('Error on batch runner input stream stdin'); | |
| 2119 print(' Previous test\'s status: $_status'); | |
| 2120 print(' Error: $err'); | |
| 2121 throw err; | |
| 2122 }); | |
| 2123 callback(); | |
| 2124 }).catchError((e) { | |
| 2125 // TODO(floitsch): should we try to report the stacktrace? | |
| 2126 print("Process error:"); | |
| 2127 print(" Command: $executable ${arguments.join(' ')} ($_arguments)"); | |
| 2128 print(" Error: $e"); | |
| 2129 // If there is an error starting a batch process, chances are that | |
| 2130 // it will always fail. So rather than re-trying a 1000+ times, we | |
| 2131 // exit. | |
| 2132 io.exit(1); | |
| 2133 return true; | |
| 2134 }); | |
| 2135 } | |
| 2136 | |
| 2137 bool _dictEquals(Map a, Map b) { | |
| 2138 if (a == null) return b == null; | |
| 2139 if (b == null) return false; | |
| 2140 for (var key in a.keys) { | |
| 2141 if (a[key] != b[key]) return false; | |
| 2142 } | |
| 2143 return true; | |
| 2144 } | |
| 2145 } | |
| 2146 | |
| 2147 | |
| 2148 /** | |
| 2149 * [TestCaseEnqueuer] takes a list of TestSuites, generates TestCases and | |
| 2150 * builds a dependency graph of all commands in every TestSuite. | |
| 2151 * | |
| 2152 * It will maintain three helper data structures | |
| 2153 * - command2node: A mapping from a [Command] to a node in the dependency graph | |
| 2154 * - command2testCases: A mapping from [Command] to all TestCases that it is | |
| 2155 * part of. | |
| 2156 * - remainingTestCases: A set of TestCases that were enqueued but are not | |
| 2157 * finished | |
| 2158 * | |
| 2159 * [Command] and it's subclasses all have hashCode/operator== methods defined | |
| 2160 * on them, so we can safely use them as keys in Map/Set objects. | |
| 2161 */ | |
| 2162 class TestCaseEnqueuer { | |
| 2163 final dgraph.Graph graph; | |
| 2164 final Function _onTestCaseAdded; | |
| 2165 | |
| 2166 final command2node = new Map<Command, dgraph.Node>(); | |
| 2167 final command2testCases = new Map<Command, List<TestCase>>(); | |
| 2168 final remainingTestCases = new Set<TestCase>(); | |
| 2169 | |
| 2170 TestCaseEnqueuer(this.graph, this._onTestCaseAdded); | |
| 2171 | |
| 2172 void enqueueTestSuites(List<TestSuite> testSuites) { | |
| 2173 void newTest(TestCase testCase) { | |
| 2174 remainingTestCases.add(testCase); | |
| 2175 | |
| 2176 var lastNode; | |
| 2177 for (var command in testCase.commands) { | |
| 2178 // Make exactly *one* node in the dependency graph for every command. | |
| 2179 // This ensures that we never have two commands c1 and c2 in the graph | |
| 2180 // with "c1 == c2". | |
| 2181 var node = command2node[command]; | |
| 2182 if (node == null) { | |
| 2183 var requiredNodes = (lastNode != null) ? [lastNode] : []; | |
| 2184 node = graph.newNode(command, requiredNodes); | |
| 2185 command2node[command] = node; | |
| 2186 command2testCases[command] = <TestCase>[]; | |
| 2187 } | |
| 2188 // Keep mapping from command to all testCases that refer to it | |
| 2189 command2testCases[command].add(testCase); | |
| 2190 | |
| 2191 lastNode = node; | |
| 2192 } | |
| 2193 _onTestCaseAdded(testCase); | |
| 2194 } | |
| 2195 | |
| 2196 // Cache information about test cases per test suite. For multiple | |
| 2197 // configurations there is no need to repeatedly search the file | |
| 2198 // system, generate tests, and search test files for options. | |
| 2199 var testCache = new Map<String, List<TestInformation>>(); | |
| 2200 | |
| 2201 Iterator<TestSuite> iterator = testSuites.iterator; | |
| 2202 void enqueueNextSuite() { | |
| 2203 if (!iterator.moveNext()) { | |
| 2204 // We're finished with building the dependency graph. | |
| 2205 graph.sealGraph(); | |
| 2206 } else { | |
| 2207 iterator.current.forEachTest(newTest, testCache, enqueueNextSuite); | |
| 2208 } | |
| 2209 } | |
| 2210 enqueueNextSuite(); | |
| 2211 } | |
| 2212 } | |
| 2213 | |
| 2214 | |
| 2215 /* | |
| 2216 * [CommandEnqueuer] will | |
| 2217 * - change node.state to NodeState.Enqueuing as soon as all dependencies have | |
| 2218 * a state of NodeState.Successful | |
| 2219 * - change node.state to NodeState.UnableToRun if one or more dependencies | |
| 2220 * have a state of NodeState.Failed/NodeState.UnableToRun. | |
| 2221 */ | |
| 2222 class CommandEnqueuer { | |
| 2223 static final INIT_STATES = [dgraph.NodeState.Initialized, | |
| 2224 dgraph.NodeState.Waiting]; | |
| 2225 static final FINISHED_STATES = [dgraph.NodeState.Successful, | |
| 2226 dgraph.NodeState.Failed, | |
| 2227 dgraph.NodeState.UnableToRun]; | |
| 2228 final dgraph.Graph _graph; | |
| 2229 | |
| 2230 CommandEnqueuer(this._graph) { | |
| 2231 var eventCondition = _graph.events.where; | |
| 2232 | |
| 2233 eventCondition((e) => e is dgraph.NodeAddedEvent).listen((event) { | |
| 2234 dgraph.Node node = event.node; | |
| 2235 _changeNodeStateIfNecessary(node); | |
| 2236 }); | |
| 2237 | |
| 2238 eventCondition((e) => e is dgraph.StateChangedEvent).listen((event) { | |
| 2239 if ([dgraph.NodeState.Waiting, | |
| 2240 dgraph.NodeState.Processing].contains(event.from)) { | |
| 2241 if (FINISHED_STATES.contains(event.to)){ | |
| 2242 for (var dependendNode in event.node.neededFor) { | |
| 2243 _changeNodeStateIfNecessary(dependendNode); | |
| 2244 } | |
| 2245 } | |
| 2246 } | |
| 2247 }); | |
| 2248 } | |
| 2249 | |
| 2250 // Called when either a new node was added or if one of it's dependencies | |
| 2251 // changed it's state. | |
| 2252 void _changeNodeStateIfNecessary(dgraph.Node node) { | |
| 2253 if (INIT_STATES.contains(node.state)) { | |
| 2254 bool anyDependenciesUnsuccessful = node.dependencies.any( | |
| 2255 (dep) => [dgraph.NodeState.Failed, | |
| 2256 dgraph.NodeState.UnableToRun].contains(dep.state)); | |
| 2257 | |
| 2258 var newState = dgraph.NodeState.Waiting; | |
| 2259 if (anyDependenciesUnsuccessful) { | |
| 2260 newState = dgraph.NodeState.UnableToRun; | |
| 2261 } else { | |
| 2262 bool allDependenciesSuccessful = node.dependencies.every( | |
| 2263 (dep) => dep.state == dgraph.NodeState.Successful); | |
| 2264 | |
| 2265 if (allDependenciesSuccessful) { | |
| 2266 newState = dgraph.NodeState.Enqueuing; | |
| 2267 } | |
| 2268 } | |
| 2269 if (node.state != newState) { | |
| 2270 _graph.changeState(node, newState); | |
| 2271 } | |
| 2272 } | |
| 2273 } | |
| 2274 } | |
| 2275 | |
| 2276 /* | |
| 2277 * [CommandQueue] will listen for nodes entering the NodeState.ENQUEUING state, | |
| 2278 * queue them up and run them. While nodes are processed they will be in the | |
| 2279 * NodeState.PROCESSING state. After running a command, the node will change | |
| 2280 * to a state of NodeState.Successful or NodeState.Failed. | |
| 2281 * | |
| 2282 * It provides a synchronous stream [completedCommands] which provides the | |
| 2283 * [CommandOutputs] for the finished commands. | |
| 2284 * | |
| 2285 * It provides a [done] future, which will complete once there are no more | |
| 2286 * nodes left in the states Initialized/Waiting/Enqueing/Processing | |
| 2287 * and the [executor] has cleaned up it's resources. | |
| 2288 */ | |
| 2289 class CommandQueue { | |
| 2290 final dgraph.Graph graph; | |
| 2291 final CommandExecutor executor; | |
| 2292 final TestCaseEnqueuer enqueuer; | |
| 2293 | |
| 2294 final Queue<Command> _runQueue = new Queue<Command>(); | |
| 2295 final _commandOutputStream = new StreamController<CommandOutput>(sync: true); | |
| 2296 final _completer = new Completer(); | |
| 2297 | |
| 2298 int _numProcesses = 0; | |
| 2299 int _maxProcesses; | |
| 2300 int _numBrowserProcesses = 0; | |
| 2301 int _maxBrowserProcesses; | |
| 2302 bool _finishing = false; | |
| 2303 bool _verbose = false; | |
| 2304 | |
| 2305 CommandQueue(this.graph, this.enqueuer, this.executor, | |
| 2306 this._maxProcesses, this._maxBrowserProcesses, this._verbose) { | |
| 2307 var eventCondition = graph.events.where; | |
| 2308 eventCondition((event) => event is dgraph.StateChangedEvent) | |
| 2309 .listen((event) { | |
| 2310 if (event.to == dgraph.NodeState.Enqueuing) { | |
| 2311 assert(event.from == dgraph.NodeState.Initialized || | |
| 2312 event.from == dgraph.NodeState.Waiting); | |
| 2313 graph.changeState(event.node, dgraph.NodeState.Processing); | |
| 2314 var command = event.node.userData; | |
| 2315 if (event.node.dependencies.length > 0) { | |
| 2316 _runQueue.addFirst(command); | |
| 2317 } else { | |
| 2318 _runQueue.add(command); | |
| 2319 } | |
| 2320 Timer.run(() => _tryRunNextCommand()); | |
| 2321 } | |
| 2322 }); | |
| 2323 // We're finished if the graph is sealed and all nodes are in a finished | |
| 2324 // state (Successful, Failed or UnableToRun). | |
| 2325 // So we're calling '_checkDone()' to check whether that condition is met | |
| 2326 // and we can cleanup. | |
| 2327 graph.events.listen((dgraph.GraphEvent event) { | |
| 2328 if (event is dgraph.GraphSealedEvent) { | |
| 2329 _checkDone(); | |
| 2330 } else if (event is dgraph.StateChangedEvent) { | |
| 2331 if (event.to == dgraph.NodeState.UnableToRun) { | |
| 2332 _checkDone(); | |
| 2333 } | |
| 2334 } | |
| 2335 }); | |
| 2336 } | |
| 2337 | |
| 2338 Stream<CommandOutput> get completedCommands => _commandOutputStream.stream; | |
| 2339 | |
| 2340 Future get done => _completer.future; | |
| 2341 | |
| 2342 void _tryRunNextCommand() { | |
| 2343 _checkDone(); | |
| 2344 | |
| 2345 if (_numProcesses < _maxProcesses && !_runQueue.isEmpty) { | |
| 2346 Command command = _runQueue.removeFirst(); | |
| 2347 var isBrowserCommand = command is BrowserTestCommand; | |
| 2348 | |
| 2349 if (isBrowserCommand && _numBrowserProcesses == _maxBrowserProcesses) { | |
| 2350 // If there is no free browser runner, put it back into the queue. | |
| 2351 _runQueue.add(command); | |
| 2352 // Don't lose a process. | |
| 2353 new Timer(new Duration(milliseconds: 100), _tryRunNextCommand); | |
| 2354 return; | |
| 2355 } | |
| 2356 | |
| 2357 _numProcesses++; | |
| 2358 if (isBrowserCommand) _numBrowserProcesses++; | |
| 2359 | |
| 2360 var node = enqueuer.command2node[command]; | |
| 2361 Iterable<TestCase> testCases = enqueuer.command2testCases[command]; | |
| 2362 // If a command is part of many TestCases we set the timeout to be | |
| 2363 // the maximum over all [TestCase.timeout]s. At some point, we might | |
| 2364 // eliminate [TestCase.timeout] completely and move it to [Command]. | |
| 2365 int timeout = testCases.map((TestCase test) => test.timeout) | |
| 2366 .fold(0, math.max); | |
| 2367 | |
| 2368 if (_verbose) { | |
| 2369 print('Running "${command.displayName}" command: $command'); | |
| 2370 } | |
| 2371 | |
| 2372 executor.runCommand(node, command, timeout).then((CommandOutput output) { | |
| 2373 assert(command == output.command); | |
| 2374 | |
| 2375 _commandOutputStream.add(output); | |
| 2376 if (output.canRunDependendCommands) { | |
| 2377 graph.changeState(node, dgraph.NodeState.Successful); | |
| 2378 } else { | |
| 2379 graph.changeState(node, dgraph.NodeState.Failed); | |
| 2380 } | |
| 2381 | |
| 2382 _numProcesses--; | |
| 2383 if (isBrowserCommand) _numBrowserProcesses--; | |
| 2384 | |
| 2385 // Don't lose a process | |
| 2386 Timer.run(() => _tryRunNextCommand()); | |
| 2387 }); | |
| 2388 } | |
| 2389 } | |
| 2390 | |
| 2391 void _checkDone() { | |
| 2392 if (!_finishing && | |
| 2393 _runQueue.isEmpty && | |
| 2394 _numProcesses == 0 && | |
| 2395 graph.isSealed && | |
| 2396 graph.stateCount(dgraph.NodeState.Initialized) == 0 && | |
| 2397 graph.stateCount(dgraph.NodeState.Waiting) == 0 && | |
| 2398 graph.stateCount(dgraph.NodeState.Enqueuing) == 0 && | |
| 2399 graph.stateCount(dgraph.NodeState.Processing) == 0) { | |
| 2400 _finishing = true; | |
| 2401 executor.cleanup().then((_) { | |
| 2402 _completer.complete(); | |
| 2403 _commandOutputStream.close(); | |
| 2404 }); | |
| 2405 } | |
| 2406 } | |
| 2407 | |
| 2408 void dumpState() { | |
| 2409 print(""); | |
| 2410 print("CommandQueue state:"); | |
| 2411 print(" Processes: used: $_numProcesses max: $_maxProcesses"); | |
| 2412 print(" BrowserProcesses: used: $_numBrowserProcesses " | |
| 2413 "max: $_maxBrowserProcesses"); | |
| 2414 print(" Finishing: $_finishing"); | |
| 2415 print(" Queue (length = ${_runQueue.length}):"); | |
| 2416 for (var command in _runQueue) { | |
| 2417 print(" $command"); | |
| 2418 } | |
| 2419 } | |
| 2420 } | |
| 2421 | |
| 2422 | |
| 2423 /* | |
| 2424 * [CommandExecutor] is responsible for executing commands. It will make sure | |
| 2425 * that the the following two constraints are satisfied | |
| 2426 * - [:numberOfProcessesUsed <= maxProcesses:] | |
| 2427 * - [:numberOfBrowserProcessesUsed <= maxBrowserProcesses:] | |
| 2428 * | |
| 2429 * It provides a [runCommand] method which will complete with a | |
| 2430 * [CommandOutput] object. | |
| 2431 * | |
| 2432 * It provides a [cleanup] method to free all the allocated resources. | |
| 2433 */ | |
| 2434 abstract class CommandExecutor { | |
| 2435 Future cleanup(); | |
| 2436 // TODO(kustermann): The [timeout] parameter should be a property of Command | |
| 2437 Future<CommandOutput> runCommand( | |
| 2438 dgraph.Node node, Command command, int timeout); | |
| 2439 } | |
| 2440 | |
| 2441 class CommandExecutorImpl implements CommandExecutor { | |
| 2442 final Map globalConfiguration; | |
| 2443 final int maxProcesses; | |
| 2444 final int maxBrowserProcesses; | |
| 2445 | |
| 2446 // For dartanalyzer batch processing we keep a list of batch processes. | |
| 2447 final _batchProcesses = new Map<String, List<BatchRunnerProcess>>(); | |
| 2448 // We keep a BrowserTestRunner for every configuration. | |
| 2449 final _browserTestRunners = new Map<Map, BrowserTestRunner>(); | |
| 2450 | |
| 2451 bool _finishing = false; | |
| 2452 | |
| 2453 CommandExecutorImpl( | |
| 2454 this.globalConfiguration, this.maxProcesses, this.maxBrowserProcesses); | |
| 2455 | |
| 2456 Future cleanup() { | |
| 2457 assert(!_finishing); | |
| 2458 _finishing = true; | |
| 2459 | |
| 2460 Future _terminateBatchRunners() { | |
| 2461 var futures = []; | |
| 2462 for (var runners in _batchProcesses.values) { | |
| 2463 futures.addAll(runners.map((runner) => runner.terminate())); | |
| 2464 } | |
| 2465 return Future.wait(futures); | |
| 2466 } | |
| 2467 | |
| 2468 Future _terminateBrowserRunners() { | |
| 2469 var futures = | |
| 2470 _browserTestRunners.values.map((runner) => runner.terminate()); | |
| 2471 return Future.wait(futures); | |
| 2472 } | |
| 2473 | |
| 2474 return Future.wait([_terminateBatchRunners(), _terminateBrowserRunners()]); | |
| 2475 } | |
| 2476 | |
| 2477 Future<CommandOutput> runCommand(node, Command command, int timeout) { | |
| 2478 assert(!_finishing); | |
| 2479 | |
| 2480 Future<CommandOutput> runCommand(int retriesLeft) { | |
| 2481 return _runCommand(command, timeout).then((CommandOutput output) { | |
| 2482 if (retriesLeft > 0 && shouldRetryCommand(output)) { | |
| 2483 DebugLogger.warning("Rerunning Command: ($retriesLeft " | |
| 2484 "attempt(s) remains) [cmd: $command]"); | |
| 2485 return runCommand(retriesLeft - 1); | |
| 2486 } else { | |
| 2487 return new Future.value(output); | |
| 2488 } | |
| 2489 }); | |
| 2490 } | |
| 2491 return runCommand(command.maxNumRetries); | |
| 2492 } | |
| 2493 | |
| 2494 Future<CommandOutput> _runCommand(Command command, int timeout) { | |
| 2495 var batchMode = !globalConfiguration['noBatch']; | |
| 2496 var dart2jsBatchMode = globalConfiguration['dart2js_batch']; | |
| 2497 | |
| 2498 if (command is BrowserTestCommand) { | |
| 2499 return _startBrowserControllerTest(command, timeout); | |
| 2500 } else if (command is CompilationCommand && dart2jsBatchMode) { | |
| 2501 return _getBatchRunner("dart2js") | |
| 2502 .runCommand("dart2js", command, timeout, command.arguments); | |
| 2503 } else if (command is AnalysisCommand && batchMode) { | |
| 2504 return _getBatchRunner(command.flavor) | |
| 2505 .runCommand(command.flavor, command, timeout, command.arguments); | |
| 2506 } else if (command is ScriptCommand) { | |
| 2507 return command.run(); | |
| 2508 } else { | |
| 2509 return new RunningProcess(command, timeout).run(); | |
| 2510 } | |
| 2511 } | |
| 2512 | |
| 2513 BatchRunnerProcess _getBatchRunner(String identifier) { | |
| 2514 // Start batch processes if needed | |
| 2515 var runners = _batchProcesses[identifier]; | |
| 2516 if (runners == null) { | |
| 2517 runners = new List<BatchRunnerProcess>(maxProcesses); | |
| 2518 for (int i = 0; i < maxProcesses; i++) { | |
| 2519 runners[i] = new BatchRunnerProcess(); | |
| 2520 } | |
| 2521 _batchProcesses[identifier] = runners; | |
| 2522 } | |
| 2523 | |
| 2524 for (var runner in runners) { | |
| 2525 if (!runner._currentlyRunning) return runner; | |
| 2526 } | |
| 2527 throw new Exception('Unable to find inactive batch runner.'); | |
| 2528 } | |
| 2529 | |
| 2530 Future<CommandOutput> _startBrowserControllerTest( | |
| 2531 BrowserTestCommand browserCommand, int timeout) { | |
| 2532 var completer = new Completer<CommandOutput>(); | |
| 2533 | |
| 2534 var callback = (BrowserTestOutput output) { | |
| 2535 completer.complete( | |
| 2536 new BrowserControllerTestOutcome(browserCommand, output)); | |
| 2537 }; | |
| 2538 | |
| 2539 BrowserTest browserTest; | |
| 2540 if (browserCommand is BrowserHtmlTestCommand) { | |
| 2541 browserTest = new HtmlTest(browserCommand.url, callback, timeout, | |
| 2542 browserCommand.expectedMessages); | |
| 2543 } else { | |
| 2544 browserTest = new BrowserTest(browserCommand.url, callback, timeout); | |
| 2545 } | |
| 2546 _getBrowserTestRunner(browserCommand.browser, browserCommand.configuration) | |
| 2547 .then((testRunner) { | |
| 2548 testRunner.queueTest(browserTest); | |
| 2549 }); | |
| 2550 | |
| 2551 return completer.future; | |
| 2552 } | |
| 2553 | |
| 2554 Future<BrowserTestRunner> _getBrowserTestRunner(String browser, | |
| 2555 Map configuration) { | |
| 2556 var localIp = globalConfiguration['local_ip']; | |
| 2557 var num_browsers = maxBrowserProcesses; | |
| 2558 if (_browserTestRunners[configuration] == null) { | |
| 2559 var testRunner = new BrowserTestRunner( | |
| 2560 configuration, localIp, browser, num_browsers); | |
| 2561 if (globalConfiguration['verbose']) { | |
| 2562 testRunner.logger = DebugLogger.info; | |
| 2563 } | |
| 2564 _browserTestRunners[configuration] = testRunner; | |
| 2565 return testRunner.start().then((started) { | |
| 2566 if (started) { | |
| 2567 return testRunner; | |
| 2568 } | |
| 2569 print("Issue starting browser test runner"); | |
| 2570 io.exit(1); | |
| 2571 }); | |
| 2572 } | |
| 2573 return new Future.value(_browserTestRunners[configuration]); | |
| 2574 } | |
| 2575 } | |
| 2576 | |
| 2577 class RecordingCommandExecutor implements CommandExecutor { | |
| 2578 TestCaseRecorder _recorder; | |
| 2579 | |
| 2580 RecordingCommandExecutor(Path path) | |
| 2581 : _recorder = new TestCaseRecorder(path); | |
| 2582 | |
| 2583 Future<CommandOutput> runCommand(node, ProcessCommand command, int timeout) { | |
| 2584 assert(node.dependencies.length == 0); | |
| 2585 assert(_cleanEnvironmentOverrides(command.environmentOverrides)); | |
| 2586 _recorder.nextCommand(command, timeout); | |
| 2587 // Return dummy CommandOutput | |
| 2588 var output = | |
| 2589 createCommandOutput(command, 0, false, [], [], const Duration(), false); | |
| 2590 return new Future.value(output); | |
| 2591 } | |
| 2592 | |
| 2593 Future cleanup() { | |
| 2594 _recorder.finish(); | |
| 2595 return new Future.value(); | |
| 2596 } | |
| 2597 | |
| 2598 // Returns [:true:] if the environment contains only 'DART_CONFIGURATION' | |
| 2599 bool _cleanEnvironmentOverrides(Map environment) { | |
| 2600 if (environment == null) return true; | |
| 2601 return environment.length == 0 || | |
| 2602 (environment.length == 1 && | |
| 2603 environment.containsKey("DART_CONFIGURATION")); | |
| 2604 | |
| 2605 } | |
| 2606 } | |
| 2607 | |
| 2608 class ReplayingCommandExecutor implements CommandExecutor { | |
| 2609 TestCaseOutputArchive _archive = new TestCaseOutputArchive(); | |
| 2610 | |
| 2611 ReplayingCommandExecutor(Path path) { | |
| 2612 _archive.loadFromPath(path); | |
| 2613 } | |
| 2614 | |
| 2615 Future cleanup() => new Future.value(); | |
| 2616 | |
| 2617 Future<CommandOutput> runCommand(node, ProcessCommand command, int timeout) { | |
| 2618 assert(node.dependencies.length == 0); | |
| 2619 return new Future.value(_archive.outputOf(command)); | |
| 2620 } | |
| 2621 } | |
| 2622 | |
| 2623 bool shouldRetryCommand(CommandOutput output) { | |
| 2624 var command = output.command; | |
| 2625 // We rerun tests on Safari because 6.2 and 7.1 are flaky. Issue 21434. | |
| 2626 if (command is BrowserTestCommand && | |
| 2627 command.retry && | |
| 2628 command.browser == 'safari' && | |
| 2629 output is BrowserControllerTestOutcome && | |
| 2630 output._rawOutcome != Expectation.PASS) { | |
| 2631 return true; | |
| 2632 } | |
| 2633 | |
| 2634 if (!output.successful) { | |
| 2635 List<String> stdout, stderr; | |
| 2636 | |
| 2637 decodeOutput() { | |
| 2638 if (stdout == null && stderr == null) { | |
| 2639 stdout = decodeUtf8(output.stderr).split("\n"); | |
| 2640 stderr = decodeUtf8(output.stderr).split("\n"); | |
| 2641 } | |
| 2642 } | |
| 2643 | |
| 2644 if (io.Platform.operatingSystem == 'linux') { | |
| 2645 decodeOutput(); | |
| 2646 // No matter which command we ran: If we get failures due to the | |
| 2647 // "xvfb-run" issue 7564, try re-running the test. | |
| 2648 bool containsFailureMsg(String line) { | |
| 2649 return line.contains(MESSAGE_CANNOT_OPEN_DISPLAY) || | |
| 2650 line.contains(MESSAGE_FAILED_TO_RUN_COMMAND); | |
| 2651 } | |
| 2652 if (stdout.any(containsFailureMsg) || stderr.any(containsFailureMsg)) { | |
| 2653 return true; | |
| 2654 } | |
| 2655 } | |
| 2656 | |
| 2657 // We currently rerun dartium tests, see issue 14074. | |
| 2658 if (command is BrowserTestCommand && | |
| 2659 command.retry && | |
| 2660 command.browser == 'dartium') { | |
| 2661 return true; | |
| 2662 } | |
| 2663 } | |
| 2664 return false; | |
| 2665 } | |
| 2666 | |
| 2667 /* | |
| 2668 * [TestCaseCompleter] will listen for | |
| 2669 * NodeState.Processing -> NodeState.{Successful,Failed} state changes and | |
| 2670 * will complete a TestCase if it is finished. | |
| 2671 * | |
| 2672 * It provides a stream [finishedTestCases], which will stream all TestCases | |
| 2673 * once they're finished. After all TestCases are done, the stream will be | |
| 2674 * closed. | |
| 2675 */ | |
| 2676 class TestCaseCompleter { | |
| 2677 static final COMPLETED_STATES = [dgraph.NodeState.Failed, | |
| 2678 dgraph.NodeState.Successful]; | |
| 2679 final dgraph.Graph graph; | |
| 2680 final TestCaseEnqueuer enqueuer; | |
| 2681 final CommandQueue commandQueue; | |
| 2682 | |
| 2683 Map<Command, CommandOutput> _outputs = new Map<Command, CommandOutput>(); | |
| 2684 bool _closed = false; | |
| 2685 StreamController<TestCase> _controller = new StreamController<TestCase>(); | |
| 2686 | |
| 2687 TestCaseCompleter(this.graph, this.enqueuer, this.commandQueue) { | |
| 2688 var eventCondition = graph.events.where; | |
| 2689 bool finishedRemainingTestCases = false; | |
| 2690 | |
| 2691 // Store all the command outputs -- they will be delivered synchronously | |
| 2692 // (i.e. before state changes in the graph) | |
| 2693 commandQueue.completedCommands.listen((CommandOutput output) { | |
| 2694 _outputs[output.command] = output; | |
| 2695 }, onDone: () { | |
| 2696 _completeTestCasesIfPossible(new List.from(enqueuer.remainingTestCases)); | |
| 2697 finishedRemainingTestCases = true; | |
| 2698 assert(enqueuer.remainingTestCases.isEmpty); | |
| 2699 _checkDone(); | |
| 2700 }); | |
| 2701 | |
| 2702 // Listen for NodeState.Processing -> NodeState.{Successful,Failed} | |
| 2703 // changes. | |
| 2704 eventCondition((event) => event is dgraph.StateChangedEvent) | |
| 2705 .listen((dgraph.StateChangedEvent event) { | |
| 2706 if (event.from == dgraph.NodeState.Processing && | |
| 2707 !finishedRemainingTestCases ) { | |
| 2708 var command = event.node.userData; | |
| 2709 | |
| 2710 assert(COMPLETED_STATES.contains(event.to)); | |
| 2711 assert(_outputs[command] != null); | |
| 2712 | |
| 2713 _completeTestCasesIfPossible(enqueuer.command2testCases[command]); | |
| 2714 _checkDone(); | |
| 2715 } | |
| 2716 }); | |
| 2717 | |
| 2718 // Listen also for GraphSealedEvent's. If there is not a single node in the | |
| 2719 // graph, we still want to finish after the graph was sealed. | |
| 2720 eventCondition((event) => event is dgraph.GraphSealedEvent) | |
| 2721 .listen((dgraph.GraphSealedEvent event) { | |
| 2722 if (!_closed && enqueuer.remainingTestCases.isEmpty) { | |
| 2723 _controller.close(); | |
| 2724 _closed = true; | |
| 2725 } | |
| 2726 }); | |
| 2727 } | |
| 2728 | |
| 2729 Stream<TestCase> get finishedTestCases => _controller.stream; | |
| 2730 | |
| 2731 void _checkDone() { | |
| 2732 if (!_closed && graph.isSealed && enqueuer.remainingTestCases.isEmpty) { | |
| 2733 _controller.close(); | |
| 2734 _closed = true; | |
| 2735 } | |
| 2736 } | |
| 2737 | |
| 2738 void _completeTestCasesIfPossible(Iterable<TestCase> testCases) { | |
| 2739 // Update TestCases with command outputs | |
| 2740 for (TestCase test in testCases) { | |
| 2741 for (var icommand in test.commands) { | |
| 2742 var output = _outputs[icommand]; | |
| 2743 if (output != null) { | |
| 2744 test.commandOutputs[icommand] = output; | |
| 2745 } | |
| 2746 } | |
| 2747 } | |
| 2748 | |
| 2749 void completeTestCase(TestCase testCase) { | |
| 2750 if (enqueuer.remainingTestCases.contains(testCase)) { | |
| 2751 _controller.add(testCase); | |
| 2752 enqueuer.remainingTestCases.remove(testCase); | |
| 2753 } else { | |
| 2754 DebugLogger.error("${testCase.displayName} would be finished twice"); | |
| 2755 } | |
| 2756 } | |
| 2757 | |
| 2758 for (var testCase in testCases) { | |
| 2759 // Ask the [testCase] if it's done. Note that we assume, that | |
| 2760 // [TestCase.isFinished] will return true if all commands were executed | |
| 2761 // or if a previous one failed. | |
| 2762 if (testCase.isFinished) { | |
| 2763 completeTestCase(testCase); | |
| 2764 } | |
| 2765 } | |
| 2766 } | |
| 2767 } | |
| 2768 | |
| 2769 | |
| 2770 class ProcessQueue { | |
| 2771 Map _globalConfiguration; | |
| 2772 | |
| 2773 Function _allDone; | |
| 2774 final dgraph.Graph _graph = new dgraph.Graph(); | |
| 2775 List<EventListener> _eventListener; | |
| 2776 | |
| 2777 ProcessQueue(this._globalConfiguration, | |
| 2778 maxProcesses, | |
| 2779 maxBrowserProcesses, | |
| 2780 DateTime startTime, | |
| 2781 testSuites, | |
| 2782 this._eventListener, | |
| 2783 this._allDone, | |
| 2784 [bool verbose = false, | |
| 2785 String recordingOutputFile, | |
| 2786 String recordedInputFile]) { | |
| 2787 void setupForListing(TestCaseEnqueuer testCaseEnqueuer) { | |
| 2788 _graph.events.where((event) => event is dgraph.GraphSealedEvent) | |
| 2789 .listen((dgraph.GraphSealedEvent event) { | |
| 2790 var testCases = new List.from(testCaseEnqueuer.remainingTestCases); | |
| 2791 testCases.sort((a, b) => a.displayName.compareTo(b.displayName)); | |
| 2792 | |
| 2793 print("\nGenerating all matching test cases ....\n"); | |
| 2794 | |
| 2795 for (TestCase testCase in testCases) { | |
| 2796 eventFinishedTestCase(testCase); | |
| 2797 print("${testCase.displayName} " | |
| 2798 "Expectations: ${testCase.expectedOutcomes.join(', ')} " | |
| 2799 "Configuration: '${testCase.configurationString}'"); | |
| 2800 } | |
| 2801 eventAllTestsKnown(); | |
| 2802 }); | |
| 2803 } | |
| 2804 | |
| 2805 var testCaseEnqueuer; | |
| 2806 CommandQueue commandQueue; | |
| 2807 | |
| 2808 void setupForRunning(TestCaseEnqueuer testCaseEnqueuer) { | |
| 2809 Timer _debugTimer; | |
| 2810 // If we haven't seen a single test finishing during a 10 minute period | |
| 2811 // something is definitly wrong, so we dump the debugging information. | |
| 2812 final debugTimerDuration = const Duration(minutes: 10); | |
| 2813 | |
| 2814 void cancelDebugTimer() { | |
| 2815 if (_debugTimer != null) { | |
| 2816 _debugTimer.cancel(); | |
| 2817 } | |
| 2818 } | |
| 2819 | |
| 2820 void resetDebugTimer() { | |
| 2821 cancelDebugTimer(); | |
| 2822 _debugTimer = new Timer(debugTimerDuration, () { | |
| 2823 print("The debug timer of test.dart expired. Please report this issue" | |
| 2824 " to ricow/kustermann and provide the following information:"); | |
| 2825 print(""); | |
| 2826 print("Graph is sealed: ${_graph.isSealed}"); | |
| 2827 print(""); | |
| 2828 _graph.DumpCounts(); | |
| 2829 print(""); | |
| 2830 var unfinishedNodeStates = [ | |
| 2831 dgraph.NodeState.Initialized, | |
| 2832 dgraph.NodeState.Waiting, | |
| 2833 dgraph.NodeState.Enqueuing, | |
| 2834 dgraph.NodeState.Processing]; | |
| 2835 | |
| 2836 for (var nodeState in unfinishedNodeStates) { | |
| 2837 if (_graph.stateCount(nodeState) > 0) { | |
| 2838 print("Commands in state '$nodeState':"); | |
| 2839 print("================================="); | |
| 2840 print(""); | |
| 2841 for (var node in _graph.nodes) { | |
| 2842 if (node.state == nodeState) { | |
| 2843 var command = node.userData; | |
| 2844 var testCases = testCaseEnqueuer.command2testCases[command]; | |
| 2845 print(" Command: $command"); | |
| 2846 for (var testCase in testCases) { | |
| 2847 print(" Enqueued by: ${testCase.configurationString} " | |
| 2848 "-- ${testCase.displayName}"); | |
| 2849 } | |
| 2850 print(""); | |
| 2851 } | |
| 2852 } | |
| 2853 print(""); | |
| 2854 print(""); | |
| 2855 } | |
| 2856 } | |
| 2857 | |
| 2858 if (commandQueue != null) { | |
| 2859 commandQueue.dumpState(); | |
| 2860 } | |
| 2861 }); | |
| 2862 } | |
| 2863 | |
| 2864 bool recording = recordingOutputFile != null; | |
| 2865 bool replaying = recordedInputFile != null; | |
| 2866 | |
| 2867 // When the graph building is finished, notify event listeners. | |
| 2868 _graph.events | |
| 2869 .where((event) => event is dgraph.GraphSealedEvent).listen((event) { | |
| 2870 eventAllTestsKnown(); | |
| 2871 }); | |
| 2872 | |
| 2873 // Queue commands as they become "runnable" | |
| 2874 var commandEnqueuer = new CommandEnqueuer(_graph); | |
| 2875 | |
| 2876 // CommandExecutor will execute commands | |
| 2877 var executor; | |
| 2878 if (recording) { | |
| 2879 executor = new RecordingCommandExecutor(new Path(recordingOutputFile)); | |
| 2880 } else if (replaying) { | |
| 2881 executor = new ReplayingCommandExecutor(new Path(recordedInputFile)); | |
| 2882 } else { | |
| 2883 executor = new CommandExecutorImpl( | |
| 2884 _globalConfiguration, maxProcesses, maxBrowserProcesses); | |
| 2885 } | |
| 2886 | |
| 2887 // Run "runnable commands" using [executor] subject to | |
| 2888 // maxProcesses/maxBrowserProcesses constraint | |
| 2889 commandQueue = new CommandQueue( | |
| 2890 _graph, testCaseEnqueuer, executor, maxProcesses, maxBrowserProcesses, | |
| 2891 verbose); | |
| 2892 | |
| 2893 // Finish test cases when all commands were run (or some failed) | |
| 2894 var testCaseCompleter = | |
| 2895 new TestCaseCompleter(_graph, testCaseEnqueuer, commandQueue); | |
| 2896 testCaseCompleter.finishedTestCases.listen( | |
| 2897 (TestCase finishedTestCase) { | |
| 2898 resetDebugTimer(); | |
| 2899 | |
| 2900 // If we're recording, we don't report any TestCases to listeners. | |
| 2901 if (!recording) { | |
| 2902 eventFinishedTestCase(finishedTestCase); | |
| 2903 } | |
| 2904 }, | |
| 2905 onDone: () { | |
| 2906 // Wait until the commandQueue/execturo is done (it may need to stop | |
| 2907 // batch runners, browser controllers, ....) | |
| 2908 commandQueue.done.then((_) { | |
| 2909 cancelDebugTimer(); | |
| 2910 eventAllTestsDone(); | |
| 2911 }); | |
| 2912 }); | |
| 2913 | |
| 2914 resetDebugTimer(); | |
| 2915 } | |
| 2916 | |
| 2917 // Build up the dependency graph | |
| 2918 testCaseEnqueuer = new TestCaseEnqueuer(_graph, (TestCase newTestCase) { | |
| 2919 eventTestAdded(newTestCase); | |
| 2920 }); | |
| 2921 | |
| 2922 // Either list or run the tests | |
| 2923 if (_globalConfiguration['list']) { | |
| 2924 setupForListing(testCaseEnqueuer); | |
| 2925 } else { | |
| 2926 setupForRunning(testCaseEnqueuer); | |
| 2927 } | |
| 2928 | |
| 2929 // Start enqueing all TestCases | |
| 2930 testCaseEnqueuer.enqueueTestSuites(testSuites); | |
| 2931 } | |
| 2932 | |
| 2933 void freeEnqueueingStructures() { | |
| 2934 CommandBuilder.instance.clearCommandCache(); | |
| 2935 } | |
| 2936 | |
| 2937 void eventFinishedTestCase(TestCase testCase) { | |
| 2938 for (var listener in _eventListener) { | |
| 2939 listener.done(testCase); | |
| 2940 } | |
| 2941 } | |
| 2942 | |
| 2943 void eventTestAdded(TestCase testCase) { | |
| 2944 for (var listener in _eventListener) { | |
| 2945 listener.testAdded(); | |
| 2946 } | |
| 2947 } | |
| 2948 | |
| 2949 void eventAllTestsKnown() { | |
| 2950 freeEnqueueingStructures(); | |
| 2951 for (var listener in _eventListener) { | |
| 2952 listener.allTestsKnown(); | |
| 2953 } | |
| 2954 } | |
| 2955 | |
| 2956 void eventAllTestsDone() { | |
| 2957 for (var listener in _eventListener) { | |
| 2958 listener.allDone(); | |
| 2959 } | |
| 2960 _allDone(); | |
| 2961 } | |
| 2962 } | |
| OLD | NEW |