Unverified Commit b3404690 authored by Yegor's avatar Yegor Committed by GitHub

Fix stack trace parsing on non-debug builds; add e2e tests (#50652)

* Fix stack trace parsing on non-debug builds; add e2e tests
parent c725f107
......@@ -7,6 +7,7 @@ web_shard_template: &WEB_SHARD_TEMPLATE
# As of October 2019, the Web shards needed more than 6G of RAM.
CPU: 2
MEMORY: 8G
CHROME_NO_SANDBOX: true
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
script:
- dart --enable-asserts ./dev/bots/test.dart
......@@ -173,6 +174,9 @@ task:
- dart --enable-asserts ./dev/bots/test.dart
- bash <(curl -s https://codecov.io/bash) -c -f packages/flutter_tools/coverage/lcov.info -F flutter_tool
- name: web_integration_tests
<< : *WEB_SHARD_TEMPLATE
- name: web_tests-0-linux
<< : *WEB_SHARD_TEMPLATE
......
// Copyright 2014 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' as io;
import 'package:meta/meta.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_static/shelf_static.dart';
import 'package:flutter_devicelab/framework/browser.dart';
/// Runs Chrome, opens the given `appUrl`, and returns the result reported by the
/// app.
///
/// The app is served from the `appDirectory`. Typically, the app is built
/// using `flutter build web` and served from `build/web`.
///
/// The launched app is expected to report the result by sending an HTTP POST
/// request to "/test-result" containing result data as plain text body of the
/// request. This function has no opinion about what that string contains.
Future<String> evalTestAppInChrome({
@required String appUrl,
@required String appDirectory,
int serverPort = 8080,
int browserDebugPort = 8081,
}) async {
io.HttpServer server;
Chrome chrome;
try {
final Completer<String> resultCompleter = Completer<String>();
server = await io.HttpServer.bind('localhost', serverPort);
final Cascade cascade = Cascade()
.add((Request request) async {
if (request.requestedUri.path.endsWith('/test-result')) {
resultCompleter.complete(await request.readAsString());
return Response.ok('Test results received');
}
return Response.notFound('');
})
.add(createStaticHandler(appDirectory));
shelf_io.serveRequests(server, cascade.handler);
final io.Directory userDataDirectory = io.Directory.systemTemp.createTempSync('chrome_user_data_');
chrome = await Chrome.launch(ChromeOptions(
headless: true,
debugPort: browserDebugPort,
url: appUrl,
userDataDirectory: userDataDirectory.path,
windowHeight: 500,
windowWidth: 500,
), onError: resultCompleter.completeError);
return await resultCompleter.future;
} finally {
chrome?.stop();
await server?.close();
}
}
......@@ -8,6 +8,8 @@ environment:
dependencies:
args: 1.5.2
crypto: 2.1.3
flutter_devicelab:
path: ../devicelab
googleapis: 0.54.0
googleapis_auth: 0.2.11+1
http: 0.12.0+4
......
......@@ -53,6 +53,15 @@ Stream<String> runAndGetStdout(String executable, List<String> arguments, {
print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
}
/// Runs the `executable` and waits until the process exits.
///
/// If the process exits with a non-zero exit code, exits this process with
/// exit code 1, unless `expectNonZeroExit` is set to true.
///
/// `outputListener` is called for every line of standard output from the
/// process, and is given the [Process] object. This can be used to interrupt
/// an indefinitely running process, for example, by waiting until the process
/// emits certain output.
Future<void> runCommand(String executable, List<String> arguments, {
String workingDirectory,
Map<String, String> environment,
......@@ -63,6 +72,7 @@ Future<void> runCommand(String executable, List<String> arguments, {
CapturedOutput output,
bool skip = false,
bool Function(String) removeLine,
void Function(String, Process) outputListener,
}) async {
assert(
(outputMode == OutputMode.capture) == (output != null),
......@@ -88,7 +98,13 @@ Future<void> runCommand(String executable, List<String> arguments, {
.transform<String>(const Utf8Decoder())
.transform(const LineSplitter())
.where((String line) => removeLine == null || !removeLine(line))
.map((String line) => '$line\n')
.map((String line) {
final String formattedLine = '$line\n';
if (outputListener != null) {
outputListener(formattedLine, process);
}
return formattedLine;
})
.transform(const Utf8Encoder());
switch (outputMode) {
case OutputMode.print:
......
......@@ -11,6 +11,7 @@ import 'package:googleapis_auth/auth_io.dart' as auth;
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'browser.dart';
import 'flutter_compact_formatter.dart';
import 'run_command.dart';
import 'utils.dart';
......@@ -116,7 +117,8 @@ Future<void> main(List<String> args) async {
'hostonly_devicelab_tests': _runHostOnlyDeviceLabTests,
'tool_coverage': _runToolCoverage,
'tool_tests': _runToolTests,
'web_tests': _runWebTests,
'web_tests': _runWebUnitTests,
'web_integration_tests': _runWebIntegrationTests,
});
} on ExitException catch (error) {
error.apply();
......@@ -522,7 +524,7 @@ Future<void> _runFrameworkCoverage() async {
}
}
Future<void> _runWebTests() async {
Future<void> _runWebUnitTests() async {
final Map<String, ShardRunner> subshards = <String, ShardRunner>{};
final Directory flutterPackageDirectory = Directory(path.join(flutterRoot, 'packages', 'flutter'));
......@@ -585,6 +587,94 @@ Future<void> _runWebTests() async {
await selectSubshard(subshards);
}
Future<void> _runWebIntegrationTests() async {
await _runWebStackTraceTest('profile');
await _runWebStackTraceTest('release');
await _runWebDebugStackTraceTest();
}
Future<void> _runWebStackTraceTest(String buildMode) async {
final String testAppDirectory = path.join(flutterRoot, 'dev', 'integration_tests', 'web');
final String appBuildDirectory = path.join('$testAppDirectory', 'build', 'web');
// Build the app.
await runCommand(
flutter,
<String>[ 'clean' ],
workingDirectory: testAppDirectory,
);
await runCommand(
flutter,
<String>[
'build',
'web',
'--$buildMode',
'-t',
'lib/stack_trace.dart',
],
workingDirectory: testAppDirectory,
environment: <String, String>{
'FLUTTER_WEB': 'true',
},
);
// Run the app.
final String result = await evalTestAppInChrome(
appUrl: 'http://localhost:8080/index.html',
appDirectory: appBuildDirectory,
);
if (result.contains('--- TEST SUCCEEDED ---')) {
print('${green}Web stack trace integration test passed.$reset');
} else {
print(result);
print('${red}Web stack trace integration test failed.$reset');
exit(1);
}
}
/// Debug mode is special because `flutter build web` doesn't build in debug mode.
///
/// Instead, we use `flutter run --debug` and sniff out the standard output.
Future<void> _runWebDebugStackTraceTest() async {
final String testAppDirectory = path.join(flutterRoot, 'dev', 'integration_tests', 'web');
final CapturedOutput output = CapturedOutput();
bool success = false;
await runCommand(
flutter,
<String>[
'run',
'--debug',
'-d',
'chrome',
'--web-run-headless',
'lib/stack_trace.dart',
],
output: output,
outputMode: OutputMode.capture,
outputListener: (String line, Process process) {
if (line.contains('--- TEST SUCCEEDED ---')) {
success = true;
}
if (success || line.contains('--- TEST FAILED ---')) {
process.stdin.add('q'.codeUnits);
}
},
workingDirectory: testAppDirectory,
environment: <String, String>{
'FLUTTER_WEB': 'true',
},
);
if (success) {
print('${green}Web stack trace integration test passed.$reset');
} else {
print(output.stdout);
print('${red}Web stack trace integration test failed.$reset');
exit(1);
}
}
Future<void> _runFlutterWebTest(String workingDirectory, List<String> tests) async {
final List<String> batch = <String>[];
for (int i = 0; i < tests.length; i += 1) {
......
// Copyright 2014 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:io' as io;
import 'package:meta/meta.dart';
import 'utils.dart' show forwardStandardStreams;
/// Options passed to Chrome when launching it.
class ChromeOptions {
ChromeOptions({
this.userDataDirectory,
this.url,
this.windowWidth = 1024,
this.windowHeight = 1024,
this.headless,
this.debugPort,
});
/// If not null passed as `--user-data-dir`.
final String userDataDirectory;
/// If not null launches a Chrome tab at this URL.
final String url;
/// The width of the Chrome window.
///
/// This is important for screenshots and benchmarks.
final int windowWidth;
/// The height of the Chrome window.
///
/// This is important for screenshots and benchmarks.
final int windowHeight;
/// Launches code in "headless" mode, which allows running Chrome in
/// environments without a display, such as LUCI and Cirrus.
final bool headless;
/// The port Chrome will use for its debugging protocol.
///
/// If null, Chrome is launched without debugging. When running in headless
/// mode without a debug port, Chrome quits immediately. For most tests it is
/// typical to set [headless] to true and set a non-null debug port.
final int debugPort;
}
/// A function called when the Chrome process encounters an error.
typedef ChromeErrorCallback = void Function(String);
/// Manages a single Chrome process.
class Chrome {
Chrome._(this._chromeProcess, this._onError) {
// If the Chrome process quits before it was asked to quit, notify the
// error listener.
_chromeProcess.exitCode.then((int exitCode) {
if (!_isStopped) {
_onError('Chrome process exited prematurely with exit code $exitCode');
}
});
}
/// Launches Chrome with the give [options].
///
/// The [onError] callback is called with an error message when the Chrome
/// process encounters an error. In particular, [onError] is called when the
/// Chrome process exits prematurely, i.e. before [stop] is called.
static Future<Chrome> launch(ChromeOptions options, { String workingDirectory, @required ChromeErrorCallback onError }) async {
final io.ProcessResult versionResult = io.Process.runSync(_findSystemChromeExecutable(), const <String>['--version']);
print('Launching ${versionResult.stdout}');
final List<String> args = <String>[
if (options.userDataDirectory != null)
'--user-data-dir=${options.userDataDirectory}',
if (options.url != null)
options.url,
if (io.Platform.environment['CHROME_NO_SANDBOX'] == 'true')
'--no-sandbox',
if (options.headless)
'--headless',
if (options.debugPort != null)
'--remote-debugging-port=${options.debugPort}',
'--window-size=${options.windowWidth},${options.windowHeight}',
'--disable-extensions',
'--disable-popup-blocking',
// Indicates that the browser is in "browse without sign-in" (Guest session) mode.
'--bwsi',
'--no-first-run',
'--no-default-browser-check',
'--disable-default-apps',
'--disable-translate',
];
final io.Process chromeProcess = await io.Process.start(
_findSystemChromeExecutable(),
args,
workingDirectory: workingDirectory,
);
forwardStandardStreams(chromeProcess);
return Chrome._(chromeProcess, onError);
}
final io.Process _chromeProcess;
final ChromeErrorCallback _onError;
bool _isStopped = false;
/// Stops the Chrome process.
void stop() {
_isStopped = true;
_chromeProcess.kill();
}
}
String _findSystemChromeExecutable() {
// On some environments, such as the Dart HHH tester, Chrome resides in a
// non-standard location and is provided via the following environment
// variable.
final String envExecutable = io.Platform.environment['CHROME_EXECUTABLE'];
if (envExecutable != null) {
return envExecutable;
}
if (io.Platform.isLinux) {
final io.ProcessResult which =
io.Process.runSync('which', <String>['google-chrome']);
if (which.exitCode != 0) {
throw Exception('Failed to locate system Chrome installation.');
}
return (which.stdout as String).trim();
} else if (io.Platform.isMacOS) {
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
} else {
throw Exception('Web benchmarks cannot run on ${io.Platform.operatingSystem} yet.');
}
}
......@@ -12,6 +12,7 @@ import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_static/shelf_static.dart';
import 'package:flutter_devicelab/framework/browser.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/utils.dart';
......@@ -74,30 +75,12 @@ Future<TaskResult> runWebBenchmark({ @required bool useCanvasKit }) async {
));
server = await io.HttpServer.bind('localhost', benchmarkServerPort);
io.Process chromeProcess;
Chrome chrome;
try {
shelf_io.serveRequests(server, cascade.handler);
final bool isChromeNoSandbox =
io.Platform.environment['CHROME_NO_SANDBOX'] == 'true';
final String dartToolDirectory = path.join('$macrobenchmarksDirectory/.dart_tool');
final String userDataDir = io.Directory(dartToolDirectory).createTempSync('chrome_user_data_').path;
final List<String> args = <String>[
'--user-data-dir=$userDataDir',
'http://localhost:$benchmarkServerPort/index.html',
if (isChromeNoSandbox)
'--no-sandbox',
'--window-size=1024,1024',
'--disable-extensions',
'--disable-popup-blocking',
// Indicates that the browser is in "browse without sign-in" (Guest session) mode.
'--bwsi',
'--no-first-run',
'--no-default-browser-check',
'--disable-default-apps',
'--disable-translate',
];
// TODO(yjbanov): temporarily disables headful Chrome until we get
// devicelab hardware that is able to run it. Our current
......@@ -106,38 +89,33 @@ Future<TaskResult> runWebBenchmark({ @required bool useCanvasKit }) async {
final bool isUncalibratedSmokeTest = io.Platform.environment['CALIBRATED'] != 'true';
// final bool isUncalibratedSmokeTest =
// io.Platform.environment['UNCALIBRATED_SMOKE_TEST'] == 'true';
if (isUncalibratedSmokeTest) {
print('Running in headless mode because running on uncalibrated hardware.');
args.add('--headless');
final ChromeOptions options = ChromeOptions(
url: 'http://localhost:$benchmarkServerPort/index.html',
userDataDirectory: userDataDir,
windowHeight: 1024,
windowWidth: 1024,
headless: isUncalibratedSmokeTest,
// When running in headless mode Chrome exits immediately unless
// a debug port is specified.
args.add('--remote-debugging-port=${benchmarkServerPort + 1}');
}
debugPort: isUncalibratedSmokeTest ? benchmarkServerPort + 1 : null,
);
chromeProcess = await startProcess(
_findSystemChromeExecutable(),
args,
print('Launching Chrome.');
chrome = await Chrome.launch(
options,
onError: (String error) {
profileData.completeError(Exception(error));
},
workingDirectory: cwd,
);
bool receivedProfileData = false;
chromeProcess.exitCode.then((int exitCode) {
if (!receivedProfileData) {
profileData.completeError(Exception(
'Chrome process existed prematurely with exit code $exitCode',
));
}
});
forwardStandardStreams(chromeProcess);
print('Waiting for the benchmark to report benchmark profile.');
final String backend = useCanvasKit ? 'canvaskit' : 'html';
final Map<String, dynamic> taskResult = <String, dynamic>{};
final List<String> benchmarkScoreKeys = <String>[];
final List<Map<String, dynamic>> profiles = await profileData.future;
print('Received profile data');
receivedProfileData = true;
for (final Map<String, dynamic> profile in profiles) {
final String benchmarkName = profile['name'] as String;
final String benchmarkScoreKey = '$benchmarkName.$backend.averageDrawFrameDuration';
......@@ -148,32 +126,7 @@ Future<TaskResult> runWebBenchmark({ @required bool useCanvasKit }) async {
return TaskResult.success(taskResult, benchmarkScoreKeys: benchmarkScoreKeys);
} finally {
server.close();
chromeProcess?.kill();
chrome.stop();
}
});
}
String _findSystemChromeExecutable() {
// On some environments, such as the Dart HHH tester, Chrome resides in a
// non-standard location and is provided via the following environment
// variable.
final String envExecutable = io.Platform.environment['CHROME_EXECUTABLE'];
if (envExecutable != null) {
return envExecutable;
}
if (io.Platform.isLinux) {
final io.ProcessResult which =
io.Process.runSync('which', <String>['google-chrome']);
if (which.exitCode != 0) {
throw Exception('Failed to locate system Chrome installation.');
}
return (which.stdout as String).trim();
} else if (io.Platform.isMacOS) {
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
} else {
throw Exception('Web benchmarks cannot run on ${io.Platform.operatingSystem} yet.');
}
}
// Copyright 2014 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:html' as html;
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:meta/dart2js.dart';
import 'package:flutter/foundation.dart';
/// Expected sequence of method calls.
const List<String> callChain = <String>['baz', 'bar', 'foo'];
final List<StackFrame> expectedProfileStackFrames = callChain.map<StackFrame>((String method) {
return StackFrame(
number: -1,
packageScheme: '<unknown>',
package: '<unknown>',
packagePath: '<unknown>',
line: -1,
column: -1,
className: 'Object',
method: method,
source: '',
);
}).toList();
// TODO(yjbanov): fix these stack traces when https://github.com/flutter/flutter/issues/50753 is fixed.
const List<StackFrame> expectedDebugStackFrames = <StackFrame>[
StackFrame(
number: -1,
packageScheme: 'package',
package: 'web_integration',
packagePath: 'stack_trace.dart.lib.js',
line: 138,
column: 15,
className: '<unknown>',
method: 'baz',
source: '',
),
StackFrame(
number: -1,
packageScheme: 'package',
package: 'web_integration',
packagePath: 'stack_trace.dart.lib.js',
line: 135,
column: 17,
className: '<unknown>',
method: 'bar',
source: '',
),
StackFrame(
number: -1,
packageScheme: 'package',
package: 'web_integration',
packagePath: 'stack_trace.dart.lib.js',
line: 132,
column: 17,
className: '<unknown>',
method: 'foo',
source: '',
),
];
/// Tests that we do not crash while parsing Web stack traces.
///
/// This test is run in debug, profile, and release modes.
void main() {
final StringBuffer output = StringBuffer();
try {
try {
foo();
} catch (expectedError, expectedStackTrace) {
final List<StackFrame> parsedFrames = StackFrame.fromStackTrace(expectedStackTrace);
if (parsedFrames.isEmpty) {
throw Exception(
'Failed to parse stack trace. Got empty list of stack frames.\n'
'Stack trace:\n$expectedStackTrace'
);
}
// Symbols in release mode are randomly obfuscated, so there's no good way to
// validate the contents. However, profile mode can be checked.
if (kProfileMode) {
_checkStackFrameContents(parsedFrames, expectedProfileStackFrames, expectedStackTrace);
}
if (kDebugMode) {
_checkStackFrameContents(parsedFrames, expectedDebugStackFrames, expectedStackTrace);
}
}
output.writeln('--- TEST SUCCEEDED ---');
} catch (unexpectedError, unexpectedStackTrace) {
output.writeln('--- UNEXPECTED EXCEPTION ---');
output.writeln(unexpectedError);
output.writeln(unexpectedStackTrace);
output.writeln('--- TEST FAILED ---');
}
print(output);
html.HttpRequest.request(
'/test-result',
method: 'POST',
sendData: '$output',
);
}
@noInline
void foo() {
bar();
}
@noInline
void bar() {
baz();
}
@noInline
void baz() {
throw Exception('Test error message');
}
void _checkStackFrameContents(List<StackFrame> parsedFrames, List<StackFrame> expectedFrames, dynamic stackTrace) {
// Filter out stack frames outside this library so this test is less brittle.
final List<StackFrame> actual = parsedFrames
.where((StackFrame frame) => callChain.contains(frame.method))
.toList();
final bool stackFramesAsExpected = ListEquality<StackFrame>(StackFrameEquality()).equals(actual, expectedFrames);
if (!stackFramesAsExpected) {
throw Exception(
'Stack frames parsed incorrectly:\n'
'Expected:\n${expectedFrames.join('\n')}\n'
'Actual:\n${actual.join('\n')}\n'
'Stack trace:\n$stackTrace'
);
}
}
/// Use custom equality to ignore [StackFrame.source], which is not important
/// for the purposes of this test.
class StackFrameEquality implements Equality<StackFrame> {
@override
bool equals(StackFrame e1, StackFrame e2) {
return e1.number == e2.number &&
e1.packageScheme == e2.packageScheme &&
e1.package == e2.package &&
e1.packagePath == e2.packagePath &&
e1.line == e2.line &&
e1.column == e2.column &&
e1.className == e2.className &&
e1.method == e2.method;
}
@override
int hash(StackFrame e) {
return hashValues(e.number, e.packageScheme, e.package, e.packagePath, e.line, e.column, e.className, e.method);
}
@override
bool isValidKey(Object o) => o is StackFrame;
}
......@@ -6,6 +6,7 @@ import 'dart:ui' show hashValues;
import 'package:meta/meta.dart';
import 'constants.dart';
import 'object.dart';
/// A object representation of a frame from a stack trace.
......@@ -86,10 +87,22 @@ class StackFrame {
.trim()
.split('\n')
.map(fromStackTraceLine)
// On the Web in non-debug builds the stack trace includes the exception
// message that precedes the stack trace itself. fromStackTraceLine will
// return null in that case. We will skip it here.
.skipWhile((StackFrame frame) => frame == null)
.toList();
}
static StackFrame _parseWebFrame(String line) {
if (kDebugMode) {
return _parseWebDebugFrame(line);
} else {
return _parseWebNonDebugFrame(line);
}
}
static StackFrame _parseWebDebugFrame(String line) {
final bool hasPackage = line.startsWith('package');
final RegExp parser = hasPackage
? RegExp(r'^(package:.+) (\d+):(\d+)\s+(.+)$')
......@@ -120,6 +133,50 @@ class StackFrame {
);
}
// Non-debug builds do not point to dart code but compiled JavaScript, so
// line numbers are meaningless. We only attempt to parse the class and
// method name, which is more or less readable in profile builds, and
// minified in release builds.
static final RegExp _webNonDebugFramePattern = RegExp(r'^\s*at ([^\s]+).*$');
// Parses `line` as a stack frame in profile and release Web builds. If not
// recognized as a stack frame, returns null.
static StackFrame _parseWebNonDebugFrame(String line) {
final Match match = _webNonDebugFramePattern.firstMatch(line);
if (match == null) {
// On the Web in non-debug builds the stack trace includes the exception
// message that precedes the stack trace itself. Example:
//
// TypeError: Cannot read property 'hello$0' of null
// at _GalleryAppState.build$1 (http://localhost:8080/main.dart.js:149790:13)
// at StatefulElement.build$0 (http://localhost:8080/main.dart.js:129138:37)
// at StatefulElement.performRebuild$0 (http://localhost:8080/main.dart.js:129032:23)
//
// Instead of crashing when a line is not recognized as a stack frame, we
// return null. The caller, such as fromStackString, can then just skip
// this frame.
return null;
}
final List<String> classAndMethod = match.group(1).split('.');
final String className = classAndMethod.length > 1 ? classAndMethod.first : '<unknown>';
final String method = classAndMethod.length > 1
? classAndMethod.skip(1).join('.')
: classAndMethod.single;
return StackFrame(
number: -1,
packageScheme: '<unknown>',
package: '<unknown>',
packagePath: '<unknown>',
line: -1,
column: -1,
className: className,
method: method,
source: line,
);
}
/// Parses a single [StackFrame] from a single line of a [StackTrace].
static StackFrame fromStackTraceLine(String line) {
assert(line != null);
......
......@@ -338,6 +338,9 @@ class RunCommand extends RunCommandBase {
DebuggingOptions _createDebuggingOptions() {
final BuildInfo buildInfo = getBuildInfo();
final int browserDebugPort = featureFlags.isWebEnabled && argResults.wasParsed('web-browser-debug-port')
? int.parse(stringArg('web-browser-debug-port'))
: null;
if (buildInfo.mode.isRelease) {
return DebuggingOptions.disabled(
buildInfo,
......@@ -345,6 +348,8 @@ class RunCommand extends RunCommandBase {
hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '',
port: featureFlags.isWebEnabled ? stringArg('web-port') : '',
webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'),
webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'),
webBrowserDebugPort: browserDebugPort,
);
} else {
return DebuggingOptions.enabled(
......@@ -367,6 +372,8 @@ class RunCommand extends RunCommandBase {
hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '',
port: featureFlags.isWebEnabled ? stringArg('web-port') : '',
webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'),
webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'),
webBrowserDebugPort: browserDebugPort,
vmserviceOutFile: stringArg('vmservice-out-file'),
// Allow forcing fast-start to off to prevent doing more work on devices that
// don't support it.
......
......@@ -536,6 +536,8 @@ class DebuggingOptions {
this.hostname,
this.port,
this.webEnableExposeUrl,
this.webRunHeadless = false,
this.webBrowserDebugPort,
this.vmserviceOutFile,
this.fastStart = false,
}) : debuggingEnabled = true;
......@@ -545,6 +547,8 @@ class DebuggingOptions {
this.port,
this.hostname,
this.webEnableExposeUrl,
this.webRunHeadless = false,
this.webBrowserDebugPort,
this.cacheSkSL = false,
}) : debuggingEnabled = false,
useTestFonts = false,
......@@ -585,6 +589,17 @@ class DebuggingOptions {
final String port;
final String hostname;
final bool webEnableExposeUrl;
/// Whether to run the browser in headless mode.
///
/// Some CI environments do not provide a display and fail to launch the
/// browser with full graphics stack. Some browsers provide a special
/// "headless" mode that runs the browser with no graphics.
final bool webRunHeadless;
/// The port the browser should use for its debugging protocol.
final int webBrowserDebugPort;
/// A file where the vmservice URL should be written after the application is started.
final String vmserviceOutFile;
final bool fastStart;
......
......@@ -173,6 +173,19 @@ abstract class FlutterCommand extends Command<void> {
'when running on remote machines.',
hide: hide,
);
argParser.addFlag('web-run-headless',
defaultsTo: false,
help: 'Launches the browser in headless mode. Currently only Chrome '
'supports this option.',
hide: true,
);
argParser.addOption('web-browser-debug-port',
help: 'The debug port the browser should use. If not specified, a '
'random port is selected. Currently only Chrome supports this option. '
'It serves the Chrome DevTools Protocol '
'(https://chromedevtools.github.io/devtools-protocol/).',
hide: true,
);
}
void usesTargetOption() {
......
......@@ -97,8 +97,11 @@ class ChromeLauncher {
/// `headless` defaults to false, and controls whether we open a headless or
/// a `headfull` browser.
///
/// `debugPort` is Chrome's debugging protocol port. If null, a random free
/// port is picked automatically.
///
/// `skipCheck` does not attempt to make a devtools connection before returning.
Future<Chrome> launch(String url, { bool headless = false, bool skipCheck = false, Directory dataDir }) async {
Future<Chrome> launch(String url, { bool headless = false, int debugPort, bool skipCheck = false, Directory dataDir }) async {
// This is a JSON file which contains configuration from the
// browser session, such as window position. It is located
// under the Chrome data-dir folder.
......@@ -117,7 +120,7 @@ class ChromeLauncher {
}
}
final int port = await globals.os.findFreePort();
final int port = debugPort ?? await globals.os.findFreePort();
final List<String> args = <String>[
chromeExecutable,
// Using a tmp directory ensures that a new instance of chrome launches
......
......@@ -134,10 +134,14 @@ class ChromeDevice extends Device {
// See [ResidentWebRunner.run] in flutter_tools/lib/src/resident_web_runner.dart
// for the web initialization and server logic.
final String url = platformArgs['uri'] as String;
_chrome = await chromeLauncher.launch(url,
_chrome = await chromeLauncher.launch(
url,
dataDir: globals.fs.currentDirectory
.childDirectory('.dart_tool')
.childDirectory('chrome-device'));
.childDirectory('chrome-device'),
headless: debuggingOptions.webRunHeadless,
debugPort: debuggingOptions.webBrowserDebugPort,
);
globals.logger.sendEvent('app.webLaunchUrl', <String, dynamic>{'url': url, 'launched': true});
......
......@@ -54,10 +54,10 @@ void main() {
resetChromeForTesting();
});
test('can launch chrome and connect to the devtools', () => testbed.run(() async {
const List<String> expected = <String>[
List<String> expectChromeArgs({int debugPort = 1234}) {
return <String>[
'example_chrome',
'--remote-debugging-port=1234',
'--remote-debugging-port=$debugPort',
'--disable-background-timer-throttling',
'--disable-extensions',
'--disable-popup-blocking',
......@@ -68,11 +68,18 @@ void main() {
'--disable-translate',
'example_url',
];
}
test('can launch chrome and connect to the devtools', () => testbed.run(() async {
await chromeLauncher.launch('example_url', skipCheck: true);
final VerificationResult result = verify(globals.processManager.start(captureAny));
expect(result.captured.single, containsAll(expectChromeArgs()));
}));
expect(result.captured.single, containsAll(expected));
test('can launch chrome with a custom debug port', () => testbed.run(() async {
await chromeLauncher.launch('example_url', skipCheck: true, debugPort: 10000);
final VerificationResult result = verify(globals.processManager.start(captureAny));
expect(result.captured.single, containsAll(expectChromeArgs(debugPort: 10000)));
}));
test('can seed chrome temp directory with existing preferences', () => testbed.run(() async {
......
......@@ -498,7 +498,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
// fast.
unawaited(_process.exitCode.then((_) {
if (!prematureExitGuard.isCompleted) {
prematureExitGuard.completeError('Process existed prematurely: ${args.join(' ')}: $_errorBuffer');
prematureExitGuard.completeError('Process exited prematurely: ${args.join(' ')}: $_errorBuffer');
}
}));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment