Skip to content

Commit 062845e

Browse files
committed
Fix main isolate auto-selection for test suite isolates
1 parent 9e3f94d commit 062845e

2 files changed

Lines changed: 238 additions & 1 deletion

File tree

packages/devtools_app_shared/lib/src/service/isolate_manager.dart

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ final class IsolateManager with DisposerMixin {
168168
_isolateCreatedController.add(event.isolate);
169169
// TODO(jacobr): we assume the first isolate started is the main isolate
170170
// but that may not always be a safe assumption.
171+
// TODO(https://github.com/flutter/devtools/issues/9747): Detect main
172+
// isolate using root library information for test connections here too,
173+
// not just in _computeMainIsolate().
171174
if (_mainIsolate.value == null) {
172175
_mainIsolate.value = event.isolate;
173176
if (_shouldReselectMainIsolate) {
@@ -236,12 +239,60 @@ final class IsolateManager with DisposerMixin {
236239

237240
final ref = _isolateStates.keys.firstWhereOrNull((IsolateRef ref) {
238241
// 'foo.dart:main()'
239-
return ref.name!.contains(':main(');
242+
return ref.name?.contains(':main(') ?? false;
240243
});
241244

245+
if (ref == null) {
246+
final rootLibraryTestSuiteRef =
247+
await _findTestSuiteByRootLibrary(service);
248+
if (rootLibraryTestSuiteRef != null) return rootLibraryTestSuiteRef;
249+
250+
// When connecting to a test run, the test package (package:test_core)
251+
// spawns each test suite in a separate isolate with a debug name
252+
// prefixed with 'test_suite:'. DevTools should connect to this isolate
253+
// rather than the test runner isolate ('main'), since the test suite
254+
// isolate is where user code actually runs.
255+
// See: https://github.com/flutter/devtools/issues/9747
256+
final testSuiteRef = _isolateStates.keys.firstWhereOrNull(
257+
(IsolateRef ref) => ref.name?.startsWith('test_suite:') ?? false,
258+
);
259+
if (testSuiteRef != null) return testSuiteRef;
260+
}
261+
242262
return ref ?? _isolateStates.keys.first;
243263
}
244264

265+
Future<IsolateRef?> _findTestSuiteByRootLibrary(VmService? service) async {
266+
for (final isolateState in _isolateStates.values) {
267+
final isolate = await isolateState.isolate;
268+
if (service != _service) return null;
269+
270+
final rootLibraryUri = isolate?.rootLib?.uri;
271+
if (rootLibraryUri == null) continue;
272+
273+
if (_isDartTestRunnerRootLibrary(rootLibraryUri)) continue;
274+
275+
if (_isLikelyUserTestRootLibrary(rootLibraryUri)) {
276+
return isolateState.isolateRef;
277+
}
278+
}
279+
280+
return null;
281+
}
282+
283+
bool _isDartTestRunnerRootLibrary(String uri) {
284+
return uri.contains('dart_test.kernel') ||
285+
uri.startsWith('package:test_core/') ||
286+
uri.startsWith('package:test_api/');
287+
}
288+
289+
bool _isLikelyUserTestRootLibrary(String uri) {
290+
return (uri.endsWith('_test.dart') ||
291+
uri.contains('/test/') ||
292+
uri.contains('\\test\\')) &&
293+
!uri.startsWith('dart:');
294+
}
295+
245296
void _setSelectedIsolate(IsolateRef? ref) {
246297
_selectedIsolate.value = ref;
247298
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright 2026 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'dart:async';
6+
7+
import 'package:devtools_app_shared/src/service/isolate_manager.dart';
8+
import 'package:flutter_test/flutter_test.dart';
9+
import 'package:vm_service/vm_service.dart';
10+
11+
/// Minimal fake VmService for IsolateManager tests.
12+
class _FakeVmService extends Fake implements VmService {
13+
/// Map of isolate id -> Isolate to return from getIsolate().
14+
final Map<String, Isolate> isolates;
15+
16+
_FakeVmService(this.isolates);
17+
18+
@override
19+
Stream<Event> get onIsolateEvent => const Stream.empty();
20+
21+
@override
22+
Stream<Event> get onDebugEvent => const Stream.empty();
23+
24+
@override
25+
Future<Isolate> getIsolate(String isolateId) async {
26+
return isolates[isolateId] ??
27+
Isolate.parse({
28+
'id': isolateId,
29+
'runnable': true,
30+
'extensionRPCs': <String>[],
31+
})!;
32+
}
33+
34+
@override
35+
Future<Success> resume(String isolateId, {String? step, int? frameIndex}) =>
36+
Future.value(Success());
37+
}
38+
39+
/// Creates a minimal runnable [Isolate] for a given [IsolateRef].
40+
Isolate _makeIsolate(IsolateRef ref, {String? rootLibraryUri}) {
41+
final json = <String, Object?>{
42+
'id': ref.id,
43+
'name': ref.name,
44+
'type': '@Isolate',
45+
'runnable': true,
46+
'extensionRPCs': <String>[],
47+
if (rootLibraryUri != null)
48+
'rootLib': {
49+
'type': '@Library',
50+
'id': 'libraries/0',
51+
'uri': rootLibraryUri,
52+
},
53+
};
54+
55+
return Isolate.parse(json)!;
56+
}
57+
58+
/// Creates an [IsolateRef] with the given name and id.
59+
IsolateRef _makeRef(String name, String id) {
60+
return IsolateRef.parse({'name': name, 'id': id, 'isSystemIsolate': false})!;
61+
}
62+
63+
void main() {
64+
group('IsolateManager._computeMainIsolate', () {
65+
late IsolateManager manager;
66+
67+
setUp(() {
68+
manager = IsolateManager();
69+
});
70+
71+
tearDown(() {
72+
manager.handleVmServiceClosed();
73+
});
74+
75+
test(
76+
'selects test_suite isolate instead of test runner when running tests',
77+
() async {
78+
// Simulates the isolate list seen when connecting to a test run:
79+
// - 'main' is the test runner isolate (wrong choice)
80+
// - 'test_suite:...' is where user code actually runs (correct choice)
81+
// - 'vm-service' is infrastructure
82+
final testRunnerRef = _makeRef('main', 'isolates/1');
83+
final testSuiteRef = _makeRef(
84+
'test_suite:file:///tmp/dart_test.kernel.dill',
85+
'isolates/2',
86+
);
87+
final vmServiceRef = _makeRef('vm-service', 'isolates/3');
88+
89+
final fakeService = _FakeVmService({
90+
'isolates/1': _makeIsolate(testRunnerRef),
91+
'isolates/2': _makeIsolate(testSuiteRef),
92+
'isolates/3': _makeIsolate(vmServiceRef),
93+
});
94+
95+
manager.vmServiceOpened(fakeService);
96+
await manager.init([testRunnerRef, testSuiteRef, vmServiceRef]);
97+
98+
expect(
99+
manager.selectedIsolate.value?.name,
100+
equals('test_suite:file:///tmp/dart_test.kernel.dill'),
101+
reason:
102+
'Should auto-select the test_suite isolate, not the test runner',
103+
);
104+
expect(
105+
manager.mainIsolate.value?.name,
106+
equals('test_suite:file:///tmp/dart_test.kernel.dill'),
107+
reason:
108+
'Main isolate should also resolve to the test_suite isolate',
109+
);
110+
},
111+
);
112+
113+
test('selects main isolate for normal (non-test) app runs', () async {
114+
final mainRef = _makeRef('main', 'isolates/1');
115+
final vmServiceRef = _makeRef('vm-service', 'isolates/2');
116+
117+
final fakeService = _FakeVmService({
118+
'isolates/1': _makeIsolate(mainRef),
119+
'isolates/2': _makeIsolate(vmServiceRef),
120+
});
121+
122+
manager.vmServiceOpened(fakeService);
123+
await manager.init([mainRef, vmServiceRef]);
124+
125+
expect(
126+
manager.selectedIsolate.value?.name,
127+
equals('main'),
128+
reason: 'Should select the main isolate for normal app runs',
129+
);
130+
});
131+
132+
test('selects isolate containing :main( for dart scripts', () async {
133+
final scriptRef = _makeRef('foo.dart:main()', 'isolates/1');
134+
135+
final fakeService = _FakeVmService({
136+
'isolates/1': _makeIsolate(scriptRef),
137+
});
138+
139+
manager.vmServiceOpened(fakeService);
140+
await manager.init([scriptRef]);
141+
142+
expect(
143+
manager.selectedIsolate.value?.name,
144+
equals('foo.dart:main()'),
145+
);
146+
});
147+
148+
test(
149+
'selects test isolate by root library when test_suite prefix is absent',
150+
() async {
151+
final testRunnerRef = _makeRef('main', 'isolates/1');
152+
final userTestRef = _makeRef('isolate-2', 'isolates/2');
153+
final vmServiceRef = _makeRef('vm-service', 'isolates/3');
154+
155+
final fakeService = _FakeVmService({
156+
'isolates/1': _makeIsolate(
157+
testRunnerRef,
158+
rootLibraryUri: 'file:///tmp/dart_test.kernel.abcd/test.dart',
159+
),
160+
'isolates/2': _makeIsolate(
161+
userTestRef,
162+
rootLibraryUri: 'package:my_app/foo_test.dart',
163+
),
164+
'isolates/3': _makeIsolate(
165+
vmServiceRef,
166+
rootLibraryUri: 'dart:developer',
167+
),
168+
});
169+
170+
manager.vmServiceOpened(fakeService);
171+
await manager.init([testRunnerRef, userTestRef, vmServiceRef]);
172+
173+
expect(
174+
manager.selectedIsolate.value?.name,
175+
equals('isolate-2'),
176+
reason:
177+
'Should choose user test isolate using root library metadata',
178+
);
179+
expect(
180+
manager.mainIsolate.value?.name,
181+
equals('isolate-2'),
182+
);
183+
},
184+
);
185+
});
186+
}

0 commit comments

Comments
 (0)