Skip to content

Commit

Permalink
[web] [test] Adding firefox install functionality to the test platform (
Browse files Browse the repository at this point in the history
flutter#13272)

* Add Firefox installing functionality to test platform. For Linux only. Refactor test platform code

* remove download.dart. Not complete for now

* uncomment firefox.dart. Adding new CL parameters.

* Licence headers added.

* adding more comments to firefox_installer

* adding test for firefox download

* address pr comments. change directory for test in .cirrus.yml

* change directory for test_web_engine_firefox_script

* removing the system test.
  • Loading branch information
nturgut authored Oct 23, 2019
1 parent 23fb1eb commit 3b97d3a
Show file tree
Hide file tree
Showing 9 changed files with 481 additions and 261 deletions.
12 changes: 12 additions & 0 deletions .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 dev/firefox_installer_test.dart
- name: build_and_test_android_unopt_debug
env:
USE_ANDROID: "True"
Expand Down
144 changes: 144 additions & 0 deletions lib/web_ui/dev/browser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// 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';

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<Uri> 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<Uri> get remoteDebuggerUrl => null;

/// The underlying process.
///
/// This will fire once the process has started successfully.
Future<Process> get _process => _processCompleter.future;
final _processCompleter = Completer<Process>();

/// 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 = <StreamSubscription>[];

/// 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<Process> 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<List<int>> 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((_) {});
}
}
85 changes: 85 additions & 0 deletions lib/web_ui/dev/chrome.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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';

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<Uri> 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<Uri>.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<Process> startBrowser(), this.remoteDebuggerUrl)
: super(startBrowser);
}
62 changes: 38 additions & 24 deletions lib/web_ui/dev/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 {
Expand All @@ -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',
Expand All @@ -97,32 +98,45 @@ 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;

/// 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';
Loading

0 comments on commit 3b97d3a

Please sign in to comment.