Skip to content

Configure DevTools to pause isolates on hot restart, and then only resume once breakpoints have been set #7234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1ee9684
Configure pausing isolates on restart
elliette Feb 15, 2024
e579803
Clean up
elliette Feb 16, 2024
37b86cf
onPauseStart only
elliette Feb 16, 2024
4e96028
service is nullable
elliette Mar 5, 2024
347ad86
Merge branch 'master' into pause-isolates
elliette Mar 5, 2024
e554e63
Update release notes
elliette Mar 5, 2024
3077212
Fix analyzer error
elliette Mar 5, 2024
f71d557
Missing semicolon :(
elliette Mar 6, 2024
7b6fb8c
Resume any newly spawned isolates
elliette Mar 7, 2024
fc60b24
Merge branch 'master' into pause-isolates
elliette Mar 7, 2024
79aca85
Detect is pause-isolates-on-start was already set
elliette Mar 7, 2024
a9e0030
Fix resuming for web apps
elliette Mar 26, 2024
ea6e838
Fix resuming for web apps
elliette Mar 26, 2024
17904c7
Use readyToResume
elliette Mar 27, 2024
3d2519a
Use readyToResume
elliette Mar 27, 2024
72a803f
Clean up
elliette Mar 28, 2024
8ba5fb0
Resolve merge conflicts
elliette Apr 15, 2024
9c4d94f
Update dds_service_extensions
elliette Apr 16, 2024
b426437
Hopefully fix integration test
elliette Apr 16, 2024
b42b842
More attempts to fix test
elliette Apr 16, 2024
77dc4d1
More attempts to fix test
elliette Apr 16, 2024
0d81940
Commit in correct directory
elliette Apr 16, 2024
874f223
More attempts to fix test
elliette Apr 16, 2024
6a46af0
Resolve merge conflicts
elliette Apr 16, 2024
49f84c8
Split service_connection_test into service_connection_test and servic…
elliette Apr 17, 2024
6bc4e18
Fix issue where wasn't actually pausing
elliette Apr 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,13 @@
// 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:collection/collection.dart';
import 'package:devtools_app/devtools_app.dart';
import 'package:devtools_app/src/service/service_extension_widgets.dart';
import 'package:devtools_app/src/service/service_extensions.dart' as extensions;
import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_shared/devtools_shared.dart';
import 'package:devtools_test/helpers.dart';
import 'package:devtools_test/integration_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:vm_service/vm_service.dart';

// To run:
// dart run integration_test/run_tests.dart --target=integration_test/test/live_connection/service_connection_test.dart
Expand Down Expand Up @@ -54,7 +45,7 @@ void main() {
// Use a range instead of an exact number because service extension
// calls are not consistent. This will still catch any spurious calls
// that are unintentionally added at start up.
const Range(40, 70).contains(vmServiceCallCount),
const Range(35, 70).contains(vmServiceCallCount),
isTrue,
reason: 'Unexpected number of vm service calls upon connection: '
'$vmServiceCallCount. If this is expected, please update this test '
Expand All @@ -69,10 +60,12 @@ void main() {
// Filter out unawaited streamListen calls.
.where((call) => call != 'streamListen')
.toList()
.sublist(0, 6),
.sublist(0, 8),
equals([
'getSupportedProtocols',
'getVersion',
'setFlag',
'requirePermissionToResume',
'getFlagList',
'getDartDevelopmentServiceVersion',
'getDartDevelopmentServiceVersion',
Expand Down Expand Up @@ -110,339 +103,4 @@ void main() {

await disconnectFromTestApp(tester);
});

testWidgets('can call services and service extensions', (tester) async {
await pumpAndConnectDevTools(tester, testApp);
await tester.pump(longDuration);

// TODO(kenz): re-work this integration test so that we do not have to be
// on the inspector screen for this to pass.
await switchToScreen(
tester,
tabIcon: ScreenMetaData.inspector.icon!,
screenId: ScreenMetaData.inspector.id,
);
await tester.pump(longDuration);

// Ensure all futures are completed before running checks.
await serviceConnection.serviceManager.service!.allFuturesCompleted;

logStatus('verify Flutter framework service extensions');
await _verifyBooleanExtension(tester);
await _verifyNumericExtension(tester);
await _verifyStringExtension(tester);

logStatus('verify Flutter engine service extensions');
expect(
await serviceConnection.queryDisplayRefreshRate,
equals(60),
);

logStatus('verify services that are registered to exactly one client');
await _verifyHotReloadAndHotRestart();
await expectLater(
serviceConnection.serviceManager.callService('fakeMethod'),
throwsException,
);

await disconnectFromTestApp(tester);
});

testWidgets('loads initial extension states from device', (tester) async {
await pumpAndConnectDevTools(tester, testApp);
await tester.pump(longDuration);

// Ensure all futures are completed before running checks.
final service = serviceConnection.serviceManager.service!;
await service.allFuturesCompleted;

final serviceExtensionsToEnable = [
(extensions.debugPaint.extension, true),
(extensions.slowAnimations.extension, 5.0),
(extensions.togglePlatformMode.extension, 'iOS'),
];

logStatus('enabling service extensions on the test device');
// Enable a service extension of each type (boolean, numeric, string).
for (final ext in serviceExtensionsToEnable) {
await serviceConnection.serviceManager.serviceExtensionManager
.setServiceExtensionState(
ext.$1,
enabled: true,
value: ext.$2,
);
}

logStatus('disconnecting from the test device');
await disconnectFromTestApp(tester);

for (final ext in serviceExtensionsToEnable) {
expect(
serviceConnection.serviceManager.serviceExtensionManager
.isServiceExtensionAvailable(ext.$1),
isFalse,
);
}

logStatus('reconnecting to the test device');
await connectToTestApp(tester, testApp);

logStatus('verify extension states have been restored from the device');
for (final ext in serviceExtensionsToEnable) {
expect(
serviceConnection.serviceManager.serviceExtensionManager
.isServiceExtensionAvailable(ext.$1),
isTrue,
);
await _verifyExtensionStateInServiceManager(
ext.$1,
enabled: true,
value: ext.$2,
);
}

await disconnectFromTestApp(tester);
});
}

Future<void> _verifyBooleanExtension(WidgetTester tester) async {
final extensionName = extensions.debugPaint.extension;
const evalExpression = 'debugPaintSizeEnabled';
final library = EvalOnDartLibrary(
'package:flutter/src/rendering/debug.dart',
serviceConnection.serviceManager.service!,
serviceManager: serviceConnection.serviceManager,
);
await _verifyExtension(
tester,
extensionName: extensionName,
evalExpression: evalExpression,
library: library,
initialValue: false,
newValue: true,
);
}

Future<void> _verifyNumericExtension(WidgetTester tester) async {
final extensionName = extensions.slowAnimations.extension;
const evalExpression = 'timeDilation';
final library = EvalOnDartLibrary(
'package:flutter/src/scheduler/binding.dart',
serviceConnection.serviceManager.service!,
serviceManager: serviceConnection.serviceManager,
);
await _verifyExtension(
tester,
extensionName: extensionName,
evalExpression: evalExpression,
library: library,
initialValue: 1.0,
newValue: 5.0,
initialValueOnDevice: '1.0',
newValueOnDevice: '5.0',
);
}

Future<void> _verifyStringExtension(WidgetTester tester) async {
final extensionName = extensions.togglePlatformMode.extension;
await _serviceExtensionAvailable(extensionName);
const evalExpression = 'defaultTargetPlatform.toString()';
final library = EvalOnDartLibrary(
'package:flutter/src/foundation/platform.dart',
serviceConnection.serviceManager.service!,
serviceManager: serviceConnection.serviceManager,
);
await _verifyExtension(
tester,
extensionName: extensionName,
evalExpression: evalExpression,
library: library,
initialValue: 'android',
newValue: 'iOS',
initialValueOnDevice: 'TargetPlatform.android',
newValueOnDevice: 'TargetPlatform.iOS',
initialValueInServiceManager: (true, 'android'),
// TODO(https://github.com/flutter/devtools/issues/2780): change this
// extension from the DevTools UI when it has a button in the inspector.
toggleExtensionFromUi: false,
);
}

Future<void> _verifyHotReloadAndHotRestart() async {
const evalExpression = 'topLevelFieldForTest';
final library = EvalOnDartLibrary(
'package:flutter_app/main.dart',
serviceConnection.serviceManager.service!,
serviceManager: serviceConnection.serviceManager,
);

// Verify the initial value of [topLevelFieldForTest].
var value = await library.eval(evalExpression, isAlive: null);
expect(value.runtimeType, InstanceRef);
expect(value!.valueAsString, 'false');

// Change the value of [topLevelFieldForTest].
await library.eval('$evalExpression = true', isAlive: null);

// Verify the value of [topLevelFieldForTest] is now changed.
value = await library.eval(evalExpression, isAlive: null);
expect(value.runtimeType, InstanceRef);
expect(value!.valueAsString, 'true');

await serviceConnection.serviceManager.performHotReload();

// Verify the value of [topLevelFieldForTest] is still changed after hot
// reload.
value = await library.eval(evalExpression, isAlive: null);
expect(value.runtimeType, InstanceRef);
expect(value!.valueAsString, 'true');

await serviceConnection.serviceManager.performHotRestart();

// Verify the value of [topLevelFieldForTest] is back to its original value
// after hot restart.
value = await library.eval(evalExpression, isAlive: null);
expect(value.runtimeType, InstanceRef);
expect(value!.valueAsString, 'false');
}

Future<void> _verifyExtension(
WidgetTester tester, {
required String extensionName,
required String evalExpression,
required EvalOnDartLibrary library,
required Object initialValue,
required Object newValue,
(bool, Object?)? initialValueInServiceManager,
String? initialValueOnDevice,
String? newValueOnDevice,
bool toggleExtensionFromUi = true,
}) async {
await _serviceExtensionAvailable(extensionName);

await _verifyExtensionStateOnTestDevice(
evalExpression: evalExpression,
expectedResult: initialValueOnDevice ?? initialValue.toString(),
library: library,
);
await _verifyExtensionStateInServiceManager(
extensionName,
enabled: initialValueInServiceManager?.$1 ?? false,
value: initialValueInServiceManager?.$2,
);

// Enable the service extension state from the service manager.
await serviceConnection.serviceManager.serviceExtensionManager
.setServiceExtensionState(
extensionName,
enabled: true,
value: newValue,
);

await _verifyExtensionStateOnTestDevice(
evalExpression: evalExpression,
expectedResult: newValueOnDevice ?? newValue.toString(),
library: library,
);
await _verifyExtensionStateInServiceManager(
extensionName,
enabled: true,
value: newValue,
);

if (toggleExtensionFromUi) {
// Disable the service extension state from the UI.
await _changeServiceExtensionFromButton(
extensionName,
evalExpression: evalExpression,
library: library,
expectedResultOnDevice: initialValueOnDevice ?? initialValue.toString(),
expectedResultInServiceManager: (false, initialValue),
tester: tester,
);
}
}

Future<void> _changeServiceExtensionFromButton(
String extensionName, {
required String evalExpression,
required EvalOnDartLibrary library,
required String? expectedResultOnDevice,
required (bool, Object?) expectedResultInServiceManager,
required WidgetTester tester,
}) async {
final serviceExtensionButtons = tester
.widgetList<ServiceExtensionButton>(find.byType(ServiceExtensionButton));
final button = serviceExtensionButtons.firstWhereOrNull(
(b) => b.extensionState.description.extension == extensionName,
);
expect(button, isNotNull);
await tester.tap(find.byWidget(button as Widget));
await tester.pumpAndSettle(shortPumpDuration);

await _verifyExtensionStateOnTestDevice(
evalExpression: evalExpression,
expectedResult: expectedResultOnDevice,
library: library,
);
await _verifyExtensionStateInServiceManager(
extensionName,
enabled: expectedResultInServiceManager.$1,
value: expectedResultInServiceManager.$2,
);
}

/// Returns a future that completes when the service extension is available.
Future<void> _serviceExtensionAvailable(String extensionName) async {
final listenable = serviceConnection.serviceManager.serviceExtensionManager
.hasServiceExtension(extensionName);

final completer = Completer<void>();
void listener() {
if (listenable.value) {
completer.safeComplete();
}
}

listener();
listenable.addListener(listener);
await completer.future;
listenable.removeListener(listener);
}

Future<void> _verifyExtensionStateOnTestDevice({
required String evalExpression,
required String? expectedResult,
required EvalOnDartLibrary library,
}) async {
final result = await library.eval(evalExpression, isAlive: null);
if (result is InstanceRef) {
expect(result.valueAsString, equals(expectedResult));
}
}

Future<void> _verifyExtensionStateInServiceManager(
String extensionName, {
required bool enabled,
required Object? value,
}) async {
final stateListenable = serviceConnection
.serviceManager.serviceExtensionManager
.getServiceExtensionState(extensionName);

// Wait for the service extension state to match the expected value.
final Completer<ServiceExtensionState> stateCompleter = Completer();
void stateListener() {
if (stateListenable.value.value == value) {
stateCompleter.complete(stateListenable.value);
}
}

stateListenable.addListener(stateListener);
stateListener();

final ServiceExtensionState state = await stateCompleter.future;
stateListenable.removeListener(stateListener);
expect(state.enabled, equals(enabled));
expect(state.value, equals(value));
}
Loading
Loading