Fix flakiness is mac integration tests (#9990)
diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md
index 7dadf83..83756df 100644
--- a/packages/devtools_app_shared/CHANGELOG.md
+++ b/packages/devtools_app_shared/CHANGELOG.md
@@ -7,6 +7,7 @@
* Fix a `RangeError` thrown by `SplitPane` when the number of children
changes between rebuilds.
* Fix garbage collection issues with the result list in `asyncEval` on both native VM and web.
+* Safely handle RPC errors and unexpected exceptions when calling service extensions in `ServiceExtensionManager`.
* The minimum Dart SDK version is bumped to 3.11.0.
* The minimum Flutter SDK version is bumped to 3.41.0.
diff --git a/packages/devtools_app_shared/lib/src/service/service_extension_manager.dart b/packages/devtools_app_shared/lib/src/service/service_extension_manager.dart
index a76f8b0..c2d9baf 100644
--- a/packages/devtools_app_shared/lib/src/service/service_extension_manager.dart
+++ b/packages/devtools_app_shared/lib/src/service/service_extension_manager.dart
@@ -222,12 +222,21 @@
if (didSendFirstFrameEvent) {
await _onFrameEventReceived();
}
+ } on SentinelException catch (_) {
+ // Service extension or isolate stopped existing while calling, so do
+ // nothing. This typically happens during hot restarts.
+ return;
} on RPCError catch (e) {
- if (e.code == RPCErrorKind.kServerError.code) {
+ if (e.code == RPCErrorKind.kServerError.code ||
+ e.isServiceDisposedError) {
// Connection disappeared
return;
}
- rethrow;
+ _log.warning('Error checking for first Flutter frame: $e');
+ return;
+ } catch (e, st) {
+ _log.warning('Error checking for first Flutter frame: $e', e, st);
+ return;
}
}
@@ -274,6 +283,8 @@
} on SentinelException catch (_) {
// Service extension stopped existing while calling, so do nothing.
// This typically happens during hot restarts.
+ } catch (e, st) {
+ _log.warning('Error adding service extension $name: $e', e, st);
}
} else {
// Set any extensions that are already enabled on the device. This will
@@ -338,6 +349,10 @@
);
await _maybeRestoreExtension(name, value);
}
+ } on SentinelException catch (_) {
+ // Service extension or isolate stopped existing while restoring, so do
+ // nothing. This typically happens during hot restarts.
+ return false;
} on RPCError catch (e) {
if (e.isServiceDisposedError) {
return false;
@@ -438,12 +453,21 @@
args: {name.substring(name.lastIndexOf('.') + 1): value},
);
}
+ } on SentinelException catch (_) {
+ // Service extension or isolate stopped existing while calling, so do
+ // nothing. This typically happens during hot restarts.
+ return false;
} on RPCError catch (e) {
- if (e.code == RPCErrorKind.kServerError.code) {
+ if (e.isServiceDisposedError ||
+ e.code == RPCErrorKind.kServerError.code) {
// The connection disappeared.
return false;
}
- rethrow;
+ _log.warning('Failed to call service extension $name: $e');
+ return false;
+ } catch (e, st) {
+ _log.warning('Failed to call service extension $name: $e', e, st);
+ return false;
}
return true;
diff --git a/packages/devtools_app_shared/test/service/service_extensions_test.dart b/packages/devtools_app_shared/test/service/service_extensions_test.dart
index 2ee39d2..c1391d6 100644
--- a/packages/devtools_app_shared/test/service/service_extensions_test.dart
+++ b/packages/devtools_app_shared/test/service/service_extensions_test.dart
@@ -2,8 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/service_extensions.dart';
-import 'package:test/test.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:vm_service/vm_service.dart';
void main() {
group('ServiceExtensions', () {
@@ -28,5 +30,95 @@
isTrue,
);
});
+
+ test(
+ 'ServiceExtensionManager handles RPCError / DWDS Promise collected gracefully',
+ () async {
+ var extensionCalls = 0;
+ final fakeService = _FakeVmService(
+ onCallServiceExtension: (method, isolateId, args) {
+ extensionCalls++;
+ throw RPCError(
+ 'callServiceExtension',
+ -32603,
+ 'Unexpected DWDS error for callServiceExtension: WipError\n-32000 Promise was collected',
+ );
+ },
+ );
+
+ final isolateManager = IsolateManager();
+ isolateManager.vmServiceOpened(fakeService);
+
+ final manager = ServiceExtensionManager(isolateManager);
+ manager.vmServiceOpened(fakeService, _FakeConnectedApp());
+
+ // Pre-enable an extension (like ErrorBadgeManager does for structuredErrors).
+ await manager.setServiceExtensionState(
+ structuredErrors.extension,
+ enabled: true,
+ value: true,
+ callExtension: false,
+ );
+
+ // Initialize isolates on IsolateManager.
+ final isolateRef = IsolateRef(
+ id: 'isolate-1',
+ number: '1',
+ name: 'main',
+ );
+ await isolateManager.init([isolateRef]);
+
+ // Wait for all async listeners and microtasks to complete.
+ await pumpEventQueue();
+
+ // Verify that the manager attempted to restore the pre-enabled extension
+ // state on the newly registered isolate (and gracefully handled the error).
+ expect(extensionCalls, equals(1));
+ },
+ );
});
}
+
+/// A fake [VmService] implementation for testing service extension calls.
+class _FakeVmService extends Fake implements VmService {
+ _FakeVmService({required this.onCallServiceExtension});
+
+ final Future<Response> Function(
+ String method,
+ String? isolateId,
+ Map<String, dynamic>? args,
+ )
+ onCallServiceExtension;
+
+ @override
+ Stream<Event> get onIsolateEvent => const Stream<Event>.empty();
+
+ @override
+ Stream<Event> get onExtensionEvent => const Stream<Event>.empty();
+
+ @override
+ Stream<Event> get onDebugEvent => const Stream<Event>.empty();
+
+ @override
+ Future<Isolate> getIsolate(String isolateId) async => Isolate(
+ id: isolateId,
+ number: '1',
+ name: 'main',
+ extensionRPCs: [structuredErrors.extension],
+ );
+
+ @override
+ Future<Response> callServiceExtension(
+ String method, {
+ String? isolateId,
+ Map<String, dynamic>? args,
+ }) {
+ return onCallServiceExtension(method, isolateId, args);
+ }
+}
+
+/// A fake [ConnectedApp] implementation that simulates a connected Flutter app.
+class _FakeConnectedApp extends Fake implements ConnectedApp {
+ @override
+ Future<bool> get isFlutterApp async => true;
+}