Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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