Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(107)

Side by Side Diff: tools/testing/dart/lib/command.dart

Issue 841193003: cleanup to tools/testing/dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: one last bit Created 5 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « tools/testing/dart/lib/browser_test.dart ('k') | tools/testing/dart/lib/dependency_graph.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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 }
OLDNEW
« no previous file with comments | « tools/testing/dart/lib/browser_test.dart ('k') | tools/testing/dart/lib/dependency_graph.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698