blob: 03ee0a9c06e733790a0607044e889311609a96d5 [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'dart:io';
import 'dart:math';
import 'android.dart';
import 'configuration.dart';
import 'path.dart';
import 'reset_safari.dart';
import 'utils.dart';
typedef void BrowserDoneCallback(BrowserTestOutput output);
typedef void TestChangedCallback(String browserId, String output, int testId);
typedef BrowserTest NextTestCallback(String browserId);
class BrowserOutput {
final StringBuffer stdout = new StringBuffer();
final StringBuffer stderr = new StringBuffer();
final StringBuffer eventLog = new StringBuffer();
}
/** Class describing the interface for communicating with browsers. */
abstract class Browser {
BrowserOutput _allBrowserOutput = new BrowserOutput();
BrowserOutput _testBrowserOutput = new BrowserOutput();
// This is called after the process is closed, before the done future
// is completed.
// Subclasses can use this to cleanup any browser specific resources
// (temp directories, profiles, etc). The function is expected to do
// it's work synchronously.
Function _cleanup;
/** The version of the browser - normally set when starting a browser */
String version = "";
// The path to the browser executable.
String _binary;
/**
* The underlying process - don't mess directly with this if you don't
* know what you are doing (this is an interactive process that needs
* special treatment to not leak).
*/
Process process;
Function logger;
/**
* Id of the browser
*/
String id;
/**
* Reset the browser to a known configuration on start-up.
* Browser specific implementations are free to ignore this.
*/
static bool resetBrowserConfiguration = false;
/** Print everything (stdout, stderr, usageLog) whenever we add to it */
bool debugPrint = false;
// This future returns when the process exits. It is also the return value
// of close()
Future done;
Browser();
factory Browser.byRuntime(Runtime runtime, String executablePath,
[bool checkedMode = false]) {
Browser browser;
switch (runtime) {
case Runtime.firefox:
browser = new Firefox();
break;
case Runtime.chrome:
browser = new Chrome();
break;
case Runtime.safari:
browser = new Safari();
break;
case Runtime.ie9:
case Runtime.ie10:
case Runtime.ie11:
browser = new IE();
break;
default:
throw "unreachable";
}
browser._binary = executablePath;
return browser;
}
static const List<String> SUPPORTED_BROWSERS = const [
'safari',
'ff',
'firefox',
'chrome',
'ie9',
'ie10',
'ie11'
];
static bool requiresFocus(String browserName) {
return browserName == "safari";
}
// TODO(kustermann): add standard support for chrome on android
static bool supportedBrowser(String name) {
return SUPPORTED_BROWSERS.contains(name);
}
void _logEvent(String event) {
String toLog = "$this ($id) - $event \n";
if (debugPrint) print("usageLog: $toLog");
if (logger != null) logger(toLog);
_allBrowserOutput.eventLog.write(toLog);
_testBrowserOutput.eventLog.write(toLog);
}
void _addStdout(String output) {
if (debugPrint) print("stdout: $output");
_allBrowserOutput.stdout.write(output);
_testBrowserOutput.stdout.write(output);
}
void _addStderr(String output) {
if (debugPrint) print("stderr: $output");
_allBrowserOutput.stderr.write(output);
_testBrowserOutput.stderr.write(output);
}
Future close() {
_logEvent("Close called on browser");
if (process != null) {
if (process.kill(ProcessSignal.sigkill)) {
_logEvent("Successfully sent kill signal to process.");
} else {
_logEvent("Sending kill signal failed.");
}
return done;
} else {
_logEvent("The process is already dead.");
return new Future.value(true);
}
}
/**
* Start the browser using the supplied argument.
* This sets up the error handling and usage logging.
*/
Future<bool> startBrowserProcess(String command, List<String> arguments,
{Map<String, String> environment}) {
return Process.start(command, arguments, environment: environment)
.then((startedProcess) {
_logEvent("Started browser using $command ${arguments.join(' ')}");
process = startedProcess;
// Used to notify when exiting, and as a return value on calls to
// close().
var doneCompleter = new Completer<bool>();
done = doneCompleter.future;
Completer stdoutDone = new Completer<Null>();
Completer stderrDone = new Completer<Null>();
bool stdoutIsDone = false;
bool stderrIsDone = false;
StreamSubscription stdoutSubscription;
StreamSubscription stderrSubscription;
// This timer is used to close stdio to the subprocess once we got
// the exitCode. Sometimes descendants of the subprocess keep stdio
// handles alive even though the direct subprocess is dead.
Timer watchdogTimer;
void closeStdout([_]) {
if (!stdoutIsDone) {
stdoutDone.complete();
stdoutIsDone = true;
if (stderrIsDone && watchdogTimer != null) {
watchdogTimer.cancel();
}
}
}
void closeStderr([_]) {
if (!stderrIsDone) {
stderrDone.complete();
stderrIsDone = true;
if (stdoutIsDone && watchdogTimer != null) {
watchdogTimer.cancel();
}
}
}
stdoutSubscription =
process.stdout.transform(utf8.decoder).listen((data) {
_addStdout(data);
}, onError: (error) {
// This should _never_ happen, but we really want this in the log
// if it actually does due to dart:io or vm bug.
_logEvent("An error occured in the process stdout handling: $error");
}, onDone: closeStdout);
stderrSubscription =
process.stderr.transform(utf8.decoder).listen((data) {
_addStderr(data);
}, onError: (error) {
// This should _never_ happen, but we really want this in the log
// if it actually does due to dart:io or vm bug.
_logEvent("An error occured in the process stderr handling: $error");
}, onDone: closeStderr);
process.exitCode.then((exitCode) {
_logEvent("Browser closed with exitcode $exitCode");
if (!stdoutIsDone || !stderrIsDone) {
watchdogTimer = new Timer(MAX_STDIO_DELAY, () {
DebugLogger.warning(
"$MAX_STDIO_DELAY_PASSED_MESSAGE (browser: $this)");
watchdogTimer = null;
stdoutSubscription.cancel();
stderrSubscription.cancel();
closeStdout();
closeStderr();
});
}
Future.wait([stdoutDone.future, stderrDone.future]).then((_) {
process = null;
if (_cleanup != null) {
_cleanup();
}
}).catchError((error) {
_logEvent("Error closing browsers: $error");
}).whenComplete(() => doneCompleter.complete(true));
});
return true;
}).catchError((error) {
_logEvent("Running $command $arguments failed with $error");
return false;
});
}
/**
* Get the output that was written so far to stdout/stderr/eventLog.
*/
BrowserOutput get allBrowserOutput => _allBrowserOutput;
BrowserOutput get testBrowserOutput => _testBrowserOutput;
void resetTestBrowserOutput() {
_testBrowserOutput = new BrowserOutput();
}
/**
* Add useful info about the browser to the _testBrowserOutput.stdout,
* where it will be reported for failing tests. Used to report which
* android device a failing test is running on.
*/
void logBrowserInfoToTestBrowserOutput() {}
String toString();
/** Starts the browser loading the given url */
Future<bool> start(String url);
/// Called when the driver page is requested, that is, when the browser first
/// contacts the test server. At this time, it's safe to assume that the
/// browser process has started and opened its first window.
///
/// This is used by [Safari] to ensure the browser window has focus.
Future<Null> onDriverPageRequested() => new Future<Null>.value();
}
class Safari extends Browser {
/**
* We get the safari version by parsing a version file
*/
static const String versionFile =
"/Applications/Safari.app/Contents/version.plist";
static const String safariBundleLocation = "/Applications/Safari.app/";
// Clears the cache if the static resetBrowserConfiguration flag is set.
// Returns false if the command to actually clear the cache did not complete.
Future<bool> resetConfiguration() async {
if (!Browser.resetBrowserConfiguration) return true;
var completer = new Completer<Null>();
handleUncaughtError(error, StackTrace stackTrace) {
if (!completer.isCompleted) {
completer.completeError(error, stackTrace);
} else {
throw new AsyncError(error, stackTrace);
}
}
Zone parent = Zone.current;
ZoneSpecification specification = new ZoneSpecification(
print: (Zone self, ZoneDelegate delegate, Zone zone, String line) {
delegate.run(parent, () {
_logEvent(line);
});
});
Future zoneWrapper() {
Uri safariUri = Uri.base.resolve(safariBundleLocation);
return new Future(() => killAndResetSafari(bundle: safariUri))
.then(completer.complete);
}
// We run killAndResetSafari in a Zone as opposed to running an external
// process. The Zone allows us to collect its output, and protect the rest
// of the test infrastructure against errors in it.
runZoned(zoneWrapper,
zoneSpecification: specification, onError: handleUncaughtError);
try {
await completer.future;
return true;
} catch (error, st) {
_logEvent("Unable to reset Safari: $error$st");
return false;
}
}
Future<String> getVersion() {
/**
* Example of the file:
* <?xml version="1.0" encoding="UTF-8"?>
* <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
* <plist version="1.0">
* <dict>
* <key>BuildVersion</key>
* <string>2</string>
* <key>CFBundleShortVersionString</key>
* <string>6.0.4</string>
* <key>CFBundleVersion</key>
* <string>8536.29.13</string>
* <key>ProjectName</key>
* <string>WebBrowser</string>
* <key>SourceVersion</key>
* <string>7536029013000000</string>
* </dict>
* </plist>
*/
File f = new File(versionFile);
return f.readAsLines().then((content) {
bool versionOnNextLine = false;
for (var line in content) {
if (versionOnNextLine) return line;
if (line.contains("CFBundleShortVersionString")) {
versionOnNextLine = true;
}
}
return null;
});
}
Future<Null> _createLaunchHTML(String path, String url) async {
var file = new File("$path/launch.html");
var randomFile = await file.open(mode: FileMode.write);
var content = '<script language="JavaScript">location = "$url"</script>';
await randomFile.writeString(content);
await randomFile.close();
}
Future<bool> start(String url) async {
_logEvent("Starting Safari browser on: $url");
if (!await resetConfiguration()) {
_logEvent("Could not clear cache");
return false;
}
String version;
try {
version = await getVersion();
} catch (error) {
_logEvent("Running $_binary --version failed with $error");
return false;
}
_logEvent("Got version: $version");
Directory userDir;
try {
userDir = await Directory.systemTemp.createTemp();
} catch (error) {
_logEvent("Error creating temporary directory: $error");
return false;
}
_cleanup = () {
userDir.deleteSync(recursive: true);
};
try {
await _createLaunchHTML(userDir.path, url);
} catch (error) {
_logEvent("Error creating launch HTML: $error");
return false;
}
var args = [
"-d",
"-i",
"-m",
"-s",
"-u",
_binary,
"${userDir.path}/launch.html"
];
try {
return startBrowserProcess("/usr/bin/caffeinate", args);
} catch (error) {
_logEvent("Error starting browser process: $error");
return false;
}
}
Future<Null> onDriverPageRequested() async {
await Process.run(
"/usr/bin/osascript", ['-e', 'tell application "Safari" to activate']);
}
String toString() => "Safari";
}
class Chrome extends Browser {
String _version = "Version not found yet";
Map<String, String> _getEnvironment() => null;
Future<bool> _getVersion() {
if (Platform.isWindows) {
// The version flag does not work on windows.
// See issue:
// https://code.google.com/p/chromium/issues/detail?id=158372
// The registry hack does not seem to work.
_version = "Can't get version on windows";
// We still validate that the binary exists so that we can give good
// feedback.
return new File(_binary).exists().then((exists) {
if (!exists) {
_logEvent("Chrome binary not available.");
_logEvent("Make sure $_binary is a valid program for running chrome");
}
return exists;
});
}
return Process.run(_binary, ["--version"]).then((var versionResult) {
if (versionResult.exitCode != 0) {
_logEvent("Failed to chrome get version");
_logEvent("Make sure $_binary is a valid program for running chrome");
return false;
}
_version = versionResult.stdout as String;
return true;
});
}
Future<bool> start(String url) {
_logEvent("Starting chrome browser on: $url");
// Get the version and log that.
return _getVersion().then<bool>((success) {
if (!success) return false;
_logEvent("Got version: $_version");
return Directory.systemTemp.createTemp().then((userDir) {
_cleanup = () {
try {
userDir.deleteSync(recursive: true);
} catch (e) {
_logEvent(
"Error: failed to delete Chrome user-data-dir ${userDir.path}"
", will try again in 40 seconds: $e");
new Timer(new Duration(seconds: 40), () {
try {
userDir.deleteSync(recursive: true);
} catch (e) {
_logEvent("Error: failed on second attempt to delete Chrome "
"user-data-dir ${userDir.path}: $e");
}
});
}
};
var args = [
"--user-data-dir=${userDir.path}",
url,
"--disable-extensions",
"--disable-popup-blocking",
"--bwsi",
"--no-first-run",
];
// TODO(rnystrom): Uncomment this to open the dev tools tab when Chrome
// is spawned. Handy for debugging tests.
// args.add("--auto-open-devtools-for-tabs");
return startBrowserProcess(_binary, args,
environment: _getEnvironment());
});
}).catchError((e) {
_logEvent("Running $_binary --version failed with $e");
return false;
});
}
String toString() => "Chrome";
}
class IE extends Browser {
Future<String> getVersion() {
var args = [
"query",
"HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Internet Explorer",
"/v",
"svcVersion"
];
return Process.run("reg", args).then((result) {
if (result.exitCode == 0) {
// The string we get back looks like this:
// HKEY_LOCAL_MACHINE\Software\Microsoft\Internet Explorer
// version REG_SZ 9.0.8112.16421
var findString = "REG_SZ";
var index = (result.stdout as String).indexOf(findString);
if (index > 0) {
return (result.stdout as String)
.substring(index + findString.length)
.trim();
}
}
return "Could not get the version of internet explorer";
});
}
// Clears the recovery cache if the static resetBrowserConfiguration flag is
// set.
Future<bool> resetConfiguration() {
if (!Browser.resetBrowserConfiguration) return new Future.value(true);
var localAppData = Platform.environment['LOCALAPPDATA'];
Directory dir = new Directory("$localAppData\\Microsoft\\"
"Internet Explorer\\Recovery");
return dir.delete(recursive: true).then((_) {
return true;
}).catchError((error) {
_logEvent("Deleting recovery dir failed with $error");
return false;
});
}
Future<bool> start(String url) {
_logEvent("Starting ie browser on: $url");
return resetConfiguration().then((_) => getVersion()).then((version) {
_logEvent("Got version: $version");
return startBrowserProcess(_binary, [url]);
});
}
String toString() => "IE";
}
class AndroidBrowserConfig {
final String name;
final String package;
final String activity;
final String action;
AndroidBrowserConfig(this.name, this.package, this.activity, this.action);
}
final contentShellOnAndroidConfig = new AndroidBrowserConfig(
'ContentShellOnAndroid',
'org.chromium.content_shell_apk',
'.ContentShellActivity',
'android.intent.action.VIEW');
class AndroidBrowser extends Browser {
final bool checkedMode;
AdbDevice _adbDevice;
AndroidBrowserConfig _config;
AndroidBrowser(
this._adbDevice, this._config, this.checkedMode, String apkPath) {
_binary = apkPath;
}
Future<bool> start(String url) {
var intent =
new Intent(_config.action, _config.package, _config.activity, url);
return _adbDevice.waitForBootCompleted().then((_) {
return _adbDevice.forceStop(_config.package);
}).then((_) {
return _adbDevice.killAll();
}).then((_) {
return _adbDevice.adbRoot();
}).then((_) {
return _adbDevice.setProp("DART_FORWARDING_PRINT", "1");
}).then((_) {
if (checkedMode) {
return _adbDevice.setProp("DART_FLAGS", "--checked");
} else {
return _adbDevice.setProp("DART_FLAGS", "");
}
}).then((_) {
return _adbDevice.installApk(new Path(_binary));
}).then((_) {
return _adbDevice.startActivity(intent).then((_) => true);
});
}
Future<bool> close() {
if (_adbDevice != null) {
return _adbDevice.forceStop(_config.package).then((_) {
return _adbDevice.killAll().then((_) => true);
});
}
return new Future.value(true);
}
void logBrowserInfoToTestBrowserOutput() {
_testBrowserOutput.stdout
.write('Android device id: ${_adbDevice.deviceId}\n');
}
String toString() => _config.name;
}
class AndroidChrome extends Browser {
static const String viewAction = 'android.intent.action.VIEW';
static const String mainAction = 'android.intent.action.MAIN';
static const String chromePackage = 'com.android.chrome';
static const String chromeActivity = '.Main';
static const String browserPackage = 'com.android.browser';
static const String browserActivity = '.BrowserActivity';
static const String firefoxPackage = 'org.mozilla.firefox';
static const String firefoxActivity = '.App';
static const String turnScreenOnPackage = 'com.google.dart.turnscreenon';
static const String turnScreenOnActivity = '.Main';
AdbDevice _adbDevice;
AndroidChrome(this._adbDevice);
Future<bool> start(String url) {
var chromeIntent =
new Intent(viewAction, chromePackage, chromeActivity, url);
var turnScreenOnIntent =
new Intent(mainAction, turnScreenOnPackage, turnScreenOnActivity);
var testing_resources_dir =
new Path('third_party/android_testing_resources');
if (!new Directory(testing_resources_dir.toNativePath()).existsSync()) {
DebugLogger.error("$testing_resources_dir doesn't exist. Exiting now.");
exit(1);
}
var chromeAPK = testing_resources_dir.append('com.android.chrome-1.apk');
var turnScreenOnAPK = testing_resources_dir.append('TurnScreenOn.apk');
var chromeConfDir = testing_resources_dir.append('chrome_configuration');
var chromeConfDirRemote = new Path('/data/user/0/com.android.chrome/');
return _adbDevice.waitForBootCompleted().then((_) {
return _adbDevice.forceStop(chromeIntent.package);
}).then((_) {
return _adbDevice.killAll();
}).then((_) {
return _adbDevice.adbRoot();
}).then((_) {
return _adbDevice.installApk(turnScreenOnAPK);
}).then((_) {
return _adbDevice.installApk(chromeAPK);
}).then((_) {
return _adbDevice.pushData(chromeConfDir, chromeConfDirRemote);
}).then((_) {
return _adbDevice.chmod('777', chromeConfDirRemote);
}).then((_) {
return _adbDevice.startActivity(turnScreenOnIntent).then((_) => true);
}).then((_) {
return _adbDevice.startActivity(chromeIntent).then((_) => true);
});
}
Future<bool> close() {
if (_adbDevice != null) {
return _adbDevice.forceStop(chromePackage).then((_) {
return _adbDevice.killAll().then((_) => true);
});
}
return new Future.value(true);
}
void logBrowserInfoToTestBrowserOutput() {
_testBrowserOutput.stdout
.write('Android device id: ${_adbDevice.deviceId}\n');
}
String toString() => "chromeOnAndroid";
}
class Firefox extends Browser {
static const String enablePopUp =
'user_pref("dom.disable_open_during_load", false);';
static const String disableDefaultCheck =
'user_pref("browser.shell.checkDefaultBrowser", false);';
static const String disableScriptTimeLimit =
'user_pref("dom.max_script_run_time", 0);';
void _createPreferenceFile(String path) {
var file = new File("$path/user.js");
var randomFile = file.openSync(mode: FileMode.write);
randomFile.writeStringSync(enablePopUp);
randomFile.writeStringSync(disableDefaultCheck);
randomFile.writeStringSync(disableScriptTimeLimit);
randomFile.close();
}
Future<bool> start(String url) {
_logEvent("Starting firefox browser on: $url");
// Get the version and log that.
return Process.run(_binary, ["--version"]).then((var versionResult) {
if (versionResult.exitCode != 0) {
_logEvent("Failed to firefox get version");
_logEvent("Make sure $_binary is a valid program for running firefox");
return new Future.value(false);
}
version = versionResult.stdout as String;
_logEvent("Got version: $version");
return Directory.systemTemp.createTemp().then((userDir) {
_createPreferenceFile(userDir.path);
_cleanup = () {
userDir.deleteSync(recursive: true);
};
var args = [
"-profile",
"${userDir.path}",
"-no-remote",
"-new-instance",
url
];
var environment = new Map<String, String>.from(Platform.environment);
environment["MOZ_CRASHREPORTER_DISABLE"] = "1";
return startBrowserProcess(_binary, args, environment: environment);
});
}).catchError((e) {
_logEvent("Running $_binary --version failed with $e");
return false;
});
}
String toString() => "Firefox";
}
/**
* Describes the current state of a browser used for testing.
*/
class BrowserStatus {
Browser browser;
BrowserTest currentTest;
// This is currently not used for anything except for error reporting.
// Given the usefulness of this in debugging issues this should not be
// removed even when we have a really stable system.
BrowserTest lastTest;
bool timeout = false;
Timer nextTestTimeout;
Stopwatch timeSinceRestart = new Stopwatch()..start();
BrowserStatus(Browser this.browser);
}
/**
* Describes a single test to be run in the browser.
*/
class BrowserTest {
// TODO(ricow): Add timeout callback instead of the string passing hack.
BrowserDoneCallback doneCallback;
String url;
int timeout;
String lastKnownMessage = '';
Stopwatch stopwatch;
// This might be null
Duration delayUntilTestStarted;
// We store this here for easy access when tests time out (instead of
// capturing this in a closure)
Timer timeoutTimer;
// Used for debugging, this is simply a unique identifier assigned to each
// test.
int id;
static int _idCounter = 0;
BrowserTest(this.url, this.doneCallback, this.timeout) {
id = _idCounter++;
}
String toJSON() => jsonEncode({'url': url, 'id': id, 'isHtmlTest': false});
}
/**
* Describes a test with a custom HTML page to be run in the browser.
*/
class HtmlTest extends BrowserTest {
List<String> expectedMessages;
HtmlTest(String url, BrowserDoneCallback doneCallback, int timeout,
this.expectedMessages)
: super(url, doneCallback, timeout) {}
String toJSON() => jsonEncode({
'url': url,
'id': id,
'isHtmlTest': true,
'expectedMessages': expectedMessages
});
}
/* Describes the output of running the test in a browser */
class BrowserTestOutput {
final Duration delayUntilTestStarted;
final Duration duration;
final String lastKnownMessage;
final BrowserOutput browserOutput;
final bool didTimeout;
BrowserTestOutput(this.delayUntilTestStarted, this.duration,
this.lastKnownMessage, this.browserOutput,
{this.didTimeout: false});
}
/// 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: 120);
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 TestConfiguration configuration;
final BrowserTestingServer testingServer;
final String localIp;
int maxNumBrowsers;
int numBrowsers = 0;
// Used to send back logs from the browser (start, stop etc)
Function logger;
int browserIdCounter = 1;
bool testingServerStarted = false;
bool underTermination = false;
int numBrowserGetTestTimeouts = 0;
DateTime lastEmptyTestQueueTime = new DateTime.now();
String _currentStartingBrowserId;
List<BrowserTest> testQueue = [];
Map<String, BrowserStatus> browserStatus = {};
Map<String, AdbDevice> adbDeviceMapping = {};
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 = {};
Map<int, String> doubleReportingOutputs = {};
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;
}
// 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;
}
BrowserTestRunner(
TestConfiguration configuration, String localIp, this.maxNumBrowsers)
: configuration = configuration,
localIp = localIp,
testingServer = new BrowserTestingServer(configuration, localIp,
Browser.requiresFocus(configuration.runtime.name)) {
testingServer.testRunner = this;
}
Future start() async {
await testingServer.start();
testingServer
..testDoneCallBack = handleResults
..testStatusUpdateCallBack = handleStatusUpdate
..testStartedCallBack = handleStarted
..nextTestCallBack = getNextTest;
if (configuration.runtime == Runtime.chromeOnAndroid) {
var idbNames = await AdbHelper.listDevices();
idleAdbDevices = new List.from(idbNames.map((id) => new AdbDevice(id)));
maxNumBrowsers = min(maxNumBrowsers, idleAdbDevices.length);
}
testingServerStarted = true;
requestBrowser();
}
/// 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() {
var id = getNextBrowserId();
var url = testingServer.getDriverUrl(id);
Browser browser;
if (configuration.runtime == Runtime.chromeOnAndroid) {
AdbDevice device = idleAdbDevices.removeLast();
adbDeviceMapping[id] = device;
browser = new AndroidChrome(device);
} else {
var path = configuration.browserLocation;
browser = new Browser.byRuntime(
configuration.runtime, path, configuration.isChecked);
browser.logger = logger;
}
browser.id = id;
markCurrentlyStarting(id);
var status = new BrowserStatus(browser);
browserStatus[id] = status;
numBrowsers++;
status.nextTestTimeout = createNextTestTimer(status);
browser.start(url);
}
void handleResults(String browserId, String output, int testId) {
var status = browserStatus[browserId];
if (testCache.containsKey(testId)) {
doubleReportingOutputs[testId] = output;
return;
}
if (status == null || status.timeout) {
// We don't do anything, this browser is currently being killed and
// replaced. The browser here can be null if we decided to kill the
// browser.
} else if (status.currentTest != null) {
status.currentTest.timeoutTimer.cancel();
status.currentTest.stopwatch.stop();
if (status.currentTest.id != testId) {
print("Expected test id ${status.currentTest.id} for"
"${status.currentTest.url}");
print("Got test id ${testId}");
print("Last test id was ${status.lastTest.id} for "
"${status.currentTest.url}");
throw ("This should never happen, wrong test id");
}
testCache[testId] = status.currentTest.url;
// Report that the test is finished now
var browserTestOutput = new BrowserTestOutput(
status.currentTest.delayUntilTestStarted,
status.currentTest.stopwatch.elapsed,
output,
status.browser.testBrowserOutput);
status.currentTest.doneCallback(browserTestOutput);
status.lastTest = status.currentTest;
status.currentTest = null;
status.nextTestTimeout = createNextTestTimer(status);
} else {
print("\nThis is bad, should never happen, handleResult no test");
print("URL: ${status.lastTest.url}");
print(output);
terminate().then((_) {
exit(1);
});
}
}
void handleStatusUpdate(String browserId, String output, int testId) {
var status = browserStatus[browserId];
if (status == null || status.timeout) {
// We don't do anything, this browser is currently being killed and
// replaced. The browser here can be null if we decided to kill the
// browser.
} else if (status.currentTest != null && status.currentTest.id == testId) {
status.currentTest.lastKnownMessage = output;
}
}
void handleStarted(String browserId, String output, int testId) {
var status = browserStatus[browserId];
if (status != null && !status.timeout && status.currentTest != null) {
status.currentTest.timeoutTimer.cancel();
status.currentTest.timeoutTimer =
createTimeoutTimer(status.currentTest, status);
status.currentTest.delayUntilTestStarted =
status.currentTest.stopwatch.elapsed;
}
}
Future handleTimeout(BrowserStatus status) async {
// 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) {
DebugLogger.error("Got test timeout for an already restarting browser");
return;
}
status.timeout = true;
timedOut.add(status.currentTest.url);
var id = status.browser.id;
status.currentTest.stopwatch.stop();
// Before closing the browser, we'll try to capture a screenshot on
// windows when using IE (to debug flakiness).
if (status.browser is IE) {
await captureInternetExplorerScreenshot(
'IE screenshot for ${status.currentTest.url}');
}
await status.browser.close();
var lastKnownMessage =
'Dom could not be fetched, since the test timed out.';
if (status.currentTest.lastKnownMessage.length > 0) {
lastKnownMessage = status.currentTest.lastKnownMessage;
}
if (status.lastTest != null) {
lastKnownMessage += '\nPrevious test was ${status.lastTest.url}';
}
// Wait until the browser is closed before reporting the test as timeout.
// This will enable us to capture stdout/stderr from the browser
// (which might provide us with information about what went wrong).
var browserTestOutput = new BrowserTestOutput(
status.currentTest.delayUntilTestStarted,
status.currentTest.stopwatch.elapsed,
lastKnownMessage,
status.browser.testBrowserOutput,
didTimeout: true);
status.currentTest.doneCallback(browserTestOutput);
status.lastTest = status.currentTest;
status.currentTest = null;
// We don't want to start a new browser if we are terminating.
if (underTermination) return;
removeBrowser(id);
requestBrowser();
}
/// 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 (configuration.runtime == Runtime.chromeOnAndroid) {
idleAdbDevices.add(adbDeviceMapping.remove(id));
}
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) {
status.nextTestTimeout.cancel();
status.nextTestTimeout = null;
}
if (testQueue.isEmpty) return null;
// We are currently terminating this browser, don't start a new test.
if (status.timeout) return null;
// 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 ((configuration.runtime == Runtime.ie10 ||
configuration.runtime == Runtime.ie11) &&
status.timeSinceRestart.elapsed > RESTART_BROWSER_INTERVAL) {
var id = status.browser.id;
// Reset stopwatch so we don't trigger again before restarting.
status.timeout = true;
status.browser.close().then((_) {
// We don't want to start a new browser if we are terminating.
if (underTermination) return;
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 = '';
} else {
// TODO(ricow): Handle this better.
print("Browser requested next test before reporting previous result");
print("This happened for browser $browserId");
print("Old test was: ${status.currentTest.url}");
print("The test before that was: ${status.lastTest.url}");
print("Timed out tests:");
for (var v in timedOut) {
print(" $v");
}
exit(1);
}
status.currentTest.timeoutTimer = createTimeoutTimer(test, status);
status.currentTest.stopwatch = new Stopwatch()..start();
// Reset the test specific output information (stdout, stderr) on the
// browser, since a new test is being started.
status.browser.resetTestBrowserOutput();
status.browser.logBrowserInfoToTestBrowserOutput();
return test;
}
/// 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);
});
}
/// 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);
});
}
void handleNextTestTimeout(BrowserStatus status) {
DebugLogger.warning(
"Browser timed out before getting next test. Restarting");
if (status.timeout) return;
numBrowserGetTestTimeouts++;
if (numBrowserGetTestTimeouts >= MAX_NEXT_TEST_TIMEOUTS) {
DebugLogger.error(
"Too many browser timeouts before getting next test. Terminating");
terminate().then((_) => exit(1));
} else {
status.timeout = true;
status.browser.close().then((_) {
removeBrowser(status.browser.id);
requestBrowser();
});
}
}
void enqueueTest(BrowserTest test) {
testQueue.add(test);
requestBrowser();
}
void printDoubleReportingTests() {
if (doubleReportingOutputs.length == 0) return;
// TODO(ricow): die on double reporting.
// Currently we just report this here, we could have a callback to the
// encapsulating environment.
print("");
print("Double reporting tests");
for (var id in doubleReportingOutputs.keys) {
print(" ${testCache[id]}");
}
DebugLogger.warning("Double reporting tests:");
for (var id in doubleReportingOutputs.keys) {
DebugLogger.warning("${testCache[id]}, output: ");
DebugLogger.warning("${doubleReportingOutputs[id]}");
DebugLogger.warning("");
DebugLogger.warning("");
}
}
// 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 = <Browser>[];
underTermination = true;
testingServer.underTermination = true;
for (BrowserStatus status in browserStatus.values) {
browsers.add(status.browser);
if (status.nextTestTimeout != null) {
status.nextTestTimeout.cancel();
status.nextTestTimeout = null;
}
}
for (var browser in browsers) {
await browser.close();
}
testingServer.errorReportingServer.close();
printDoubleReportingTests();
}
}
class BrowserTestingServer {
final TestConfiguration configuration;
/// Interface of the testing server:
///
/// GET /driver/BROWSER_ID -- This will get the driver page to fetch
/// and run tests ...
/// GET /next_test/BROWSER_ID -- returns "WAIT" "TERMINATE" or "url#id"
/// where url is the test to run, and id is the id of the test.
/// If there are currently no available tests the waitSignal is send
/// back. If we are in the process of terminating the terminateSignal
/// is send back and the browser will stop requesting new tasks.
/// POST /report/BROWSER_ID?id=NUM -- sends back the dom of the executed
/// test
final String localIp;
final bool requiresFocus;
BrowserTestRunner testRunner;
static const String driverPath = "/driver";
static const String nextTestPath = "/next_test";
static const String reportPath = "/report";
static const String statusUpdatePath = "/status_update";
static const String startedPath = "/started";
static const String waitSignal = "WAIT";
static const String terminateSignal = "TERMINATE";
var testCount = 0;
HttpServer errorReportingServer;
bool underTermination = false;
TestChangedCallback testDoneCallBack;
TestChangedCallback testStatusUpdateCallBack;
TestChangedCallback testStartedCallBack;
NextTestCallback nextTestCallBack;
BrowserTestingServer(this.configuration, this.localIp, this.requiresFocus);
Future start() {
return HttpServer.bind(localIp, configuration.testDriverErrorPort)
.then(setupErrorServer)
.then(setupDispatchingServer);
}
void setupErrorServer(HttpServer server) {
errorReportingServer = server;
void errorReportingHandler(HttpRequest request) {
StringBuffer buffer = new StringBuffer();
request.transform(utf8.decoder).listen((data) {
buffer.write(data);
}, onDone: () {
String back = buffer.toString();
request.response.headers.set("Access-Control-Allow-Origin", "*");
request.response.done.catchError((error) {
DebugLogger.error("Error getting error from browser"
"on uri ${request.uri.path}: $error");
});
request.response.close();
DebugLogger.error("Error from browser on : "
"${request.uri.path}, data: $back");
}, onError: (error) {
print(error);
});
}
void errorHandler(e) {
if (!underTermination) print("Error occured in httpserver: $e");
}
errorReportingServer.listen(errorReportingHandler, onError: errorHandler);
}
void setupDispatchingServer(_) {
var server = configuration.servers.server;
void noCache(HttpRequest request) {
request.response.headers
.set("Cache-Control", "no-cache, no-store, must-revalidate");
}
int testId(HttpRequest request) =>
int.parse(request.uri.queryParameters["id"]);
String browserId(HttpRequest request, String prefix) =>
request.uri.path.substring(prefix.length + 1);
server.addHandler(reportPath, (HttpRequest request) {
noCache(request);
handleReport(request, browserId(request, reportPath), testId(request),
isStatusUpdate: false);
});
server.addHandler(statusUpdatePath, (HttpRequest request) {
noCache(request);
handleReport(
request, browserId(request, statusUpdatePath), testId(request),
isStatusUpdate: true);
});
server.addHandler(startedPath, (HttpRequest request) {
noCache(request);
handleStarted(request, browserId(request, startedPath), testId(request));
});
void sendPageHandler(HttpRequest request) {
// Do NOT make this method async. We need to call catchError below
// synchronously to avoid unhandled asynchronous errors.
noCache(request);
Future<String> textResponse;
if (request.uri.path.startsWith(driverPath)) {
textResponse = getDriverPage(browserId(request, driverPath));
request.response.headers.set('Content-Type', 'text/html');
} else if (request.uri.path.startsWith(nextTestPath)) {
textResponse = new Future<String>.value(
getNextTest(browserId(request, nextTestPath)));
request.response.headers.set('Content-Type', 'text/plain');
} else {
textResponse = new Future<String>.value("");
}
request.response.done.catchError((error) async {
if (!underTermination) {
String text = await textResponse;
print("URI ${request.uri}");
print("text $text");
throw "Error returning content to browser: $error";
}
});
textResponse.then((String text) async {
request.response.write(text);
await request.listen(null).asFuture();
// Ignoring the returned closure as it returns the 'done' future
// which already has catchError installed above.
request.response.close();
});
}
server.addHandler(driverPath, sendPageHandler);
server.addHandler(nextTestPath, sendPageHandler);
}
void handleReport(HttpRequest request, String browserId, int testId,
{bool isStatusUpdate}) {
StringBuffer buffer = new StringBuffer();
request.transform(utf8.decoder).listen((data) {
buffer.write(data);
}, onDone: () {
String back = buffer.toString();
request.response.close();
if (isStatusUpdate) {
testStatusUpdateCallBack(browserId, back, testId);
} else {
testDoneCallBack(browserId, back, testId);
}
// TODO(ricow): We should do something smart if we get an error here.
}, onError: (error) {
DebugLogger.error("$error");
});
}
void handleStarted(HttpRequest request, String browserId, int testId) {
StringBuffer buffer = new StringBuffer();
// If an error occurs while receiving the data from the request stream,
// we don't handle it specially. We can safely ignore it, since the started
// events are not crucial.
request.transform(utf8.decoder).listen((data) {
buffer.write(data);
}, onDone: () {
String back = buffer.toString();
request.response.close();
testStartedCallBack(browserId, back, testId);
}, onError: (error) {
DebugLogger.error("$error");
});
}
String getNextTest(String browserId) {
var nextTest = nextTestCallBack(browserId);
if (underTermination) {
// Browsers will be killed shortly, send them a terminate signal so
// that they stop pulling.
return terminateSignal;
}
return nextTest == null ? waitSignal : nextTest.toJSON();
}
String getDriverUrl(String browserId) {
if (errorReportingServer == null) {
print("Bad browser testing server, you are not started yet. Can't "
"produce driver url");
exit(1);
// This should never happen - exit immediately;
}
return "http://$localIp:${configuration.servers.port}/driver/$browserId";
}
Future<String> getDriverPage(String browserId) async {
await testRunner.browserStatus[browserId].browser.onDriverPageRequested();
var errorReportingUrl =
"http://$localIp:${errorReportingServer.port}/$browserId";
String driverContent = """
<!DOCTYPE html><html>
<head>
<title>Driving page</title>
<style>
.big-notice {
background-color: red;
color: white;
font-weight: bold;
font-size: xx-large;
text-align: center;
}
.controller.box {
white-space: nowrap;
overflow: scroll;
height: 6em;
}
body {
font-family: sans-serif;
}
body div {
padding-top: 10px;
}
</style>
<script type='text/javascript'>
var STATUS_UPDATE_INTERVAL = 10000;
function startTesting() {
var number_of_tests = 0;
var current_id;
var next_id;
// Has the test in the current iframe reported that it is done?
var test_completed = true;
// Has the test in the current iframe reported that it is started?
var test_started = false;
var testing_window;
var embedded_iframe_div = document.getElementById('embedded_iframe_div');
var embedded_iframe = document.getElementById('embedded_iframe');
var number_div = document.getElementById('number');
var executing_div = document.getElementById('currently_executing');
var error_div = document.getElementById('unhandled_error');
var use_iframe = ${configuration.runtime.requiresIFrame};
var start = new Date();
// Object that holds the state of an HTML test
var html_test;
function newTaskHandler() {
if (this.readyState == this.DONE) {
if (this.status == 200) {
if (this.responseText == '$waitSignal') {
setTimeout(getNextTask, 500);
} else if (this.responseText == '$terminateSignal') {
// Don't do anything, we will be killed shortly.
} else {
var elapsed = new Date() - start;
var nextTask = JSON.parse(this.responseText);
var url = nextTask.url;
next_id = nextTask.id;
if (nextTask.isHtmlTest) {
html_test = {
expected_messages: nextTask.expectedMessages,
found_message_count: 0,
double_received_messages: [],
unexpected_messages: [],
found_messages: {}
};
for (var i = 0; i < html_test.expected_messages.length; ++i) {
html_test.found_messages[html_test.expected_messages[i]] = 0;
}
} else {
html_test = null;
}
run(url);
}
} else {
reportError('Could not contact the server and get a new task');
}
}
}
function contactBrowserController(method,
path,
callback,
msg,
isUrlEncoded) {
var client = new XMLHttpRequest();
client.onreadystatechange = callback;
client.open(method, path);
if (isUrlEncoded) {
client.setRequestHeader('Content-type',
'application/x-www-form-urlencoded');
}
client.send(msg);
}
function getNextTask() {
// Until we have the next task we set the current_id to a specific
// negative value.
contactBrowserController(
'GET', '$nextTestPath/$browserId', newTaskHandler, "", false);
}
function childError(message, filename, lineno, colno, error) {
sendStatusUpdate();
if (error) {
reportMessage('FAIL:' + filename + ':' + lineno +
':' + colno + ':' + message + '\\n' + error.stack, false, false);
} else if (filename) {
reportMessage('FAIL:' + filename + ':' + lineno +
':' + colno + ':' + message, false, false);
} else {
reportMessage('FAIL: ' + message, false, false);
}
return true;
}
function setChildHandlers(e) {
embedded_iframe.contentWindow.addEventListener('message',
childMessageHandler,
false);
embedded_iframe.contentWindow.onerror = childError;
reportMessage("First message from html test", true, false);
html_test.handlers_installed = true;
sendRepeatingStatusUpdate();
}
function checkChildHandlersInstalled() {
if (!html_test.handlers_installed) {
reportMessage("First message from html test", true, false);
reportMessage(
'FAIL: Html test did not call ' +
'window.parent.dispatchEvent(new Event("detect_errors")) ' +
'as its first action', false, false);
}
}
function run(url) {
number_of_tests++;
number_div.innerHTML = number_of_tests;
executing_div.innerHTML = url;
if (use_iframe) {
if (html_test) {
window.addEventListener('detect_errors', setChildHandlers, false);
embedded_iframe.onload = checkChildHandlersInstalled;
} else {
embedded_iframe.onload = null;
}
embedded_iframe_div.removeChild(embedded_iframe);
embedded_iframe = document.createElement('iframe');
embedded_iframe.id = "embedded_iframe";
embedded_iframe.width='800px';
embedded_iframe.height='600px';
embedded_iframe_div.appendChild(embedded_iframe);
embedded_iframe.src = url;
} else {
if (typeof testing_window != 'undefined') {
testing_window.close();
}
testing_window = window.open(url);
}
test_started = false;
test_completed = false;
}
window.onerror = function (message, url, lineNumber) {
if (url) {
reportError(url + ':' + lineNumber + ':' + message);
} else {
reportError(message);
}
}
function reportError(msg) {
function handleReady() {
if (this.readyState == this.DONE && this.status != 200) {
var error = 'Sending back error did not succeeed: ' + this.status;
error = error + '. Failed to send msg: ' + msg;
error_div.innerHTML = error;
}
}
contactBrowserController(
'POST', '$errorReportingUrl?test=1', handleReady, msg, true);
}
function reportMessage(msg, isFirstMessage, isStatusUpdate) {
if (isFirstMessage) {
if (test_started) {
reportMessage(
"FAIL: test started more than once (test reloads itself) " +
msg, false, false);
return;
}
current_id = next_id;
test_started = true;
contactBrowserController(
'POST', '$startedPath/${browserId}?id=' + current_id,
function () {}, msg, true);
} else if (isStatusUpdate) {
contactBrowserController(
'POST', '$statusUpdatePath/${browserId}?id=' + current_id,
function() {}, msg, true);
} else {
var is_double_report = test_completed;
var retry = 0;
test_completed = true;
function reportDoneMessage() {
contactBrowserController(
'POST', '$reportPath/${browserId}?id=' + current_id,
handleReady, msg, true);
}
function handleReady() {
if (this.readyState == this.DONE) {
if (this.status == 200) {
if (!is_double_report) {
getNextTask();
}
} else {
reportError('Error sending result to server. Status: ' +
this.status + ' Retry: ' + retry);
retry++;
if (retry < 3) {
setTimeout(reportDoneMessage, 1000);
}
}
}
}
reportDoneMessage();
}
}
function parseResult(result) {
var parsedData = null;
try {
parsedData = JSON.parse(result);
} catch(error) { }
return parsedData;
}
// Browser tests send JSON messages to the driver window, handled here.
function messageHandler(e) {
var msg = e.data;
if (typeof msg != 'string') return;
var expectedSource =
use_iframe ? embedded_iframe.contentWindow : testing_window;
if (e.source != expectedSource) {
reportError("Message received from old test window: " + msg);
return;
}
var parsedData = parseResult(msg);
if (parsedData) {
// Only if the JSON message contains all required parameters,
// will we handle it and post it back to the test controller.
if ('message' in parsedData &&
'is_first_message' in parsedData &&
'is_status_update' in parsedData &&
'is_done' in parsedData) {
var message = parsedData['message'];
var isFirstMessage = parsedData['is_first_message'];
var isStatusUpdate = parsedData['is_status_update'];
var isDone = parsedData['is_done'];
if (!isFirstMessage && !isStatusUpdate) {
if (!isDone) {
alert("Bug in test_controller.js: " +
"isFirstMessage/isStatusUpdate/isDone were all false");
}
}
reportMessage(message, isFirstMessage, isStatusUpdate);
}
}
}
function sendStatusUpdate () {
var dom =
embedded_iframe.contentWindow.document.documentElement.innerHTML;
var message = 'Status:\\n';
if (html_test != null) {
message +=
' Messages received multiple times:\\n' +
' ' + html_test.double_received_messages + '\\n' +
' Unexpected messages:\\n' +
' ' + html_test.unexpected_messages + '\\n';
}
message += ' DOM:\\n' +
' ' + dom;
reportMessage(message, false, true);
}
function sendRepeatingStatusUpdate() {
sendStatusUpdate();
setTimeout(sendRepeatingStatusUpdate, STATUS_UPDATE_INTERVAL);
}
// HTML tests post messages to their own window, handled by this handler.
// This handler is installed on the child window when it sends the
// 'detect_errors' event. Every HTML test must send 'detect_errors' to
// its parent window as its first action, so all errors will be caught.
function childMessageHandler(e) {
var msg = e.data;
if (typeof msg != 'string') return;
if (msg in html_test.found_messages) {
html_test.found_messages[msg]++;
if (html_test.found_messages[msg] == 1) {
html_test.found_message_count++;
} else {
html_test.double_received_messages.push(msg);
sendStatusUpdate();
}
} else {
html_test.unexpected_messages.push(msg);
sendStatusUpdate();
}
if (html_test.found_message_count ==
html_test.expected_messages.length) {
reportMessage('Test done: PASS', false, false);
}
}
if (!html_test) {
window.addEventListener('message', messageHandler, false);
waitForDone = false;
}
getNextTask();
}
</script>
</head>
<body onload="startTesting()">
<div class='big-notice'>
Please keep this window in focus at all times.
</div>
<div>
Some browsers, Safari, in particular, may pause JavaScript when not
visible to conserve power consumption and CPU resources. In addition,
some tests of focus events will not work correctly if this window doesn't
have focus. It's also advisable to close any other programs that may open
modal dialogs, for example, Chrome with Calendar open.
</div>
<div class="controller box">
Dart test driver, number of tests: <span id="number"></span><br>
Currently executing: <span id="currently_executing"></span><br>
Unhandled error: <span id="unhandled_error"></span>
</div>
<div id="embedded_iframe_div" class="test box">
<iframe id="embedded_iframe"></iframe>
</div>
</body>
</html>
""";
return driverContent;
}
}
Future captureInternetExplorerScreenshot(String message) async {
if (Platform.environment['USERNAME'] != 'chrome-bot') {
return;
}
print('--------------------------------------------------------------------');
final String date =
new DateTime.now().toUtc().toIso8601String().replaceAll(':', '_');
final screenshotName = 'ie_screenshot_${date}.png';
// The "capture_screen.ps1" script is next to "test.dart" in "tools/"
final powerShellScript =
Platform.script.resolve('../../capture_screenshot.ps1').toFilePath();
final screenshotFile =
Platform.script.resolve('../../../$screenshotName').toFilePath();
final args = [
'-ExecutionPolicy',
'ByPass',
'-File',
powerShellScript,
screenshotFile
];
final ProcessResult result =
await Process.run('powershell.exe', args, runInShell: true);
if (result.exitCode != 0) {
print('[$message] Failed to capture IE screenshot on windows: '
'powershell.exe "${args.join(' ')}" returned with:\n'
'exit code: ${result.exitCode}\n'
'stdout: ${result.stdout}\n'
'stderr: ${result.stderr}');
} else {
final storageUrl = 'gs://dart-temp-crash-archive/$screenshotName';
final args = [
r'e:\b\depot_tools\gsutil.py',
'cp',
screenshotFile,
storageUrl,
];
final ProcessResult result = await Process.run('python', args);
if (result.exitCode != 0) {
print('[$message] Failed upload captured IE screenshot to cloud storage: '
'"${args.join(' ')}" returned with:\n'
'exit code: ${result.exitCode}\n'
'stdout: ${result.stdout}\n'
'stderr: ${result.stderr}');
} else {
print('[$message] Successfully uploaded screenshot to $storageUrl');
}
new File(screenshotFile).deleteSync();
}
print('--------------------------------------------------------------------');
}