Skip to content

Commit b764dfb

Browse files
authored
Use service extension in networking integration test (#9323)
1 parent a9bf8c9 commit b764dfb

File tree

6 files changed

+119
-129
lines changed

6 files changed

+119
-129
lines changed

packages/devtools_app/integration_test/test/live_connection/network_screen_test.dart

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import 'package:devtools_app/src/shared/table/table.dart' show DevToolsTable;
1010
import 'package:devtools_test/helpers.dart';
1111
import 'package:devtools_test/integration_test.dart';
1212
import 'package:flutter_test/flutter_test.dart';
13-
import 'package:http/http.dart' as http;
1413
import 'package:integration_test/integration_test.dart';
1514

1615
// To run:
@@ -28,63 +27,62 @@ void main() {
2827

2928
tearDown(() async {
3029
await resetHistory();
31-
await http.get(Uri.parse('http://localhost:${testApp.controlPort}/exit/'));
3230
});
3331

34-
testWidgets('nnn', (tester) async {
32+
testWidgets('network screen test', timeout: mediumTimeout, (tester) async {
3533
await pumpAndConnectDevTools(tester, testApp);
3634
await _prepareNetworkScreen(tester);
3735

38-
final helper = _NetworkScreenHelper(tester, testApp.controlPort!);
36+
final helper = _NetworkScreenHelper(tester);
3937

4038
// Instruct the app to make a GET request via the dart:io HttpClient.
41-
await helper.triggerRequest('get/');
39+
await helper.triggerRequest('get');
4240
_expectInRequestTable('GET');
4341
await helper.clear();
4442

4543
// Instruct the app to make a POST request via the dart:io HttpClient.
46-
await helper.triggerRequest('post/');
44+
await helper.triggerRequest('post');
4745
_expectInRequestTable('POST');
4846
await helper.clear();
4947

5048
// Instruct the app to make a PUT request via the dart:io HttpClient.
51-
await helper.triggerRequest('put/');
49+
await helper.triggerRequest('put');
5250
_expectInRequestTable('PUT');
5351
await helper.clear();
5452

5553
// Instruct the app to make a DELETE request via the dart:io HttpClient.
56-
await helper.triggerRequest('delete/');
54+
await helper.triggerRequest('delete');
5755
_expectInRequestTable('DELETE');
5856
await helper.clear();
5957

6058
// Instruct the app to make a GET request via the 'http' package.
61-
await helper.triggerRequest('packageHttp/get/');
59+
await helper.triggerRequest('packageHttpGet');
6260
_expectInRequestTable('GET');
6361
await helper.clear();
6462

6563
// Instruct the app to make a POST request via the 'http' package.
66-
await helper.triggerRequest('packageHttp/post/');
64+
await helper.triggerRequest('packageHttpPost');
6765
_expectInRequestTable('POST');
6866
await helper.clear();
6967

7068
// Instruct the app to make a GET request via Dio.
71-
await helper.triggerRequest('dio/get/');
69+
await helper.triggerRequest('dioGet');
7270
_expectInRequestTable('GET');
7371
await helper.clear();
7472

7573
// Instruct the app to make a POST request via Dio.
76-
await helper.triggerRequest('dio/post/');
74+
await helper.triggerRequest('dioPost');
7775
_expectInRequestTable('POST');
76+
77+
await helper.triggerExit();
7878
});
7979
}
8080

8181
final class _NetworkScreenHelper {
82-
_NetworkScreenHelper(this._tester, this._controlPort);
82+
_NetworkScreenHelper(this._tester);
8383

8484
final WidgetTester _tester;
8585

86-
final int _controlPort;
87-
8886
Future<void> clear() async {
8987
// Press the 'Clear' button between tests.
9088
await _tester.tap(find.text('Clear'));
@@ -95,11 +93,28 @@ final class _NetworkScreenHelper {
9593
);
9694
}
9795

98-
Future<void> triggerRequest(String path) async {
99-
await http.get(Uri.parse('http://localhost:$_controlPort/$path'));
96+
Future<void> triggerExit() async {
97+
final response = await serviceConnection.serviceManager
98+
.callServiceExtensionOnMainIsolate('ext.networking_app.exit');
99+
logStatus(response.toString());
100+
100101
await Future.delayed(const Duration(milliseconds: 200));
101102
await _tester.pump(safePumpDuration);
102103
}
104+
105+
Future<void> triggerRequest(
106+
String requestType, {
107+
bool hasBody = false,
108+
}) async {
109+
final response = await serviceConnection.serviceManager
110+
.callServiceExtensionOnMainIsolate(
111+
'ext.networking_app.makeRequest',
112+
args: {'requestType': requestType, 'hasBody': hasBody},
113+
);
114+
logStatus(response.toString());
115+
116+
await _tester.pump(safePumpDuration);
117+
}
103118
}
104119

105120
void _expectInRequestTable(String text) {

packages/devtools_app/integration_test/test_infra/run/_test_app_driver.dart

Lines changed: 20 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,6 @@ class TestDartCliApp extends IntegrationTestApp {
196196
: super(appPath, TestAppDevice.cli);
197197

198198
static const vmServicePrefix = 'The Dart VM service is listening on ';
199-
static const controlPortKey = 'controlPort';
200-
201-
int? get controlPort => _controlPort;
202-
late final int? _controlPort;
203199

204200
@override
205201
Future<void> startProcess() async {
@@ -216,75 +212,44 @@ class TestDartCliApp extends IntegrationTestApp {
216212

217213
@override
218214
Future<void> waitForAppStart() async {
219-
final vmServiceUriString = await _waitFor(
220-
message: vmServicePrefix,
221-
timeout: IntegrationTestApp._appStartTimeout,
222-
);
215+
final vmServiceUriString = await _waitForVmServicePrefix();
223216
final vmServiceUri = Uri.parse(vmServiceUriString);
224-
_controlPort = await _waitFor(
225-
message: controlPortKey,
226-
timeout: const Duration(seconds: 1),
227-
optional: true,
228-
);
229217

230218
// Map to WS URI.
231219
_vmServiceWsUri = convertToWebSocketUrl(serviceProtocolUrl: vmServiceUri);
232220
}
233221

234-
/// Waits for [message] to appear on stdout.
222+
/// Waits for [vmServicePrefix] to appear on stdout.
235223
///
236-
/// After [timeout], if no such message has appeared, then either `null` is
237-
/// returned, if [optional] is `true`, or an exception is thrown, if
238-
/// [optional] is `false`.
239-
Future<T> _waitFor<T>({
240-
required String message,
241-
Duration? timeout,
242-
bool optional = false,
243-
}) {
244-
final response = Completer<T>();
224+
/// After a timeout, if no such message has appeared, then an exception is
225+
/// thrown.
226+
Future<String> _waitForVmServicePrefix() {
227+
final response = Completer<String>();
245228
late StreamSubscription<String> sub;
246229
sub = stdoutController.stream.listen(
247-
(String line) => _handleStdout(
248-
line,
249-
subscription: sub,
250-
response: response,
251-
message: message,
252-
),
230+
(String line) =>
231+
_handleStdout(line, subscription: sub, response: response),
253232
);
254233

255-
if (optional) {
256-
return response.future
257-
.timeout(
258-
timeout ?? IntegrationTestApp._defaultTimeout,
259-
onTimeout: () => null as T,
260-
)
261-
.whenComplete(() => sub.cancel());
262-
}
263-
264-
return _timeoutWithMessages<T>(
234+
return _timeoutWithMessages<String>(
265235
() => response.future,
266-
timeout: timeout,
267-
message: 'Did not receive expected message: $message.',
236+
timeout: IntegrationTestApp._appStartTimeout,
237+
message: 'Did not receive expected message: $vmServicePrefix.',
268238
).whenComplete(() => sub.cancel());
269239
}
270240

271-
void _handleStdout<T>(
241+
void _handleStdout(
272242
String line, {
273243
required StreamSubscription<String> subscription,
274-
required Completer<T> response,
275-
required String message,
244+
required Completer<String> response,
276245
}) async {
277-
if (message == vmServicePrefix && line.startsWith(vmServicePrefix)) {
278-
final vmServiceUri = line.substring(
279-
line.indexOf(vmServicePrefix) + vmServicePrefix.length,
280-
);
281-
await subscription.cancel();
282-
response.complete(vmServiceUri as T);
283-
} else if (message == controlPortKey && line.contains(controlPortKey)) {
284-
final asJson = jsonDecode(line) as Map;
285-
await subscription.cancel();
286-
response.complete(asJson[controlPortKey] as T);
287-
}
246+
if (!line.startsWith(vmServicePrefix)) return;
247+
248+
final vmServiceUri = line.substring(
249+
line.indexOf(vmServicePrefix) + vmServicePrefix.length,
250+
);
251+
await subscription.cancel();
252+
response.complete(vmServiceUri);
288253
}
289254
}
290255

packages/devtools_app/integration_test/test_infra/run/run_test.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,7 @@ Future<void> runFlutterIntegrationTest(
115115
// Run the flutter integration test.
116116
final testRunner = IntegrationTestRunner();
117117
try {
118-
final testArgs = <String, Object?>{
119-
if (!offline) 'service_uri': testAppUri,
120-
if (testApp is TestDartCliApp) 'control_port': testApp.controlPort,
121-
};
118+
final testArgs = <String, Object?>{if (!offline) 'service_uri': testAppUri};
122119
final testTarget = testRunnerArgs.testTarget!;
123120
debugLog('starting test run for $testTarget');
124121
await testRunner.run(

packages/devtools_app/test/test_infra/fixtures/networking_app/bin/main.dart

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,69 @@
66

77
import 'dart:async';
88
import 'dart:convert' show json;
9+
import 'dart:developer';
910
import 'dart:io' as io;
1011

1112
import 'package:dio/dio.dart';
1213
import 'package:http/http.dart' as http;
1314

1415
void main() async {
1516
final testServer = await _bindTestServer();
16-
await _bindControlServer(testServer);
17+
_registerMakeRequestExtension(testServer);
18+
}
19+
20+
void _registerMakeRequestExtension(io.HttpServer testServer) {
21+
final client = _HttpClient(testServer.port);
22+
registerExtension('ext.networking_app.makeRequest', (_, parameters) async {
23+
final hasBody = bool.tryParse(parameters['hasBody'] ?? 'false') ?? false;
24+
final requestType = parameters['requestType'];
25+
if (requestType == null) {
26+
return ServiceExtensionResponse.error(
27+
ServiceExtensionResponse.invalidParams,
28+
json.encode({'error': 'Missing "requestType" field'}),
29+
);
30+
}
31+
switch (requestType) {
32+
case 'get':
33+
client.get();
34+
case 'post':
35+
client.post(hasBody: hasBody);
36+
case 'put':
37+
client.put(hasBody: hasBody);
38+
case 'delete':
39+
client.delete(hasBody: hasBody);
40+
case 'dioGet':
41+
client.dioGet();
42+
case 'dioPost':
43+
client.dioPost(hasBody: hasBody);
44+
case 'packageHttpGet':
45+
client.packageHttpGet();
46+
case 'packageHttpPost':
47+
client.packageHttpPost(hasBody: hasBody);
48+
case 'packageHttpPostStreamed':
49+
client.packageHttpPostStreamed();
50+
default:
51+
return ServiceExtensionResponse.error(
52+
ServiceExtensionResponse.invalidParams,
53+
json.encode({'error': 'Unknown requestType: "$requestType"'}),
54+
);
55+
}
56+
return ServiceExtensionResponse.result(json.encode({'type': 'success'}));
57+
});
58+
59+
registerExtension('ext.networking_app.exit', (_, parameters) async {
60+
// This service extension needs to trigger `io.exit(0)`, and also return a
61+
// value. (You might expect `Future.microtask(() => io.exit(0))` to be
62+
// sufficient, but that results in DevTools erroring, saying that the
63+
// connected app unxexpectedly disconnected; it seems that returning a value
64+
// needs to work through some microtasks.) A 200 ms delay seems to work, so
65+
// that the following `ServiceExtensionResponse` makes it all the way to
66+
// DevTools, and _then_ we can exit.
67+
unawaited(
68+
Future.delayed(const Duration(milliseconds: 200)).then((_) => io.exit(0)),
69+
);
70+
return ServiceExtensionResponse.result(json.encode({'type': 'success'}));
71+
});
1772
}
1873

1974
/// Binds a "test" HTTP server to an available port.
@@ -30,50 +85,6 @@ Future<io.HttpServer> _bindTestServer() async {
3085
return server;
3186
}
3287

33-
/// Binds a "control" HTTP server to an available port.
34-
///
35-
/// This server has an HTTP client, and can receive commands for that client to
36-
/// send requests to the "test" HTTP server.
37-
Future<io.HttpServer> _bindControlServer(io.HttpServer testServer) async {
38-
final client = _HttpClient(testServer.port);
39-
40-
final server = await io.HttpServer.bind(io.InternetAddress.loopbackIPv4, 0);
41-
print(json.encode({'controlPort': server.port}));
42-
server.listen((request) async {
43-
request.response.headers
44-
..add('Access-Control-Allow-Origin', '*')
45-
..add('Access-Control-Allow-Methods', 'POST,GET,DELETE,PUT,OPTIONS');
46-
final path = request.uri.path;
47-
final hasBody = path.contains('/body/');
48-
request.response
49-
..statusCode = 200
50-
..write('received request at: "$path"');
51-
52-
if (path.startsWith('/get/')) {
53-
client.get();
54-
} else if (path.startsWith('/post/')) {
55-
client.post(hasBody: hasBody);
56-
} else if (path.startsWith('/put/')) {
57-
client.put(hasBody: hasBody);
58-
} else if (path.startsWith('/delete/')) {
59-
client.delete(hasBody: hasBody);
60-
} else if (path.startsWith('/dio/get/')) {
61-
client.dioGet();
62-
} else if (path.startsWith('/dio/post/')) {
63-
client.dioPost(hasBody: hasBody);
64-
} else if (path.startsWith('/packageHttp/post/')) {
65-
client.packageHttpPost(hasBody: hasBody);
66-
} else if (path.startsWith('/packageHttp/postStreamed/')) {
67-
client.packageHttpPostStreamed();
68-
} else if (path.startsWith('/exit/')) {
69-
client.close();
70-
io.exit(0);
71-
}
72-
await request.response.close();
73-
});
74-
return server;
75-
}
76-
7788
// TODO(https://github.com/flutter/devtools/issues/8223): Test support for
7889
// WebSockets.
7990
// TODO(https://github.com/flutter/devtools/issues/4829): Test support for the
@@ -136,6 +147,13 @@ class _HttpClient {
136147
print('Received DELETE response: $response');
137148
}
138149

150+
void packageHttpGet() async {
151+
print('Sending package:http GET...');
152+
// No body.
153+
final response = await http.get(_uri);
154+
print('Received package:http GET response: $response');
155+
}
156+
139157
void packageHttpPost({bool hasBody = false}) async {
140158
print('Sending package:http POST...');
141159
final response = await http.post(

packages/devtools_extensions/lib/src/template/devtools_extension.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ T _accessGlobalOrThrow<T>({required String globalName}) {
8989
throw StateError(
9090
"'$globalName' has not been initialized yet. You can only access "
9191
"'$globalName' below the 'DevToolsExtension' widget in the widget "
92-
"tree, since it is initialized as part of the 'DevToolsExtension'"
92+
"tree, since it is initialized as part of the 'DevToolsExtension' "
9393
"state's 'initState' lifecycle method.",
9494
);
9595
}

0 commit comments

Comments
 (0)