Enable expression evaluation in debugger for web platform (#53595)
diff --git a/packages/flutter_tools/lib/src/build_runner/devfs_web.dart b/packages/flutter_tools/lib/src/build_runner/devfs_web.dart
index e2e02d4..030b75c 100644
--- a/packages/flutter_tools/lib/src/build_runner/devfs_web.dart
+++ b/packages/flutter_tools/lib/src/build_runner/devfs_web.dart
@@ -36,6 +36,39 @@
import '../web/bootstrap.dart';
import '../web/chrome.dart';
+/// An expression compiler connecting to FrontendServer
+///
+/// This is only used in development mode
+class WebExpressionCompiler implements ExpressionCompiler {
+ WebExpressionCompiler(this._generator);
+
+ final ResidentCompiler _generator;
+
+ @override
+ Future<ExpressionCompilationResult> compileExpressionToJs(
+ String isolateId,
+ String libraryUri,
+ int line,
+ int column,
+ Map<String, String> jsModules,
+ Map<String, String> jsFrameValues,
+ String moduleName,
+ String expression,
+ ) async {
+ final CompilerOutput compilerOutput = await _generator.compileExpressionToJs(libraryUri,
+ line, column, jsModules, jsFrameValues, moduleName, expression);
+
+ if (compilerOutput != null && compilerOutput.outputFilename != null) {
+ final String content = utf8.decode(
+ globals.fs.file(compilerOutput.outputFilename).readAsBytesSync());
+ return ExpressionCompilationResult(
+ content, compilerOutput.errorCount > 0);
+ }
+
+ throw Exception('Failed to compile $expression');
+ }
+}
+
/// A web server which handles serving JavaScript and assets.
///
/// This is only used in development mode.
@@ -85,7 +118,8 @@
UrlTunneller urlTunneller,
BuildMode buildMode,
bool enableDwds,
- Uri entrypoint, {
+ Uri entrypoint,
+ ExpressionCompiler expressionCompiler, {
bool testMode = false,
}) async {
try {
@@ -170,6 +204,7 @@
serverPathForModule,
serverPathForAppUri,
),
+ expressionCompiler: expressionCompiler
);
shelf.Pipeline pipeline = const shelf.Pipeline();
if (enableDwds) {
@@ -501,6 +536,7 @@
@required this.buildMode,
@required this.enableDwds,
@required this.entrypoint,
+ @required this.expressionCompiler,
this.testMode = false,
});
@@ -512,6 +548,7 @@
final BuildMode buildMode;
final bool enableDwds;
final bool testMode;
+ final ExpressionCompiler expressionCompiler;
WebAssetServer webAssetServer;
@@ -572,6 +609,7 @@
buildMode,
enableDwds,
entrypoint,
+ expressionCompiler,
testMode: testMode,
);
_baseUri = Uri.parse('http://$hostname:$port');
diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
index 8e8b156..a014da4 100644
--- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
@@ -421,6 +421,7 @@
buildMode: debuggingOptions.buildInfo.mode,
enableDwds: _enableDwds,
entrypoint: globals.fs.file(target).uri,
+ expressionCompiler: WebExpressionCompiler(device.generator),
);
final Uri url = await device.devFS.create();
if (debuggingOptions.buildInfo.isDebug) {
diff --git a/packages/flutter_tools/lib/src/codegen.dart b/packages/flutter_tools/lib/src/codegen.dart
index e49cd0a..8e8c9ac 100644
--- a/packages/flutter_tools/lib/src/codegen.dart
+++ b/packages/flutter_tools/lib/src/codegen.dart
@@ -161,6 +161,15 @@
}
@override
+ Future<CompilerOutput> compileExpressionToJs(
+ String libraryUri, int line, int column, Map<String, String> jsModules,
+ Map<String, String> jsFrameValues, String moduleName, String expression
+ ) {
+ return _residentCompiler.compileExpressionToJs(
+ libraryUri, line, column, jsModules, jsFrameValues, moduleName, expression);
+ }
+
+ @override
Future<CompilerOutput> recompile(String mainPath, List<Uri> invalidatedFiles, {String outputPath, String packagesFilePath}) async {
if (_codegenDaemon.lastStatus != CodegenStatus.Succeeded && _codegenDaemon.lastStatus != CodegenStatus.Failed) {
await _codegenDaemon.buildResults.firstWhere((CodegenStatus status) {
diff --git a/packages/flutter_tools/lib/src/commands/update_packages.dart b/packages/flutter_tools/lib/src/commands/update_packages.dart
index 7abe67c..7c6307c 100644
--- a/packages/flutter_tools/lib/src/commands/update_packages.dart
+++ b/packages/flutter_tools/lib/src/commands/update_packages.dart
@@ -26,7 +26,8 @@
'mockito': '^4.1.0', // Prevent mockito from downgrading to 4.0.0
'vm_service_client': '0.2.6+2', // Final version before being marked deprecated.
'video_player': '0.10.6', // 0.10.7 fails a gallery smoke test for toString.
- 'package_config': '1.9.1'
+ 'package_config': '1.9.1',
+ 'flutter_template_images': '1.0.0', // 1.0.1 breaks windows tests
};
class UpdatePackagesCommand extends FlutterCommand {
diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart
index 2694ab0..cdfa2fc 100644
--- a/packages/flutter_tools/lib/src/compile.dart
+++ b/packages/flutter_tools/lib/src/compile.dart
@@ -431,6 +431,31 @@
compiler._compileExpression(this);
}
+class _CompileExpressionToJsRequest extends _CompilationRequest {
+ _CompileExpressionToJsRequest(
+ Completer<CompilerOutput> completer,
+ this.libraryUri,
+ this.line,
+ this.column,
+ this.jsModules,
+ this.jsFrameValues,
+ this.moduleName,
+ this.expression,
+ ) : super(completer);
+
+ final String libraryUri;
+ final int line;
+ final int column;
+ final Map<String, String> jsModules;
+ final Map<String, String> jsFrameValues;
+ final String moduleName;
+ final String expression;
+
+ @override
+ Future<CompilerOutput> _run(DefaultResidentCompiler compiler) async =>
+ compiler._compileExpressionToJs(this);
+}
+
class _RejectRequest extends _CompilationRequest {
_RejectRequest(Completer<CompilerOutput> completer) : super(completer);
@@ -489,6 +514,36 @@
bool isStatic,
);
+ /// Compiles [expression] in [libraryUri] at [line]:[column] to JavaScript
+ /// in [moduleName].
+ ///
+ /// Values listed in [jsFrameValues] are substituted for their names in the
+ /// [expression].
+ ///
+ /// Ensures that all [jsModules] are loaded and accessible inside the
+ /// expression.
+ ///
+ /// Example values of parameters:
+ /// [moduleName] is of the form '/packages/hello_world_main.dart'
+ /// [jsFrameValues] is a map from js variable name to its primitive value
+ /// or another variable name, for example
+ /// { 'x': '1', 'y': 'y', 'o': 'null' }
+ /// [jsModules] is a map from variable name to the module name, where
+ /// variable name is the name originally used in JavaScript to contain the
+ /// module object, for example:
+ /// { 'dart':'dart_sdk', 'main': '/packages/hello_world_main.dart' }
+ /// Returns a [CompilerOutput] including the name of the file containing the
+ /// compilation result and a number of errors
+ Future<CompilerOutput> compileExpressionToJs(
+ String libraryUri,
+ int line,
+ int column,
+ Map<String, String> jsModules,
+ Map<String, String> jsFrameValues,
+ String moduleName,
+ String expression,
+ );
+
/// Should be invoked when results of compilation are accepted by the client.
///
/// Either [accept] or [reject] should be called after every [recompile] call.
@@ -779,6 +834,52 @@
}
@override
+ Future<CompilerOutput> compileExpressionToJs(
+ String libraryUri,
+ int line,
+ int column,
+ Map<String, String> jsModules,
+ Map<String, String> jsFrameValues,
+ String moduleName,
+ String expression,
+ ) {
+ if (!_controller.hasListener) {
+ _controller.stream.listen(_handleCompilationRequest);
+ }
+
+ final Completer<CompilerOutput> completer = Completer<CompilerOutput>();
+ _controller.add(
+ _CompileExpressionToJsRequest(
+ completer, libraryUri, line, column, jsModules, jsFrameValues, moduleName, expression)
+ );
+ return completer.future;
+ }
+
+ Future<CompilerOutput> _compileExpressionToJs(_CompileExpressionToJsRequest request) async {
+ _stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false);
+
+ // 'compile-expression-to-js' should be invoked after compiler has been started,
+ // program was compiled.
+ if (_server == null) {
+ return null;
+ }
+
+ final String inputKey = Uuid().generateV4();
+ _server.stdin.writeln('compile-expression-to-js $inputKey');
+ _server.stdin.writeln(request.libraryUri ?? '');
+ _server.stdin.writeln(request.line);
+ _server.stdin.writeln(request.column);
+ request.jsModules?.forEach((String k, String v) { _server.stdin.writeln('$k:$v'); });
+ _server.stdin.writeln(inputKey);
+ request.jsFrameValues?.forEach((String k, String v) { _server.stdin.writeln('$k:$v'); });
+ _server.stdin.writeln(inputKey);
+ _server.stdin.writeln(request.moduleName ?? '');
+ _server.stdin.writeln(request.expression ?? '');
+
+ return _stdoutHandler.compilerOutput.future;
+ }
+
+ @override
void accept() {
if (_compileRequestNeedsConfirmation) {
_server.stdin.writeln('accept');
diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml
index 27a7736..590e377 100644
--- a/packages/flutter_tools/pubspec.yaml
+++ b/packages/flutter_tools/pubspec.yaml
@@ -11,7 +11,7 @@
# To update these, use "flutter update-packages --force-upgrade".
archive: 2.0.13
args: 1.6.0
- dwds: 3.0.1
+ dwds: 3.0.2
completion: 0.2.2
coverage: 0.13.9
crypto: 2.1.4
@@ -112,4 +112,4 @@
# Exclude this package from the hosted API docs.
nodoc: true
-# PUBSPEC CHECKSUM: 65c1
+# PUBSPEC CHECKSUM: 53c2
diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
index b4adf27..d7c1abf 100644
--- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
+++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
@@ -373,6 +373,7 @@
enableDwds: false,
entrypoint: Uri.base,
testMode: true,
+ expressionCompiler: null,
);
webDevFS.requireJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true);
diff --git a/packages/flutter_tools/test/integration.shard/expression_evaluation_web_test.dart b/packages/flutter_tools/test/integration.shard/expression_evaluation_web_test.dart
new file mode 100644
index 0000000..f362026
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/expression_evaluation_web_test.dart
@@ -0,0 +1,178 @@
+// 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';
+
+import 'package:file/file.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+
+import 'package:vm_service/vm_service.dart';
+
+import '../src/common.dart';
+import 'test_data/basic_project.dart';
+import 'test_data/tests_project.dart';
+import 'test_driver.dart';
+import 'test_utils.dart';
+
+void batch1() {
+ final BasicProject _project = BasicProject();
+ Directory tempDir;
+ FlutterRunTestDriver _flutter;
+
+ Future<void> initProject() async {
+ tempDir = createResolvedTempDirectorySync('run_expression_eval_test.');
+ await _project.setUpIn(tempDir);
+ _flutter = FlutterRunTestDriver(tempDir);
+ }
+
+ Future<void> cleanProject() async {
+ await _flutter.stop();
+ tryToDelete(tempDir);
+ }
+
+ Future<void> breakInBuildMethod(FlutterTestDriver flutter) async {
+ await _flutter.breakAt(
+ _project.buildMethodBreakpointUri,
+ _project.buildMethodBreakpointLine,
+ );
+ }
+
+ Future<void> breakInTopLevelFunction(FlutterTestDriver flutter) async {
+ await _flutter.breakAt(
+ _project.topLevelFunctionBreakpointUri,
+ _project.topLevelFunctionBreakpointLine,
+ );
+ }
+
+ test('flutter run expression evaluation - can evaluate trivial expressions in top level function', () async {
+ await initProject();
+ await _flutter.run(withDebugger: true, chrome: true);
+ await breakInTopLevelFunction(_flutter);
+ await evaluateTrivialExpressions(_flutter);
+ await cleanProject();
+ }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+ test('flutter run expression evaluation - can evaluate trivial expressions in build method', () async {
+ await initProject();
+ await _flutter.run(withDebugger: true, chrome: true);
+ await breakInBuildMethod(_flutter);
+ await evaluateTrivialExpressions(_flutter);
+ await cleanProject();
+ }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+ test('flutter run expression evaluation - can evaluate complex expressions in top level function', () async {
+ await initProject();
+ await _flutter.run(withDebugger: true, chrome: true);
+ await breakInTopLevelFunction(_flutter);
+ await evaluateComplexExpressions(_flutter);
+ await cleanProject();
+ }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+ test('flutter run expression evaluation - can evaluate complex expressions in build method', () async {
+ await initProject();
+ await _flutter.run(withDebugger: true, chrome: true);
+ await breakInBuildMethod(_flutter);
+ await evaluateComplexExpressions(_flutter);
+ await cleanProject();
+ }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+ test('flutter run expression evaluation - can evaluate expressions returning complex objects in top level function', () async {
+ await initProject();
+ await _flutter.run(withDebugger: true, chrome: true);
+ await breakInTopLevelFunction(_flutter);
+ await evaluateComplexReturningExpressions(_flutter);
+ await cleanProject();
+ }, skip: 'Evaluate on objects is not supported for web yet');
+
+ test('flutter run expression evaluation - can evaluate expressions returning complex objects in build method', () async {
+ await initProject();
+ await _flutter.run(withDebugger: true, chrome: true);
+ await breakInBuildMethod(_flutter);
+ await evaluateComplexReturningExpressions(_flutter);
+ await cleanProject();
+ }, skip: 'Evaluate on objects is not supported for web yet');
+}
+
+void batch2() {
+ final TestsProject _project = TestsProject();
+ Directory tempDir;
+ FlutterRunTestDriver _flutter;
+
+ Future<void> initProject() async {
+ tempDir = createResolvedTempDirectorySync('test_expression_eval_test.');
+ await _project.setUpIn(tempDir);
+ _flutter = FlutterRunTestDriver(tempDir);
+ }
+
+ Future<void> cleanProject() async {
+ await _flutter.stop();
+ tryToDelete(tempDir);
+ }
+
+ Future<void> breakInMethod(FlutterTestDriver flutter) async {
+ await _flutter.breakAt(
+ _project.breakpointAppUri,
+ _project.breakpointLine,
+ );
+ }
+
+ test('flutter test expression evaluation - can evaluate trivial expressions in a test', () async {
+ await initProject();
+ await _flutter.run(withDebugger: true, chrome: true, script: _project.testFilePath);
+ await breakInMethod(_flutter);
+ await evaluateTrivialExpressions(_flutter);
+ await cleanProject();
+ }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+ test('flutter test expression evaluation - can evaluate complex expressions in a test', () async {
+ await initProject();
+ await _flutter.run(withDebugger: true, chrome: true, script: _project.testFilePath);
+ await breakInMethod(_flutter);
+ await evaluateComplexExpressions(_flutter);
+ await cleanProject();
+ }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+ test('flutter test expression evaluation - can evaluate expressions returning complex objects in a test', () async {
+ await initProject();
+ await _flutter.run(withDebugger: true, chrome: true, script: _project.testFilePath);
+ await breakInMethod(_flutter);
+ await evaluateComplexReturningExpressions(_flutter);
+ await cleanProject();
+ }, skip: 'Evaluate on objects is not supported for web yet');
+}
+
+Future<void> evaluateTrivialExpressions(FlutterTestDriver flutter) async {
+ InstanceRef res;
+
+ res = await flutter.evaluateInFrame('"test"');
+ expect(res.kind == InstanceKind.kString && res.valueAsString == 'test', isTrue);
+
+ res = await flutter.evaluateInFrame('1');
+ expect(res.kind == InstanceKind.kDouble && res.valueAsString == 1.toString(), isTrue);
+
+ res = await flutter.evaluateInFrame('true');
+ expect(res.kind == InstanceKind.kBool && res.valueAsString == true.toString(), isTrue);
+}
+
+Future<void> evaluateComplexExpressions(FlutterTestDriver flutter) async {
+ final InstanceRef res = await flutter.evaluateInFrame('new DateTime.now().year');
+ expect(res.kind == InstanceKind.kDouble && res.valueAsString == DateTime.now().year.toString(), isTrue);
+}
+
+Future<void> evaluateComplexReturningExpressions(FlutterTestDriver flutter) async {
+ final DateTime now = DateTime.now();
+ final InstanceRef resp = await flutter.evaluateInFrame('new DateTime.now()');
+ expect(resp.classRef.name, equals('DateTime'));
+ // Ensure we got a reasonable approximation. The more accurate we try to
+ // make this, the more likely it'll fail due to differences in the time
+ // in the remote VM and the local VM at the time the code runs.
+ final InstanceRef res = await flutter.evaluate(resp.id, r'"$year-$month-$day"');
+ expect(res.valueAsString, equals('${now.year}-${now.month}-${now.day}'));
+}
+
+void main() {
+ batch1();
+ batch2();
+}
diff --git a/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart b/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart
index c452bb6..566c052 100644
--- a/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart
+++ b/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart
@@ -49,6 +49,7 @@
String get testFilePath => globals.fs.path.join(dir.path, 'test', 'test.dart');
Uri get breakpointUri => Uri.file(testFilePath);
+ Uri get breakpointAppUri => Uri.parse('org-dartlang-app:///test.dart');
int get breakpointLine => lineContaining(testContent, '// BREAKPOINT');
}
diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart
index c8193e7..0c3e0c9 100644
--- a/packages/flutter_tools/test/integration.shard/test_driver.dart
+++ b/packages/flutter_tools/test/integration.shard/test_driver.dart
@@ -436,6 +436,7 @@
bool pauseOnExceptions = false,
bool chrome = false,
File pidFile,
+ String script,
}) async {
await _setupProcess(
<String>[
@@ -453,6 +454,7 @@
startPaused: startPaused,
pauseOnExceptions: pauseOnExceptions,
pidFile: pidFile,
+ script: script,
);
}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 5a8d2d7..ec57c94 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -646,6 +646,20 @@
) async {
return null;
}
+
+ @override
+ Future<CompilerOutput> compileExpressionToJs(
+ String libraryUri,
+ int line,
+ int column,
+ Map<String, String> jsModules,
+ Map<String, String> jsFrameValues,
+ String moduleName,
+ String expression,
+ ) async {
+ return null;
+ }
+
@override
Future<CompilerOutput> recompile(String mainPath, List<Uri> invalidatedFiles, { String outputPath, String packagesFilePath }) async {
globals.fs.file(outputPath).createSync(recursive: true);