Fix stack trace parsing on non-debug builds; add e2e tests (#50652)
* Fix stack trace parsing on non-debug builds; add e2e tests
This commit is contained in:
parent
c725f107a1
commit
b34046903b
@ -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
|
||||
|
||||
|
59
dev/bots/browser.dart
Normal file
59
dev/bots/browser.dart
Normal file
@ -0,0 +1,59 @@
|
||||
// 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) {
|
||||
|
138
dev/devicelab/lib/framework/browser.dart
Normal file
138
dev/devicelab/lib/framework/browser.dart
Normal file
@ -0,0 +1,138 @@
|
||||
// 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.');
|
||||
}
|
||||
}
|
||||
|
162
dev/integration_tests/web/lib/stack_trace.dart
Normal file
162
dev/integration_tests/web/lib/stack_trace.dart
Normal file
@ -0,0 +1,162 @@
|
||||
// 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');
|
||||
}
|
||||
}));
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user