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

Unified Diff: tools/testing/dart/browser_controller.dart

Issue 1859523002: Start browsers sequentially in testing scripts, as needed. (Closed) Base URL: git@github.com:dart-lang/sdk.git@master
Patch Set: Address comments Created 4 years, 8 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | tools/testing/dart/test_runner.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: tools/testing/dart/browser_controller.dart
diff --git a/tools/testing/dart/browser_controller.dart b/tools/testing/dart/browser_controller.dart
index a6d061cf9bf248ff2b0506c5877135ec14badd37..2e5c1e736c06ed574a5cf43756042f6a2e21dc4e 100644
--- a/tools/testing/dart/browser_controller.dart
+++ b/tools/testing/dart/browser_controller.dart
@@ -7,6 +7,7 @@ import "dart:async";
import "dart:convert" show LineSplitter, UTF8, JSON;
import "dart:core";
import "dart:io";
+import "dart:math" show max, min;
import 'android.dart';
import 'http_server.dart';
@@ -143,7 +144,7 @@ abstract class Browser {
* Start the browser using the supplied argument.
* This sets up the error handling and usage logging.
*/
- Future<bool> startBrowser(String command,
+ Future<bool> startBrowserProcess(String command,
List<String> arguments,
{Map<String,String> environment}) {
return Process.start(command, arguments, environment: environment)
@@ -382,7 +383,7 @@ class Safari extends Browser {
_cleanup = () { userDir.deleteSync(recursive: true); };
_createLaunchHTML(userDir.path, url);
var args = ["${userDir.path}/launch.html"];
- return startBrowser(_binary, args);
+ return startBrowserProcess(_binary, args);
});
}).catchError((error) {
_logEvent("Running $_binary --version failed with $error");
@@ -442,7 +443,7 @@ class Chrome extends Browser {
var args = ["--user-data-dir=${userDir.path}", url,
"--disable-extensions", "--disable-popup-blocking",
"--bwsi", "--no-first-run"];
- return startBrowser(_binary, args, environment: _getEnvironment());
+ return startBrowserProcess(_binary, args, environment: _getEnvironment());
});
}).catchError((e) {
_logEvent("Running $_binary --version failed with $e");
@@ -484,7 +485,7 @@ class SafariMobileSimulator extends Safari {
"iPhoneSimulator7.1.sdk/Applications/MobileSafari.app/"
"MobileSafari",
"-u", url];
- return startBrowser(_binary, args)
+ return startBrowserProcess(_binary, args)
.catchError((e) {
_logEvent("Running $_binary --version failed with $e");
return false;
@@ -556,7 +557,7 @@ class IE extends Browser {
_logEvent("Starting ie browser on: $url");
return clearCache().then((_) => getVersion()).then((version) {
_logEvent("Got version: $version");
- return startBrowser(_binary, [url]);
+ return startBrowserProcess(_binary, [url]);
});
}
String toString() => "IE";
@@ -646,7 +647,6 @@ class AndroidChrome extends Browser {
static const String firefoxPackage = 'org.mozilla.firefox';
static const String turnScreenOnPackage = 'com.google.dart.turnscreenon';
- AndroidEmulator _emulator;
AdbDevice _adbDevice;
AndroidChrome(this._adbDevice);
@@ -746,7 +746,7 @@ class Firefox extends Browser {
"-no-remote", "-new-instance", url];
var environment = new Map<String,String>.from(Platform.environment);
environment["MOZ_CRASHREPORTER_DISABLE"] = "1";
- return startBrowser(_binary, args, environment: environment);
+ return startBrowserProcess(_binary, args, environment: environment);
});
}).catchError((e) {
@@ -762,7 +762,7 @@ class Firefox extends Browser {
/**
* Describes the current state of a browser used for testing.
*/
-class BrowserTestingStatus {
+class BrowserStatus {
Browser browser;
BrowserTest currentTest;
@@ -772,9 +772,9 @@ class BrowserTestingStatus {
BrowserTest lastTest;
bool timeout = false;
Timer nextTestTimeout;
- Stopwatch timeSinceRestart = new Stopwatch();
+ Stopwatch timeSinceRestart = new Stopwatch()..start();
- BrowserTestingStatus(Browser this.browser);
+ BrowserStatus(Browser this.browser);
}
@@ -842,144 +842,155 @@ class BrowserTestOutput {
this.browserOutput, {this.didTimeout: false});
}
-/**
- * Encapsulates all the functionality for running tests in browsers.
- * The interface is rather simple. After starting, the runner tests
- * are simply added to the queue and a the supplied callbacks are called
- * whenever a test completes.
- */
+
+/// Encapsulates all the functionality for running tests in browsers.
+/// Tests are added to the queue and the supplied callbacks are called
+/// when a test completes.
+/// BrowserTestRunner starts up to maxNumBrowser instances of the browser,
+/// to run the tests, starting them sequentially, as needed, so only
+/// one is starting up at a time.
+/// BrowserTestRunner starts a BrowserTestingServer, which serves a
+/// driver page to the browsers, serves tests, and receives results and
+/// requests back from the browsers.
class BrowserTestRunner {
static const int MAX_NEXT_TEST_TIMEOUTS = 10;
static const Duration NEXT_TEST_TIMEOUT = const Duration(seconds: 60);
static const Duration RESTART_BROWSER_INTERVAL = const Duration(seconds: 60);
+ /// If the queue was recently empty, don't start another browser.
+ static const Duration MIN_NONEMPTY_QUEUE_TIME = const Duration(seconds: 1);
+
final Map configuration;
+ BrowserTestingServer testingServer;
final String localIp;
String browserName;
- final int maxNumBrowsers;
+ int maxNumBrowsers;
bool checkedMode;
+ int numBrowsers = 0;
// Used to send back logs from the browser (start, stop etc)
Function logger;
- int browserIdCount = 0;
+ int browserIdCounter = 1;
+
+ bool testingServerStarted = false;
bool underTermination = false;
int numBrowserGetTestTimeouts = 0;
-
+ DateTime lastEmptyTestQueueTime = new DateTime.now();
+ String _currentStartingBrowserId;
List<BrowserTest> testQueue = new List<BrowserTest>();
- Map<String, BrowserTestingStatus> browserStatus =
- new Map<String, BrowserTestingStatus>();
+ Map<String, BrowserStatus> browserStatus =
+ new Map<String, BrowserStatus>();
var adbDeviceMapping = new Map<String, AdbDevice>();
+ List<AdbDevice> idleAdbDevices;
+
// This cache is used to guarantee that we never see double reporting.
// If we do we need to provide developers with this information.
// We don't add urls to the cache until we have run it.
Map<int, String> testCache = new Map<int, String>();
+
Map<int, String> doubleReportingOutputs = new Map<int, String>();
+ List<String> timedOut = [];
+
+ // We will start a new browser when the test queue hasn't been empty
+ // recently, we have fewer than maxNumBrowsers browsers, and there is
+ // no other browser instance currently starting up.
+ bool get queueWasEmptyRecently {
+ return testQueue.isEmpty ||
+ new DateTime.now().difference(lastEmptyTestQueueTime) <
+ MIN_NONEMPTY_QUEUE_TIME;
+ }
- BrowserTestingServer testingServer;
+ // While a browser is starting, but has not requested its first test, its
+ // browserId is stored in _currentStartingBrowserId.
+ // When no browser is currently starting, _currentStartingBrowserId is null.
+ bool get aBrowserIsCurrentlyStarting => _currentStartingBrowserId != null;
+ void markCurrentlyStarting(String id) {
+ _currentStartingBrowserId = id;
+ }
+ void markNotCurrentlyStarting(String id) {
+ if (_currentStartingBrowserId == id) _currentStartingBrowserId = null;
+ }
- /**
- * The TestRunner takes the testingServer in as a constructor parameter in
- * case we wish to have a testing server with different behavior (such as the
- * case for performance testing.
- */
+ // If [browserName] doesn't support opening new windows, we use new iframes
+ // instead.
+ bool get useIframe =>
+ !Browser.BROWSERS_WITH_WINDOW_SUPPORT.contains(browserName);
+
+ /// The optional testingServer parameter allows callers to pass in
+ /// a testing server with different behavior than the default
+ /// BrowserTestServer. The url handlers of the testingServer are
+ /// overwritten, so an existing handler can't be shared between instances.
BrowserTestRunner(this.configuration,
this.localIp,
this.browserName,
this.maxNumBrowsers,
{BrowserTestingServer this.testingServer}) {
checkedMode = configuration['checked'];
+ if (browserName == 'ff') browserName = 'firefox';
}
- Future<bool> start() {
- // If [browserName] doesn't support opening new windows, we use new iframes
- // instead.
- bool useIframe =
- !Browser.BROWSERS_WITH_WINDOW_SUPPORT.contains(browserName);
+ Future start() async {
if (testingServer == null) {
testingServer = new BrowserTestingServer(
configuration, localIp, useIframe);
}
- return testingServer.start().then((_) {
- testingServer.testDoneCallBack = handleResults;
- testingServer.testStatusUpdateCallBack = handleStatusUpdate;
- testingServer.testStartedCallBack = handleStarted;
- testingServer.nextTestCallBack = getNextTest;
- return getBrowsers().then((browsers) {
- var futures = [];
- for (var browser in browsers) {
- var url = testingServer.getDriverUrl(browser.id);
- var future = browser.start(url).then((success) {
- if (success) {
- var status = new BrowserTestingStatus(browser);
- browserStatus[browser.id] = status;
- status.nextTestTimeout = createNextTestTimer(status);
- status.timeSinceRestart.start();
- }
- return success;
- });
- futures.add(future);
- }
- return Future.wait(futures).then((values) {
- return !values.contains(false);
- });
- });
- });
+ await testingServer.start();
+ testingServer
+ ..testDoneCallBack = handleResults
+ ..testStatusUpdateCallBack = handleStatusUpdate
+ ..testStartedCallBack = handleStarted
+ ..nextTestCallBack = getNextTest;
+ if (browserName == 'chromeOnAndroid') {
+ var idbNames = await AdbHelper.listDevices();
+ idleAdbDevices = new List.from(idbNames.map((id) => new AdbDevice(id)));
+ maxNumBrowsers = min(maxNumBrowsers, idleAdbDevices.length);
+ }
+ testingServerStarted = true;
+ requestBrowser();
}
- Future<List<Browser>> getBrowsers() {
- // TODO(kustermann): This is a hackisch way to accomplish it and should
- // be encapsulated
- var browsersCompleter = new Completer();
- var androidBrowserCreationMapping = {
- 'chromeOnAndroid' : (AdbDevice device) => new AndroidChrome(device),
- 'ContentShellOnAndroid' : (AdbDevice device) => new AndroidBrowser(
- device,
- contentShellOnAndroidConfig,
- checkedMode,
- configuration['drt']),
- 'DartiumOnAndroid' : (AdbDevice device) => new AndroidBrowser(
- device,
- dartiumOnAndroidConfig,
- checkedMode,
- configuration['dartium']),
- };
- if (androidBrowserCreationMapping.containsKey(browserName)) {
- AdbHelper.listDevices().then((deviceIds) {
- if (deviceIds.length > 0) {
- var browsers = [];
- for (int i = 0; i < deviceIds.length; i++) {
- var id = "BROWSER$i";
- var device = new AdbDevice(deviceIds[i]);
- adbDeviceMapping[id] = device;
- var browser = androidBrowserCreationMapping[browserName](device);
- browsers.add(browser);
- // We store this in case we need to kill the browser.
- browser.id = id;
- }
- browsersCompleter.complete(browsers);
- } else {
- throw new StateError("No android devices found.");
- }
- });
+ /// requestBrowser() is called whenever we might want to start an additional
+ /// browser instance.
+ /// It is called when starting the BrowserTestRunner, and whenever a browser
+ /// is killed, whenever a new test is enqueued, or whenever a browser
+ /// finishes a test.
+ /// So we are guaranteed that this will always eventually be called, as long
+ /// as the test queue isn't empty.
+ void requestBrowser() {
+ if (!testingServerStarted) return;
+ if (underTermination) return;
+ if (numBrowsers == maxNumBrowsers) return;
+ if (aBrowserIsCurrentlyStarting) return;
+ if (numBrowsers > 0 && queueWasEmptyRecently) return;
+ createBrowser();
+ }
+
+ String getNextBrowserId() => "BROWSER${browserIdCounter++}";
+
+ void createBrowser() {
+ final String id = getNextBrowserId();
+ final String url = testingServer.getDriverUrl(id);
+ Browser browser;
+ if (browserName == 'chromeOnAndroid') {
+ AdbDevice device = idleAdbDevices.removeLast();
+ adbDeviceMapping[id] = device;
+ browser = new AndroidChrome(device);
} else {
- var browsers = [];
- for (int i = 0; i < maxNumBrowsers; i++) {
- var id = "BROWSER$browserIdCount";
- browserIdCount++;
- var browser = getInstance();
- browsers.add(browser);
- // We store this in case we need to kill the browser.
- browser.id = id;
- }
- browsersCompleter.complete(browsers);
+ String path = Locations.getBrowserLocation(browserName, configuration);
+ browser = new Browser.byName(browserName, path, checkedMode);
+ browser.logger = logger;
}
- return browsersCompleter.future;
+ browser.id = id;
+ markCurrentlyStarting(id);
+ final status = new BrowserStatus(browser);
+ browserStatus[id] = status;
+ numBrowsers++;
+ status.nextTestTimeout = createNextTestTimer(status);
+ browser.start(url);
}
- var timedOut = [];
-
void handleResults(String browserId, String output, int testId) {
var status = browserStatus[browserId];
if (testCache.containsKey(testId)) {
@@ -1050,7 +1061,7 @@ class BrowserTestRunner {
}
}
- void handleTimeout(BrowserTestingStatus status) {
+ void handleTimeout(BrowserStatus status) {
// We simply kill the browser and starts up a new one!
// We could be smarter here, but it does not seems like it is worth it.
if (status.timeout) {
@@ -1084,63 +1095,24 @@ class BrowserTestRunner {
// We don't want to start a new browser if we are terminating.
if (underTermination) return;
- restartBrowser(id);
+ removeBrowser(id);
+ requestBrowser();
});
}
- void restartBrowser(String id) {
- if (browserName.contains('OnAndroid')) {
- DebugLogger.info("Restarting browser $id");
- }
- var browser;
- var new_id = id;
+ /// Remove a browser that has closed from our data structures that track
+ /// open browsers. Check if we want to replace it with a new browser.
+ void removeBrowser(String id) {
if (browserName == 'chromeOnAndroid') {
- browser = new AndroidChrome(adbDeviceMapping[id]);
- } else if (browserName == 'ContentShellOnAndroid') {
- browser = new AndroidBrowser(adbDeviceMapping[id],
- contentShellOnAndroidConfig,
- checkedMode,
- configuration['drt']);
- } else if (browserName == 'DartiumOnAndroid') {
- browser = new AndroidBrowser(adbDeviceMapping[id],
- dartiumOnAndroidConfig,
- checkedMode,
- configuration['dartium']);
- } else {
- browserStatus.remove(id);
- browser = getInstance();
- new_id = "BROWSER$browserIdCount";
- browserIdCount++;
+ idleAdbDevices.add(adbDeviceMapping.remove(id));
}
- browser.id = new_id;
- var status = new BrowserTestingStatus(browser);
- browserStatus[new_id] = status;
- status.nextTestTimeout = createNextTestTimer(status);
- status.timeSinceRestart.start();
- browser.start(testingServer.getDriverUrl(new_id)).then((success) {
- // We may have started terminating in the mean time.
- if (underTermination) {
- if (status.nextTestTimeout != null) {
- status.nextTestTimeout.cancel();
- status.nextTestTimeout = null;
- }
- browser.close().then((success) {
- // We should never hit this, print it out.
- if (!success) {
- print("Could not kill browser ($id) started due to timeout");
- }
- });
- return;
- }
- if (!success) {
- // TODO(ricow): Handle this better.
- print("This is bad, should never happen, could not start browser");
- exit(1);
- }
- });
+ markNotCurrentlyStarting(id);
+ browserStatus.remove(id);
+ --numBrowsers;
}
BrowserTest getNextTest(String browserId) {
+ markNotCurrentlyStarting(browserId);
var status = browserStatus[browserId];
if (status == null) return null;
if (status.nextTestTimeout != null) {
@@ -1152,12 +1124,10 @@ class BrowserTestRunner {
// We are currently terminating this browser, don't start a new test.
if (status.timeout) return null;
- // Restart content_shell and dartium on Android if they have been
+ // Restart Internet Explorer if it has been
// running for longer than RESTART_BROWSER_INTERVAL. The tests have
// had flaky timeouts, and this may help.
- if ((browserName == 'ContentShellOnAndroid' ||
- browserName == 'DartiumOnAndroid' ||
- browserName == 'ie10' ||
+ if ((browserName == 'ie10' ||
browserName == 'ie11') &&
status.timeSinceRestart.elapsed > RESTART_BROWSER_INTERVAL) {
var id = status.browser.id;
@@ -1166,13 +1136,20 @@ class BrowserTestRunner {
status.browser.close().then((_) {
// We don't want to start a new browser if we are terminating.
if (underTermination) return;
- restartBrowser(id);
+ removeBrowser(id);
+ requestBrowser();
});
// Don't send a test to the browser we are restarting.
return null;
}
BrowserTest test = testQueue.removeLast();
+ // If our queue isn't empty, try starting more browsers
+ if (testQueue.isEmpty) {
+ lastEmptyTestQueueTime = new DateTime.now();
+ } else {
+ requestBrowser();
+ }
if (status.currentTest == null) {
status.currentTest = test;
status.currentTest.lastKnownMessage = '';
@@ -1196,19 +1173,18 @@ class BrowserTestRunner {
// browser, since a new test is being started.
status.browser.resetTestBrowserOutput();
status.browser.logBrowserInfoToTestBrowserOutput();
- if (browserName.contains('OnAndroid')) {
- DebugLogger.info("Browser $browserId getting test ${test.url}");
- }
-
return test;
}
- Timer createTimeoutTimer(BrowserTest test, BrowserTestingStatus status) {
+ /// Creates a timer that is active while a test is running on a browser.
+ Timer createTimeoutTimer(BrowserTest test, BrowserStatus status) {
return new Timer(new Duration(seconds: test.timeout),
() { handleTimeout(status); });
}
- Timer createNextTestTimer(BrowserTestingStatus status) {
+ /// Creates a timer that is active while no test is running on the
+ /// browser. It has finished one test, and it has not requested a new test.
+ Timer createNextTestTimer(BrowserStatus status) {
return new Timer(BrowserTestRunner.NEXT_TEST_TIMEOUT,
() { handleNextTestTimeout(status); });
}
@@ -1224,12 +1200,16 @@ class BrowserTestRunner {
terminate().then((_) => exit(1));
} else {
status.timeout = true;
- status.browser.close().then((_) => restartBrowser(status.browser.id));
+ status.browser.close().then((_) {
+ removeBrowser(status.browser.id);
+ requestBrowser();
+ });
}
}
- void queueTest(BrowserTest test) {
+ void enqueueTest(BrowserTest test) {
testQueue.add(test);
+ requestBrowser();
}
void printDoubleReportingTests() {
@@ -1252,39 +1232,24 @@ class BrowserTestRunner {
}
}
- Future<bool> terminate() {
+ // TODO(26191): Call a unified fatalError(), that shuts down all subprocesses.
+ // This just kills the browsers in this BrowserTestRunner instance.
+ Future terminate() async {
var browsers = [];
underTermination = true;
testingServer.underTermination = true;
- for (BrowserTestingStatus status in browserStatus.values) {
+ for (BrowserStatus status in browserStatus.values) {
browsers.add(status.browser);
if (status.nextTestTimeout != null) {
status.nextTestTimeout.cancel();
status.nextTestTimeout = null;
}
}
- // Success if all the browsers closed successfully.
- bool success = true;
- Future closeBrowser(Browser b) {
- return b.close().then((bool closeSucceeded) {
- if (!closeSucceeded) {
- success = false;
- }
- });
+ for (Browser b in browsers) {
+ await b.close();
}
- return Future.forEach(browsers, closeBrowser).then((_) {
- testingServer.errorReportingServer.close();
- printDoubleReportingTests();
- return success;
- });
- }
-
- Browser getInstance() {
- if (browserName == 'ff') browserName = 'firefox';
- var path = Locations.getBrowserLocation(browserName, configuration);
- var browser = new Browser.byName(browserName, path, checkedMode);
- browser.logger = logger;
- return browser;
+ testingServer.errorReportingServer.close();
+ printDoubleReportingTests();
}
}
« no previous file with comments | « no previous file | tools/testing/dart/test_runner.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698