diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart index a60bdfb8ca..30f2e6b9a6 100644 --- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart +++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart @@ -17,6 +17,7 @@ import 'package:mime/mime.dart' as mime; import 'package:package_config/package_config.dart'; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf; +import 'package:vm_service/vm_service.dart' as vm_service; import '../artifacts.dart'; import '../asset.dart'; @@ -35,6 +36,7 @@ import '../dart/package_map.dart'; import '../devfs.dart'; import '../globals.dart' as globals; import '../project.dart'; +import '../vmservice.dart'; import '../web/bootstrap.dart'; import '../web/chrome.dart'; import '../web/compile.dart'; @@ -592,10 +594,11 @@ class WebAssetServer implements AssetReader { } class ConnectionResult { - ConnectionResult(this.appConnection, this.debugConnection); + ConnectionResult(this.appConnection, this.debugConnection, this.vmService); final AppConnection appConnection; final DebugConnection debugConnection; + final vm_service.VmService vmService; } /// The web specific DevFS implementation. @@ -665,8 +668,12 @@ class WebDevFS implements DevFS { if (firstConnection.isCompleted) { appConnection.runMain(); } else { + final vm_service.VmService vmService = await createVmServiceDelegate( + Uri.parse(debugConnection.uri), + logger: globals.logger, + ); firstConnection - .complete(ConnectionResult(appConnection, debugConnection)); + .complete(ConnectionResult(appConnection, debugConnection, vmService)); } } on Exception catch (error, stackTrace) { if (!firstConnection.isCompleted) { diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index 8510a22c4d..4b3b4991ea 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -155,7 +155,7 @@ class ResidentWebRunner extends ResidentRunner { if (_instance != null) { return _instance; } - final vmservice.VmService service =_connectionResult?.debugConnection?.vmService; + final vmservice.VmService service =_connectionResult?.vmService; final Uri websocketUri = Uri.parse(_connectionResult.debugConnection.uri); final Uri httpUri = _httpUriFromWebsocketUri(websocketUri); return _instance ??= FlutterVmService(service, wsAddress: websocketUri, httpAddress: httpUri); @@ -835,7 +835,7 @@ class ResidentWebRunner extends ResidentRunner { _connectionResult.appConnection.runMain(); } else { StreamSubscription resumeSub; - resumeSub = _connectionResult.debugConnection.vmService.onDebugEvent + resumeSub = _vmService.service.onDebugEvent .listen((vmservice.Event event) { if (event.type == vmservice.EventKind.kResume) { _connectionResult.appConnection.runMain(); diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 0a416573f7..020030dc7e 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -326,6 +326,22 @@ Future connectToVmService( ); } +Future createVmServiceDelegate( + Uri wsUri, { + io.CompressionOptions compression = io.CompressionOptions.compressionDefault, + @required Logger logger, + }) async { + final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression, logger: logger); + return vm_service.VmService( + channel, + channel.add, + log: null, + disposeHandler: () async { + await channel.close(); + }, + ); +} + Future _connect( Uri httpUri, { ReloadSources reloadSources, @@ -338,14 +354,8 @@ Future _connect( @required Logger logger, }) async { final Uri wsUri = httpUri.replace(scheme: 'ws', path: urlContext.join(httpUri.path, 'ws')); - final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression, logger: logger); - final vm_service.VmService delegateService = vm_service.VmService( - channel, - channel.add, - log: null, - disposeHandler: () async { - await channel.close(); - }, + final vm_service.VmService delegateService = await createVmServiceDelegate( + wsUri, compression: compression, logger: logger, ); final vm_service.VmService service = await setUpVmService( diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 3b025df2ee..3814cfe214 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -130,7 +130,11 @@ void main() { when(mockFlutterDevice.devFS).thenReturn(mockWebDevFS); when(mockFlutterDevice.device).thenReturn(mockDevice); when(mockWebDevFS.connect(any)).thenAnswer((Invocation invocation) async { - return ConnectionResult(mockAppConnection, mockDebugConnection); + return ConnectionResult( + mockAppConnection, + mockDebugConnection, + mockDebugConnection.vmService, + ); }); fileSystem.file('.packages').writeAsStringSync('\n'); }); diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart index 08b9ae8e19..e7b6d639f5 100644 --- a/packages/flutter_tools/test/integration.shard/test_driver.dart +++ b/packages/flutter_tools/test/integration.shard/test_driver.dart @@ -63,6 +63,7 @@ abstract class FlutterTestDriver { Stream get stdout => _stdout.stream; int get vmServicePort => _vmServiceWsUri.port; bool get hasExited => _hasExited; + Uri get vmServiceWsUri => _vmServiceWsUri; String lastTime = ''; void _debugPrint(String message, { String topic = '' }) { @@ -219,7 +220,7 @@ abstract class FlutterTestDriver { return _flutterIsolateId; } - Future _getFlutterIsolate() async { + Future getFlutterIsolate() async { final Isolate isolate = await _vmService.getIsolate(await _getFlutterIsolateId()); return isolate; } @@ -281,7 +282,7 @@ abstract class FlutterTestDriver { // Cancel the subscription on either of the above. await pauseSubscription.cancel(); - return _getFlutterIsolate(); + return getFlutterIsolate(); }, task: 'Waiting for isolate to pause', ); @@ -294,7 +295,7 @@ abstract class FlutterTestDriver { Future stepOut({ bool waitForNextPause = true }) => _resume(StepOption.kOut, waitForNextPause); Future isAtAsyncSuspension() async { - final Isolate isolate = await _getFlutterIsolate(); + final Isolate isolate = await getFlutterIsolate(); return isolate.pauseEvent.atAsyncSuspension == true; } diff --git a/packages/flutter_tools/test/web.shard/expression_evaluation_web_test.dart b/packages/flutter_tools/test/web.shard/expression_evaluation_web_test.dart index 61ccfb1a42..f315f504cc 100644 --- a/packages/flutter_tools/test/web.shard/expression_evaluation_web_test.dart +++ b/packages/flutter_tools/test/web.shard/expression_evaluation_web_test.dart @@ -13,160 +13,163 @@ import '../integration.shard/test_driver.dart'; import '../integration.shard/test_utils.dart'; import '../src/common.dart'; -void batch1() { - final BasicProject _project = BasicProject(); - Directory tempDir; - FlutterRunTestDriver _flutter; +void main() { + group('Flutter run for web', () { + final BasicProject project = BasicProject(); + Directory tempDir; + FlutterRunTestDriver flutter; - Future initProject() async { - tempDir = createResolvedTempDirectorySync('run_expression_eval_test.'); - await _project.setUpIn(tempDir); - _flutter = FlutterRunTestDriver(tempDir); - } + setUp(() async { + tempDir = createResolvedTempDirectorySync('run_expression_eval_test.'); + await project.setUpIn(tempDir); + flutter = FlutterRunTestDriver(tempDir); + }); - Future cleanProject() async { - await _flutter.stop(); - tryToDelete(tempDir); - } + tearDown(() async { + await flutter.stop(); + tryToDelete(tempDir); + }); - Future start({bool expressionEvaluation}) { - // The non-test project has a loop around its breakpoints. - // No need to start paused as all breakpoint would be eventually reached. - return _flutter.run( - withDebugger: true, chrome: true, - expressionEvaluation: expressionEvaluation, - additionalCommandArgs: ['--verbose']); - } + Future start({bool expressionEvaluation}) async { + // The non-test project has a loop around its breakpoints. + // No need to start paused as all breakpoint would be eventually reached. + await flutter.run( + withDebugger: true, chrome: true, + expressionEvaluation: expressionEvaluation, + additionalCommandArgs: ['--verbose']); + } - Future breakInBuildMethod(FlutterTestDriver flutter) async { - await _flutter.breakAt( - _project.buildMethodBreakpointUri, - _project.buildMethodBreakpointLine, - ); - } + Future breakInBuildMethod(FlutterTestDriver flutter) async { + await flutter.breakAt( + project.buildMethodBreakpointUri, + project.buildMethodBreakpointLine, + ); + } - Future breakInTopLevelFunction(FlutterTestDriver flutter) async { - await _flutter.breakAt( - _project.topLevelFunctionBreakpointUri, - _project.topLevelFunctionBreakpointLine, - ); - } + Future breakInTopLevelFunction(FlutterTestDriver flutter) async { + await flutter.breakAt( + project.topLevelFunctionBreakpointUri, + project.topLevelFunctionBreakpointLine, + ); + } - testWithoutContext('flutter run expression evaluation - error if expression evaluation disabled', () async { - await initProject(); - await start(expressionEvaluation: false); - await breakInTopLevelFunction(_flutter); - await failToEvaluateExpression(_flutter); - await cleanProject(); + testWithoutContext('cannot evaluate expression if feature is disabled', () async { + await start(expressionEvaluation: false); + await breakInTopLevelFunction(flutter); + await failToEvaluateExpression(flutter); + }); + + testWithoutContext('shows no native javascript objects in static scope', () async { + await start(expressionEvaluation: true); + await breakInTopLevelFunction(flutter); + await checkStaticScope(flutter); + }); + + testWithoutContext('can handle compilation errors', () async { + await start(expressionEvaluation: true); + await breakInTopLevelFunction(flutter); + await evaluateErrorExpressions(flutter); + }); + + testWithoutContext('can evaluate trivial expressions in top level function', () async { + await start(expressionEvaluation: true); + await breakInTopLevelFunction(flutter); + await evaluateTrivialExpressions(flutter); + }); + + testWithoutContext('can evaluate trivial expressions in build method', () async { + await start(expressionEvaluation: true); + await breakInBuildMethod(flutter); + await evaluateTrivialExpressions(flutter); + }); + + testWithoutContext('can evaluate complex expressions in top level function', () async { + await start(expressionEvaluation: true); + await breakInTopLevelFunction(flutter); + await evaluateComplexExpressions(flutter); + }); + + testWithoutContext('can evaluate complex expressions in build method', () async { + await start(expressionEvaluation: true); + await breakInBuildMethod(flutter); + await evaluateComplexExpressions(flutter); + }); + + testWithoutContext('can evaluate trivial expressions in library without pause', () async { + await start(expressionEvaluation: true); + await evaluateTrivialExpressionsInLibrary(flutter); + }); + + testWithoutContext('can evaluate complex expressions in library without pause', () async { + await start(expressionEvaluation: true); + await evaluateComplexExpressionsInLibrary(flutter); + }); }); - testWithoutContext('flutter run expression evaluation - no native javascript objects in static scope', () async { - await initProject(); - await start(expressionEvaluation: true); - await breakInTopLevelFunction(_flutter); - await checkStaticScope(_flutter); - await cleanProject(); - }); - testWithoutContext('flutter run expression evaluation - can handle compilation errors', () async { - await initProject(); - await start(expressionEvaluation: true); - await breakInTopLevelFunction(_flutter); - await evaluateErrorExpressions(_flutter); - await cleanProject(); - }); + group('Flutter test for web', () { + final TestsProject project = TestsProject(); + Directory tempDir; + FlutterRunTestDriver flutter; - testWithoutContext('flutter run expression evaluation - can evaluate trivial expressions in top level function', () async { - await initProject(); - await start(expressionEvaluation: true); - await breakInTopLevelFunction(_flutter); - await evaluateTrivialExpressions(_flutter); - await cleanProject(); - }); + setUp(() async { + tempDir = createResolvedTempDirectorySync('run_expression_eval_test.'); + await project.setUpIn(tempDir); + flutter = FlutterRunTestDriver(tempDir); + }); - testWithoutContext('flutter run expression evaluation - can evaluate trivial expressions in build method', () async { - await initProject(); - await start(expressionEvaluation: true); - await breakInBuildMethod(_flutter); - await evaluateTrivialExpressions(_flutter); - await cleanProject(); - }); + tearDown(() async { + await flutter.stop(); + tryToDelete(tempDir); + }); - testWithoutContext('flutter run expression evaluation - can evaluate complex expressions in top level function', () async { - await initProject(); - await start(expressionEvaluation: true); - await breakInTopLevelFunction(_flutter); - await evaluateComplexExpressions(_flutter); - await cleanProject(); - }); + Future breakInMethod(FlutterTestDriver flutter) async { + await flutter.addBreakpoint( + project.breakpointAppUri, + project.breakpointLine, + ); + await flutter.resume(); + return flutter.waitForPause(); + } - testWithoutContext('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(); - }); -} + Future startPaused({bool expressionEvaluation}) { + // The test project does not have a loop around its breakpoints. + // Start paused so we can set a breakpoint before passing it + // in the execution. + return flutter.run( + withDebugger: true, chrome: true, + expressionEvaluation: expressionEvaluation, + startPaused: true, script: project.testFilePath, + additionalCommandArgs: ['--verbose']); + } -void batch2() { - final TestsProject _project = TestsProject(); - Directory tempDir; - FlutterRunTestDriver _flutter; + testWithoutContext('cannot evaluate expressions if feature is disabled', () async { + await startPaused(expressionEvaluation: false); + await breakInMethod(flutter); + await failToEvaluateExpression(flutter); + }); - Future initProject() async { - tempDir = createResolvedTempDirectorySync('test_expression_eval_test.'); - await _project.setUpIn(tempDir); - _flutter = FlutterRunTestDriver(tempDir); - } + testWithoutContext('can evaluate trivial expressions in a test', () async { + await startPaused(expressionEvaluation: true); + await breakInMethod(flutter); + await evaluateTrivialExpressions(flutter); + }); - Future cleanProject() async { - await _flutter.stop(); - tryToDelete(tempDir); - } + testWithoutContext('can evaluate complex expressions in a test', () async { + await startPaused(expressionEvaluation: true); + await breakInMethod(flutter); + await evaluateComplexExpressions(flutter); + }); - Future breakInMethod(FlutterTestDriver flutter) async { - await _flutter.addBreakpoint( - _project.breakpointAppUri, - _project.breakpointLine, - ); - await _flutter.resume(); - await _flutter.waitForPause(); - } + testWithoutContext('can evaluate trivial expressions in library without pause', () async { + await startPaused(expressionEvaluation: true); + await evaluateTrivialExpressionsInLibrary(flutter); + }); - Future startPaused({bool expressionEvaluation}) { - // The test project does not have a loop around its breakpoints. - // Start paused so we can set a breakpoint before passing it - // in the execution. - return _flutter.run( - withDebugger: true, chrome: true, - expressionEvaluation: expressionEvaluation, - startPaused: true, script: _project.testFilePath, - additionalCommandArgs: ['--verbose']); - } - - testWithoutContext('flutter test expression evaluation - error if expression evaluation disabled', () async { - await initProject(); - await startPaused(expressionEvaluation: false); - await breakInMethod(_flutter); - await failToEvaluateExpression(_flutter); - await cleanProject(); - }); - - testWithoutContext('flutter test expression evaluation - can evaluate trivial expressions in a test', () async { - await initProject(); - await startPaused(expressionEvaluation: true); - await breakInMethod(_flutter); - await evaluateTrivialExpressions(_flutter); - await cleanProject(); - }); - - testWithoutContext('flutter test expression evaluation - can evaluate complex expressions in a test', () async { - await initProject(); - await startPaused(expressionEvaluation: true); - await breakInMethod(_flutter); - await evaluateComplexExpressions(_flutter); - await cleanProject(); + testWithoutContext('can evaluate complex expressions in library without pause', () async { + await startPaused(expressionEvaluation: true); + await evaluateComplexExpressionsInLibrary(flutter); + }); }); } @@ -208,6 +211,28 @@ Future evaluateComplexExpressions(FlutterTestDriver flutter) async { expectInstance(res, InstanceKind.kDouble, DateTime.now().year.toString()); } +Future evaluateTrivialExpressionsInLibrary(FlutterTestDriver flutter) async { + final LibraryRef library = await getRootLibrary(flutter); + final ObjRef res = await flutter.evaluate(library.id, '"test"'); + expectInstance(res, InstanceKind.kString, 'test'); +} + +Future evaluateComplexExpressionsInLibrary(FlutterTestDriver flutter) async { + final LibraryRef library = await getRootLibrary(flutter); + final ObjRef res = await flutter.evaluate(library.id, 'new DateTime.now().year'); + expectInstance(res, InstanceKind.kDouble, DateTime.now().year.toString()); +} + +Future getRootLibrary(FlutterTestDriver flutter) async { + // `isolate.rootLib` returns incorrect library, so find the + // entrypoint manually here instead. + // + // Issue: https://github.com/dart-lang/sdk/issues/44760 + final Isolate isolate = await flutter.getFlutterIsolate(); + return isolate.libraries + .firstWhere((LibraryRef l) => l.uri.contains('org-dartlang-app')); +} + void expectInstance(ObjRef result, String kind, String message) { expect(result, const TypeMatcher() @@ -220,8 +245,3 @@ void expectError(ObjRef result, String message) { const TypeMatcher() .having((ErrorRef instance) => instance.message, 'message', message)); } - -void main() { - batch1(); - batch2(); -} diff --git a/packages/flutter_tools/test/web.shard/vm_service_web_test.dart b/packages/flutter_tools/test/web.shard/vm_service_web_test.dart new file mode 100644 index 0000000000..4bf95b30c0 --- /dev/null +++ b/packages/flutter_tools/test/web.shard/vm_service_web_test.dart @@ -0,0 +1,139 @@ +// 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. + +// @dart = 2.8 + +import 'dart:async'; + +import 'package:file/file.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:vm_service/vm_service.dart'; +import 'package:vm_service/vm_service_io.dart'; + +import '../integration.shard/test_data/basic_project.dart'; +import '../integration.shard/test_driver.dart'; +import '../integration.shard/test_utils.dart'; +import '../src/common.dart'; + +void main() { + Directory tempDir; + final BasicProjectWithUnaryMain project = BasicProjectWithUnaryMain(); + FlutterRunTestDriver flutter; + + group('Clients of flutter run on web with DDS enabled', () { + setUp(() async { + tempDir = createResolvedTempDirectorySync('run_test.'); + await project.setUpIn(tempDir); + flutter = FlutterRunTestDriver(tempDir, spawnDdsInstance: true); + }); + + tearDown(() async { + await flutter.stop(); + tryToDelete(tempDir); + }); + + testWithoutContext('can validate flutter version', () async { + await flutter.run( + withDebugger: true, chrome: true, + additionalCommandArgs: ['--verbose']); + + expect(flutter.vmServiceWsUri, isNotNull); + + final VmService client = + await vmServiceConnectUri('${flutter.vmServiceWsUri}'); + await validateFlutterVersion(client); + }); + + testWithoutContext('can validate flutter version in parallel', () async { + await flutter.run( + withDebugger: true, chrome: true, + additionalCommandArgs: ['--verbose']); + + expect(flutter.vmServiceWsUri, isNotNull); + + final VmService client1 = + await vmServiceConnectUri('${flutter.vmServiceWsUri}'); + + final VmService client2 = + await vmServiceConnectUri('${flutter.vmServiceWsUri}'); + + await Future.wait(>[ + validateFlutterVersion(client1), + validateFlutterVersion(client2)] + ); + }, skip: 'DDS failure: https://github.com/dart-lang/sdk/issues/45569'); + }); + + group('Clients of flutter run on web with DDS disabled', () { + setUp(() async { + tempDir = createResolvedTempDirectorySync('run_test.'); + await project.setUpIn(tempDir); + flutter = FlutterRunTestDriver(tempDir, spawnDdsInstance: false); + }); + + tearDown(() async { + await flutter.stop(); + tryToDelete(tempDir); + }); + + testWithoutContext('can validate flutter version', () async { + await flutter.run( + withDebugger: true, chrome: true, + additionalCommandArgs: ['--verbose']); + + expect(flutter.vmServiceWsUri, isNotNull); + + final VmService client = + await vmServiceConnectUri('${flutter.vmServiceWsUri}'); + await validateFlutterVersion(client); + }); + + + testWithoutContext('can validate flutter version in parallel', () async { + await flutter.run( + withDebugger: true, chrome: true, + additionalCommandArgs: ['--verbose']); + + expect(flutter.vmServiceWsUri, isNotNull); + + final VmService client1 = + await vmServiceConnectUri('${flutter.vmServiceWsUri}'); + + final VmService client2 = + await vmServiceConnectUri('${flutter.vmServiceWsUri}'); + + await Future.wait(>[ + validateFlutterVersion(client1), + validateFlutterVersion(client2)] + ); + }); + }); +} + +Future validateFlutterVersion(VmService client) async { + String method; + + final Future registration = expectLater( + client.onEvent('Service'), + emitsThrough(predicate((Event e) { + if (e.kind == EventKind.kServiceRegistered && + e.service == 'flutterVersion') { + method = e.method; + return true; + } + return false; + })) + ); + + await client.streamListen('Service'); + await registration; + await client.streamCancel('Service'); + + final dynamic version1 = await client.callServiceExtension(method); + expect(version1, const TypeMatcher() + .having((Success r) => r.type, 'type', 'Success') + .having((Success r) => r.json['frameworkVersion'], 'frameworkVersion', isNotNull)); + + await client.dispose(); +}