From f374f86af1d4f8281c2d7af01b943918fe910791 Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Mon, 21 Oct 2019 15:55:44 -0700 Subject: [PATCH 1/9] Add Firefox installing functionality to test platform. For Linux only. Refactor test platform code --- lib/web_ui/dev/browser.dart | 140 +++++++++++++++ lib/web_ui/dev/chrome.dart | 82 +++++++++ lib/web_ui/dev/common.dart | 62 ++++--- lib/web_ui/dev/download.dart | 28 +++ lib/web_ui/dev/felt.dart | 4 +- lib/web_ui/dev/firefox.dart | 82 +++++++++ lib/web_ui/dev/firefox_installer.dart | 81 ++++++++- lib/web_ui/dev/test_platform.dart | 241 +------------------------- lib/web_ui/dev/test_runner.dart | 1 + 9 files changed, 459 insertions(+), 262 deletions(-) create mode 100644 lib/web_ui/dev/browser.dart create mode 100644 lib/web_ui/dev/chrome.dart create mode 100644 lib/web_ui/dev/download.dart create mode 100644 lib/web_ui/dev/firefox.dart diff --git a/lib/web_ui/dev/browser.dart b/lib/web_ui/dev/browser.dart new file mode 100644 index 0000000000000..852ec335f9134 --- /dev/null +++ b/lib/web_ui/dev/browser.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:pedantic/pedantic.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:typed_data/typed_buffers.dart'; + +import 'package:test_api/src/utils.dart'; // ignore: implementation_imports + +/// An interface for running browser instances. +/// +/// This is intentionally coarse-grained: browsers are controlled primary from +/// inside a single tab. Thus this interface only provides support for closing +/// the browser and seeing if it closes itself. +/// +/// Any errors starting or running the browser process are reported through +/// [onExit]. +abstract class Browser { + String get name; + + /// The Observatory URL for this browser. + /// + /// This will return `null` for browsers that aren't running the Dart VM, or + /// if the Observatory URL can't be found. + Future get observatoryUrl => null; + + /// The remote debugger URL for this browser. + /// + /// This will return `null` for browsers that don't support remote debugging, + /// or if the remote debugging URL can't be found. + Future get remoteDebuggerUrl => null; + + /// The underlying process. + /// + /// This will fire once the process has started successfully. + Future get _process => _processCompleter.future; + final _processCompleter = Completer(); + + /// Whether [close] has been called. + var _closed = false; + + /// A future that completes when the browser exits. + /// + /// If there's a problem starting or running the browser, this will complete + /// with an error. + Future get onExit => _onExitCompleter.future; + final _onExitCompleter = Completer(); + + /// Standard IO streams for the underlying browser process. + final _ioSubscriptions = []; + + /// Creates a new browser. + /// + /// This is intended to be called by subclasses. They pass in [startBrowser], + /// which asynchronously returns the browser process. Any errors in + /// [startBrowser] (even those raised asynchronously after it returns) are + /// piped to [onExit] and will cause the browser to be killed. + Browser(Future startBrowser()) { + // Don't return a Future here because there's no need for the caller to wait + // for the process to actually start. They should just wait for the HTTP + // request instead. + runZoned(() async { + var process = await startBrowser(); + _processCompleter.complete(process); + + var output = Uint8Buffer(); + drainOutput(Stream> stream) { + try { + _ioSubscriptions + .add(stream.listen(output.addAll, cancelOnError: true)); + } on StateError catch (_) {} + } + + // If we don't drain the stdout and stderr the process can hang. + drainOutput(process.stdout); + drainOutput(process.stderr); + + var exitCode = await process.exitCode; + + // This hack dodges an otherwise intractable race condition. When the user + // presses Control-C, the signal is sent to the browser and the test + // runner at the same time. It's possible for the browser to exit before + // the [Browser.close] is called, which would trigger the error below. + // + // A negative exit code signals that the process exited due to a signal. + // However, it's possible that this signal didn't come from the user's + // Control-C, in which case we do want to throw the error. The only way to + // resolve the ambiguity is to wait a brief amount of time and see if this + // browser is actually closed. + if (!_closed && exitCode < 0) { + await Future.delayed(Duration(milliseconds: 200)); + } + + if (!_closed && exitCode != 0) { + var outputString = utf8.decode(output); + var message = '$name failed with exit code $exitCode.'; + if (outputString.isNotEmpty) { + message += '\nStandard output:\n$outputString'; + } + + throw Exception(message); + } + + _onExitCompleter.complete(); + }, onError: (error, StackTrace stackTrace) { + // Ignore any errors after the browser has been closed. + if (_closed) return; + + // Make sure the process dies even if the error wasn't fatal. + _process.then((process) => process.kill()); + + if (stackTrace == null) stackTrace = Trace.current(); + if (_onExitCompleter.isCompleted) return; + _onExitCompleter.completeError( + Exception('Failed to run $name: ${getErrorMessage(error)}.'), + stackTrace); + }); + } + + /// Kills the browser process. + /// + /// Returns the same [Future] as [onExit], except that it won't emit + /// exceptions. + Future close() async { + _closed = true; + + // If we don't manually close the stream the test runner can hang. + // For example this happens with Chrome Headless. + // See SDK issue: https://github.com/dart-lang/sdk/issues/31264 + for (var stream in _ioSubscriptions) { + unawaited(stream.cancel()); + } + + (await _process).kill(); + + // Swallow exceptions. The user should explicitly use [onExit] for these. + return onExit.catchError((_) {}); + } +} diff --git a/lib/web_ui/dev/chrome.dart b/lib/web_ui/dev/chrome.dart new file mode 100644 index 0000000000000..6f895833ff948 --- /dev/null +++ b/lib/web_ui/dev/chrome.dart @@ -0,0 +1,82 @@ + +import 'dart:async'; +import 'dart:io'; + +import 'package:pedantic/pedantic.dart'; + +import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports + +import 'browser.dart'; +import 'chrome_installer.dart'; +import 'common.dart'; + +/// A class for running an instance of Chrome. +/// +/// Most of the communication with the browser is expected to happen via HTTP, +/// so this exposes a bare-bones API. The browser starts as soon as the class is +/// constructed, and is killed when [close] is called. +/// +/// Any errors starting or running the process are reported through [onExit]. +class Chrome extends Browser { + @override + final name = 'Chrome'; + + @override + final Future remoteDebuggerUrl; + + static String version; + + /// Starts a new instance of Chrome open to the given [url], which may be a + /// [Uri] or a [String]. + factory Chrome(Uri url, {bool debug = false}) { + assert(version != null); + var remoteDebuggerCompleter = Completer.sync(); + return Chrome._(() async { + final BrowserInstallation installation = await getOrInstallChrome( + version, + infoLog: isCirrus ? stdout : DevNull(), + ); + + // A good source of various Chrome CLI options: + // https://peter.sh/experiments/chromium-command-line-switches/ + // + // Things to try: + // --font-render-hinting + // --enable-font-antialiasing + // --gpu-rasterization-msaa-sample-count + // --disable-gpu + // --disallow-non-exact-resource-reuse + // --disable-font-subpixel-positioning + final bool isChromeNoSandbox = Platform.environment['CHROME_NO_SANDBOX'] == 'true'; + var dir = createTempDir(); + var args = [ + '--user-data-dir=$dir', + url.toString(), + if (!debug) '--headless', + if (isChromeNoSandbox) '--no-sandbox', + '--window-size=$kMaxScreenshotWidth,$kMaxScreenshotHeight', // When headless, this is the actual size of the viewport + '--disable-extensions', + '--disable-popup-blocking', + '--bwsi', + '--no-first-run', + '--no-default-browser-check', + '--disable-default-apps', + '--disable-translate', + '--remote-debugging-port=$kDevtoolsPort', + ]; + + final Process process = await Process.start(installation.executable, args); + + remoteDebuggerCompleter.complete(getRemoteDebuggerUrl( + Uri.parse('http://localhost:${kDevtoolsPort}'))); + + unawaited(process.exitCode + .then((_) => Directory(dir).deleteSync(recursive: true))); + + return process; + }, remoteDebuggerCompleter.future); + } + + Chrome._(Future startBrowser(), this.remoteDebuggerUrl) + : super(startBrowser); +} diff --git a/lib/web_ui/dev/common.dart b/lib/web_ui/dev/common.dart index 99d764b08e060..e96872da283c5 100644 --- a/lib/web_ui/dev/common.dart +++ b/lib/web_ui/dev/common.dart @@ -8,6 +8,12 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart'; +/// The port number for debugging. +const int kDevtoolsPort = 12345; +const int kMaxScreenshotWidth = 1024; +const int kMaxScreenshotHeight = 1024; +const double kMaxDiffRateFailure = 0.28 / 100; // 0.28% + class BrowserInstallerException implements Exception { BrowserInstallerException(this.message); @@ -60,20 +66,16 @@ class _LinuxBinding implements PlatformBinding { path.join(versionDir.path, 'chrome-linux', 'chrome'); @override - String getFirefoxDownloadUrl(String version) { - return 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/firefox-${version}.tar.bz2'; - } + String getFirefoxDownloadUrl(String version) => + 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/firefox-${version}.tar.bz2'; @override - String getFirefoxExecutablePath(io.Directory versionDir) { - // TODO: implement getFirefoxExecutablePath - return null; - } + String getFirefoxExecutablePath(io.Directory versionDir) => + path.join(versionDir.path, 'firefox', 'firefox'); @override - String getFirefoxLatestVersionUrl() { - return 'https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US'; - } + String getFirefoxLatestVersionUrl() => + 'https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US'; } class _MacBinding implements PlatformBinding { @@ -87,7 +89,6 @@ class _MacBinding implements PlatformBinding { String getChromeDownloadUrl(String version) => '$_kBaseDownloadUrl/Mac%2F$version%2Fchrome-mac.zip?alt=media'; - @override String getChromeExecutablePath(io.Directory versionDir) => path.join( versionDir.path, 'chrome-mac', @@ -97,28 +98,24 @@ class _MacBinding implements PlatformBinding { 'Chromium'); @override - String getFirefoxDownloadUrl(String version) { - // TODO: implement getFirefoxDownloadUrl - return null; - } + String getFirefoxDownloadUrl(String version) => + 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/mac/en-US/firefox-${version}.dmg'; @override String getFirefoxExecutablePath(io.Directory versionDir) { - // TODO: implement getFirefoxExecutablePath - return null; + throw UnimplementedError(); } @override - String getFirefoxLatestVersionUrl() { - return 'https://download.mozilla.org/?product=firefox-latest&os=osx&lang=en-US'; - } + String getFirefoxLatestVersionUrl() => + 'https://download.mozilla.org/?product=firefox-latest&os=osx&lang=en-US'; } class BrowserInstallation { - const BrowserInstallation({ - @required this.version, - @required this.executable, - }); + const BrowserInstallation( + {@required this.version, + @required this.executable, + fetchLatestChromeVersion}); /// Browser version. final String version; @@ -126,3 +123,20 @@ class BrowserInstallation { /// Path the the browser executable. final String executable; } + +/// A string sink that swallows all input. +class DevNull implements StringSink { + @override + void write(Object obj) {} + + @override + void writeAll(Iterable objects, [String separator = ""]) {} + + @override + void writeCharCode(int charCode) {} + + @override + void writeln([Object obj = ""]) {} +} + +bool get isCirrus => io.Platform.environment['CIRRUS_CI'] == 'true'; diff --git a/lib/web_ui/dev/download.dart b/lib/web_ui/dev/download.dart new file mode 100644 index 0000000000000..57f708177f896 --- /dev/null +++ b/lib/web_ui/dev/download.dart @@ -0,0 +1,28 @@ +// Copyright 2013 The Flutter Authors. 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 'package:args/command_runner.dart'; + +import 'common.dart'; +import 'firefox_installer.dart'; + +class DownloadCommand extends Command { + DownloadCommand(); + + @override + String get name => 'download'; + + @override + String get description => 'Deletes build caches and artifacts.'; + + @override + FutureOr run() async { + BrowserInstallation installation = await getOrInstallFirefox('69.0.2'); + + print('Firefox installed: ${installation.version}'); + print('Firefox installed: ${installation.executable}'); + } +} diff --git a/lib/web_ui/dev/felt.dart b/lib/web_ui/dev/felt.dart index 1830c051b9ef6..172d9444916e8 100644 --- a/lib/web_ui/dev/felt.dart +++ b/lib/web_ui/dev/felt.dart @@ -8,6 +8,7 @@ import 'package:args/command_runner.dart'; import 'build.dart'; import 'clean.dart'; +import 'download.dart'; import 'licenses.dart'; import 'test_runner.dart'; @@ -18,7 +19,8 @@ CommandRunner runner = CommandRunner( ..addCommand(CleanCommand()) ..addCommand(LicensesCommand()) ..addCommand(TestCommand()) - ..addCommand(BuildCommand()); + ..addCommand(BuildCommand()) + ..addCommand(DownloadCommand()); void main(List args) async { if (args.isEmpty) { diff --git a/lib/web_ui/dev/firefox.dart b/lib/web_ui/dev/firefox.dart new file mode 100644 index 0000000000000..d63d9a524ca81 --- /dev/null +++ b/lib/web_ui/dev/firefox.dart @@ -0,0 +1,82 @@ + +// import 'dart:async'; +// import 'dart:io'; + +// import 'package:pedantic/pedantic.dart'; + +// import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports + +// import 'browser.dart'; +// import 'firefox_installer.dart'; +// import 'common.dart'; + +// /// A class for running an instance of Firefox. +// /// +// /// Most of the communication with the browser is expected to happen via HTTP, +// /// so this exposes a bare-bones API. The browser starts as soon as the class is +// /// constructed, and is killed when [close] is called. +// /// +// /// Any errors starting or running the process are reported through [onExit]. +// class Firefox extends Browser { +// @override +// final name = 'Firefox'; + +// @override +// final Future remoteDebuggerUrl; + +// static String version; + +// /// Starts a new instance of Firefox open to the given [url], which may be a +// /// [Uri] or a [String]. +// factory Firefox(Uri url, {bool debug = false}) { +// assert(version != null); +// var remoteDebuggerCompleter = Completer.sync(); +// return Firefox._(() async { +// final BrowserInstallation installation = await getOrInstallFirefox( +// version, +// infoLog: isCirrus ? stdout : DevNull(), +// ); + +// // A good source of various Firefox CLI options: +// // https://peter.sh/experiments/chromium-command-line-switches/ +// // +// // Things to try: +// // --font-render-hinting +// // --enable-font-antialiasing +// // --gpu-rasterization-msaa-sample-count +// // --disable-gpu +// // --disallow-non-exact-resource-reuse +// // --disable-font-subpixel-positioning +// final bool isFirefoxNoSandbox = Platform.environment['CHROME_NO_SANDBOX'] == 'true'; +// var dir = createTempDir(); +// var args = [ +// '--user-data-dir=$dir', +// url.toString(), +// if (!debug) '--headless', +// if (isFirefoxNoSandbox) '--no-sandbox', +// '--window-size=$kMaxScreenshotWidth,$kMaxScreenshotHeight', // When headless, this is the actual size of the viewport +// '--disable-extensions', +// '--disable-popup-blocking', +// '--bwsi', +// '--no-first-run', +// '--no-default-browser-check', +// '--disable-default-apps', +// '--disable-translate', +// '--remote-debugging-port=$kDevtoolsPort', +// ]; + +// final Process process = await Process.start(installation.executable, args); + +// remoteDebuggerCompleter.complete(getRemoteDebuggerUrl( +// Uri.parse('http://localhost:$_kFirefoxDevtoolsPort'))); + +// unawaited(process.exitCode +// .then((_) => Directory(dir).deleteSync(recursive: true))); + +// return process; +// }, remoteDebuggerCompleter.future); +// } + +// Firefox._(Future startBrowser(), this.remoteDebuggerUrl) +// : super(startBrowser); +// } diff --git a/lib/web_ui/dev/firefox_installer.dart b/lib/web_ui/dev/firefox_installer.dart index ccf7a7e0d8eb9..14c6bcc4a145e 100644 --- a/lib/web_ui/dev/firefox_installer.dart +++ b/lib/web_ui/dev/firefox_installer.dart @@ -10,6 +10,59 @@ import 'package:path/path.dart' as path; import 'common.dart'; import 'environment.dart'; +/// Returns the installation of Firefox, installing it if necessary. +/// +/// If [requestedVersion] is null, uses the version specified on the +/// command-line. If not specified on the command-line, uses the version +/// specified in the "browser_lock.yaml" file. +/// +/// If [requestedVersion] is not null, installs that version. The value +/// may be "latest" (the latest stable Firefox version), "system" +/// (manually installed Firefox on the current operating system), or an +/// exact version number such as 69.0.3. Versions of Firefox can be found here: +/// +/// https://download-installer.cdn.mozilla.net/pub/firefox/releases/ +Future getOrInstallFirefox( + String requestedVersion, { + StringSink infoLog, +}) async { + + // Installation support is implemented only for Linux for now. + if(!io.Platform.isLinux) { + throw UnimplementedError(); + } + + infoLog ??= io.stdout; + + if (requestedVersion == 'system') { + return BrowserInstallation( + version: 'system', + executable: await _findSystemFirefoxExecutable(), + ); + } + + FirefoxInstaller installer; + try { + installer = requestedVersion == 'latest' + ? await FirefoxInstaller.latest() + : FirefoxInstaller(version: requestedVersion); + + if (installer.isInstalled) { + infoLog.writeln( + 'Installation was skipped because Firefox version ${installer.version} is already installed.'); + } else { + infoLog.writeln('Installing Firefox version: ${installer.version}'); + await installer.install(); + final BrowserInstallation installation = installer.getInstallation(); + infoLog.writeln( + 'Installations complete. To launch it run ${installation.executable}'); + } + return installer.getInstallation(); + } finally { + installer?.close(); + } +} + /// Manages the installation of a particular [version] of Firefox. class FirefoxInstaller { factory FirefoxInstaller({ @@ -96,7 +149,7 @@ class FirefoxInstaller { )); final io.File downloadedFile = - io.File(path.join(versionDir.path, 'firefox.zip')); + io.File(path.join(versionDir.path, 'firefox-${version}.tar.bz2')); await download.stream.pipe(downloadedFile.openWrite()); return downloadedFile; @@ -105,7 +158,19 @@ class FirefoxInstaller { /// Uncompress the downloaded browser files. /// See [version]. Future _uncompress(io.File downloadedFile) async { - /// TODO(nturgut): Implement Install. + final io.ProcessResult unzipResult = await io.Process.run('tar', [ + '-x', + '-f', + downloadedFile.path, + '-C', + versionDir.path, + ]); + + if (unzipResult.exitCode != 0) { + throw BrowserInstallerException( + 'Failed to unzip the downloaded Firefox archive ${downloadedFile.path}.\n' + 'The unzip process exited with code ${unzipResult.exitCode}.'); + } } void close() { @@ -113,6 +178,18 @@ class FirefoxInstaller { } } +Future _findSystemFirefoxExecutable() async { + final io.ProcessResult which = + await io.Process.run('which', ['firefox']); + + if (which.exitCode != 0) { + throw BrowserInstallerException( + 'Failed to locate system Firefox installation.'); + } + + return which.stdout; +} + /// Fetches the latest available Chrome build version. Future fetchLatestFirefoxVersion() async { final RegExp forFirefoxVersion = RegExp("firefox-[0-9.]\+[0-9]"); diff --git a/lib/web_ui/dev/test_platform.dart b/lib/web_ui/dev/test_platform.dart index 99236d759b8f5..bf1b5f95614d0 100644 --- a/lib/web_ui/dev/test_platform.dart +++ b/lib/web_ui/dev/test_platform.dart @@ -12,16 +12,13 @@ import 'package:http_multi_server/http_multi_server.dart'; import 'package:image/image.dart'; import 'package:package_resolver/package_resolver.dart'; import 'package:path/path.dart' as p; -import 'package:pedantic/pedantic.dart'; import 'package:pool/pool.dart'; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf_static/shelf_static.dart'; import 'package:shelf_web_socket/shelf_web_socket.dart'; import 'package:shelf_packages_handler/shelf_packages_handler.dart'; -import 'package:stack_trace/stack_trace.dart'; import 'package:stream_channel/stream_channel.dart'; -import 'package:typed_data/typed_buffers.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports @@ -40,17 +37,12 @@ import 'package:test_core/src/runner/load_exception.dart'; // ignore: implementa import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' as wip; -import 'chrome_installer.dart'; +import 'browser.dart'; +import 'chrome.dart'; import 'common.dart'; import 'environment.dart' as env; import 'goldens.dart'; -/// The port number Chrome exposes for debugging. -const int _kChromeDevtoolsPort = 12345; -const int _kMaxScreenshotWidth = 1024; -const int _kMaxScreenshotHeight = 1024; -const double _kMaxDiffRateFailure = 0.28/100; // 0.28% - class BrowserPlatform extends PlatformPlugin { /// Starts the server. /// @@ -149,7 +141,7 @@ class BrowserPlatform extends PlatformPlugin { final bool write = requestData['write']; final double maxDiffRate = requestData['maxdiffrate']; final Map region = requestData['region']; - final String result = await _diffScreenshot(filename, write, maxDiffRate ?? _kMaxDiffRateFailure, region); + final String result = await _diffScreenshot(filename, write, maxDiffRate ?? kMaxDiffRateFailure, region); return shelf.Response.ok(json.encode(result)); } @@ -189,7 +181,7 @@ To automatically create this file call matchGoldenFile('$filename', write: true) } final wip.ChromeConnection chromeConnection = - wip.ChromeConnection('localhost', _kChromeDevtoolsPort); + wip.ChromeConnection('localhost', kDevtoolsPort); final wip.ChromeTab chromeTab = await chromeConnection.getTab( (wip.ChromeTab chromeTab) => chromeTab.url.contains('localhost')); final wip.WipConnection wipConnection = await chromeTab.connect(); @@ -211,8 +203,8 @@ To automatically create this file call matchGoldenFile('$filename', write: true) // Setting hardware-independent screen parameters: // https://chromedevtools.github.io/devtools-protocol/tot/Emulation await wipConnection.sendCommand('Emulation.setDeviceMetricsOverride', { - 'width': _kMaxScreenshotWidth, - 'height': _kMaxScreenshotHeight, + 'width': kMaxScreenshotWidth, + 'height': kMaxScreenshotHeight, 'deviceScaleFactor': 1, 'mobile': false, }); @@ -840,225 +832,4 @@ class _BrowserEnvironment implements Environment { CancelableOperation displayPause() => _manager._displayPause(); } -/// An interface for running browser instances. -/// -/// This is intentionally coarse-grained: browsers are controlled primary from -/// inside a single tab. Thus this interface only provides support for closing -/// the browser and seeing if it closes itself. -/// -/// Any errors starting or running the browser process are reported through -/// [onExit]. -abstract class Browser { - String get name; - - /// The Observatory URL for this browser. - /// - /// This will return `null` for browsers that aren't running the Dart VM, or - /// if the Observatory URL can't be found. - Future get observatoryUrl => null; - - /// The remote debugger URL for this browser. - /// - /// This will return `null` for browsers that don't support remote debugging, - /// or if the remote debugging URL can't be found. - Future get remoteDebuggerUrl => null; - - /// The underlying process. - /// - /// This will fire once the process has started successfully. - Future get _process => _processCompleter.future; - final _processCompleter = Completer(); - - /// Whether [close] has been called. - var _closed = false; - - /// A future that completes when the browser exits. - /// - /// If there's a problem starting or running the browser, this will complete - /// with an error. - Future get onExit => _onExitCompleter.future; - final _onExitCompleter = Completer(); - - /// Standard IO streams for the underlying browser process. - final _ioSubscriptions = []; - - /// Creates a new browser. - /// - /// This is intended to be called by subclasses. They pass in [startBrowser], - /// which asynchronously returns the browser process. Any errors in - /// [startBrowser] (even those raised asynchronously after it returns) are - /// piped to [onExit] and will cause the browser to be killed. - Browser(Future startBrowser()) { - // Don't return a Future here because there's no need for the caller to wait - // for the process to actually start. They should just wait for the HTTP - // request instead. - runZoned(() async { - var process = await startBrowser(); - _processCompleter.complete(process); - - var output = Uint8Buffer(); - drainOutput(Stream> stream) { - try { - _ioSubscriptions - .add(stream.listen(output.addAll, cancelOnError: true)); - } on StateError catch (_) {} - } - - // If we don't drain the stdout and stderr the process can hang. - drainOutput(process.stdout); - drainOutput(process.stderr); - - var exitCode = await process.exitCode; - - // This hack dodges an otherwise intractable race condition. When the user - // presses Control-C, the signal is sent to the browser and the test - // runner at the same time. It's possible for the browser to exit before - // the [Browser.close] is called, which would trigger the error below. - // - // A negative exit code signals that the process exited due to a signal. - // However, it's possible that this signal didn't come from the user's - // Control-C, in which case we do want to throw the error. The only way to - // resolve the ambiguity is to wait a brief amount of time and see if this - // browser is actually closed. - if (!_closed && exitCode < 0) { - await Future.delayed(Duration(milliseconds: 200)); - } - - if (!_closed && exitCode != 0) { - var outputString = utf8.decode(output); - var message = '$name failed with exit code $exitCode.'; - if (outputString.isNotEmpty) { - message += '\nStandard output:\n$outputString'; - } - - throw Exception(message); - } - - _onExitCompleter.complete(); - }, onError: (error, StackTrace stackTrace) { - // Ignore any errors after the browser has been closed. - if (_closed) return; - - // Make sure the process dies even if the error wasn't fatal. - _process.then((process) => process.kill()); - - if (stackTrace == null) stackTrace = Trace.current(); - if (_onExitCompleter.isCompleted) return; - _onExitCompleter.completeError( - Exception('Failed to run $name: ${getErrorMessage(error)}.'), - stackTrace); - }); - } - - /// Kills the browser process. - /// - /// Returns the same [Future] as [onExit], except that it won't emit - /// exceptions. - Future close() async { - _closed = true; - - // If we don't manually close the stream the test runner can hang. - // For example this happens with Chrome Headless. - // See SDK issue: https://github.com/dart-lang/sdk/issues/31264 - for (var stream in _ioSubscriptions) { - unawaited(stream.cancel()); - } - - (await _process).kill(); - - // Swallow exceptions. The user should explicitly use [onExit] for these. - return onExit.catchError((_) {}); - } -} - -/// A class for running an instance of Chrome. -/// -/// Most of the communication with the browser is expected to happen via HTTP, -/// so this exposes a bare-bones API. The browser starts as soon as the class is -/// constructed, and is killed when [close] is called. -/// -/// Any errors starting or running the process are reported through [onExit]. -class Chrome extends Browser { - @override - final name = 'Chrome'; - - @override - final Future remoteDebuggerUrl; - - static String version; - - /// Starts a new instance of Chrome open to the given [url], which may be a - /// [Uri] or a [String]. - factory Chrome(Uri url, {bool debug = false}) { - assert(version != null); - var remoteDebuggerCompleter = Completer.sync(); - return Chrome._(() async { - final BrowserInstallation installation = await getOrInstallChrome( - version, - infoLog: isCirrus ? stdout : _DevNull(), - ); - - // A good source of various Chrome CLI options: - // https://peter.sh/experiments/chromium-command-line-switches/ - // - // Things to try: - // --font-render-hinting - // --enable-font-antialiasing - // --gpu-rasterization-msaa-sample-count - // --disable-gpu - // --disallow-non-exact-resource-reuse - // --disable-font-subpixel-positioning - final bool isChromeNoSandbox = Platform.environment['CHROME_NO_SANDBOX'] == 'true'; - var dir = createTempDir(); - var args = [ - '--user-data-dir=$dir', - url.toString(), - if (!debug) '--headless', - if (isChromeNoSandbox) '--no-sandbox', - '--window-size=$_kMaxScreenshotWidth,$_kMaxScreenshotHeight', // When headless, this is the actual size of the viewport - '--disable-extensions', - '--disable-popup-blocking', - '--bwsi', - '--no-first-run', - '--no-default-browser-check', - '--disable-default-apps', - '--disable-translate', - '--remote-debugging-port=$_kChromeDevtoolsPort', - ]; - - final Process process = await Process.start(installation.executable, args); - - remoteDebuggerCompleter.complete(getRemoteDebuggerUrl( - Uri.parse('http://localhost:$_kChromeDevtoolsPort'))); - - unawaited(process.exitCode - .then((_) => Directory(dir).deleteSync(recursive: true))); - - return process; - }, remoteDebuggerCompleter.future); - } - - Chrome._(Future startBrowser(), this.remoteDebuggerUrl) - : super(startBrowser); -} - -/// A string sink that swallows all input. -class _DevNull implements StringSink { - @override - void write(Object obj) { - } - - @override - void writeAll(Iterable objects, [String separator = ""]) { - } - - @override - void writeCharCode(int charCode) { - } - - @override - void writeln([Object obj = ""]) { - } -} - bool get isCirrus => Platform.environment['CIRRUS_CI'] == 'true'; diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart index a2a1851407b6a..842b4e4e7adfd 100644 --- a/lib/web_ui/dev/test_runner.dart +++ b/lib/web_ui/dev/test_runner.dart @@ -14,6 +14,7 @@ import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_im import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports +import 'chrome.dart'; import 'chrome_installer.dart'; import 'test_platform.dart'; import 'environment.dart'; From fb430770f39e52599a1ff82537cd13ea5e706346 Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Mon, 21 Oct 2019 15:57:34 -0700 Subject: [PATCH 2/9] remove download.dart. Not complete for now --- lib/web_ui/dev/download.dart | 28 ---------------------------- lib/web_ui/dev/felt.dart | 4 +--- 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 lib/web_ui/dev/download.dart diff --git a/lib/web_ui/dev/download.dart b/lib/web_ui/dev/download.dart deleted file mode 100644 index 57f708177f896..0000000000000 --- a/lib/web_ui/dev/download.dart +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2013 The Flutter Authors. 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 'package:args/command_runner.dart'; - -import 'common.dart'; -import 'firefox_installer.dart'; - -class DownloadCommand extends Command { - DownloadCommand(); - - @override - String get name => 'download'; - - @override - String get description => 'Deletes build caches and artifacts.'; - - @override - FutureOr run() async { - BrowserInstallation installation = await getOrInstallFirefox('69.0.2'); - - print('Firefox installed: ${installation.version}'); - print('Firefox installed: ${installation.executable}'); - } -} diff --git a/lib/web_ui/dev/felt.dart b/lib/web_ui/dev/felt.dart index 172d9444916e8..1830c051b9ef6 100644 --- a/lib/web_ui/dev/felt.dart +++ b/lib/web_ui/dev/felt.dart @@ -8,7 +8,6 @@ import 'package:args/command_runner.dart'; import 'build.dart'; import 'clean.dart'; -import 'download.dart'; import 'licenses.dart'; import 'test_runner.dart'; @@ -19,8 +18,7 @@ CommandRunner runner = CommandRunner( ..addCommand(CleanCommand()) ..addCommand(LicensesCommand()) ..addCommand(TestCommand()) - ..addCommand(BuildCommand()) - ..addCommand(DownloadCommand()); + ..addCommand(BuildCommand()); void main(List args) async { if (args.isEmpty) { From 54d3267f0f127e7faeefca9ff60c6d6afdd23014 Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Mon, 21 Oct 2019 16:23:06 -0700 Subject: [PATCH 3/9] uncomment firefox.dart. Adding new CL parameters. --- lib/web_ui/dev/firefox.dart | 123 ++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 69 deletions(-) diff --git a/lib/web_ui/dev/firefox.dart b/lib/web_ui/dev/firefox.dart index d63d9a524ca81..ed817485f09ac 100644 --- a/lib/web_ui/dev/firefox.dart +++ b/lib/web_ui/dev/firefox.dart @@ -1,82 +1,67 @@ +import 'dart:async'; +import 'dart:io'; -// import 'dart:async'; -// import 'dart:io'; +import 'package:pedantic/pedantic.dart'; -// import 'package:pedantic/pedantic.dart'; +import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports -// import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports +import 'browser.dart'; +import 'firefox_installer.dart'; +import 'common.dart'; -// import 'browser.dart'; -// import 'firefox_installer.dart'; -// import 'common.dart'; +/// A class for running an instance of Firefox. +/// +/// Most of the communication with the browser is expected to happen via HTTP, +/// so this exposes a bare-bones API. The browser starts as soon as the class is +/// constructed, and is killed when [close] is called. +/// +/// Any errors starting or running the process are reported through [onExit]. +class Firefox extends Browser { + @override + final name = 'Firefox'; -// /// A class for running an instance of Firefox. -// /// -// /// Most of the communication with the browser is expected to happen via HTTP, -// /// so this exposes a bare-bones API. The browser starts as soon as the class is -// /// constructed, and is killed when [close] is called. -// /// -// /// Any errors starting or running the process are reported through [onExit]. -// class Firefox extends Browser { -// @override -// final name = 'Firefox'; + @override + final Future remoteDebuggerUrl; -// @override -// final Future remoteDebuggerUrl; + static String version; -// static String version; + /// Starts a new instance of Firefox open to the given [url], which may be a + /// [Uri] or a [String]. + factory Firefox(Uri url, {bool debug = false}) { + assert(version != null); + var remoteDebuggerCompleter = Completer.sync(); + return Firefox._(() async { + final BrowserInstallation installation = await getOrInstallFirefox( + version, + infoLog: isCirrus ? stdout : DevNull(), + ); -// /// Starts a new instance of Firefox open to the given [url], which may be a -// /// [Uri] or a [String]. -// factory Firefox(Uri url, {bool debug = false}) { -// assert(version != null); -// var remoteDebuggerCompleter = Completer.sync(); -// return Firefox._(() async { -// final BrowserInstallation installation = await getOrInstallFirefox( -// version, -// infoLog: isCirrus ? stdout : DevNull(), -// ); + // A good source of various Firefox Command Line options: + // https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#Browser + // + var dir = createTempDir(); + var args = [ + url.toString(), + if (!debug) '--headless', + '-width $kMaxScreenshotWidth' + '-height $kMaxScreenshotHeight', + '-new-window', + '-new-instance', + '--start-debugger-server $kDevtoolsPort', + ]; -// // A good source of various Firefox CLI options: -// // https://peter.sh/experiments/chromium-command-line-switches/ -// // -// // Things to try: -// // --font-render-hinting -// // --enable-font-antialiasing -// // --gpu-rasterization-msaa-sample-count -// // --disable-gpu -// // --disallow-non-exact-resource-reuse -// // --disable-font-subpixel-positioning -// final bool isFirefoxNoSandbox = Platform.environment['CHROME_NO_SANDBOX'] == 'true'; -// var dir = createTempDir(); -// var args = [ -// '--user-data-dir=$dir', -// url.toString(), -// if (!debug) '--headless', -// if (isFirefoxNoSandbox) '--no-sandbox', -// '--window-size=$kMaxScreenshotWidth,$kMaxScreenshotHeight', // When headless, this is the actual size of the viewport -// '--disable-extensions', -// '--disable-popup-blocking', -// '--bwsi', -// '--no-first-run', -// '--no-default-browser-check', -// '--disable-default-apps', -// '--disable-translate', -// '--remote-debugging-port=$kDevtoolsPort', -// ]; + final Process process = await Process.start(installation.executable, args); -// final Process process = await Process.start(installation.executable, args); + remoteDebuggerCompleter.complete(getRemoteDebuggerUrl( + Uri.parse('http://localhost:$kDevtoolsPort'))); -// remoteDebuggerCompleter.complete(getRemoteDebuggerUrl( -// Uri.parse('http://localhost:$_kFirefoxDevtoolsPort'))); + unawaited(process.exitCode + .then((_) => Directory(dir).deleteSync(recursive: true))); -// unawaited(process.exitCode -// .then((_) => Directory(dir).deleteSync(recursive: true))); + return process; + }, remoteDebuggerCompleter.future); + } -// return process; -// }, remoteDebuggerCompleter.future); -// } - -// Firefox._(Future startBrowser(), this.remoteDebuggerUrl) -// : super(startBrowser); -// } + Firefox._(Future startBrowser(), this.remoteDebuggerUrl) + : super(startBrowser); +} From 04dd97d9b4095a089e7b62dcc53bc6df2f75f104 Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Tue, 22 Oct 2019 12:48:44 -0700 Subject: [PATCH 4/9] Licence headers added. --- lib/web_ui/dev/browser.dart | 4 ++++ lib/web_ui/dev/chrome.dart | 3 +++ lib/web_ui/dev/firefox.dart | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/lib/web_ui/dev/browser.dart b/lib/web_ui/dev/browser.dart index 852ec335f9134..8301289acf541 100644 --- a/lib/web_ui/dev/browser.dart +++ b/lib/web_ui/dev/browser.dart @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. 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:io'; diff --git a/lib/web_ui/dev/chrome.dart b/lib/web_ui/dev/chrome.dart index 6f895833ff948..f2d1e3a6e9b63 100644 --- a/lib/web_ui/dev/chrome.dart +++ b/lib/web_ui/dev/chrome.dart @@ -1,3 +1,6 @@ +// Copyright 2013 The Flutter Authors. 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:io'; diff --git a/lib/web_ui/dev/firefox.dart b/lib/web_ui/dev/firefox.dart index ed817485f09ac..e1a43301eb128 100644 --- a/lib/web_ui/dev/firefox.dart +++ b/lib/web_ui/dev/firefox.dart @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. 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:io'; From 92489dbe4df7f8e3c93851d744d4b3487a1a509f Mon Sep 17 00:00:00 2001 From: nturgut Date: Tue, 22 Oct 2019 16:32:22 -0700 Subject: [PATCH 5/9] adding more comments to firefox_installer --- lib/web_ui/dev/firefox_installer.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/dev/firefox_installer.dart b/lib/web_ui/dev/firefox_installer.dart index 14c6bcc4a145e..fda7b5b11b655 100644 --- a/lib/web_ui/dev/firefox_installer.dart +++ b/lib/web_ui/dev/firefox_installer.dart @@ -27,7 +27,8 @@ Future getOrInstallFirefox( StringSink infoLog, }) async { - // Installation support is implemented only for Linux for now. + // These tests are aimed to run only on the Linux containers in Cirrus. + // Therefore Firefox installation is implemented only for Linux now. if(!io.Platform.isLinux) { throw UnimplementedError(); } From a9f7e33d57f57ad84b8d7a829598e069d97fb7db Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Wed, 23 Oct 2019 10:29:09 -0700 Subject: [PATCH 6/9] adding test for firefox download --- .cirrus.yml | 12 +++++ lib/web_ui/dev/firefox_installer.dart | 3 +- lib/web_ui/dev/firefox_installer_test.dart | 60 ++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 lib/web_ui/dev/firefox_installer_test.dart diff --git a/.cirrus.yml b/.cirrus.yml index 9a091276ebe63..a463d9219850a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -63,6 +63,18 @@ task: test_framework_script: | cd $FRAMEWORK_PATH/flutter/packages/flutter ../../bin/flutter test --local-engine=host_debug_unopt + - name: build_and_test_web_linux_firefox + compile_host_script: | + cd $ENGINE_PATH/src + ./flutter/tools/gn --unoptimized --full-dart-sdk + ninja -C out/host_debug_unopt + test_web_engine_firefox_script: | + cd $ENGINE_PATH/src/flutter/web_sdk/web_engine_tester + $ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/pub get + cd $ENGINE_PATH/src/flutter/lib/web_ui + $ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/pub get + export DART="$ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/dart" + $DART lib/web_ui/dev/firefox_installer_test.dart - name: build_and_test_android_unopt_debug env: USE_ANDROID: "True" diff --git a/lib/web_ui/dev/firefox_installer.dart b/lib/web_ui/dev/firefox_installer.dart index fda7b5b11b655..641b9ad638591 100644 --- a/lib/web_ui/dev/firefox_installer.dart +++ b/lib/web_ui/dev/firefox_installer.dart @@ -26,10 +26,9 @@ Future getOrInstallFirefox( String requestedVersion, { StringSink infoLog, }) async { - // These tests are aimed to run only on the Linux containers in Cirrus. // Therefore Firefox installation is implemented only for Linux now. - if(!io.Platform.isLinux) { + if (!io.Platform.isLinux) { throw UnimplementedError(); } diff --git a/lib/web_ui/dev/firefox_installer_test.dart b/lib/web_ui/dev/firefox_installer_test.dart new file mode 100644 index 0000000000000..613cd51b1e7d0 --- /dev/null +++ b/lib/web_ui/dev/firefox_installer_test.dart @@ -0,0 +1,60 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('vm') + +import 'dart:io' as io; + +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import 'common.dart'; +import 'environment.dart'; +import 'firefox_installer.dart'; + +void main() async { + void deleteFirefoxInstallIfExists() { + final io.Directory firefoxInstallationDir = io.Directory( + path.join(environment.webUiDartToolDir.path, 'firefox'), + ); + + if (firefoxInstallationDir.existsSync()) { + firefoxInstallationDir.deleteSync(recursive: true); + } + } + + setUpAll(() { + deleteFirefoxInstallIfExists(); + }); + + tearDown(() { + deleteFirefoxInstallIfExists(); + }); + + test('installs a given version of Firefox', () async { + FirefoxInstaller installer = FirefoxInstaller(version: '69.0.2'); + expect(installer.isInstalled, isFalse); + + BrowserInstallation installation = await getOrInstallFirefox('69.0.2'); + + expect(installation.version, '69.0.2'); + expect(installer.isInstalled, isTrue); + }); + + test('can find the system version when a firefox is already installed', + () async { + final logSink = StringBuffer(); + await getOrInstallFirefox('69.0.2', infoLog: logSink); + expect(logSink.length, greaterThanOrEqualTo(1)); + + final logSinkForInstallation = StringBuffer(); + expect(logSinkForInstallation.isEmpty, isTrue); + await getOrInstallFirefox('system', infoLog: logSinkForInstallation); + + print(logSinkForInstallation.toString()); + + // There are no logs on installation since the system version is used. + expect(logSinkForInstallation.isEmpty, isTrue); + }); +} From d4b31a1048daaf96204fce7a44399e9e5d18e23b Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Wed, 23 Oct 2019 10:54:37 -0700 Subject: [PATCH 7/9] address pr comments. change directory for test in .cirrus.yml --- lib/web_ui/dev/firefox_installer_test.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/dev/firefox_installer_test.dart b/lib/web_ui/dev/firefox_installer_test.dart index 613cd51b1e7d0..0e265c369a787 100644 --- a/lib/web_ui/dev/firefox_installer_test.dart +++ b/lib/web_ui/dev/firefox_installer_test.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@TestOn('vm') +@TestOn('vm && linux') import 'dart:io' as io; @@ -40,12 +40,15 @@ void main() async { expect(installation.version, '69.0.2'); expect(installer.isInstalled, isTrue); + expect(io.File(installation.executable).existsSync(), isTrue); }); test('can find the system version when a firefox is already installed', () async { final logSink = StringBuffer(); - await getOrInstallFirefox('69.0.2', infoLog: logSink); + BrowserInstallation installation = + await getOrInstallFirefox('69.0.2', infoLog: logSink); + expect(io.File(installation.executable).existsSync(), isTrue); expect(logSink.length, greaterThanOrEqualTo(1)); final logSinkForInstallation = StringBuffer(); From 9b4222b36998c7c431713dcbd23b650fae77da8f Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Wed, 23 Oct 2019 10:57:23 -0700 Subject: [PATCH 8/9] change directory for test_web_engine_firefox_script --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index a463d9219850a..1f9e1e6b505ee 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -74,7 +74,7 @@ task: cd $ENGINE_PATH/src/flutter/lib/web_ui $ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/pub get export DART="$ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/dart" - $DART lib/web_ui/dev/firefox_installer_test.dart + $DART dev/firefox_installer_test.dart - name: build_and_test_android_unopt_debug env: USE_ANDROID: "True" From 13deaf4e24c5c32e76cebf12181b9c5bb2f09f9b Mon Sep 17 00:00:00 2001 From: Nurhan Turgut Date: Wed, 23 Oct 2019 12:43:09 -0700 Subject: [PATCH 9/9] removing the system test. --- lib/web_ui/dev/firefox_installer_test.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/web_ui/dev/firefox_installer_test.dart b/lib/web_ui/dev/firefox_installer_test.dart index 0e265c369a787..0d6c73e6b129a 100644 --- a/lib/web_ui/dev/firefox_installer_test.dart +++ b/lib/web_ui/dev/firefox_installer_test.dart @@ -42,22 +42,4 @@ void main() async { expect(installer.isInstalled, isTrue); expect(io.File(installation.executable).existsSync(), isTrue); }); - - test('can find the system version when a firefox is already installed', - () async { - final logSink = StringBuffer(); - BrowserInstallation installation = - await getOrInstallFirefox('69.0.2', infoLog: logSink); - expect(io.File(installation.executable).existsSync(), isTrue); - expect(logSink.length, greaterThanOrEqualTo(1)); - - final logSinkForInstallation = StringBuffer(); - expect(logSinkForInstallation.isEmpty, isTrue); - await getOrInstallFirefox('system', infoLog: logSinkForInstallation); - - print(logSinkForInstallation.toString()); - - // There are no logs on installation since the system version is used. - expect(logSinkForInstallation.isEmpty, isTrue); - }); }