| // Copyright 2019 The Chromium 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 'package:devtools_app/src/banner_messages.dart'; |
| import 'package:devtools_app/src/connected_app.dart'; |
| import 'package:devtools_app/src/debugger/debugger_controller.dart'; |
| import 'package:devtools_app/src/initializer.dart' as initializer; |
| import 'package:devtools_app/src/logging/logging_controller.dart'; |
| import 'package:devtools_app/src/memory/memory_controller.dart' |
| as flutter_memory; |
| import 'package:devtools_app/src/memory/memory_controller.dart'; |
| import 'package:devtools_app/src/performance/performance_controller.dart'; |
| import 'package:devtools_app/src/profiler/cpu_profile_model.dart'; |
| import 'package:devtools_app/src/profiler/profile_granularity.dart'; |
| import 'package:devtools_app/src/service_extensions.dart' as extensions; |
| import 'package:devtools_app/src/service_manager.dart'; |
| import 'package:devtools_app/src/stream_value_listenable.dart'; |
| import 'package:devtools_app/src/timeline/timeline_controller.dart'; |
| import 'package:devtools_app/src/utils.dart'; |
| import 'package:devtools_app/src/vm_flags.dart' as vm_flags; |
| import 'package:devtools_app/src/vm_service_wrapper.dart'; |
| import 'package:devtools_testing/support/cpu_profile_test_data.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:vm_service/vm_service.dart'; |
| |
| class FakeServiceManager extends Fake implements ServiceConnectionManager { |
| FakeServiceManager({ |
| bool useFakeService = false, |
| this.hasConnection = true, |
| this.availableServices = const [], |
| Timeline timelineData, |
| }) : service = useFakeService |
| ? FakeVmService(_flagManager, timelineData) |
| : MockVmService() { |
| _flagManager.service = service; |
| } |
| |
| static final _flagManager = VmFlagManager(); |
| |
| final List<String> availableServices; |
| |
| final MockVM _mockVM = MockVM(); |
| |
| @override |
| final VmServiceWrapper service; |
| |
| @override |
| Future<VmService> onServiceAvailable = Future.value(); |
| |
| @override |
| bool get isServiceAvailable => hasConnection; |
| |
| @override |
| final ConnectedApp connectedApp = MockConnectedApp(); |
| |
| @override |
| Stream<VmServiceWrapper> get onConnectionClosed => const Stream.empty(); |
| |
| @override |
| Stream<VmServiceWrapper> get onConnectionAvailable => Stream.value(service); |
| |
| @override |
| Future<double> getDisplayRefreshRate() async => 60; |
| |
| @override |
| bool hasConnection; |
| |
| @override |
| final IsolateManager isolateManager = FakeIsolateManager(); |
| |
| @override |
| VM get vm => _mockVM; |
| |
| @override |
| final VmFlagManager vmFlagManager = _flagManager; |
| |
| @override |
| final FakeServiceExtensionManager serviceExtensionManager = |
| FakeServiceExtensionManager(); |
| |
| @override |
| ValueListenable<bool> registeredServiceListenable(String name) { |
| if (availableServices.contains(name)) { |
| return ImmediateValueNotifier(true); |
| } |
| return ImmediateValueNotifier(false); |
| } |
| |
| @override |
| Future<Response> getFlutterVersion() { |
| return Future.value(Response.parse({ |
| 'type': 'Success', |
| 'frameworkVersion': '1.19.0-2.0.pre.59', |
| 'channel': 'unknown', |
| 'repositoryUrl': 'unknown source', |
| 'frameworkRevision': '74432fa91c8ffbc555ffc2701309e8729380a012', |
| 'frameworkCommitDate': '2020-05-14 13:05:34 -0700', |
| 'engineRevision': 'ae2222f47e788070c09020311b573542b9706a78', |
| 'dartSdkVersion': '2.9.0 (build 2.9.0-8.0.dev d6fed1f624)', |
| 'frameworkRevisionShort': '74432fa91c', |
| 'engineRevisionShort': 'ae2222f47e', |
| })); |
| } |
| |
| @override |
| Stream<bool> get onStateChange => stateChangeStream.stream; |
| |
| StreamController<bool> stateChangeStream = StreamController(); |
| |
| void changeState(bool value) { |
| hasConnection = value; |
| stateChangeStream.add(value); |
| } |
| } |
| |
| class FakeVmService extends Fake implements VmServiceWrapper { |
| FakeVmService( |
| this._vmFlagManager, |
| this._timelineData, |
| ); |
| |
| /// Specifies the return value of `httpEnableTimelineLogging`. |
| bool httpEnableTimelineLoggingResult = true; |
| |
| final VmFlagManager _vmFlagManager; |
| final Timeline _timelineData; |
| |
| final _flags = <String, dynamic>{ |
| 'flags': <Flag>[ |
| Flag( |
| name: 'flag 1 name', |
| comment: 'flag 1 comment contains some very long text ' |
| 'that the renderer will have to wrap around to prevent ' |
| 'it from overflowing the screen. This will cause a ' |
| 'failure if one of the two Row entries the flags lay out ' |
| 'in is not wrapped in an Expanded(), which tells the Row ' |
| 'allocate only the remaining space to the Expanded. ' |
| 'Without the expanded, the underlying RichTexts will try ' |
| 'to consume as much of the layout as they can and cause ' |
| 'an overflow.', |
| valueAsString: 'flag 1 value', |
| modified: false, |
| ), |
| Flag( |
| name: vm_flags.profiler, |
| comment: 'Mock Flag', |
| valueAsString: 'true', |
| modified: false, |
| ), |
| Flag( |
| name: vm_flags.profilePeriod, |
| comment: 'Mock Flag', |
| valueAsString: ProfileGranularity.medium.value, |
| modified: false, |
| ), |
| ], |
| }; |
| |
| @override |
| Future<void> forEachIsolate(Future<void> Function(IsolateRef) callback) => |
| callback( |
| IsolateRef.parse( |
| { |
| 'id': 'fake_isolate_id', |
| }, |
| ), |
| ); |
| |
| @override |
| Future<Isolate> getIsolate(String isolateId) { |
| return Future.value(MockIsolate()); |
| } |
| |
| @override |
| Future<ScriptList> getScripts(String isolateId) { |
| return Future.value(ScriptList(scripts: [])); |
| } |
| |
| @override |
| Future<Stack> getStack(String isolateId) { |
| return Future.value(Stack(frames: [], messages: [])); |
| } |
| |
| @override |
| Future<Success> setFlag(String name, String value) { |
| final List<Flag> flags = _flags['flags']; |
| final existingFlag = |
| flags.firstWhere((f) => f.name == name, orElse: () => null); |
| if (existingFlag != null) { |
| existingFlag.valueAsString = value; |
| } else { |
| flags.add(Flag.parse({ |
| 'name': name, |
| 'comment': 'Mock Flag', |
| 'modified': true, |
| 'valueAsString': value, |
| })); |
| } |
| |
| final fakeVmFlagUpdateEvent = Event( |
| kind: EventKind.kVMFlagUpdate, |
| flag: name, |
| newValue: value, |
| timestamp: 1, // 1 is arbitrary. |
| ); |
| _vmFlagManager.handleVmEvent(fakeVmFlagUpdateEvent); |
| return Future.value(Success()); |
| } |
| |
| @override |
| Future<FlagList> getFlagList() => Future.value(FlagList.parse(_flags)); |
| |
| final _vmTimelineFlags = <String, dynamic>{ |
| 'type': 'TimelineFlags', |
| 'recordedStreams': [], |
| 'availableStreams': [], |
| }; |
| |
| @override |
| Future<Success> setVMTimelineFlags(List<String> recordedStreams) async { |
| _vmTimelineFlags['recordedStreams'] = recordedStreams; |
| return Future.value(Success()); |
| } |
| |
| @override |
| Future<TimelineFlags> getVMTimelineFlags() => |
| Future.value(TimelineFlags.parse(_vmTimelineFlags)); |
| |
| @override |
| Future<Timeline> getVMTimeline({ |
| int timeOriginMicros, |
| int timeExtentMicros, |
| }) async { |
| if (_timelineData == null) { |
| throw StateError('timelineData was not provided to FakeServiceManager'); |
| } |
| return _timelineData; |
| } |
| |
| @override |
| Future<Success> clearVMTimeline() => Future.value(Success()); |
| |
| @override |
| Future<CpuProfileData> getCpuProfileTimeline( |
| String isolateId, |
| int origin, |
| int extent, |
| ) { |
| return Future.value(CpuProfileData.parse(goldenCpuProfileDataJson)); |
| } |
| |
| @override |
| Future<Success> clearCpuSamples(String isolateId) => Future.value(Success()); |
| |
| @override |
| Future<bool> isHttpTimelineLoggingAvailable(String isolateId) => |
| Future.value(true); |
| |
| @override |
| Future<HttpTimelineLoggingState> getHttpEnableTimelineLogging( |
| String isolateId) async => |
| HttpTimelineLoggingState(enabled: httpEnableTimelineLoggingResult); |
| |
| @override |
| Future<Success> setHttpEnableTimelineLogging( |
| String isolateId, |
| bool enable, |
| ) async => |
| Success(); |
| |
| @override |
| Future<Timestamp> getVMTimelineMicros() async => Timestamp(timestamp: 0); |
| |
| @override |
| Stream<Event> onEvent(String streamName) => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onStdoutEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onStderrEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onGCEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onLoggingEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onExtensionEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onDebugEvent => const Stream.empty(); |
| } |
| |
| class FakeIsolateManager extends Fake implements IsolateManager { |
| @override |
| IsolateRef get selectedIsolate => IsolateRef.parse({'id': 'fake_isolate_id'}); |
| |
| @override |
| Stream<IsolateRef> get onSelectedIsolateChanged => const Stream.empty(); |
| } |
| |
| class MockServiceManager extends Mock implements ServiceConnectionManager {} |
| |
| class MockVmService extends Mock implements VmServiceWrapper {} |
| |
| class MockIsolate extends Mock implements Isolate {} |
| |
| class MockConnectedApp extends Mock implements ConnectedApp {} |
| |
| class MockBannerMessagesController extends Mock |
| implements BannerMessagesController {} |
| |
| class MockLoggingController extends Mock implements LoggingController {} |
| |
| class MockMemoryController extends Mock implements MemoryController {} |
| |
| class MockFlutterMemoryController extends Mock |
| implements flutter_memory.MemoryController {} |
| |
| class MockTimelineController extends Mock implements TimelineController {} |
| |
| class MockPerformanceController extends Mock implements PerformanceController {} |
| |
| class MockDebuggerController extends Mock implements DebuggerController {} |
| |
| class MockVM extends Mock implements VM {} |
| |
| /// Fake that simplifies writing UI tests that depend on the |
| /// ServiceExtensionManager. |
| // TODO(jacobr): refactor ServiceExtensionManager so this fake can reuse more |
| // code from ServiceExtensionManager instead of reimplementing it. |
| class FakeServiceExtensionManager extends Fake |
| implements ServiceExtensionManager { |
| bool _firstFrameEventReceived = false; |
| |
| final Map<String, StreamController<bool>> _serviceExtensionController = {}; |
| final Map<String, StreamController<ServiceExtensionState>> |
| _serviceExtensionStateController = {}; |
| |
| final Map<String, ValueListenable<bool>> _serviceExtensionListenables = {}; |
| |
| /// All available service extensions. |
| final _serviceExtensions = <String>{}; |
| |
| /// All service extensions that are currently enabled. |
| final Map<String, ServiceExtensionState> _enabledServiceExtensions = {}; |
| |
| /// Temporarily stores service extensions that we need to add. We should not |
| /// add extensions until the first frame event has been received |
| /// [_firstFrameEventReceived]. |
| final Set<String> _pendingServiceExtensions = {}; |
| |
| @override |
| Completer<void> extensionStatesUpdated = Completer(); |
| |
| /// Hook to simulate receiving the first frame event. |
| /// |
| /// Service extensions are only reported once a frame has been received. |
| void fakeFrame() async { |
| await _onFrameEventReceived(); |
| } |
| |
| Map<String, dynamic> extensionValueOnDevice = {}; |
| |
| @override |
| ValueListenable<bool> hasServiceExtensionListener(String name) { |
| return _serviceExtensionListenables.putIfAbsent( |
| name, |
| () => StreamValueListenable<bool>( |
| (notifier) { |
| return hasServiceExtension(name, (value) { |
| notifier.value = value; |
| }); |
| }, |
| () => _hasServiceExtensionNow(name), |
| ), |
| ); |
| } |
| |
| bool _hasServiceExtensionNow(String name) { |
| return _serviceExtensions.contains(name); |
| } |
| |
| /// Hook for tests to call to simulate adding a service extension. |
| Future<void> fakeAddServiceExtension(String name) async { |
| if (_firstFrameEventReceived) { |
| assert(_pendingServiceExtensions.isEmpty); |
| await _addServiceExtension(name); |
| } else { |
| _pendingServiceExtensions.add(name); |
| } |
| } |
| |
| /// Hook for tests to call to fake changing the state of a service |
| /// extension. |
| void fakeServiceExtensionStateChanged( |
| final String name, |
| String valueFromJson, |
| ) async { |
| final extension = extensions.serviceExtensionsAllowlist[name]; |
| if (extension != null) { |
| final dynamic value = _getExtensionValueFromJson(name, valueFromJson); |
| |
| final enabled = |
| extension is extensions.ToggleableServiceExtensionDescription |
| ? value == extension.enabledValue |
| // For extensions that have more than two states |
| // (enabled / disabled), we will always consider them to be |
| // enabled with the current value. |
| : true; |
| |
| await setServiceExtensionState( |
| name, |
| enabled, |
| value, |
| callExtension: false, |
| ); |
| } |
| } |
| |
| dynamic _getExtensionValueFromJson(String name, String valueFromJson) { |
| final expectedValueType = |
| extensions.serviceExtensionsAllowlist[name].values.first.runtimeType; |
| switch (expectedValueType) { |
| case bool: |
| return valueFromJson == 'true' ? true : false; |
| case int: |
| case double: |
| return num.parse(valueFromJson); |
| default: |
| return valueFromJson; |
| } |
| } |
| |
| Future<void> _onFrameEventReceived() async { |
| if (_firstFrameEventReceived) { |
| // The first frame event was already received. |
| return; |
| } |
| _firstFrameEventReceived = true; |
| |
| for (String extension in _pendingServiceExtensions) { |
| await _addServiceExtension(extension); |
| } |
| extensionStatesUpdated.complete(); |
| _pendingServiceExtensions.clear(); |
| } |
| |
| Future<void> _addServiceExtension(String name) async { |
| final streamController = _getServiceExtensionController(name); |
| |
| _serviceExtensions.add(name); |
| streamController.add(true); |
| |
| if (_enabledServiceExtensions.containsKey(name)) { |
| // Restore any previously enabled states by calling their service |
| // extension. This will restore extension states on the device after a hot |
| // restart. [_enabledServiceExtensions] will be empty on page refresh or |
| // initial start. |
| await callServiceExtension(name, _enabledServiceExtensions[name].value); |
| } else { |
| // Set any extensions that are already enabled on the device. This will |
| // enable extension states in DevTools on page refresh or initial start. |
| await _restoreExtensionFromDevice(name); |
| } |
| } |
| |
| Future<void> _restoreExtensionFromDevice(String name) async { |
| if (!extensions.serviceExtensionsAllowlist.containsKey(name)) { |
| return; |
| } |
| final extensionDescription = extensions.serviceExtensionsAllowlist[name]; |
| final value = extensionValueOnDevice[name]; |
| if (extensionDescription |
| is extensions.ToggleableServiceExtensionDescription) { |
| if (value == extensionDescription.enabledValue) { |
| await setServiceExtensionState(name, true, value, callExtension: false); |
| } |
| } else { |
| await setServiceExtensionState(name, true, value, callExtension: false); |
| } |
| } |
| |
| Future<void> callServiceExtension(String name, dynamic value) async { |
| extensionValueOnDevice[name] = value; |
| } |
| |
| @override |
| void resetAvailableExtensions() { |
| extensionStatesUpdated = Completer(); |
| _firstFrameEventReceived = false; |
| _pendingServiceExtensions.clear(); |
| _serviceExtensions.clear(); |
| _serviceExtensionController |
| .forEach((String name, StreamController<bool> stream) { |
| stream.add(false); |
| }); |
| } |
| |
| /// Sets the state for a service extension and makes the call to the VMService. |
| @override |
| Future<void> setServiceExtensionState( |
| String name, |
| bool enabled, |
| dynamic value, { |
| bool callExtension = true, |
| }) async { |
| if (callExtension && _serviceExtensions.contains(name)) { |
| await callServiceExtension(name, value); |
| } |
| |
| final StreamController<ServiceExtensionState> streamController = |
| _getServiceExtensionStateController(name); |
| streamController.add(ServiceExtensionState(enabled, value)); |
| |
| // Add or remove service extension from [enabledServiceExtensions]. |
| if (enabled) { |
| _enabledServiceExtensions[name] = ServiceExtensionState(enabled, value); |
| } else { |
| _enabledServiceExtensions.remove(name); |
| } |
| } |
| |
| @override |
| bool isServiceExtensionAvailable(String name) { |
| return _serviceExtensions.contains(name) || |
| _pendingServiceExtensions.contains(name); |
| } |
| |
| @override |
| StreamSubscription<bool> hasServiceExtension( |
| String name, |
| void onData(bool value), |
| ) { |
| if (_serviceExtensions.contains(name) && onData != null) { |
| onData(true); |
| } |
| final StreamController<bool> streamController = |
| _getServiceExtensionController(name); |
| return streamController.stream.listen(onData); |
| } |
| |
| @override |
| StreamSubscription<ServiceExtensionState> getServiceExtensionState( |
| String name, |
| void onData(ServiceExtensionState state), |
| ) { |
| if (_enabledServiceExtensions.containsKey(name) && onData != null) { |
| onData(_enabledServiceExtensions[name]); |
| } |
| final StreamController<ServiceExtensionState> streamController = |
| _getServiceExtensionStateController(name); |
| return streamController.stream.listen(onData); |
| } |
| |
| StreamController<bool> _getServiceExtensionController(String name) { |
| return _getStreamController( |
| name, |
| _serviceExtensionController, |
| onFirstListenerSubscribed: () { |
| // If the service extension is in [_serviceExtensions], then we have been |
| // waiting for a listener to add the initial true event. Otherwise, the |
| // service extension is not available, so we should add a false event. |
| _serviceExtensionController[name] |
| .add(_serviceExtensions.contains(name)); |
| }, |
| ); |
| } |
| |
| StreamController<ServiceExtensionState> _getServiceExtensionStateController( |
| String name) { |
| return _getStreamController( |
| name, |
| _serviceExtensionStateController, |
| onFirstListenerSubscribed: () { |
| // If the service extension is enabled, add the current state as the first |
| // event. Otherwise, add a disabled state as the first event. |
| if (_enabledServiceExtensions.containsKey(name)) { |
| assert(_enabledServiceExtensions[name].enabled); |
| _serviceExtensionStateController[name] |
| .add(_enabledServiceExtensions[name]); |
| } else { |
| _serviceExtensionStateController[name] |
| .add(ServiceExtensionState(false, null)); |
| } |
| }, |
| ); |
| } |
| } |
| |
| /// Given a map of Strings to StreamControllers [streamControllers], get the |
| /// stream controller for the given name. If it does not exist, initialize a |
| /// generic stream controller and map it to the name. |
| StreamController<T> _getStreamController<T>( |
| String name, Map<String, StreamController<T>> streamControllers, |
| {@required void onFirstListenerSubscribed()}) { |
| streamControllers.putIfAbsent( |
| name, |
| () => StreamController<T>.broadcast(onListen: onFirstListenerSubscribed), |
| ); |
| return streamControllers[name]; |
| } |
| |
| Future<void> ensureInspectorDependencies() async { |
| assert( |
| !kIsWeb, |
| 'Attempted to resolve a package path from web code.\n' |
| 'Package path resolution uses dart:io, which is not available in web.' |
| '\n' |
| "To fix this, mark the failing test as @TestOn('vm')", |
| ); |
| await initializer.ensureInspectorDependencies(); |
| } |
| |
| void mockIsFlutterApp(MockConnectedApp connectedApp) { |
| when(connectedApp.isFlutterAppNow).thenReturn(true); |
| when(connectedApp.isFlutterApp).thenAnswer((_) => Future.value(true)); |
| } |