| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 library browser; | 4 library browser; |
| 5 | 5 |
| 6 import "dart:async"; | 6 import "dart:async"; |
| 7 import "dart:convert" show LineSplitter, UTF8, JSON; | 7 import "dart:convert" show LineSplitter, UTF8, JSON; |
| 8 import "dart:core"; | 8 import "dart:core"; |
| 9 import "dart:io"; | 9 import "dart:io"; |
| 10 import "dart:math" show max, min; |
| 10 | 11 |
| 11 import 'android.dart'; | 12 import 'android.dart'; |
| 12 import 'http_server.dart'; | 13 import 'http_server.dart'; |
| 13 import 'path.dart'; | 14 import 'path.dart'; |
| 14 import 'utils.dart'; | 15 import 'utils.dart'; |
| 15 | 16 |
| 16 class BrowserOutput { | 17 class BrowserOutput { |
| 17 final StringBuffer stdout = new StringBuffer(); | 18 final StringBuffer stdout = new StringBuffer(); |
| 18 final StringBuffer stderr = new StringBuffer(); | 19 final StringBuffer stderr = new StringBuffer(); |
| 19 final StringBuffer eventLog = new StringBuffer(); | 20 final StringBuffer eventLog = new StringBuffer(); |
| (...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 136 } else { | 137 } else { |
| 137 _logEvent("The process is already dead."); | 138 _logEvent("The process is already dead."); |
| 138 return new Future.value(true); | 139 return new Future.value(true); |
| 139 } | 140 } |
| 140 } | 141 } |
| 141 | 142 |
| 142 /** | 143 /** |
| 143 * Start the browser using the supplied argument. | 144 * Start the browser using the supplied argument. |
| 144 * This sets up the error handling and usage logging. | 145 * This sets up the error handling and usage logging. |
| 145 */ | 146 */ |
| 146 Future<bool> startBrowser(String command, | 147 Future<bool> startBrowserProcess(String command, |
| 147 List<String> arguments, | 148 List<String> arguments, |
| 148 {Map<String,String> environment}) { | 149 {Map<String,String> environment}) { |
| 149 return Process.start(command, arguments, environment: environment) | 150 return Process.start(command, arguments, environment: environment) |
| 150 .then((startedProcess) { | 151 .then((startedProcess) { |
| 151 _logEvent("Started browser using $command ${arguments.join(' ')}"); | 152 _logEvent("Started browser using $command ${arguments.join(' ')}"); |
| 152 process = startedProcess; | 153 process = startedProcess; |
| 153 // Used to notify when exiting, and as a return value on calls to | 154 // Used to notify when exiting, and as a return value on calls to |
| 154 // close(). | 155 // close(). |
| 155 var doneCompleter = new Completer(); | 156 var doneCompleter = new Completer(); |
| 156 done = doneCompleter.future; | 157 done = doneCompleter.future; |
| (...skipping 218 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 375 _logEvent("Could not clear cache"); | 376 _logEvent("Could not clear cache"); |
| 376 return false; | 377 return false; |
| 377 } | 378 } |
| 378 // Get the version and log that. | 379 // Get the version and log that. |
| 379 return getVersion().then((version) { | 380 return getVersion().then((version) { |
| 380 _logEvent("Got version: $version"); | 381 _logEvent("Got version: $version"); |
| 381 return Directory.systemTemp.createTemp().then((userDir) { | 382 return Directory.systemTemp.createTemp().then((userDir) { |
| 382 _cleanup = () { userDir.deleteSync(recursive: true); }; | 383 _cleanup = () { userDir.deleteSync(recursive: true); }; |
| 383 _createLaunchHTML(userDir.path, url); | 384 _createLaunchHTML(userDir.path, url); |
| 384 var args = ["${userDir.path}/launch.html"]; | 385 var args = ["${userDir.path}/launch.html"]; |
| 385 return startBrowser(_binary, args); | 386 return startBrowserProcess(_binary, args); |
| 386 }); | 387 }); |
| 387 }).catchError((error) { | 388 }).catchError((error) { |
| 388 _logEvent("Running $_binary --version failed with $error"); | 389 _logEvent("Running $_binary --version failed with $error"); |
| 389 return false; | 390 return false; |
| 390 }); | 391 }); |
| 391 }); | 392 }); |
| 392 }); | 393 }); |
| 393 } | 394 } |
| 394 | 395 |
| 395 String toString() => "Safari"; | 396 String toString() => "Safari"; |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 435 // Get the version and log that. | 436 // Get the version and log that. |
| 436 return _getVersion().then((success) { | 437 return _getVersion().then((success) { |
| 437 if (!success) return false; | 438 if (!success) return false; |
| 438 _logEvent("Got version: $_version"); | 439 _logEvent("Got version: $_version"); |
| 439 | 440 |
| 440 return Directory.systemTemp.createTemp().then((userDir) { | 441 return Directory.systemTemp.createTemp().then((userDir) { |
| 441 _cleanup = () { userDir.deleteSync(recursive: true); }; | 442 _cleanup = () { userDir.deleteSync(recursive: true); }; |
| 442 var args = ["--user-data-dir=${userDir.path}", url, | 443 var args = ["--user-data-dir=${userDir.path}", url, |
| 443 "--disable-extensions", "--disable-popup-blocking", | 444 "--disable-extensions", "--disable-popup-blocking", |
| 444 "--bwsi", "--no-first-run"]; | 445 "--bwsi", "--no-first-run"]; |
| 445 return startBrowser(_binary, args, environment: _getEnvironment()); | 446 return startBrowserProcess(_binary, args, environment: _getEnvironment()
); |
| 446 }); | 447 }); |
| 447 }).catchError((e) { | 448 }).catchError((e) { |
| 448 _logEvent("Running $_binary --version failed with $e"); | 449 _logEvent("Running $_binary --version failed with $e"); |
| 449 return false; | 450 return false; |
| 450 }); | 451 }); |
| 451 } | 452 } |
| 452 | 453 |
| 453 String toString() => "Chrome"; | 454 String toString() => "Chrome"; |
| 454 } | 455 } |
| 455 | 456 |
| (...skipping 21 matching lines...) Expand all Loading... |
| 477 if (!success) { | 478 if (!success) { |
| 478 _logEvent("Could not clear cache, exiting"); | 479 _logEvent("Could not clear cache, exiting"); |
| 479 return false; | 480 return false; |
| 480 } | 481 } |
| 481 var args = ["-SimulateApplication", | 482 var args = ["-SimulateApplication", |
| 482 "/Applications/Xcode.app/Contents/Developer/Platforms/" | 483 "/Applications/Xcode.app/Contents/Developer/Platforms/" |
| 483 "iPhoneSimulator.platform/Developer/SDKs/" | 484 "iPhoneSimulator.platform/Developer/SDKs/" |
| 484 "iPhoneSimulator7.1.sdk/Applications/MobileSafari.app/" | 485 "iPhoneSimulator7.1.sdk/Applications/MobileSafari.app/" |
| 485 "MobileSafari", | 486 "MobileSafari", |
| 486 "-u", url]; | 487 "-u", url]; |
| 487 return startBrowser(_binary, args) | 488 return startBrowserProcess(_binary, args) |
| 488 .catchError((e) { | 489 .catchError((e) { |
| 489 _logEvent("Running $_binary --version failed with $e"); | 490 _logEvent("Running $_binary --version failed with $e"); |
| 490 return false; | 491 return false; |
| 491 }); | 492 }); |
| 492 }); | 493 }); |
| 493 } | 494 } |
| 494 | 495 |
| 495 String toString() => "SafariMobileSimulator"; | 496 String toString() => "SafariMobileSimulator"; |
| 496 } | 497 } |
| 497 | 498 |
| (...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 549 .catchError((error) { | 550 .catchError((error) { |
| 550 _logEvent("Deleting recovery dir failed with $error"); | 551 _logEvent("Deleting recovery dir failed with $error"); |
| 551 return false; | 552 return false; |
| 552 }); | 553 }); |
| 553 } | 554 } |
| 554 | 555 |
| 555 Future<bool> start(String url) { | 556 Future<bool> start(String url) { |
| 556 _logEvent("Starting ie browser on: $url"); | 557 _logEvent("Starting ie browser on: $url"); |
| 557 return clearCache().then((_) => getVersion()).then((version) { | 558 return clearCache().then((_) => getVersion()).then((version) { |
| 558 _logEvent("Got version: $version"); | 559 _logEvent("Got version: $version"); |
| 559 return startBrowser(_binary, [url]); | 560 return startBrowserProcess(_binary, [url]); |
| 560 }); | 561 }); |
| 561 } | 562 } |
| 562 String toString() => "IE"; | 563 String toString() => "IE"; |
| 563 | 564 |
| 564 } | 565 } |
| 565 | 566 |
| 566 | 567 |
| 567 class AndroidBrowserConfig { | 568 class AndroidBrowserConfig { |
| 568 final String name; | 569 final String name; |
| 569 final String package; | 570 final String package; |
| (...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 639 | 640 |
| 640 | 641 |
| 641 class AndroidChrome extends Browser { | 642 class AndroidChrome extends Browser { |
| 642 static const String viewAction = 'android.intent.action.VIEW'; | 643 static const String viewAction = 'android.intent.action.VIEW'; |
| 643 static const String mainAction = 'android.intent.action.MAIN'; | 644 static const String mainAction = 'android.intent.action.MAIN'; |
| 644 static const String chromePackage = 'com.android.chrome'; | 645 static const String chromePackage = 'com.android.chrome'; |
| 645 static const String browserPackage = 'com.android.browser'; | 646 static const String browserPackage = 'com.android.browser'; |
| 646 static const String firefoxPackage = 'org.mozilla.firefox'; | 647 static const String firefoxPackage = 'org.mozilla.firefox'; |
| 647 static const String turnScreenOnPackage = 'com.google.dart.turnscreenon'; | 648 static const String turnScreenOnPackage = 'com.google.dart.turnscreenon'; |
| 648 | 649 |
| 649 AndroidEmulator _emulator; | |
| 650 AdbDevice _adbDevice; | 650 AdbDevice _adbDevice; |
| 651 | 651 |
| 652 AndroidChrome(this._adbDevice); | 652 AndroidChrome(this._adbDevice); |
| 653 | 653 |
| 654 Future<bool> start(String url) { | 654 Future<bool> start(String url) { |
| 655 var browserIntent = new Intent( | 655 var browserIntent = new Intent( |
| 656 viewAction, browserPackage, '.BrowserActivity', url); | 656 viewAction, browserPackage, '.BrowserActivity', url); |
| 657 var chromeIntent = new Intent(viewAction, chromePackage, '.Main', url); | 657 var chromeIntent = new Intent(viewAction, chromePackage, '.Main', url); |
| 658 var firefoxIntent = new Intent(viewAction, firefoxPackage, '.App', url); | 658 var firefoxIntent = new Intent(viewAction, firefoxPackage, '.App', url); |
| 659 var turnScreenOnIntent = | 659 var turnScreenOnIntent = |
| (...skipping 79 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 739 version = versionResult.stdout; | 739 version = versionResult.stdout; |
| 740 _logEvent("Got version: $version"); | 740 _logEvent("Got version: $version"); |
| 741 | 741 |
| 742 return Directory.systemTemp.createTemp().then((userDir) { | 742 return Directory.systemTemp.createTemp().then((userDir) { |
| 743 _createPreferenceFile(userDir.path); | 743 _createPreferenceFile(userDir.path); |
| 744 _cleanup = () { userDir.deleteSync(recursive: true); }; | 744 _cleanup = () { userDir.deleteSync(recursive: true); }; |
| 745 var args = ["-profile", "${userDir.path}", | 745 var args = ["-profile", "${userDir.path}", |
| 746 "-no-remote", "-new-instance", url]; | 746 "-no-remote", "-new-instance", url]; |
| 747 var environment = new Map<String,String>.from(Platform.environment); | 747 var environment = new Map<String,String>.from(Platform.environment); |
| 748 environment["MOZ_CRASHREPORTER_DISABLE"] = "1"; | 748 environment["MOZ_CRASHREPORTER_DISABLE"] = "1"; |
| 749 return startBrowser(_binary, args, environment: environment); | 749 return startBrowserProcess(_binary, args, environment: environment); |
| 750 | 750 |
| 751 }); | 751 }); |
| 752 }).catchError((e) { | 752 }).catchError((e) { |
| 753 _logEvent("Running $_binary --version failed with $e"); | 753 _logEvent("Running $_binary --version failed with $e"); |
| 754 return false; | 754 return false; |
| 755 }); | 755 }); |
| 756 } | 756 } |
| 757 | 757 |
| 758 String toString() => "Firefox"; | 758 String toString() => "Firefox"; |
| 759 } | 759 } |
| 760 | 760 |
| 761 | 761 |
| 762 /** | 762 /** |
| 763 * Describes the current state of a browser used for testing. | 763 * Describes the current state of a browser used for testing. |
| 764 */ | 764 */ |
| 765 class BrowserTestingStatus { | 765 class BrowserStatus { |
| 766 Browser browser; | 766 Browser browser; |
| 767 BrowserTest currentTest; | 767 BrowserTest currentTest; |
| 768 | 768 |
| 769 // This is currently not used for anything except for error reporting. | 769 // This is currently not used for anything except for error reporting. |
| 770 // Given the usefulness of this in debugging issues this should not be | 770 // Given the usefulness of this in debugging issues this should not be |
| 771 // removed even when we have a really stable system. | 771 // removed even when we have a really stable system. |
| 772 BrowserTest lastTest; | 772 BrowserTest lastTest; |
| 773 bool timeout = false; | 773 bool timeout = false; |
| 774 Timer nextTestTimeout; | 774 Timer nextTestTimeout; |
| 775 Stopwatch timeSinceRestart = new Stopwatch(); | 775 Stopwatch timeSinceRestart = new Stopwatch()..start(); |
| 776 | 776 |
| 777 BrowserTestingStatus(Browser this.browser); | 777 BrowserStatus(Browser this.browser); |
| 778 } | 778 } |
| 779 | 779 |
| 780 | 780 |
| 781 /** | 781 /** |
| 782 * Describes a single test to be run in the browser. | 782 * Describes a single test to be run in the browser. |
| 783 */ | 783 */ |
| 784 class BrowserTest { | 784 class BrowserTest { |
| 785 // TODO(ricow): Add timeout callback instead of the string passing hack. | 785 // TODO(ricow): Add timeout callback instead of the string passing hack. |
| 786 Function doneCallback; | 786 Function doneCallback; |
| 787 String url; | 787 String url; |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 835 final String lastKnownMessage; | 835 final String lastKnownMessage; |
| 836 | 836 |
| 837 final BrowserOutput browserOutput; | 837 final BrowserOutput browserOutput; |
| 838 final bool didTimeout; | 838 final bool didTimeout; |
| 839 | 839 |
| 840 BrowserTestOutput( | 840 BrowserTestOutput( |
| 841 this.delayUntilTestStarted, this.duration, this.lastKnownMessage, | 841 this.delayUntilTestStarted, this.duration, this.lastKnownMessage, |
| 842 this.browserOutput, {this.didTimeout: false}); | 842 this.browserOutput, {this.didTimeout: false}); |
| 843 } | 843 } |
| 844 | 844 |
| 845 /** | 845 |
| 846 * Encapsulates all the functionality for running tests in browsers. | 846 /// Encapsulates all the functionality for running tests in browsers. |
| 847 * The interface is rather simple. After starting, the runner tests | 847 /// Tests are added to the queue and the supplied callbacks are called |
| 848 * are simply added to the queue and a the supplied callbacks are called | 848 /// when a test completes. |
| 849 * whenever a test completes. | 849 /// BrowserTestRunner starts up to maxNumBrowser instances of the browser, |
| 850 */ | 850 /// to run the tests, starting them sequentially, as needed, so only |
| 851 /// one is starting up at a time. |
| 852 /// BrowserTestRunner starts a BrowserTestingServer, which serves a |
| 853 /// driver page to the browsers, serves tests, and receives results and |
| 854 /// requests back from the browsers. |
| 851 class BrowserTestRunner { | 855 class BrowserTestRunner { |
| 852 static const int MAX_NEXT_TEST_TIMEOUTS = 10; | 856 static const int MAX_NEXT_TEST_TIMEOUTS = 10; |
| 853 static const Duration NEXT_TEST_TIMEOUT = const Duration(seconds: 60); | 857 static const Duration NEXT_TEST_TIMEOUT = const Duration(seconds: 60); |
| 854 static const Duration RESTART_BROWSER_INTERVAL = const Duration(seconds: 60); | 858 static const Duration RESTART_BROWSER_INTERVAL = const Duration(seconds: 60); |
| 855 | 859 |
| 860 /// If the queue was recently empty, don't start another browser. |
| 861 static const Duration MIN_NONEMPTY_QUEUE_TIME = const Duration(seconds: 1); |
| 862 |
| 856 final Map configuration; | 863 final Map configuration; |
| 864 BrowserTestingServer testingServer; |
| 857 | 865 |
| 858 final String localIp; | 866 final String localIp; |
| 859 String browserName; | 867 String browserName; |
| 860 final int maxNumBrowsers; | 868 int maxNumBrowsers; |
| 861 bool checkedMode; | 869 bool checkedMode; |
| 870 int numBrowsers = 0; |
| 862 // Used to send back logs from the browser (start, stop etc) | 871 // Used to send back logs from the browser (start, stop etc) |
| 863 Function logger; | 872 Function logger; |
| 864 int browserIdCount = 0; | |
| 865 | 873 |
| 874 int browserIdCounter = 1; |
| 875 |
| 876 bool testingServerStarted = false; |
| 866 bool underTermination = false; | 877 bool underTermination = false; |
| 867 int numBrowserGetTestTimeouts = 0; | 878 int numBrowserGetTestTimeouts = 0; |
| 868 | 879 DateTime lastEmptyTestQueueTime = new DateTime.now(); |
| 880 String _currentStartingBrowserId; |
| 869 List<BrowserTest> testQueue = new List<BrowserTest>(); | 881 List<BrowserTest> testQueue = new List<BrowserTest>(); |
| 870 Map<String, BrowserTestingStatus> browserStatus = | 882 Map<String, BrowserStatus> browserStatus = |
| 871 new Map<String, BrowserTestingStatus>(); | 883 new Map<String, BrowserStatus>(); |
| 872 | 884 |
| 873 var adbDeviceMapping = new Map<String, AdbDevice>(); | 885 var adbDeviceMapping = new Map<String, AdbDevice>(); |
| 886 List<AdbDevice> idleAdbDevices; |
| 887 |
| 874 // This cache is used to guarantee that we never see double reporting. | 888 // This cache is used to guarantee that we never see double reporting. |
| 875 // If we do we need to provide developers with this information. | 889 // If we do we need to provide developers with this information. |
| 876 // We don't add urls to the cache until we have run it. | 890 // We don't add urls to the cache until we have run it. |
| 877 Map<int, String> testCache = new Map<int, String>(); | 891 Map<int, String> testCache = new Map<int, String>(); |
| 892 |
| 878 Map<int, String> doubleReportingOutputs = new Map<int, String>(); | 893 Map<int, String> doubleReportingOutputs = new Map<int, String>(); |
| 894 List<String> timedOut = []; |
| 879 | 895 |
| 880 BrowserTestingServer testingServer; | 896 // We will start a new browser when the test queue hasn't been empty |
| 897 // recently, we have fewer than maxNumBrowsers browsers, and there is |
| 898 // no other browser instance currently starting up. |
| 899 bool get queueWasEmptyRecently { |
| 900 return testQueue.isEmpty || |
| 901 new DateTime.now().difference(lastEmptyTestQueueTime) < |
| 902 MIN_NONEMPTY_QUEUE_TIME; |
| 903 } |
| 881 | 904 |
| 882 /** | 905 // While a browser is starting, but has not requested its first test, its |
| 883 * The TestRunner takes the testingServer in as a constructor parameter in | 906 // browserId is stored in _currentStartingBrowserId. |
| 884 * case we wish to have a testing server with different behavior (such as the | 907 // When no browser is currently starting, _currentStartingBrowserId is null. |
| 885 * case for performance testing. | 908 bool get aBrowserIsCurrentlyStarting => _currentStartingBrowserId != null; |
| 886 */ | 909 void markCurrentlyStarting(String id) { |
| 910 _currentStartingBrowserId = id; |
| 911 } |
| 912 void markNotCurrentlyStarting(String id) { |
| 913 if (_currentStartingBrowserId == id) _currentStartingBrowserId = null; |
| 914 } |
| 915 |
| 916 // If [browserName] doesn't support opening new windows, we use new iframes |
| 917 // instead. |
| 918 bool get useIframe => |
| 919 !Browser.BROWSERS_WITH_WINDOW_SUPPORT.contains(browserName); |
| 920 |
| 921 /// The optional testingServer parameter allows callers to pass in |
| 922 /// a testing server with different behavior than the default |
| 923 /// BrowserTestServer. The url handlers of the testingServer are |
| 924 /// overwritten, so an existing handler can't be shared between instances. |
| 887 BrowserTestRunner(this.configuration, | 925 BrowserTestRunner(this.configuration, |
| 888 this.localIp, | 926 this.localIp, |
| 889 this.browserName, | 927 this.browserName, |
| 890 this.maxNumBrowsers, | 928 this.maxNumBrowsers, |
| 891 {BrowserTestingServer this.testingServer}) { | 929 {BrowserTestingServer this.testingServer}) { |
| 892 checkedMode = configuration['checked']; | 930 checkedMode = configuration['checked']; |
| 931 if (browserName == 'ff') browserName = 'firefox'; |
| 893 } | 932 } |
| 894 | 933 |
| 895 Future<bool> start() { | 934 Future start() async { |
| 896 // If [browserName] doesn't support opening new windows, we use new iframes | |
| 897 // instead. | |
| 898 bool useIframe = | |
| 899 !Browser.BROWSERS_WITH_WINDOW_SUPPORT.contains(browserName); | |
| 900 if (testingServer == null) { | 935 if (testingServer == null) { |
| 901 testingServer = new BrowserTestingServer( | 936 testingServer = new BrowserTestingServer( |
| 902 configuration, localIp, useIframe); | 937 configuration, localIp, useIframe); |
| 903 } | 938 } |
| 904 return testingServer.start().then((_) { | 939 await testingServer.start(); |
| 905 testingServer.testDoneCallBack = handleResults; | 940 testingServer |
| 906 testingServer.testStatusUpdateCallBack = handleStatusUpdate; | 941 ..testDoneCallBack = handleResults |
| 907 testingServer.testStartedCallBack = handleStarted; | 942 ..testStatusUpdateCallBack = handleStatusUpdate |
| 908 testingServer.nextTestCallBack = getNextTest; | 943 ..testStartedCallBack = handleStarted |
| 909 return getBrowsers().then((browsers) { | 944 ..nextTestCallBack = getNextTest; |
| 910 var futures = []; | 945 if (browserName == 'chromeOnAndroid') { |
| 911 for (var browser in browsers) { | 946 var idbNames = await AdbHelper.listDevices(); |
| 912 var url = testingServer.getDriverUrl(browser.id); | 947 idleAdbDevices = new List.from(idbNames.map((id) => new AdbDevice(id))); |
| 913 var future = browser.start(url).then((success) { | 948 maxNumBrowsers = min(maxNumBrowsers, idleAdbDevices.length); |
| 914 if (success) { | 949 } |
| 915 var status = new BrowserTestingStatus(browser); | 950 testingServerStarted = true; |
| 916 browserStatus[browser.id] = status; | 951 requestBrowser(); |
| 917 status.nextTestTimeout = createNextTestTimer(status); | |
| 918 status.timeSinceRestart.start(); | |
| 919 } | |
| 920 return success; | |
| 921 }); | |
| 922 futures.add(future); | |
| 923 } | |
| 924 return Future.wait(futures).then((values) { | |
| 925 return !values.contains(false); | |
| 926 }); | |
| 927 }); | |
| 928 }); | |
| 929 } | 952 } |
| 930 | 953 |
| 931 Future<List<Browser>> getBrowsers() { | 954 /// requestBrowser() is called whenever we might want to start an additional |
| 932 // TODO(kustermann): This is a hackisch way to accomplish it and should | 955 /// browser instance. |
| 933 // be encapsulated | 956 /// It is called when starting the BrowserTestRunner, and whenever a browser |
| 934 var browsersCompleter = new Completer(); | 957 /// is killed, whenever a new test is enqueued, or whenever a browser |
| 935 var androidBrowserCreationMapping = { | 958 /// finishes a test. |
| 936 'chromeOnAndroid' : (AdbDevice device) => new AndroidChrome(device), | 959 /// So we are guaranteed that this will always eventually be called, as long |
| 937 'ContentShellOnAndroid' : (AdbDevice device) => new AndroidBrowser( | 960 /// as the test queue isn't empty. |
| 938 device, | 961 void requestBrowser() { |
| 939 contentShellOnAndroidConfig, | 962 if (!testingServerStarted) return; |
| 940 checkedMode, | 963 if (underTermination) return; |
| 941 configuration['drt']), | 964 if (numBrowsers == maxNumBrowsers) return; |
| 942 'DartiumOnAndroid' : (AdbDevice device) => new AndroidBrowser( | 965 if (aBrowserIsCurrentlyStarting) return; |
| 943 device, | 966 if (numBrowsers > 0 && queueWasEmptyRecently) return; |
| 944 dartiumOnAndroidConfig, | 967 createBrowser(); |
| 945 checkedMode, | |
| 946 configuration['dartium']), | |
| 947 }; | |
| 948 if (androidBrowserCreationMapping.containsKey(browserName)) { | |
| 949 AdbHelper.listDevices().then((deviceIds) { | |
| 950 if (deviceIds.length > 0) { | |
| 951 var browsers = []; | |
| 952 for (int i = 0; i < deviceIds.length; i++) { | |
| 953 var id = "BROWSER$i"; | |
| 954 var device = new AdbDevice(deviceIds[i]); | |
| 955 adbDeviceMapping[id] = device; | |
| 956 var browser = androidBrowserCreationMapping[browserName](device); | |
| 957 browsers.add(browser); | |
| 958 // We store this in case we need to kill the browser. | |
| 959 browser.id = id; | |
| 960 } | |
| 961 browsersCompleter.complete(browsers); | |
| 962 } else { | |
| 963 throw new StateError("No android devices found."); | |
| 964 } | |
| 965 }); | |
| 966 } else { | |
| 967 var browsers = []; | |
| 968 for (int i = 0; i < maxNumBrowsers; i++) { | |
| 969 var id = "BROWSER$browserIdCount"; | |
| 970 browserIdCount++; | |
| 971 var browser = getInstance(); | |
| 972 browsers.add(browser); | |
| 973 // We store this in case we need to kill the browser. | |
| 974 browser.id = id; | |
| 975 } | |
| 976 browsersCompleter.complete(browsers); | |
| 977 } | |
| 978 return browsersCompleter.future; | |
| 979 } | 968 } |
| 980 | 969 |
| 981 var timedOut = []; | 970 String getNextBrowserId() => "BROWSER${browserIdCounter++}"; |
| 971 |
| 972 void createBrowser() { |
| 973 final String id = getNextBrowserId(); |
| 974 final String url = testingServer.getDriverUrl(id); |
| 975 Browser browser; |
| 976 if (browserName == 'chromeOnAndroid') { |
| 977 AdbDevice device = idleAdbDevices.removeLast(); |
| 978 adbDeviceMapping[id] = device; |
| 979 browser = new AndroidChrome(device); |
| 980 } else { |
| 981 String path = Locations.getBrowserLocation(browserName, configuration); |
| 982 browser = new Browser.byName(browserName, path, checkedMode); |
| 983 browser.logger = logger; |
| 984 } |
| 985 browser.id = id; |
| 986 markCurrentlyStarting(id); |
| 987 final status = new BrowserStatus(browser); |
| 988 browserStatus[id] = status; |
| 989 numBrowsers++; |
| 990 status.nextTestTimeout = createNextTestTimer(status); |
| 991 browser.start(url); |
| 992 } |
| 982 | 993 |
| 983 void handleResults(String browserId, String output, int testId) { | 994 void handleResults(String browserId, String output, int testId) { |
| 984 var status = browserStatus[browserId]; | 995 var status = browserStatus[browserId]; |
| 985 if (testCache.containsKey(testId)) { | 996 if (testCache.containsKey(testId)) { |
| 986 doubleReportingOutputs[testId] = output; | 997 doubleReportingOutputs[testId] = output; |
| 987 return; | 998 return; |
| 988 } | 999 } |
| 989 | 1000 |
| 990 if (status == null || status.timeout) { | 1001 if (status == null || status.timeout) { |
| 991 // We don't do anything, this browser is currently being killed and | 1002 // We don't do anything, this browser is currently being killed and |
| (...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1043 | 1054 |
| 1044 if (status != null && !status.timeout && status.currentTest != null) { | 1055 if (status != null && !status.timeout && status.currentTest != null) { |
| 1045 status.currentTest.timeoutTimer.cancel(); | 1056 status.currentTest.timeoutTimer.cancel(); |
| 1046 status.currentTest.timeoutTimer = | 1057 status.currentTest.timeoutTimer = |
| 1047 createTimeoutTimer(status.currentTest, status); | 1058 createTimeoutTimer(status.currentTest, status); |
| 1048 status.currentTest.delayUntilTestStarted = | 1059 status.currentTest.delayUntilTestStarted = |
| 1049 status.currentTest.stopwatch.elapsed; | 1060 status.currentTest.stopwatch.elapsed; |
| 1050 } | 1061 } |
| 1051 } | 1062 } |
| 1052 | 1063 |
| 1053 void handleTimeout(BrowserTestingStatus status) { | 1064 void handleTimeout(BrowserStatus status) { |
| 1054 // We simply kill the browser and starts up a new one! | 1065 // We simply kill the browser and starts up a new one! |
| 1055 // We could be smarter here, but it does not seems like it is worth it. | 1066 // We could be smarter here, but it does not seems like it is worth it. |
| 1056 if (status.timeout) { | 1067 if (status.timeout) { |
| 1057 DebugLogger.error( | 1068 DebugLogger.error( |
| 1058 "Got test timeout for an already restarting browser"); | 1069 "Got test timeout for an already restarting browser"); |
| 1059 return; | 1070 return; |
| 1060 } | 1071 } |
| 1061 status.timeout = true; | 1072 status.timeout = true; |
| 1062 timedOut.add(status.currentTest.url); | 1073 timedOut.add(status.currentTest.url); |
| 1063 var id = status.browser.id; | 1074 var id = status.browser.id; |
| (...skipping 13 matching lines...) Expand all Loading... |
| 1077 status.currentTest.stopwatch.elapsed, | 1088 status.currentTest.stopwatch.elapsed, |
| 1078 lastKnownMessage, | 1089 lastKnownMessage, |
| 1079 status.browser.testBrowserOutput, | 1090 status.browser.testBrowserOutput, |
| 1080 didTimeout: true); | 1091 didTimeout: true); |
| 1081 status.currentTest.doneCallback(browserTestOutput); | 1092 status.currentTest.doneCallback(browserTestOutput); |
| 1082 status.lastTest = status.currentTest; | 1093 status.lastTest = status.currentTest; |
| 1083 status.currentTest = null; | 1094 status.currentTest = null; |
| 1084 | 1095 |
| 1085 // We don't want to start a new browser if we are terminating. | 1096 // We don't want to start a new browser if we are terminating. |
| 1086 if (underTermination) return; | 1097 if (underTermination) return; |
| 1087 restartBrowser(id); | 1098 removeBrowser(id); |
| 1099 requestBrowser(); |
| 1088 }); | 1100 }); |
| 1089 } | 1101 } |
| 1090 | 1102 |
| 1091 void restartBrowser(String id) { | 1103 /// Remove a browser that has closed from our data structures that track |
| 1092 if (browserName.contains('OnAndroid')) { | 1104 /// open browsers. Check if we want to replace it with a new browser. |
| 1093 DebugLogger.info("Restarting browser $id"); | 1105 void removeBrowser(String id) { |
| 1106 if (browserName == 'chromeOnAndroid') { |
| 1107 idleAdbDevices.add(adbDeviceMapping.remove(id)); |
| 1094 } | 1108 } |
| 1095 var browser; | 1109 markNotCurrentlyStarting(id); |
| 1096 var new_id = id; | 1110 browserStatus.remove(id); |
| 1097 if (browserName == 'chromeOnAndroid') { | 1111 --numBrowsers; |
| 1098 browser = new AndroidChrome(adbDeviceMapping[id]); | |
| 1099 } else if (browserName == 'ContentShellOnAndroid') { | |
| 1100 browser = new AndroidBrowser(adbDeviceMapping[id], | |
| 1101 contentShellOnAndroidConfig, | |
| 1102 checkedMode, | |
| 1103 configuration['drt']); | |
| 1104 } else if (browserName == 'DartiumOnAndroid') { | |
| 1105 browser = new AndroidBrowser(adbDeviceMapping[id], | |
| 1106 dartiumOnAndroidConfig, | |
| 1107 checkedMode, | |
| 1108 configuration['dartium']); | |
| 1109 } else { | |
| 1110 browserStatus.remove(id); | |
| 1111 browser = getInstance(); | |
| 1112 new_id = "BROWSER$browserIdCount"; | |
| 1113 browserIdCount++; | |
| 1114 } | |
| 1115 browser.id = new_id; | |
| 1116 var status = new BrowserTestingStatus(browser); | |
| 1117 browserStatus[new_id] = status; | |
| 1118 status.nextTestTimeout = createNextTestTimer(status); | |
| 1119 status.timeSinceRestart.start(); | |
| 1120 browser.start(testingServer.getDriverUrl(new_id)).then((success) { | |
| 1121 // We may have started terminating in the mean time. | |
| 1122 if (underTermination) { | |
| 1123 if (status.nextTestTimeout != null) { | |
| 1124 status.nextTestTimeout.cancel(); | |
| 1125 status.nextTestTimeout = null; | |
| 1126 } | |
| 1127 browser.close().then((success) { | |
| 1128 // We should never hit this, print it out. | |
| 1129 if (!success) { | |
| 1130 print("Could not kill browser ($id) started due to timeout"); | |
| 1131 } | |
| 1132 }); | |
| 1133 return; | |
| 1134 } | |
| 1135 if (!success) { | |
| 1136 // TODO(ricow): Handle this better. | |
| 1137 print("This is bad, should never happen, could not start browser"); | |
| 1138 exit(1); | |
| 1139 } | |
| 1140 }); | |
| 1141 } | 1112 } |
| 1142 | 1113 |
| 1143 BrowserTest getNextTest(String browserId) { | 1114 BrowserTest getNextTest(String browserId) { |
| 1115 markNotCurrentlyStarting(browserId); |
| 1144 var status = browserStatus[browserId]; | 1116 var status = browserStatus[browserId]; |
| 1145 if (status == null) return null; | 1117 if (status == null) return null; |
| 1146 if (status.nextTestTimeout != null) { | 1118 if (status.nextTestTimeout != null) { |
| 1147 status.nextTestTimeout.cancel(); | 1119 status.nextTestTimeout.cancel(); |
| 1148 status.nextTestTimeout = null; | 1120 status.nextTestTimeout = null; |
| 1149 } | 1121 } |
| 1150 if (testQueue.isEmpty) return null; | 1122 if (testQueue.isEmpty) return null; |
| 1151 | 1123 |
| 1152 // We are currently terminating this browser, don't start a new test. | 1124 // We are currently terminating this browser, don't start a new test. |
| 1153 if (status.timeout) return null; | 1125 if (status.timeout) return null; |
| 1154 | 1126 |
| 1155 // Restart content_shell and dartium on Android if they have been | 1127 // Restart Internet Explorer if it has been |
| 1156 // running for longer than RESTART_BROWSER_INTERVAL. The tests have | 1128 // running for longer than RESTART_BROWSER_INTERVAL. The tests have |
| 1157 // had flaky timeouts, and this may help. | 1129 // had flaky timeouts, and this may help. |
| 1158 if ((browserName == 'ContentShellOnAndroid' || | 1130 if ((browserName == 'ie10' || |
| 1159 browserName == 'DartiumOnAndroid' || | |
| 1160 browserName == 'ie10' || | |
| 1161 browserName == 'ie11') && | 1131 browserName == 'ie11') && |
| 1162 status.timeSinceRestart.elapsed > RESTART_BROWSER_INTERVAL) { | 1132 status.timeSinceRestart.elapsed > RESTART_BROWSER_INTERVAL) { |
| 1163 var id = status.browser.id; | 1133 var id = status.browser.id; |
| 1164 // Reset stopwatch so we don't trigger again before restarting. | 1134 // Reset stopwatch so we don't trigger again before restarting. |
| 1165 status.timeout = true; | 1135 status.timeout = true; |
| 1166 status.browser.close().then((_) { | 1136 status.browser.close().then((_) { |
| 1167 // We don't want to start a new browser if we are terminating. | 1137 // We don't want to start a new browser if we are terminating. |
| 1168 if (underTermination) return; | 1138 if (underTermination) return; |
| 1169 restartBrowser(id); | 1139 removeBrowser(id); |
| 1140 requestBrowser(); |
| 1170 }); | 1141 }); |
| 1171 // Don't send a test to the browser we are restarting. | 1142 // Don't send a test to the browser we are restarting. |
| 1172 return null; | 1143 return null; |
| 1173 } | 1144 } |
| 1174 | 1145 |
| 1175 BrowserTest test = testQueue.removeLast(); | 1146 BrowserTest test = testQueue.removeLast(); |
| 1147 // If our queue isn't empty, try starting more browsers |
| 1148 if (testQueue.isEmpty) { |
| 1149 lastEmptyTestQueueTime = new DateTime.now(); |
| 1150 } else { |
| 1151 requestBrowser(); |
| 1152 } |
| 1176 if (status.currentTest == null) { | 1153 if (status.currentTest == null) { |
| 1177 status.currentTest = test; | 1154 status.currentTest = test; |
| 1178 status.currentTest.lastKnownMessage = ''; | 1155 status.currentTest.lastKnownMessage = ''; |
| 1179 } else { | 1156 } else { |
| 1180 // TODO(ricow): Handle this better. | 1157 // TODO(ricow): Handle this better. |
| 1181 print("Browser requested next test before reporting previous result"); | 1158 print("Browser requested next test before reporting previous result"); |
| 1182 print("This happened for browser $browserId"); | 1159 print("This happened for browser $browserId"); |
| 1183 print("Old test was: ${status.currentTest.url}"); | 1160 print("Old test was: ${status.currentTest.url}"); |
| 1184 print("The test before that was: ${status.lastTest.url}"); | 1161 print("The test before that was: ${status.lastTest.url}"); |
| 1185 print("Timed out tests:"); | 1162 print("Timed out tests:"); |
| 1186 for (var v in timedOut) { | 1163 for (var v in timedOut) { |
| 1187 print(" $v"); | 1164 print(" $v"); |
| 1188 } | 1165 } |
| 1189 exit(1); | 1166 exit(1); |
| 1190 } | 1167 } |
| 1191 | 1168 |
| 1192 status.currentTest.timeoutTimer = createTimeoutTimer(test, status); | 1169 status.currentTest.timeoutTimer = createTimeoutTimer(test, status); |
| 1193 status.currentTest.stopwatch = new Stopwatch()..start(); | 1170 status.currentTest.stopwatch = new Stopwatch()..start(); |
| 1194 | 1171 |
| 1195 // Reset the test specific output information (stdout, stderr) on the | 1172 // Reset the test specific output information (stdout, stderr) on the |
| 1196 // browser, since a new test is being started. | 1173 // browser, since a new test is being started. |
| 1197 status.browser.resetTestBrowserOutput(); | 1174 status.browser.resetTestBrowserOutput(); |
| 1198 status.browser.logBrowserInfoToTestBrowserOutput(); | 1175 status.browser.logBrowserInfoToTestBrowserOutput(); |
| 1199 if (browserName.contains('OnAndroid')) { | |
| 1200 DebugLogger.info("Browser $browserId getting test ${test.url}"); | |
| 1201 } | |
| 1202 | |
| 1203 return test; | 1176 return test; |
| 1204 } | 1177 } |
| 1205 | 1178 |
| 1206 Timer createTimeoutTimer(BrowserTest test, BrowserTestingStatus status) { | 1179 /// Creates a timer that is active while a test is running on a browser. |
| 1180 Timer createTimeoutTimer(BrowserTest test, BrowserStatus status) { |
| 1207 return new Timer(new Duration(seconds: test.timeout), | 1181 return new Timer(new Duration(seconds: test.timeout), |
| 1208 () { handleTimeout(status); }); | 1182 () { handleTimeout(status); }); |
| 1209 } | 1183 } |
| 1210 | 1184 |
| 1211 Timer createNextTestTimer(BrowserTestingStatus status) { | 1185 /// Creates a timer that is active while no test is running on the |
| 1186 /// browser. It has finished one test, and it has not requested a new test. |
| 1187 Timer createNextTestTimer(BrowserStatus status) { |
| 1212 return new Timer(BrowserTestRunner.NEXT_TEST_TIMEOUT, | 1188 return new Timer(BrowserTestRunner.NEXT_TEST_TIMEOUT, |
| 1213 () { handleNextTestTimeout(status); }); | 1189 () { handleNextTestTimeout(status); }); |
| 1214 } | 1190 } |
| 1215 | 1191 |
| 1216 void handleNextTestTimeout(status) { | 1192 void handleNextTestTimeout(status) { |
| 1217 DebugLogger.warning( | 1193 DebugLogger.warning( |
| 1218 "Browser timed out before getting next test. Restarting"); | 1194 "Browser timed out before getting next test. Restarting"); |
| 1219 if (status.timeout) return; | 1195 if (status.timeout) return; |
| 1220 numBrowserGetTestTimeouts++; | 1196 numBrowserGetTestTimeouts++; |
| 1221 if (numBrowserGetTestTimeouts >= MAX_NEXT_TEST_TIMEOUTS) { | 1197 if (numBrowserGetTestTimeouts >= MAX_NEXT_TEST_TIMEOUTS) { |
| 1222 DebugLogger.error( | 1198 DebugLogger.error( |
| 1223 "Too many browser timeouts before getting next test. Terminating"); | 1199 "Too many browser timeouts before getting next test. Terminating"); |
| 1224 terminate().then((_) => exit(1)); | 1200 terminate().then((_) => exit(1)); |
| 1225 } else { | 1201 } else { |
| 1226 status.timeout = true; | 1202 status.timeout = true; |
| 1227 status.browser.close().then((_) => restartBrowser(status.browser.id)); | 1203 status.browser.close().then((_) { |
| 1204 removeBrowser(status.browser.id); |
| 1205 requestBrowser(); |
| 1206 }); |
| 1228 } | 1207 } |
| 1229 } | 1208 } |
| 1230 | 1209 |
| 1231 void queueTest(BrowserTest test) { | 1210 void enqueueTest(BrowserTest test) { |
| 1232 testQueue.add(test); | 1211 testQueue.add(test); |
| 1212 requestBrowser(); |
| 1233 } | 1213 } |
| 1234 | 1214 |
| 1235 void printDoubleReportingTests() { | 1215 void printDoubleReportingTests() { |
| 1236 if (doubleReportingOutputs.length == 0) return; | 1216 if (doubleReportingOutputs.length == 0) return; |
| 1237 // TODO(ricow): die on double reporting. | 1217 // TODO(ricow): die on double reporting. |
| 1238 // Currently we just report this here, we could have a callback to the | 1218 // Currently we just report this here, we could have a callback to the |
| 1239 // encapsulating environment. | 1219 // encapsulating environment. |
| 1240 print(""); | 1220 print(""); |
| 1241 print("Double reporting tests"); | 1221 print("Double reporting tests"); |
| 1242 for (var id in doubleReportingOutputs.keys) { | 1222 for (var id in doubleReportingOutputs.keys) { |
| 1243 print(" ${testCache[id]}"); | 1223 print(" ${testCache[id]}"); |
| 1244 } | 1224 } |
| 1245 | 1225 |
| 1246 DebugLogger.warning("Double reporting tests:"); | 1226 DebugLogger.warning("Double reporting tests:"); |
| 1247 for (var id in doubleReportingOutputs.keys) { | 1227 for (var id in doubleReportingOutputs.keys) { |
| 1248 DebugLogger.warning("${testCache[id]}, output: "); | 1228 DebugLogger.warning("${testCache[id]}, output: "); |
| 1249 DebugLogger.warning("${doubleReportingOutputs[id]}"); | 1229 DebugLogger.warning("${doubleReportingOutputs[id]}"); |
| 1250 DebugLogger.warning(""); | 1230 DebugLogger.warning(""); |
| 1251 DebugLogger.warning(""); | 1231 DebugLogger.warning(""); |
| 1252 } | 1232 } |
| 1253 } | 1233 } |
| 1254 | 1234 |
| 1255 Future<bool> terminate() { | 1235 // TODO(26191): Call a unified fatalError(), that shuts down all subprocesses. |
| 1236 // This just kills the browsers in this BrowserTestRunner instance. |
| 1237 Future terminate() async { |
| 1256 var browsers = []; | 1238 var browsers = []; |
| 1257 underTermination = true; | 1239 underTermination = true; |
| 1258 testingServer.underTermination = true; | 1240 testingServer.underTermination = true; |
| 1259 for (BrowserTestingStatus status in browserStatus.values) { | 1241 for (BrowserStatus status in browserStatus.values) { |
| 1260 browsers.add(status.browser); | 1242 browsers.add(status.browser); |
| 1261 if (status.nextTestTimeout != null) { | 1243 if (status.nextTestTimeout != null) { |
| 1262 status.nextTestTimeout.cancel(); | 1244 status.nextTestTimeout.cancel(); |
| 1263 status.nextTestTimeout = null; | 1245 status.nextTestTimeout = null; |
| 1264 } | 1246 } |
| 1265 } | 1247 } |
| 1266 // Success if all the browsers closed successfully. | 1248 for (Browser b in browsers) { |
| 1267 bool success = true; | 1249 await b.close(); |
| 1268 Future closeBrowser(Browser b) { | |
| 1269 return b.close().then((bool closeSucceeded) { | |
| 1270 if (!closeSucceeded) { | |
| 1271 success = false; | |
| 1272 } | |
| 1273 }); | |
| 1274 } | 1250 } |
| 1275 return Future.forEach(browsers, closeBrowser).then((_) { | 1251 testingServer.errorReportingServer.close(); |
| 1276 testingServer.errorReportingServer.close(); | 1252 printDoubleReportingTests(); |
| 1277 printDoubleReportingTests(); | |
| 1278 return success; | |
| 1279 }); | |
| 1280 } | |
| 1281 | |
| 1282 Browser getInstance() { | |
| 1283 if (browserName == 'ff') browserName = 'firefox'; | |
| 1284 var path = Locations.getBrowserLocation(browserName, configuration); | |
| 1285 var browser = new Browser.byName(browserName, path, checkedMode); | |
| 1286 browser.logger = logger; | |
| 1287 return browser; | |
| 1288 } | 1253 } |
| 1289 } | 1254 } |
| 1290 | 1255 |
| 1291 class BrowserTestingServer { | 1256 class BrowserTestingServer { |
| 1292 final Map configuration; | 1257 final Map configuration; |
| 1293 /// Interface of the testing server: | 1258 /// Interface of the testing server: |
| 1294 /// | 1259 /// |
| 1295 /// GET /driver/BROWSER_ID -- This will get the driver page to fetch | 1260 /// GET /driver/BROWSER_ID -- This will get the driver page to fetch |
| 1296 /// and run tests ... | 1261 /// and run tests ... |
| 1297 /// GET /next_test/BROWSER_ID -- returns "WAIT" "TERMINATE" or "url#id" | 1262 /// GET /next_test/BROWSER_ID -- returns "WAIT" "TERMINATE" or "url#id" |
| (...skipping 498 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1796 </div> | 1761 </div> |
| 1797 <div id="embedded_iframe_div" class="test box"> | 1762 <div id="embedded_iframe_div" class="test box"> |
| 1798 <iframe style="width:100%;height:100%;" id="embedded_iframe"></iframe> | 1763 <iframe style="width:100%;height:100%;" id="embedded_iframe"></iframe> |
| 1799 </div> | 1764 </div> |
| 1800 </body> | 1765 </body> |
| 1801 </html> | 1766 </html> |
| 1802 """; | 1767 """; |
| 1803 return driverContent; | 1768 return driverContent; |
| 1804 } | 1769 } |
| 1805 } | 1770 } |
| OLD | NEW |