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 ...@@ -7,6 +7,7 @@ web_shard_template: &WEB_SHARD_TEMPLATE
# As of October 2019, the Web shards needed more than 6G of RAM. # As of October 2019, the Web shards needed more than 6G of RAM.
CPU: 2 CPU: 2
MEMORY: 8G MEMORY: 8G
CHROME_NO_SANDBOX: true
GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095] GOLD_SERVICE_ACCOUNT: ENCRYPTED[3afeea5ac7201151c3d0dc9648862f0462b5e4f55dc600ca8b692319622f7c3eda3d577b1b16cc2ef0311b7314c1c095]
script: script:
- dart --enable-asserts ./dev/bots/test.dart - dart --enable-asserts ./dev/bots/test.dart
...@@ -173,6 +174,9 @@ task: ...@@ -173,6 +174,9 @@ task:
- dart --enable-asserts ./dev/bots/test.dart - 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 - 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 - name: web_tests-0-linux
<< : *WEB_SHARD_TEMPLATE << : *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: ...@@ -8,6 +8,8 @@ environment:
dependencies: dependencies:
args: 1.5.2 args: 1.5.2
crypto: 2.1.3 crypto: 2.1.3
flutter_devicelab:
path: ../devicelab
googleapis: 0.54.0 googleapis: 0.54.0
googleapis_auth: 0.2.11+1 googleapis_auth: 0.2.11+1
http: 0.12.0+4 http: 0.12.0+4
......
...@@ -53,6 +53,15 @@ Stream<String> runAndGetStdout(String executable, List<String> arguments, { ...@@ -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'); 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, { Future<void> runCommand(String executable, List<String> arguments, {
String workingDirectory, String workingDirectory,
Map<String, String> environment, Map<String, String> environment,
...@@ -63,6 +72,7 @@ Future<void> runCommand(String executable, List<String> arguments, { ...@@ -63,6 +72,7 @@ Future<void> runCommand(String executable, List<String> arguments, {
CapturedOutput output, CapturedOutput output,
bool skip = false, bool skip = false,
bool Function(String) removeLine, bool Function(String) removeLine,
void Function(String, Process) outputListener,
}) async { }) async {
assert( assert(
(outputMode == OutputMode.capture) == (output != null), (outputMode == OutputMode.capture) == (output != null),
...@@ -88,7 +98,13 @@ Future<void> runCommand(String executable, List<String> arguments, { ...@@ -88,7 +98,13 @@ Future<void> runCommand(String executable, List<String> arguments, {
.transform<String>(const Utf8Decoder()) .transform<String>(const Utf8Decoder())
.transform(const LineSplitter()) .transform(const LineSplitter())
.where((String line) => removeLine == null || !removeLine(line)) .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()); .transform(const Utf8Encoder());
switch (outputMode) { switch (outputMode) {
case OutputMode.print: case OutputMode.print:
......
...@@ -11,6 +11,7 @@ import 'package:googleapis_auth/auth_io.dart' as auth; ...@@ -11,6 +11,7 @@ import 'package:googleapis_auth/auth_io.dart' as auth;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'browser.dart';
import 'flutter_compact_formatter.dart'; import 'flutter_compact_formatter.dart';
import 'run_command.dart'; import 'run_command.dart';
import 'utils.dart'; import 'utils.dart';
...@@ -116,7 +117,8 @@ Future<void> main(List<String> args) async { ...@@ -116,7 +117,8 @@ Future<void> main(List<String> args) async {
'hostonly_devicelab_tests': _runHostOnlyDeviceLabTests, 'hostonly_devicelab_tests': _runHostOnlyDeviceLabTests,
'tool_coverage': _runToolCoverage, 'tool_coverage': _runToolCoverage,
'tool_tests': _runToolTests, 'tool_tests': _runToolTests,
'web_tests': _runWebTests, 'web_tests': _runWebUnitTests,
'web_integration_tests': _runWebIntegrationTests,
}); });
} on ExitException catch (error) { } on ExitException catch (error) {
error.apply(); error.apply();
...@@ -522,7 +524,7 @@ Future<void> _runFrameworkCoverage() async { ...@@ -522,7 +524,7 @@ Future<void> _runFrameworkCoverage() async {
} }
} }
Future<void> _runWebTests() async { Future<void> _runWebUnitTests() async {
final Map<String, ShardRunner> subshards = <String, ShardRunner>{}; final Map<String, ShardRunner> subshards = <String, ShardRunner>{};
final Directory flutterPackageDirectory = Directory(path.join(flutterRoot, 'packages', 'flutter')); final Directory flutterPackageDirectory = Directory(path.join(flutterRoot, 'packages', 'flutter'));
...@@ -585,6 +587,94 @@ Future<void> _runWebTests() async { ...@@ -585,6 +587,94 @@ Future<void> _runWebTests() async {
await selectSubshard(subshards); 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 { Future<void> _runFlutterWebTest(String workingDirectory, List<String> tests) async {
final List<String> batch = <String>[]; final List<String> batch = <String>[];
for (int i = 0; i < tests.length; i += 1) { 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'; ...@@ -12,6 +12,7 @@ import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_static/shelf_static.dart'; 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/framework.dart';
import 'package:flutter_devicelab/framework/utils.dart'; import 'package:flutter_devicelab/framework/utils.dart';
...@@ -74,30 +75,12 @@ Future<TaskResult> runWebBenchmark({ @required bool useCanvasKit }) async { ...@@ -74,30 +75,12 @@ Future<TaskResult> runWebBenchmark({ @required bool useCanvasKit }) async {
)); ));
server = await io.HttpServer.bind('localhost', benchmarkServerPort); server = await io.HttpServer.bind('localhost', benchmarkServerPort);
io.Process chromeProcess; Chrome chrome;
try { try {
shelf_io.serveRequests(server, cascade.handler); 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 dartToolDirectory = path.join('$macrobenchmarksDirectory/.dart_tool');
final String userDataDir = io.Directory(dartToolDirectory).createTempSync('chrome_user_data_').path; 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 // TODO(yjbanov): temporarily disables headful Chrome until we get
// devicelab hardware that is able to run it. Our current // devicelab hardware that is able to run it. Our current
...@@ -106,38 +89,33 @@ Future<TaskResult> runWebBenchmark({ @required bool useCanvasKit }) async { ...@@ -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['CALIBRATED'] != 'true';
// final bool isUncalibratedSmokeTest = // final bool isUncalibratedSmokeTest =
// io.Platform.environment['UNCALIBRATED_SMOKE_TEST'] == 'true'; // io.Platform.environment['UNCALIBRATED_SMOKE_TEST'] == 'true';
if (isUncalibratedSmokeTest) { final ChromeOptions options = ChromeOptions(
print('Running in headless mode because running on uncalibrated hardware.'); url: 'http://localhost:$benchmarkServerPort/index.html',
args.add('--headless'); userDataDirectory: userDataDir,
windowHeight: 1024,
windowWidth: 1024,
headless: isUncalibratedSmokeTest,
// When running in headless mode Chrome exits immediately unless // When running in headless mode Chrome exits immediately unless
// a debug port is specified. // a debug port is specified.
args.add('--remote-debugging-port=${benchmarkServerPort + 1}'); debugPort: isUncalibratedSmokeTest ? benchmarkServerPort + 1 : null,
} );
chromeProcess = await startProcess( print('Launching Chrome.');
_findSystemChromeExecutable(), chrome = await Chrome.launch(
args, options,
onError: (String error) {
profileData.completeError(Exception(error));
},
workingDirectory: cwd, 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.'); print('Waiting for the benchmark to report benchmark profile.');
final String backend = useCanvasKit ? 'canvaskit' : 'html'; final String backend = useCanvasKit ? 'canvaskit' : 'html';
final Map<String, dynamic> taskResult = <String, dynamic>{}; final Map<String, dynamic> taskResult = <String, dynamic>{};
final List<String> benchmarkScoreKeys = <String>[]; final List<String> benchmarkScoreKeys = <String>[];
final List<Map<String, dynamic>> profiles = await profileData.future; final List<Map<String, dynamic>> profiles = await profileData.future;
print('Received profile data'); print('Received profile data');
receivedProfileData = true;
for (final Map<String, dynamic> profile in profiles) { for (final Map<String, dynamic> profile in profiles) {
final String benchmarkName = profile['name'] as String; final String benchmarkName = profile['name'] as String;
final String benchmarkScoreKey = '$benchmarkName.$backend.averageDrawFrameDuration'; final String benchmarkScoreKey = '$benchmarkName.$backend.averageDrawFrameDuration';
...@@ -148,32 +126,7 @@ Future<TaskResult> runWebBenchmark({ @required bool useCanvasKit }) async { ...@@ -148,32 +126,7 @@ Future<TaskResult> runWebBenchmark({ @required bool useCanvasKit }) async {
return TaskResult.success(taskResult, benchmarkScoreKeys: benchmarkScoreKeys); return TaskResult.success(taskResult, benchmarkScoreKeys: benchmarkScoreKeys);
} finally { } finally {
server.close(); 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; ...@@ -6,6 +6,7 @@ import 'dart:ui' show hashValues;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'constants.dart';
import 'object.dart'; import 'object.dart';
/// A object representation of a frame from a stack trace. /// A object representation of a frame from a stack trace.
...@@ -86,10 +87,22 @@ class StackFrame { ...@@ -86,10 +87,22 @@ class StackFrame {
.trim() .trim()
.split('\n') .split('\n')
.map(fromStackTraceLine) .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(); .toList();
} }
static StackFrame _parseWebFrame(String line) { 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 bool hasPackage = line.startsWith('package');
final RegExp parser = hasPackage final RegExp parser = hasPackage
? RegExp(r'^(package:.+) (\d+):(\d+)\s+(.+)$') ? RegExp(r'^(package:.+) (\d+):(\d+)\s+(.+)$')
...@@ -120,6 +133,50 @@ class StackFrame { ...@@ -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]. /// Parses a single [StackFrame] from a single line of a [StackTrace].
static StackFrame fromStackTraceLine(String line) { static StackFrame fromStackTraceLine(String line) {
assert(line != null); assert(line != null);
......
...@@ -338,6 +338,9 @@ class RunCommand extends RunCommandBase { ...@@ -338,6 +338,9 @@ class RunCommand extends RunCommandBase {
DebuggingOptions _createDebuggingOptions() { DebuggingOptions _createDebuggingOptions() {
final BuildInfo buildInfo = getBuildInfo(); 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) { if (buildInfo.mode.isRelease) {
return DebuggingOptions.disabled( return DebuggingOptions.disabled(
buildInfo, buildInfo,
...@@ -345,6 +348,8 @@ class RunCommand extends RunCommandBase { ...@@ -345,6 +348,8 @@ class RunCommand extends RunCommandBase {
hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '',
port: featureFlags.isWebEnabled ? stringArg('web-port') : '', port: featureFlags.isWebEnabled ? stringArg('web-port') : '',
webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'), webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'),
webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'),
webBrowserDebugPort: browserDebugPort,
); );
} else { } else {
return DebuggingOptions.enabled( return DebuggingOptions.enabled(
...@@ -367,6 +372,8 @@ class RunCommand extends RunCommandBase { ...@@ -367,6 +372,8 @@ class RunCommand extends RunCommandBase {
hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '',
port: featureFlags.isWebEnabled ? stringArg('web-port') : '', port: featureFlags.isWebEnabled ? stringArg('web-port') : '',
webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'), webEnableExposeUrl: featureFlags.isWebEnabled && boolArg('web-allow-expose-url'),
webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'),
webBrowserDebugPort: browserDebugPort,
vmserviceOutFile: stringArg('vmservice-out-file'), vmserviceOutFile: stringArg('vmservice-out-file'),
// Allow forcing fast-start to off to prevent doing more work on devices that // Allow forcing fast-start to off to prevent doing more work on devices that
// don't support it. // don't support it.
......
...@@ -536,6 +536,8 @@ class DebuggingOptions { ...@@ -536,6 +536,8 @@ class DebuggingOptions {
this.hostname, this.hostname,
this.port, this.port,
this.webEnableExposeUrl, this.webEnableExposeUrl,
this.webRunHeadless = false,
this.webBrowserDebugPort,
this.vmserviceOutFile, this.vmserviceOutFile,
this.fastStart = false, this.fastStart = false,
}) : debuggingEnabled = true; }) : debuggingEnabled = true;
...@@ -545,6 +547,8 @@ class DebuggingOptions { ...@@ -545,6 +547,8 @@ class DebuggingOptions {
this.port, this.port,
this.hostname, this.hostname,
this.webEnableExposeUrl, this.webEnableExposeUrl,
this.webRunHeadless = false,
this.webBrowserDebugPort,
this.cacheSkSL = false, this.cacheSkSL = false,
}) : debuggingEnabled = false, }) : debuggingEnabled = false,
useTestFonts = false, useTestFonts = false,
...@@ -585,6 +589,17 @@ class DebuggingOptions { ...@@ -585,6 +589,17 @@ class DebuggingOptions {
final String port; final String port;
final String hostname; final String hostname;
final bool webEnableExposeUrl; 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. /// A file where the vmservice URL should be written after the application is started.
final String vmserviceOutFile; final String vmserviceOutFile;
final bool fastStart; final bool fastStart;
......
...@@ -173,6 +173,19 @@ abstract class FlutterCommand extends Command<void> { ...@@ -173,6 +173,19 @@ abstract class FlutterCommand extends Command<void> {
'when running on remote machines.', 'when running on remote machines.',
hide: hide, 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() { void usesTargetOption() {
......
...@@ -97,8 +97,11 @@ class ChromeLauncher { ...@@ -97,8 +97,11 @@ class ChromeLauncher {
/// `headless` defaults to false, and controls whether we open a headless or /// `headless` defaults to false, and controls whether we open a headless or
/// a `headfull` browser. /// 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. /// `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 // This is a JSON file which contains configuration from the
// browser session, such as window position. It is located // browser session, such as window position. It is located
// under the Chrome data-dir folder. // under the Chrome data-dir folder.
...@@ -117,7 +120,7 @@ class ChromeLauncher { ...@@ -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>[ final List<String> args = <String>[
chromeExecutable, chromeExecutable,
// Using a tmp directory ensures that a new instance of chrome launches // Using a tmp directory ensures that a new instance of chrome launches
......
...@@ -134,10 +134,14 @@ class ChromeDevice extends Device { ...@@ -134,10 +134,14 @@ class ChromeDevice extends Device {
// See [ResidentWebRunner.run] in flutter_tools/lib/src/resident_web_runner.dart // See [ResidentWebRunner.run] in flutter_tools/lib/src/resident_web_runner.dart
// for the web initialization and server logic. // for the web initialization and server logic.
final String url = platformArgs['uri'] as String; final String url = platformArgs['uri'] as String;
_chrome = await chromeLauncher.launch(url, _chrome = await chromeLauncher.launch(
url,
dataDir: globals.fs.currentDirectory dataDir: globals.fs.currentDirectory
.childDirectory('.dart_tool') .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}); globals.logger.sendEvent('app.webLaunchUrl', <String, dynamic>{'url': url, 'launched': true});
......
...@@ -54,10 +54,10 @@ void main() { ...@@ -54,10 +54,10 @@ void main() {
resetChromeForTesting(); resetChromeForTesting();
}); });
test('can launch chrome and connect to the devtools', () => testbed.run(() async { List<String> expectChromeArgs({int debugPort = 1234}) {
const List<String> expected = <String>[ return <String>[
'example_chrome', 'example_chrome',
'--remote-debugging-port=1234', '--remote-debugging-port=$debugPort',
'--disable-background-timer-throttling', '--disable-background-timer-throttling',
'--disable-extensions', '--disable-extensions',
'--disable-popup-blocking', '--disable-popup-blocking',
...@@ -68,11 +68,18 @@ void main() { ...@@ -68,11 +68,18 @@ void main() {
'--disable-translate', '--disable-translate',
'example_url', 'example_url',
]; ];
}
test('can launch chrome and connect to the devtools', () => testbed.run(() async {
await chromeLauncher.launch('example_url', skipCheck: true); await chromeLauncher.launch('example_url', skipCheck: true);
final VerificationResult result = verify(globals.processManager.start(captureAny)); 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 { test('can seed chrome temp directory with existing preferences', () => testbed.run(() async {
......
...@@ -498,7 +498,7 @@ class FlutterRunTestDriver extends FlutterTestDriver { ...@@ -498,7 +498,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
// fast. // fast.
unawaited(_process.exitCode.then((_) { unawaited(_process.exitCode.then((_) {
if (!prematureExitGuard.isCompleted) { 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