Version 2.18.0-70.0.dev

Merge commit '38438fb165f4b2c16118d76059c842589c6332f4' into 'dev'
diff --git a/pkg/test_runner/bin/launch_browser.dart b/pkg/test_runner/bin/launch_browser.dart
index a92be18..4b81a69 100644
--- a/pkg/test_runner/bin/launch_browser.dart
+++ b/pkg/test_runner/bin/launch_browser.dart
@@ -35,7 +35,6 @@
   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]);
 }
diff --git a/pkg/test_runner/lib/src/browser_controller.dart b/pkg/test_runner/lib/src/browser_controller.dart
index 607c5f1..9466097 100644
--- a/pkg/test_runner/lib/src/browser_controller.dart
+++ b/pkg/test_runner/lib/src/browser_controller.dart
@@ -7,10 +7,11 @@
 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 @@
   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 @@
 
   /// 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 @@
       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 @@
     _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 @@
   ///
   /// 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 @@
     // 	     <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 @@
     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 @@
 
   AndroidChrome(this._adbDevice);
 
+  @override
   Future<bool> start(String url) {
     var chromeIntent = Intent(viewAction, chromePackage, chromeActivity, url);
     var turnScreenOnIntent =
@@ -617,6 +573,7 @@
     });
   }
 
+  @override
   Future<bool> close() {
     if (_adbDevice != null) {
       return _adbDevice.forceStop(chromePackage).then((_) {
@@ -631,10 +588,18 @@
         .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 @@
     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 @@
 
   final TestConfiguration configuration;
   final BrowserTestingServer testingServer;
+  final Browser Function(TestConfiguration configuration) browserFactory;
 
   final String localIp;
   int maxNumBrowsers;
@@ -774,7 +742,7 @@
   /// 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 @@
     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 @@
       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 @@
   /// 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 @@
       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 @@
     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 @@
       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 @@
 
   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 @@
   }
 
   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 = """
diff --git a/pkg/test_runner/lib/src/configuration.dart b/pkg/test_runner/lib/src/configuration.dart
index d98b8b8..cf71c99 100644
--- a/pkg/test_runner/lib/src/configuration.dart
+++ b/pkg/test_runner/lib/src/configuration.dart
@@ -297,9 +297,6 @@
       case Runtime.firefox:
         location = firefoxPath;
         break;
-      case Runtime.safari:
-        location = safariPath;
-        break;
     }
 
     if (location != null) return location;
@@ -317,9 +314,6 @@
             '/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'
       },
diff --git a/pkg/test_runner/lib/src/options.dart b/pkg/test_runner/lib/src/options.dart
index 34a00ff..d0a67e1 100644
--- a/pkg/test_runner/lib/src/options.dart
+++ b/pkg/test_runner/lib/src/options.dart
@@ -302,8 +302,6 @@
     _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),
diff --git a/pkg/test_runner/lib/src/process_queue.dart b/pkg/test_runner/lib/src/process_queue.dart
index 2965f44..bfce029 100644
--- a/pkg/test_runner/lib/src/process_queue.dart
+++ b/pkg/test_runner/lib/src/process_queue.dart
@@ -787,7 +787,7 @@
   }
 
   Future<CommandOutput> _startBrowserControllerTest(
-      BrowserTestCommand browserCommand, int timeout) {
+      BrowserTestCommand browserCommand, int timeout) async {
     var completer = Completer<CommandOutput>();
 
     callback(BrowserTestOutput output) {
@@ -795,11 +795,16 @@
     }
 
     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 @@
         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];
   }
diff --git a/pkg/test_runner/lib/src/reset_safari.dart b/pkg/test_runner/lib/src/reset_safari.dart
deleted file mode 100644
index 212de21..0000000
--- a/pkg/test_runner/lib/src/reset_safari.dart
+++ /dev/null
@@ -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();
-}
diff --git a/pkg/test_runner/pubspec.yaml b/pkg/test_runner/pubspec.yaml
index 506b9b2..e78bc08 100644
--- a/pkg/test_runner/pubspec.yaml
+++ b/pkg/test_runner/pubspec.yaml
@@ -19,6 +19,8 @@
     path: ../smith
   status_file:
     path: ../status_file
+  webdriver:
+    path: ../../third_party/pkg/webdriver
 dev_dependencies:
   analyzer:
     path: ../../pkg/analyzer
diff --git a/pkg/test_runner/test/browser_controller_test.dart b/pkg/test_runner/test/browser_controller_test.dart
new file mode 100644
index 0000000..c801de2
--- /dev/null
+++ b/pkg/test_runner/test/browser_controller_test.dart
@@ -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);
+}
diff --git a/pkg/test_runner/test/browser_test_runner_test.dart b/pkg/test_runner/test/browser_test_runner_test.dart
new file mode 100644
index 0000000..e9bf1a6
--- /dev/null
+++ b/pkg/test_runner/test/browser_test_runner_test.dart
@@ -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');
+}
diff --git a/runtime/tests/vm/vm.status b/runtime/tests/vm/vm.status
index 7c963b6..7db7cad 100644
--- a/runtime/tests/vm/vm.status
+++ b/runtime/tests/vm/vm.status
@@ -53,11 +53,6 @@
 dart/transferable_throws_oom_test: SkipByDesign # This test tries to allocate too much memory on purpose. Still dartbug.com/37188
 dart_2/transferable_throws_oom_test: SkipByDesign # This test tries to allocate too much memory on purpose. Still dartbug.com/37188
 
-[ $builder_tag == obfuscated ]
-dart_2/causal_stacks/async_throws_stack_lazy_test: SkipByDesign # Asserts exact stacktrace output.
-dart_2/causal_stacks/async_throws_stack_no_causal_test: SkipByDesign # Asserts exact stacktrace output.
-dart_2/causal_stacks/sync_async_start_pkg_test_test: SkipByDesign # Asserts exact stacktrace output.
-
 [ $builder_tag == optimization_counter_threshold ]
 cc/*: Skip # Many tests want see unoptimized code running
 dart/appjit*: SkipByDesign # Test needs to a particular opt-counter value
@@ -427,6 +422,16 @@
 dart/run_appended_aot_snapshot_test: SkipByDesign # Tests the precompiled runtime.
 dart_2/run_appended_aot_snapshot_test: SkipByDesign # Tests the precompiled runtime.
 
+[ $builder_tag == dwarf || $builder_tag == obfuscated ]
+dart/causal_stacks/async_throws_stack_lazy_test: SkipByDesign # Asserts exact stacktrace output.
+dart/causal_stacks/async_throws_stack_no_causal_test: SkipByDesign # Asserts exact stacktrace output.
+dart/causal_stacks/flutter_regress_100441_test: SkipByDesign # Asserts exact stacktrace output.
+dart/causal_stacks/sync_async_start_pkg_test_test: SkipByDesign # Asserts exact stacktrace output.
+dart_2/causal_stacks/async_throws_stack_lazy_test: SkipByDesign # Asserts exact stacktrace output.
+dart_2/causal_stacks/async_throws_stack_no_causal_test: SkipByDesign # Asserts exact stacktrace output.
+dart_2/causal_stacks/flutter_regress_100441_test: SkipByDesign # Asserts exact stacktrace output.
+dart_2/causal_stacks/sync_async_start_pkg_test_test: SkipByDesign # Asserts exact stacktrace output.
+
 [ $compiler == dart2analyzer || $compiler == dart2js ]
 dart/data_uri*test: Skip # Data uri's not supported by dart2js or the analyzer.
 dart_2/data_uri*test: Skip # Data uri's not supported by dart2js or the analyzer.
diff --git a/tools/VERSION b/tools/VERSION
index 3e99c74..448f620 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 18
 PATCH 0
-PRERELEASE 69
+PRERELEASE 70
 PRERELEASE_PATCH 0
\ No newline at end of file
diff --git a/tools/bots/try_benchmarks.sh b/tools/bots/try_benchmarks.sh
index 9d2db12..93003aa 100755
--- a/tools/bots/try_benchmarks.sh
+++ b/tools/bots/try_benchmarks.sh
@@ -331,6 +331,7 @@
     out/ReleaseX64/dart-sdk/bin/dart compile js --sound-null-safety --out=out.js -m hello.dart
     third_party/d8/linux/x64/d8 --stack_size=1024 sdk/lib/_internal/js_runtime/lib/preambles/d8.js out.js
     out/ReleaseX64/dart-sdk/bin/dart compile js --out=out.js -m hello.dart
+    out/ReleaseX64/dart-sdk/bin/dart --print_metrics compile js --out=out.js -m hello.dart
     LD_LIBRARY_PATH=third_party/firefox_jsshell/ third_party/firefox_jsshell/js -f sdk/lib/_internal/js_runtime/lib/preambles/jsshell.js -f out.js
     out/ReleaseX64/dart-sdk/bin/dart compile js --sound-null-safety --out=out.js -m hello.dart
     LD_LIBRARY_PATH=third_party/firefox_jsshell/ third_party/firefox_jsshell/js -f sdk/lib/_internal/js_runtime/lib/preambles/jsshell.js -f out.js