[infra] Use safaridriver to launch Safari

For some reason, we launched Safari from a file, which since macOS 10.14
requires the user to confirm the operation in a modal dialog which
caused timeouts.

* Removed unused http test driver port option.
* Added some basic tests for test.py's browser interactions.

Bug: b/208186791
Change-Id: I070529148d37bf312f219c576abca3da972f73e6
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/204202
Commit-Queue: Alexander Thomas <athom@google.com>
Reviewed-by: William Hesse <whesse@google.com>
This commit is contained in:
Alexander Thomas 2022-04-29 08:57:41 +00:00 committed by Commit Bot
parent c4c9dc5994
commit 11a6820f7c
9 changed files with 358 additions and 507 deletions

View file

@ -35,7 +35,6 @@ void main(List<String> arguments) {
var configuration = TestConfiguration(
configuration: Configuration(
"dummy-configuration", null, null, null, runtime, null));
var executable = configuration.browserLocation;
var browser = Browser.byRuntime(runtime, executable);
var browser = Browser.fromConfiguration(configuration);
browser.start(arguments[1]);
}

View file

@ -7,10 +7,11 @@ import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:webdriver/io.dart';
import 'android.dart';
import 'configuration.dart';
import 'path.dart';
import 'reset_safari.dart';
import 'utils.dart';
typedef BrowserDoneCallback = void Function(BrowserTestOutput output);
@ -39,10 +40,7 @@ abstract class Browser {
Function _cleanup;
/// The version of the browser - normally set when starting a browser
String version = "";
/// The path to the browser executable.
String _binary;
Future<String> get version;
/// 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
@ -63,18 +61,18 @@ abstract class Browser {
/// This future returns when the process exits. It is also the return value
/// of close()
Future done;
Future<bool> done;
Browser();
factory Browser.byRuntime(Runtime runtime, String executablePath) {
static Browser fromConfiguration(TestConfiguration configuration) {
Browser browser;
switch (runtime) {
switch (configuration.runtime) {
case Runtime.firefox:
browser = Firefox();
browser = Firefox(configuration.browserLocation);
break;
case Runtime.chrome:
browser = Chrome();
browser = Chrome(configuration.browserLocation);
break;
case Runtime.safari:
browser = Safari();
@ -82,13 +80,12 @@ abstract class Browser {
case Runtime.ie9:
case Runtime.ie10:
case Runtime.ie11:
browser = IE();
browser = IE(configuration.browserLocation);
break;
default:
throw "unreachable";
}
browser._binary = executablePath;
return browser;
}
@ -131,7 +128,7 @@ abstract class Browser {
_testBrowserOutput.stderr.write(output);
}
Future close() {
Future<bool> close() {
_logEvent("Close called on browser");
if (process != null) {
if (process.kill(ProcessSignal.sigkill)) {
@ -258,58 +255,87 @@ abstract class Browser {
///
/// This is used by [Safari] to ensure the browser window has focus.
Future<Null> onDriverPageRequested() => Future.value();
@override
String toString() => '$runtimeType';
}
class Safari extends Browser {
/// We get the safari version by parsing a version file
static const String versionFile =
"/Applications/Safari.app/Contents/version.plist";
abstract class WebDriverBrowser extends Browser {
static int _nextPort = 4444;
final int _port = _nextPort++;
WebDriver _driver;
static const String safariBundleLocation = "/Applications/Safari.app/";
String get driverExecutable;
List<String> get driverArguments;
Map<String, dynamic> get desiredCapabilities;
// 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 = Completer<Null>();
handleUncaughtError(error, StackTrace stackTrace) {
if (!completer.isCompleted) {
completer.completeError(error, stackTrace);
} else {
throw AsyncError(error, stackTrace);
}
}
var parent = Zone.current;
var specification = ZoneSpecification(
print: (Zone self, ZoneDelegate delegate, Zone zone, String line) {
delegate.run(parent, () {
_logEvent(line);
});
});
Future zoneWrapper() {
var safariUri = Uri.base.resolve(safariBundleLocation);
return 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.
runZonedGuarded(zoneWrapper, handleUncaughtError,
zoneSpecification: specification);
try {
await completer.future;
return true;
} catch (error, st) {
_logEvent("Unable to reset Safari: $error$st");
@override
Future<bool> start(String url) async {
_logEvent('Starting $this browser on: $url');
if (!await startBrowserProcess(
driverExecutable, ['--port', '$_port', ...driverArguments])) {
return false;
}
await _createDriver();
await _driver.get(url);
try {
_logEvent('Got version: ${await version}');
} catch (error) {
_logEvent('Failed to get version.\nError: $error');
return false;
}
return true;
}
Future<void> _createDriver() async {
for (var i = 5; i >= 0; i--) {
// Give the driver process some time to be ready to accept connections.
await Future.delayed(const Duration(seconds: 1));
try {
_driver = await createDriver(
uri: Uri.parse('http://localhost:$_port/'),
desired: desiredCapabilities);
} catch (error) {
if (i > 0) {
_logEvent(
'Failed to create driver ($i retries left).\nError: $error');
} else {
_logEvent('Failed to create driver.\nError: $error');
await close();
rethrow;
}
}
if (_driver != null) break;
}
}
Future<String> getVersion() {
@override
Future<bool> close() async {
try {
await _driver?.quit();
// Give the driver process some time to be quit the browser.
await Future.delayed(const Duration(seconds: 1));
} finally {
process?.kill();
}
return true;
}
}
class Safari extends WebDriverBrowser {
/// We get the safari version by parsing a version file
static const versionFile = '/Applications/Safari.app/Contents/version.plist';
@override
final driverExecutable = '/usr/bin/safaridriver';
@override
final driverArguments = <String>[];
@override
final desiredCapabilities = <String, dynamic>{
'browserName': 'safari',
};
@override
Future<String> get version async {
// 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">
@ -327,189 +353,119 @@ class Safari extends Browser {
// <string>7536029013000000</string>
// </dict>
// </plist>
return File(versionFile).readAsLines().then((content) {
var versionOnNextLine = false;
for (var line in content) {
if (versionOnNextLine) return line;
if (line.contains("CFBundleShortVersionString")) {
versionOnNextLine = true;
}
}
return null;
});
final versionLine = (await File(versionFile).readAsLines())
.skipWhile((line) => !line.contains("CFBundleShortVersionString"))
.skip(1)
.take(1);
return versionLine.isEmpty ? 'unknown' : versionLine.first;
}
Future<Null> _createLaunchHTML(String path, String url) async {
var file = 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";
Chrome(this._binary);
final String _binary;
Map<String, String> _getEnvironment() => null;
Future<bool> _getVersion() {
@override
Future<String> get version async {
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 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 "unknown on windows";
}
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;
});
final result = await Process.run(_binary, ["--version"]);
if (result.exitCode != 0) {
_logEvent("Failed to get chrome version");
_logEvent("Make sure $_binary is a valid program for running chrome");
throw StateError(
"Failed to get chrome version.\nExit code: ${result.exitCode}");
}
return result.stdout as String;
}
Future<bool> start(String url) {
@override
Future<bool> start(String url) async {
_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");
Timer(const 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 = [
"--bwsi",
"--disable-component-update",
"--disable-extensions",
"--disable-popup-blocking",
"--no-first-run",
"--use-mock-keychain",
"--user-data-dir=${userDir.path}",
url,
];
// 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");
if (!await File(_binary).exists()) {
_logEvent("Chrome binary not available.");
_logEvent("Make sure $_binary is a valid program for running chrome");
return false;
});
}
}
try {
_logEvent("Got version: ${await version}");
final userDir = await Directory.systemTemp.createTemp();
_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");
Timer(const 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 = [
"--bwsi",
"--disable-component-update",
"--disable-extensions",
"--disable-popup-blocking",
"--no-first-run",
"--use-mock-keychain",
"--user-data-dir=${userDir.path}",
url,
];
String toString() => "Chrome";
// 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());
} catch (e) {
_logEvent("Starting chrome failed with $e");
return false;
}
}
}
class IE extends Browser {
Future<String> getVersion() {
IE(this._binary);
final String _binary;
@override
Future<String> get version async {
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";
});
final result = await Process.run("reg", args);
if (result.exitCode != 0) {
throw StateError("Could not get the version of internet explorer");
}
// 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();
}
throw StateError("Could not get the version of internet explorer");
}
// Clears the recovery cache and allows popups on localhost if the static
@ -529,24 +485,23 @@ class IE extends Browser {
var localAppData = Platform.environment['LOCALAPPDATA'];
var dir = Directory("$localAppData\\Microsoft\\"
"Internet Explorer\\Recovery");
return dir.delete(recursive: true).then((_) {
try {
dir.delete(recursive: true);
return true;
}).catchError((error) {
} catch (error) {
_logEvent("Deleting recovery dir failed with $error");
return false;
});
}
}
Future<bool> start(String url) {
@override
Future<bool> start(String url) async {
_logEvent("Starting ie browser on: $url");
return resetConfiguration().then((_) => getVersion()).then((version) {
_logEvent("Got version: $version");
return startBrowserProcess(_binary, [url]);
});
await resetConfiguration();
_logEvent("Got version: ${await version}");
return startBrowserProcess(_binary, [url]);
}
String toString() => "IE";
Future<void> _setRegistryKey(String key, String value,
{String data, String type}) async {
var args = <String>[
@ -580,6 +535,7 @@ class AndroidChrome extends Browser {
AndroidChrome(this._adbDevice);
@override
Future<bool> start(String url) {
var chromeIntent = Intent(viewAction, chromePackage, chromeActivity, url);
var turnScreenOnIntent =
@ -617,6 +573,7 @@ class AndroidChrome extends Browser {
});
}
@override
Future<bool> close() {
if (_adbDevice != null) {
return _adbDevice.forceStop(chromePackage).then((_) {
@ -631,10 +588,18 @@ class AndroidChrome extends Browser {
.write('Android device id: ${_adbDevice.deviceId}\n');
}
@override
final Future<String> version = Future.value('unknown');
@override
String toString() => "chromeOnAndroid";
}
class Firefox extends Browser {
Firefox(this._binary);
final String _binary;
static const String enablePopUp =
'user_pref("dom.disable_open_during_load", false);';
static const String disableDefaultCheck =
@ -651,41 +616,43 @@ class Firefox extends Browser {
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 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 = 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;
});
@override
Future<String> get version async {
final result = await Process.run(_binary, ["--version"]);
if (result.exitCode != 0) {
_logEvent("Failed to get firefox version");
_logEvent("Make sure $_binary is a valid program for running firefox");
throw StateError(
"Failed to get firefox version.\nExit code: ${result.exitCode}");
}
return result.stdout as String;
}
String toString() => "Firefox";
@override
Future<bool> start(String url) async {
_logEvent("Starting firefox browser on: $url");
try {
_logEvent("Got version: ${await version}");
final userDir = await Directory.systemTemp.createTemp();
_createPreferenceFile(userDir.path);
_cleanup = () {
userDir.deleteSync(recursive: true);
};
var args = [
"-profile",
"${userDir.path}",
"-no-remote",
"-new-instance",
url
];
var environment = Map<String, String>.from(Platform.environment);
environment["MOZ_CRASHREPORTER_DISABLE"] = "1";
return startBrowserProcess(_binary, args, environment: environment);
} catch (e) {
_logEvent("Starting firefox failed with $e");
return false;
}
}
}
/// Describes the current state of a browser used for testing.
@ -766,6 +733,7 @@ class BrowserTestRunner {
final TestConfiguration configuration;
final BrowserTestingServer testingServer;
final Browser Function(TestConfiguration configuration) browserFactory;
final String localIp;
int maxNumBrowsers;
@ -774,7 +742,7 @@ class BrowserTestRunner {
/// Used to send back logs from the browser (start, stop etc.).
Function logger;
int browserIdCounter = 1;
static int browserIdCounter = 1;
bool testingServerStarted = false;
bool underTermination = false;
@ -816,13 +784,14 @@ class BrowserTestRunner {
if (_currentStartingBrowserId == id) _currentStartingBrowserId = null;
}
BrowserTestRunner(this.configuration, this.localIp, this.maxNumBrowsers)
BrowserTestRunner(this.configuration, this.localIp, this.maxNumBrowsers,
[this.browserFactory = Browser.fromConfiguration])
: testingServer = BrowserTestingServer(configuration, localIp,
Browser.requiresFocus(configuration.runtime.name)) {
testingServer.testRunner = this;
}
Future start() async {
Future<bool> start() async {
await testingServer.start();
testingServer
..testDoneCallBack = handleResults
@ -835,7 +804,7 @@ class BrowserTestRunner {
maxNumBrowsers = min(maxNumBrowsers, idleAdbDevices.length);
}
testingServerStarted = true;
requestBrowser();
return requestBrowser();
}
/// requestBrowser() is called whenever we might want to start an additional
@ -846,18 +815,18 @@ class BrowserTestRunner {
/// 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();
Future<bool> requestBrowser() async {
if (!testingServerStarted) return false;
if (underTermination) return false;
if (numBrowsers == maxNumBrowsers) return false;
if (aBrowserIsCurrentlyStarting) return false;
if (numBrowsers > 0 && queueWasEmptyRecently) return false;
return createBrowser();
}
String getNextBrowserId() => "BROWSER${browserIdCounter++}";
static String getNextBrowserId() => "BROWSER${browserIdCounter++}";
void createBrowser() {
Future<bool> createBrowser() {
var id = getNextBrowserId();
var url = testingServer.getDriverUrl(id);
@ -867,8 +836,7 @@ class BrowserTestRunner {
adbDeviceMapping[id] = device;
browser = AndroidChrome(device);
} else {
var path = configuration.browserLocation;
browser = Browser.byRuntime(configuration.runtime, path);
browser = browserFactory(configuration);
browser.logger = logger;
}
@ -878,7 +846,7 @@ class BrowserTestRunner {
browserStatus[id] = status;
numBrowsers++;
status.nextTestTimeout = createNextTestTimer(status);
browser.start(url);
return browser.start(url);
}
void handleResults(String browserId, String output, int testId) {
@ -1049,7 +1017,7 @@ class BrowserTestRunner {
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("The test before that was: ${status.lastTest?.url}");
print("Timed out tests:");
for (var v in timedOut) {
print(" $v");
@ -1182,10 +1150,11 @@ class BrowserTestingServer {
BrowserTestingServer(this.configuration, this.localIp, this.requiresFocus);
Future start() {
return HttpServer.bind(localIp, configuration.testDriverErrorPort)
.then(setupErrorServer)
.then(setupDispatchingServer);
Future start() async {
var server =
await HttpServer.bind(localIp, configuration.testDriverErrorPort);
setupErrorServer(server);
setupDispatchingServer(server);
}
void setupErrorServer(HttpServer server) {
@ -1335,7 +1304,7 @@ class BrowserTestingServer {
}
Future<String> getDriverPage(String browserId) async {
await testRunner.browserStatus[browserId].browser.onDriverPageRequested();
await testRunner.browserStatus[browserId]?.browser?.onDriverPageRequested();
var errorReportingUrl =
"http://$localIp:${errorReportingServer.port}/$browserId";
var driverContent = """

View file

@ -297,9 +297,6 @@ class TestConfiguration {
case Runtime.firefox:
location = firefoxPath;
break;
case Runtime.safari:
location = safariPath;
break;
}
if (location != null) return location;
@ -317,9 +314,6 @@ class TestConfiguration {
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
System.linux: 'google-chrome'
},
Runtime.safari: {
System.mac: '/Applications/Safari.app/Contents/MacOS/Safari'
},
Runtime.ie9: {
System.win: 'C:\\Program Files\\Internet Explorer\\iexplore.exe'
},

View file

@ -302,8 +302,6 @@ used for browsers to connect to.''',
_Option.int('test_server_cross_origin_port',
'Port for test http server cross origin.',
defaultsTo: 0, hide: true),
_Option.int('test_driver_port', 'Port for http test driver server.',
defaultsTo: 0, hide: true),
_Option.int(
'test_driver_error_port', 'Port for http test driver server errors.',
defaultsTo: 0, hide: true),

View file

@ -787,7 +787,7 @@ class CommandExecutorImpl implements CommandExecutor {
}
Future<CommandOutput> _startBrowserControllerTest(
BrowserTestCommand browserCommand, int timeout) {
BrowserTestCommand browserCommand, int timeout) async {
var completer = Completer<CommandOutput>();
callback(BrowserTestOutput output) {
@ -795,11 +795,16 @@ class CommandExecutorImpl implements CommandExecutor {
}
var browserTest = BrowserTest(browserCommand.url, callback, timeout);
_getBrowserTestRunner(browserCommand.configuration).then((testRunner) {
testRunner.enqueueTest(browserTest);
});
return completer.future;
for (var failures = 0; failures < 10; failures++) {
var testRunner =
await _getBrowserTestRunner(browserCommand.configuration);
if (testRunner != null) {
testRunner.enqueueTest(browserTest);
return completer.future;
}
}
print('FATAL: Failed to get a browser test runner 10 times in a row.');
io.exit(1);
}
Future<BrowserTestRunner> _getBrowserTestRunner(
@ -811,7 +816,11 @@ class CommandExecutorImpl implements CommandExecutor {
testRunner.logger = DebugLogger.info;
}
_browserTestRunners[configuration] = testRunner;
await testRunner.start();
if (!await testRunner.start()) {
DebugLogger.error('Failed to start browser test runner.');
_browserTestRunners.remove(configuration);
await testRunner.terminate();
}
}
return _browserTestRunners[configuration];
}

View file

@ -1,212 +0,0 @@
// Copyright (c) 2016, 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.
/// Helper program for killing and resetting all Safari settings to a known
/// state that works well for testing dart2js output in Safari.
///
/// Warning: this will delete all your Safari settings and bookmarks.
library testing.reset_safari;
import 'dart:async' show Future, Timer;
import 'dart:io' show Directory, File, Platform, Process;
const String defaultSafariBundleLocation = "/Applications/Safari.app/";
const String relativeSafariLocation = "Contents/MacOS/Safari";
const String lsofLocation = "/usr/sbin/lsof";
const String killLocation = "/bin/kill";
const String pkillLocation = "/usr/bin/pkill";
const String safari = "com.apple.Safari";
const String defaultsLocation = "/usr/bin/defaults";
final List<String> safariSettings = <String>[
"Library/Caches/$safari",
"Library/Safari",
"Library/Saved Application State/$safari.savedState",
"Library/Caches/Metadata/Safari",
"Library/Preferences/$safari.plist",
];
const Duration defaultPollDelay = Duration(milliseconds: 1);
final String cpgi = "$safari.ContentPageGroupIdentifier";
final String knownSafariPreference = '''
{
DefaultBrowserPromptingState2 = 2;
StartPageViewControllerMode = 0;
TestDriveOriginBrowser = 1;
TestDriveUserDecision = 2;
TestDriveState = 3;
AlwaysRestoreSessionAtLaunch = 0;
NewTabBehavior = 1;
NewWindowBehavior = 1;
LastSafariVersionWithWelcomePage = "9.0";
OpenNewTabsInFront = 0;
TabCreationPolicy = 0;
IncludeDevelopMenu = 1;
WebKitDeveloperExtrasEnabledPreferenceKey = 1;
"$cpgi.WebKit2DeveloperExtrasEnabled" = 1;
AutoFillCreditCardData = 0;
AutoFillMiscellaneousForms = 0;
AutoFillPasswords = 0;
SuppressSearchSuggestions = 1;
PreloadTopHit = 0;
ShowFavoritesUnderSmartSearchField = 0;
WebsiteSpecificSearchEnabled = 0;
WarnAboutFraudulentWebsites = 0;
WebKitJavaScriptEnabled = 1;
"$cpgi.WebKit2JavaScriptEnabled" = 1;
WebKitJavaScriptCanOpenWindowsAutomatically = 1;
"$cpgi.WebKit2JavaScriptCanOpenWindowsAutomatically" = 1;
"$cpgi.WebKit2WebGLEnabled" = 1;
WebGLDefaultLoadPolicy = WebGLPolicyAllowNoSecurityRestrictions;
"$cpgi.WebKit2PluginsEnabled" = 0;
BlockStoragePolicy = 1;
WebKitStorageBlockingPolicy = 0;
"$cpgi.WebKit2StorageBlockingPolicy" = 0;
SafariGeolocationPermissionPolicy = 0;
CanPromptForPushNotifications = 0;
InstallExtensionUpdatesAutomatically = 0;
ShowFullURLInSmartSearchField = 1;
"$cpgi.WebKit2PlugInSnapshottingEnabled" = 0;
}
''';
Future<Null> get pollDelay => Future.delayed(defaultPollDelay);
String signalArgument(String defaultSignal,
{bool force = false, bool testOnly = false}) {
if (force && testOnly) {
throw ArgumentError("[force] and [testOnly] can't both be true.");
}
if (force) return "-KILL";
if (testOnly) return "-0";
return defaultSignal;
}
Future<int> kill(List<String> pids,
{bool force = false, bool testOnly = false}) async {
var arguments = [signalArgument("-TERM", force: force, testOnly: testOnly)]
..addAll(pids);
var result = await Process.run(killLocation, arguments);
return result.exitCode;
}
Future<int> pkill(String pattern,
{bool force = false, bool testOnly = false}) async {
var arguments = [
signalArgument("-HUP", force: force, testOnly: testOnly),
pattern
];
var result = await Process.run(pkillLocation, arguments);
return result.exitCode;
}
Uri validatedBundleName(Uri bundle) {
if (bundle == null) return Uri.base.resolve(defaultSafariBundleLocation);
if (!bundle.path.endsWith("/")) {
throw ArgumentError("Bundle ('$bundle') must end with a slash ('/').");
}
return bundle;
}
Future<Null> killSafari({Uri bundle}) async {
bundle = validatedBundleName(bundle);
var safariBinary = bundle.resolve(relativeSafariLocation);
var result =
await Process.run(lsofLocation, ["-t", safariBinary.toFilePath()]);
if (result.exitCode == 0) {
var stdout = result.stdout as String;
var pids =
stdout.split("\n").where((String line) => line.isNotEmpty).toList();
var timer = Timer(const Duration(seconds: 10), () {
print("Kill -9 Safari $pids");
kill(pids, force: true);
});
var exitCode = await kill(pids);
while (exitCode == 0) {
await pollDelay;
print("Polling Safari $pids");
exitCode = await kill(pids, testOnly: true);
}
timer.cancel();
}
var timer = Timer(const Duration(seconds: 10), () {
print("Kill -9 $safari");
pkill(safari, force: true);
});
var exitCode = await pkill(safari);
while (exitCode == 0) {
await pollDelay;
print("Polling $safari");
exitCode = await pkill(safari, testOnly: true);
}
timer.cancel();
}
Future<Null> deleteIfExists(Uri uri) async {
var directory = Directory.fromUri(uri);
if (await directory.exists()) {
print("Deleting directory '$uri'.");
await directory.delete(recursive: true);
} else {
var file = File.fromUri(uri);
if (await file.exists()) {
print("Deleting file '$uri'.");
await file.delete();
} else {
print("File '$uri' not found.");
}
}
}
Future<Null> resetSafariSettings() async {
var home = Platform.environment["HOME"];
if (!home.endsWith("/")) {
home = "$home/";
}
var homeDirectory = Uri.base.resolve(home);
for (var setting in safariSettings) {
await deleteIfExists(homeDirectory.resolve(setting));
}
var result = await Process.run(
defaultsLocation, <String>["write", safari, knownSafariPreference]);
if (result.exitCode != 0) {
throw "Unable to reset Safari settings: ${result.stdout}${result.stderr}";
}
}
Future<Null> killAndResetSafari({Uri bundle}) async {
bundle = validatedBundleName(bundle);
await killSafari(bundle: bundle);
await resetSafariSettings();
}
Future<Null> main() async {
await killAndResetSafari();
}

View file

@ -19,6 +19,8 @@ dependencies:
path: ../smith
status_file:
path: ../status_file
webdriver:
path: ../../third_party/pkg/webdriver
dev_dependencies:
analyzer:
path: ../../pkg/analyzer

View file

@ -0,0 +1,52 @@
// Copyright (c) 2022, 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:io';
import 'package:expect/expect.dart';
import 'package:test_runner/src/browser_controller.dart';
void main() async {
if (Platform.environment.containsKey('CHROME_PATH')) {
print('Testing Chrome');
await testChrome();
}
if (Platform.environment.containsKey('FIREFOX_PATH')) {
print('Testing Firefox');
await testFirefox();
}
if (Platform.isMacOS) {
print('Testing Safari');
await testSafari();
}
}
Future<void> testChrome() {
return testBrowser(Chrome(Platform.environment['CHROME_PATH']));
}
Future<void> testFirefox() {
return testBrowser(Firefox(Platform.environment['FIREFOX_PATH']));
}
Future<void> testSafari() {
return testBrowser(Safari());
}
Future<void> testBrowser(Browser browser) async {
browser.debugPrint = true;
await browser.version;
await testStartStop(browser);
}
Future<void> testStartStop(Browser browser) async {
var closed = false;
try {
Expect.isTrue(await browser.start('about:blank'));
} finally {
closed = await browser.close();
}
Expect.isTrue(closed);
}

View file

@ -0,0 +1,40 @@
// Copyright (c) 2022, 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 'package:expect/expect.dart';
import 'package:test_runner/src/browser_controller.dart';
import 'package:test_runner/src/configuration.dart';
void main() async {
var configuration = TestConfiguration(
configuration: Configuration.parse(
const String.fromEnvironment("test_runner.configuration"),
{'runtime': 'vm'}),
isVerbose: false,
localIP: '127.0.0.1',
testDriverErrorPort: 0,
testServerPort: 0,
testServerCrossOriginPort: 0,
);
await configuration.startServers();
try {
var testRunner =
BrowserTestRunner(configuration, '127.0.0.1', 1, (_) => FakeBrowser());
await testRunner.start();
try {
Expect.isTrue(testRunner.testingServerStarted);
Expect.equals(1, testRunner.numBrowsers);
} finally {
await testRunner.terminate();
}
} finally {
configuration.stopServers();
}
}
class FakeBrowser extends Browser {
Future<bool> start(String url) => Future.value(true);
Future<bool> close() => Future.value(true);
Future<String> version = Future.value('fake version');
}