Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 3b97d3a

Browse files
authored
[web] [test] Adding firefox install functionality to the test platform (#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.
1 parent 23fb1eb commit 3b97d3a

File tree

9 files changed

+481
-261
lines changed

9 files changed

+481
-261
lines changed

.cirrus.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ task:
6363
test_framework_script: |
6464
cd $FRAMEWORK_PATH/flutter/packages/flutter
6565
../../bin/flutter test --local-engine=host_debug_unopt
66+
- name: build_and_test_web_linux_firefox
67+
compile_host_script: |
68+
cd $ENGINE_PATH/src
69+
./flutter/tools/gn --unoptimized --full-dart-sdk
70+
ninja -C out/host_debug_unopt
71+
test_web_engine_firefox_script: |
72+
cd $ENGINE_PATH/src/flutter/web_sdk/web_engine_tester
73+
$ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/pub get
74+
cd $ENGINE_PATH/src/flutter/lib/web_ui
75+
$ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/pub get
76+
export DART="$ENGINE_PATH/src/out/host_debug_unopt/dart-sdk/bin/dart"
77+
$DART dev/firefox_installer_test.dart
6678
- name: build_and_test_android_unopt_debug
6779
env:
6880
USE_ANDROID: "True"

lib/web_ui/dev/browser.dart

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:convert';
7+
import 'dart:io';
8+
9+
import 'package:pedantic/pedantic.dart';
10+
import 'package:stack_trace/stack_trace.dart';
11+
import 'package:typed_data/typed_buffers.dart';
12+
13+
import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
14+
15+
/// An interface for running browser instances.
16+
///
17+
/// This is intentionally coarse-grained: browsers are controlled primary from
18+
/// inside a single tab. Thus this interface only provides support for closing
19+
/// the browser and seeing if it closes itself.
20+
///
21+
/// Any errors starting or running the browser process are reported through
22+
/// [onExit].
23+
abstract class Browser {
24+
String get name;
25+
26+
/// The Observatory URL for this browser.
27+
///
28+
/// This will return `null` for browsers that aren't running the Dart VM, or
29+
/// if the Observatory URL can't be found.
30+
Future<Uri> get observatoryUrl => null;
31+
32+
/// The remote debugger URL for this browser.
33+
///
34+
/// This will return `null` for browsers that don't support remote debugging,
35+
/// or if the remote debugging URL can't be found.
36+
Future<Uri> get remoteDebuggerUrl => null;
37+
38+
/// The underlying process.
39+
///
40+
/// This will fire once the process has started successfully.
41+
Future<Process> get _process => _processCompleter.future;
42+
final _processCompleter = Completer<Process>();
43+
44+
/// Whether [close] has been called.
45+
var _closed = false;
46+
47+
/// A future that completes when the browser exits.
48+
///
49+
/// If there's a problem starting or running the browser, this will complete
50+
/// with an error.
51+
Future get onExit => _onExitCompleter.future;
52+
final _onExitCompleter = Completer();
53+
54+
/// Standard IO streams for the underlying browser process.
55+
final _ioSubscriptions = <StreamSubscription>[];
56+
57+
/// Creates a new browser.
58+
///
59+
/// This is intended to be called by subclasses. They pass in [startBrowser],
60+
/// which asynchronously returns the browser process. Any errors in
61+
/// [startBrowser] (even those raised asynchronously after it returns) are
62+
/// piped to [onExit] and will cause the browser to be killed.
63+
Browser(Future<Process> startBrowser()) {
64+
// Don't return a Future here because there's no need for the caller to wait
65+
// for the process to actually start. They should just wait for the HTTP
66+
// request instead.
67+
runZoned(() async {
68+
var process = await startBrowser();
69+
_processCompleter.complete(process);
70+
71+
var output = Uint8Buffer();
72+
drainOutput(Stream<List<int>> stream) {
73+
try {
74+
_ioSubscriptions
75+
.add(stream.listen(output.addAll, cancelOnError: true));
76+
} on StateError catch (_) {}
77+
}
78+
79+
// If we don't drain the stdout and stderr the process can hang.
80+
drainOutput(process.stdout);
81+
drainOutput(process.stderr);
82+
83+
var exitCode = await process.exitCode;
84+
85+
// This hack dodges an otherwise intractable race condition. When the user
86+
// presses Control-C, the signal is sent to the browser and the test
87+
// runner at the same time. It's possible for the browser to exit before
88+
// the [Browser.close] is called, which would trigger the error below.
89+
//
90+
// A negative exit code signals that the process exited due to a signal.
91+
// However, it's possible that this signal didn't come from the user's
92+
// Control-C, in which case we do want to throw the error. The only way to
93+
// resolve the ambiguity is to wait a brief amount of time and see if this
94+
// browser is actually closed.
95+
if (!_closed && exitCode < 0) {
96+
await Future.delayed(Duration(milliseconds: 200));
97+
}
98+
99+
if (!_closed && exitCode != 0) {
100+
var outputString = utf8.decode(output);
101+
var message = '$name failed with exit code $exitCode.';
102+
if (outputString.isNotEmpty) {
103+
message += '\nStandard output:\n$outputString';
104+
}
105+
106+
throw Exception(message);
107+
}
108+
109+
_onExitCompleter.complete();
110+
}, onError: (error, StackTrace stackTrace) {
111+
// Ignore any errors after the browser has been closed.
112+
if (_closed) return;
113+
114+
// Make sure the process dies even if the error wasn't fatal.
115+
_process.then((process) => process.kill());
116+
117+
if (stackTrace == null) stackTrace = Trace.current();
118+
if (_onExitCompleter.isCompleted) return;
119+
_onExitCompleter.completeError(
120+
Exception('Failed to run $name: ${getErrorMessage(error)}.'),
121+
stackTrace);
122+
});
123+
}
124+
125+
/// Kills the browser process.
126+
///
127+
/// Returns the same [Future] as [onExit], except that it won't emit
128+
/// exceptions.
129+
Future close() async {
130+
_closed = true;
131+
132+
// If we don't manually close the stream the test runner can hang.
133+
// For example this happens with Chrome Headless.
134+
// See SDK issue: https://github.com/dart-lang/sdk/issues/31264
135+
for (var stream in _ioSubscriptions) {
136+
unawaited(stream.cancel());
137+
}
138+
139+
(await _process).kill();
140+
141+
// Swallow exceptions. The user should explicitly use [onExit] for these.
142+
return onExit.catchError((_) {});
143+
}
144+
}

lib/web_ui/dev/chrome.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:io';
7+
8+
import 'package:pedantic/pedantic.dart';
9+
10+
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
11+
12+
import 'browser.dart';
13+
import 'chrome_installer.dart';
14+
import 'common.dart';
15+
16+
/// A class for running an instance of Chrome.
17+
///
18+
/// Most of the communication with the browser is expected to happen via HTTP,
19+
/// so this exposes a bare-bones API. The browser starts as soon as the class is
20+
/// constructed, and is killed when [close] is called.
21+
///
22+
/// Any errors starting or running the process are reported through [onExit].
23+
class Chrome extends Browser {
24+
@override
25+
final name = 'Chrome';
26+
27+
@override
28+
final Future<Uri> remoteDebuggerUrl;
29+
30+
static String version;
31+
32+
/// Starts a new instance of Chrome open to the given [url], which may be a
33+
/// [Uri] or a [String].
34+
factory Chrome(Uri url, {bool debug = false}) {
35+
assert(version != null);
36+
var remoteDebuggerCompleter = Completer<Uri>.sync();
37+
return Chrome._(() async {
38+
final BrowserInstallation installation = await getOrInstallChrome(
39+
version,
40+
infoLog: isCirrus ? stdout : DevNull(),
41+
);
42+
43+
// A good source of various Chrome CLI options:
44+
// https://peter.sh/experiments/chromium-command-line-switches/
45+
//
46+
// Things to try:
47+
// --font-render-hinting
48+
// --enable-font-antialiasing
49+
// --gpu-rasterization-msaa-sample-count
50+
// --disable-gpu
51+
// --disallow-non-exact-resource-reuse
52+
// --disable-font-subpixel-positioning
53+
final bool isChromeNoSandbox = Platform.environment['CHROME_NO_SANDBOX'] == 'true';
54+
var dir = createTempDir();
55+
var args = [
56+
'--user-data-dir=$dir',
57+
url.toString(),
58+
if (!debug) '--headless',
59+
if (isChromeNoSandbox) '--no-sandbox',
60+
'--window-size=$kMaxScreenshotWidth,$kMaxScreenshotHeight', // When headless, this is the actual size of the viewport
61+
'--disable-extensions',
62+
'--disable-popup-blocking',
63+
'--bwsi',
64+
'--no-first-run',
65+
'--no-default-browser-check',
66+
'--disable-default-apps',
67+
'--disable-translate',
68+
'--remote-debugging-port=$kDevtoolsPort',
69+
];
70+
71+
final Process process = await Process.start(installation.executable, args);
72+
73+
remoteDebuggerCompleter.complete(getRemoteDebuggerUrl(
74+
Uri.parse('http://localhost:${kDevtoolsPort}')));
75+
76+
unawaited(process.exitCode
77+
.then((_) => Directory(dir).deleteSync(recursive: true)));
78+
79+
return process;
80+
}, remoteDebuggerCompleter.future);
81+
}
82+
83+
Chrome._(Future<Process> startBrowser(), this.remoteDebuggerUrl)
84+
: super(startBrowser);
85+
}

lib/web_ui/dev/common.dart

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import 'package:meta/meta.dart';
88
import 'package:path/path.dart' as path;
99
import 'package:yaml/yaml.dart';
1010

11+
/// The port number for debugging.
12+
const int kDevtoolsPort = 12345;
13+
const int kMaxScreenshotWidth = 1024;
14+
const int kMaxScreenshotHeight = 1024;
15+
const double kMaxDiffRateFailure = 0.28 / 100; // 0.28%
16+
1117
class BrowserInstallerException implements Exception {
1218
BrowserInstallerException(this.message);
1319

@@ -60,20 +66,16 @@ class _LinuxBinding implements PlatformBinding {
6066
path.join(versionDir.path, 'chrome-linux', 'chrome');
6167

6268
@override
63-
String getFirefoxDownloadUrl(String version) {
64-
return 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/firefox-${version}.tar.bz2';
65-
}
69+
String getFirefoxDownloadUrl(String version) =>
70+
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/firefox-${version}.tar.bz2';
6671

6772
@override
68-
String getFirefoxExecutablePath(io.Directory versionDir) {
69-
// TODO: implement getFirefoxExecutablePath
70-
return null;
71-
}
73+
String getFirefoxExecutablePath(io.Directory versionDir) =>
74+
path.join(versionDir.path, 'firefox', 'firefox');
7275

7376
@override
74-
String getFirefoxLatestVersionUrl() {
75-
return 'https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US';
76-
}
77+
String getFirefoxLatestVersionUrl() =>
78+
'https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US';
7779
}
7880

7981
class _MacBinding implements PlatformBinding {
@@ -87,7 +89,6 @@ class _MacBinding implements PlatformBinding {
8789
String getChromeDownloadUrl(String version) =>
8890
'$_kBaseDownloadUrl/Mac%2F$version%2Fchrome-mac.zip?alt=media';
8991

90-
@override
9192
String getChromeExecutablePath(io.Directory versionDir) => path.join(
9293
versionDir.path,
9394
'chrome-mac',
@@ -97,32 +98,45 @@ class _MacBinding implements PlatformBinding {
9798
'Chromium');
9899

99100
@override
100-
String getFirefoxDownloadUrl(String version) {
101-
// TODO: implement getFirefoxDownloadUrl
102-
return null;
103-
}
101+
String getFirefoxDownloadUrl(String version) =>
102+
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/mac/en-US/firefox-${version}.dmg';
104103

105104
@override
106105
String getFirefoxExecutablePath(io.Directory versionDir) {
107-
// TODO: implement getFirefoxExecutablePath
108-
return null;
106+
throw UnimplementedError();
109107
}
110108

111109
@override
112-
String getFirefoxLatestVersionUrl() {
113-
return 'https://download.mozilla.org/?product=firefox-latest&os=osx&lang=en-US';
114-
}
110+
String getFirefoxLatestVersionUrl() =>
111+
'https://download.mozilla.org/?product=firefox-latest&os=osx&lang=en-US';
115112
}
116113

117114
class BrowserInstallation {
118-
const BrowserInstallation({
119-
@required this.version,
120-
@required this.executable,
121-
});
115+
const BrowserInstallation(
116+
{@required this.version,
117+
@required this.executable,
118+
fetchLatestChromeVersion});
122119

123120
/// Browser version.
124121
final String version;
125122

126123
/// Path the the browser executable.
127124
final String executable;
128125
}
126+
127+
/// A string sink that swallows all input.
128+
class DevNull implements StringSink {
129+
@override
130+
void write(Object obj) {}
131+
132+
@override
133+
void writeAll(Iterable objects, [String separator = ""]) {}
134+
135+
@override
136+
void writeCharCode(int charCode) {}
137+
138+
@override
139+
void writeln([Object obj = ""]) {}
140+
}
141+
142+
bool get isCirrus => io.Platform.environment['CIRRUS_CI'] == 'true';

0 commit comments

Comments
 (0)