Unverified Commit 3a0d8377 authored by Anna Gringauze's avatar Anna Gringauze Committed by GitHub

Enable expression evaluation in debugger for web platform (#53595)

parent 191f86ea
...@@ -36,6 +36,39 @@ import '../globals.dart' as globals; ...@@ -36,6 +36,39 @@ import '../globals.dart' as globals;
import '../web/bootstrap.dart'; import '../web/bootstrap.dart';
import '../web/chrome.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. /// A web server which handles serving JavaScript and assets.
/// ///
/// This is only used in development mode. /// This is only used in development mode.
...@@ -85,7 +118,8 @@ class WebAssetServer implements AssetReader { ...@@ -85,7 +118,8 @@ class WebAssetServer implements AssetReader {
UrlTunneller urlTunneller, UrlTunneller urlTunneller,
BuildMode buildMode, BuildMode buildMode,
bool enableDwds, bool enableDwds,
Uri entrypoint, { Uri entrypoint,
ExpressionCompiler expressionCompiler, {
bool testMode = false, bool testMode = false,
}) async { }) async {
try { try {
...@@ -170,6 +204,7 @@ class WebAssetServer implements AssetReader { ...@@ -170,6 +204,7 @@ class WebAssetServer implements AssetReader {
serverPathForModule, serverPathForModule,
serverPathForAppUri, serverPathForAppUri,
), ),
expressionCompiler: expressionCompiler
); );
shelf.Pipeline pipeline = const shelf.Pipeline(); shelf.Pipeline pipeline = const shelf.Pipeline();
if (enableDwds) { if (enableDwds) {
...@@ -501,6 +536,7 @@ class WebDevFS implements DevFS { ...@@ -501,6 +536,7 @@ class WebDevFS implements DevFS {
@required this.buildMode, @required this.buildMode,
@required this.enableDwds, @required this.enableDwds,
@required this.entrypoint, @required this.entrypoint,
@required this.expressionCompiler,
this.testMode = false, this.testMode = false,
}); });
...@@ -512,6 +548,7 @@ class WebDevFS implements DevFS { ...@@ -512,6 +548,7 @@ class WebDevFS implements DevFS {
final BuildMode buildMode; final BuildMode buildMode;
final bool enableDwds; final bool enableDwds;
final bool testMode; final bool testMode;
final ExpressionCompiler expressionCompiler;
WebAssetServer webAssetServer; WebAssetServer webAssetServer;
...@@ -572,6 +609,7 @@ class WebDevFS implements DevFS { ...@@ -572,6 +609,7 @@ class WebDevFS implements DevFS {
buildMode, buildMode,
enableDwds, enableDwds,
entrypoint, entrypoint,
expressionCompiler,
testMode: testMode, testMode: testMode,
); );
_baseUri = Uri.parse('http://$hostname:$port'); _baseUri = Uri.parse('http://$hostname:$port');
......
...@@ -421,6 +421,7 @@ class _ResidentWebRunner extends ResidentWebRunner { ...@@ -421,6 +421,7 @@ class _ResidentWebRunner extends ResidentWebRunner {
buildMode: debuggingOptions.buildInfo.mode, buildMode: debuggingOptions.buildInfo.mode,
enableDwds: _enableDwds, enableDwds: _enableDwds,
entrypoint: globals.fs.file(target).uri, entrypoint: globals.fs.file(target).uri,
expressionCompiler: WebExpressionCompiler(device.generator),
); );
final Uri url = await device.devFS.create(); final Uri url = await device.devFS.create();
if (debuggingOptions.buildInfo.isDebug) { if (debuggingOptions.buildInfo.isDebug) {
......
...@@ -160,6 +160,15 @@ class CodeGeneratingResidentCompiler implements ResidentCompiler { ...@@ -160,6 +160,15 @@ class CodeGeneratingResidentCompiler implements ResidentCompiler {
return _residentCompiler.compileExpression(expression, definitions, typeDefinitions, libraryUri, klass, isStatic); return _residentCompiler.compileExpression(expression, definitions, typeDefinitions, libraryUri, klass, isStatic);
} }
@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 @override
Future<CompilerOutput> recompile(String mainPath, List<Uri> invalidatedFiles, {String outputPath, String packagesFilePath}) async { Future<CompilerOutput> recompile(String mainPath, List<Uri> invalidatedFiles, {String outputPath, String packagesFilePath}) async {
if (_codegenDaemon.lastStatus != CodegenStatus.Succeeded && _codegenDaemon.lastStatus != CodegenStatus.Failed) { if (_codegenDaemon.lastStatus != CodegenStatus.Succeeded && _codegenDaemon.lastStatus != CodegenStatus.Failed) {
......
...@@ -26,7 +26,8 @@ const Map<String, String> _kManuallyPinnedDependencies = <String, String>{ ...@@ -26,7 +26,8 @@ const Map<String, String> _kManuallyPinnedDependencies = <String, String>{
'mockito': '^4.1.0', // Prevent mockito from downgrading to 4.0.0 '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. '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. '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 { class UpdatePackagesCommand extends FlutterCommand {
......
...@@ -431,6 +431,31 @@ class _CompileExpressionRequest extends _CompilationRequest { ...@@ -431,6 +431,31 @@ class _CompileExpressionRequest extends _CompilationRequest {
compiler._compileExpression(this); 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 { class _RejectRequest extends _CompilationRequest {
_RejectRequest(Completer<CompilerOutput> completer) : super(completer); _RejectRequest(Completer<CompilerOutput> completer) : super(completer);
...@@ -489,6 +514,36 @@ abstract class ResidentCompiler { ...@@ -489,6 +514,36 @@ abstract class ResidentCompiler {
bool isStatic, 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. /// Should be invoked when results of compilation are accepted by the client.
/// ///
/// Either [accept] or [reject] should be called after every [recompile] call. /// Either [accept] or [reject] should be called after every [recompile] call.
...@@ -778,6 +833,52 @@ class DefaultResidentCompiler implements ResidentCompiler { ...@@ -778,6 +833,52 @@ class DefaultResidentCompiler implements ResidentCompiler {
return _stdoutHandler.compilerOutput.future; return _stdoutHandler.compilerOutput.future;
} }
@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 @override
void accept() { void accept() {
if (_compileRequestNeedsConfirmation) { if (_compileRequestNeedsConfirmation) {
......
...@@ -11,7 +11,7 @@ dependencies: ...@@ -11,7 +11,7 @@ dependencies:
# To update these, use "flutter update-packages --force-upgrade". # To update these, use "flutter update-packages --force-upgrade".
archive: 2.0.13 archive: 2.0.13
args: 1.6.0 args: 1.6.0
dwds: 3.0.1 dwds: 3.0.2
completion: 0.2.2 completion: 0.2.2
coverage: 0.13.9 coverage: 0.13.9
crypto: 2.1.4 crypto: 2.1.4
...@@ -112,4 +112,4 @@ dartdoc: ...@@ -112,4 +112,4 @@ dartdoc:
# Exclude this package from the hosted API docs. # Exclude this package from the hosted API docs.
nodoc: true nodoc: true
# PUBSPEC CHECKSUM: 65c1 # PUBSPEC CHECKSUM: 53c2
...@@ -373,6 +373,7 @@ void main() { ...@@ -373,6 +373,7 @@ void main() {
enableDwds: false, enableDwds: false,
entrypoint: Uri.base, entrypoint: Uri.base,
testMode: true, testMode: true,
expressionCompiler: null,
); );
webDevFS.requireJS.createSync(recursive: true); webDevFS.requireJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true); webDevFS.stackTraceMapper.createSync(recursive: true);
......
// 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();
}
...@@ -49,6 +49,7 @@ class TestsProject extends Project { ...@@ -49,6 +49,7 @@ class TestsProject extends Project {
String get testFilePath => globals.fs.path.join(dir.path, 'test', 'test.dart'); String get testFilePath => globals.fs.path.join(dir.path, 'test', 'test.dart');
Uri get breakpointUri => Uri.file(testFilePath); Uri get breakpointUri => Uri.file(testFilePath);
Uri get breakpointAppUri => Uri.parse('org-dartlang-app:///test.dart');
int get breakpointLine => lineContaining(testContent, '// BREAKPOINT'); int get breakpointLine => lineContaining(testContent, '// BREAKPOINT');
} }
...@@ -436,6 +436,7 @@ class FlutterRunTestDriver extends FlutterTestDriver { ...@@ -436,6 +436,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
bool pauseOnExceptions = false, bool pauseOnExceptions = false,
bool chrome = false, bool chrome = false,
File pidFile, File pidFile,
String script,
}) async { }) async {
await _setupProcess( await _setupProcess(
<String>[ <String>[
...@@ -453,6 +454,7 @@ class FlutterRunTestDriver extends FlutterTestDriver { ...@@ -453,6 +454,7 @@ class FlutterRunTestDriver extends FlutterTestDriver {
startPaused: startPaused, startPaused: startPaused,
pauseOnExceptions: pauseOnExceptions, pauseOnExceptions: pauseOnExceptions,
pidFile: pidFile, pidFile: pidFile,
script: script,
); );
} }
......
...@@ -646,6 +646,20 @@ class MockResidentCompiler extends BasicMock implements ResidentCompiler { ...@@ -646,6 +646,20 @@ class MockResidentCompiler extends BasicMock implements ResidentCompiler {
) async { ) async {
return null; 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 @override
Future<CompilerOutput> recompile(String mainPath, List<Uri> invalidatedFiles, { String outputPath, String packagesFilePath }) async { Future<CompilerOutput> recompile(String mainPath, List<Uri> invalidatedFiles, { String outputPath, String packagesFilePath }) async {
globals.fs.file(outputPath).createSync(recursive: true); globals.fs.file(outputPath).createSync(recursive: true);
......
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