| // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| // VMOptions=--timeline_streams=Dart |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:math'; |
| import 'dart:typed_data'; |
| import 'package:expect/expect.dart'; |
| import 'package:observatory/service_io.dart'; |
| import 'package:test/test.dart'; |
| |
| import 'test_helper.dart'; |
| |
| final rng = Random(); |
| |
| // Enable to test redirects. |
| const shouldTestRedirects = false; |
| |
| const maxRequestDelayMs = 3000; |
| const maxResponseDelayMs = 500; |
| const serverShutdownDelayMs = 2000; |
| |
| void randomlyAddCookie(HttpResponse response) { |
| if (rng.nextInt(3) == 0) { |
| response.cookies.add(Cookie('Cookie-Monster', 'Me-want-cookie!')); |
| } |
| } |
| |
| Future<bool> randomlyRedirect(HttpServer server, HttpResponse response) async { |
| if (shouldTestRedirects && rng.nextInt(5) == 0) { |
| final redirectUri = Uri(host: 'www.google.com', port: 80); |
| response.redirect(redirectUri); |
| return true; |
| } |
| return false; |
| } |
| |
| // Execute HTTP requests with random delays so requests have some overlap. This |
| // way we can be certain that timeline events are matching up properly even when |
| // connections are interrupted or can't be established. |
| Future<void> executeWithRandomDelay(Function f) => |
| Future<void>.delayed(Duration(milliseconds: rng.nextInt(maxRequestDelayMs))) |
| .then((_) async { |
| try { |
| await f(); |
| } on HttpException catch (_) {} on SocketException catch (_) {} on StateError catch (_) {} on OSError catch (_) {} |
| }); |
| |
| Uri randomlyAddRequestParams(Uri uri) { |
| const possiblePathSegments = <String>['foo', 'bar', 'baz', 'foobar']; |
| final segmentSubset = |
| possiblePathSegments.sublist(0, rng.nextInt(possiblePathSegments.length)); |
| uri = uri.replace(pathSegments: segmentSubset); |
| if (rng.nextInt(3) == 0) { |
| uri = uri.replace(queryParameters: { |
| 'foo': 'bar', |
| 'year': '2019', |
| }); |
| } |
| return uri; |
| } |
| |
| Future<HttpServer> startServer() async { |
| final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); |
| server.listen((request) async { |
| final response = request.response; |
| response.write(request.method); |
| randomlyAddCookie(response); |
| if (await randomlyRedirect(server, response)) { |
| // Redirect calls close() on the response. |
| return; |
| } |
| // Randomly delay response. |
| await Future.delayed( |
| Duration(milliseconds: rng.nextInt(maxResponseDelayMs))); |
| response.close(); |
| }); |
| return server; |
| } |
| |
| Future<void> testMain() async { |
| // Ensure there's a chance some requests will be interrupted. |
| Expect.isTrue(maxRequestDelayMs > serverShutdownDelayMs); |
| Expect.isTrue(maxResponseDelayMs < serverShutdownDelayMs); |
| |
| final server = await startServer(); |
| HttpClient.enableTimelineLogging = true; |
| final client = HttpClient(); |
| final requests = <Future>[]; |
| final address = |
| Uri(scheme: 'http', host: server.address.host, port: server.port); |
| |
| // HTTP DELETE |
| for (int i = 0; i < 10; ++i) { |
| final future = executeWithRandomDelay(() async { |
| final r = await client.deleteUrl(randomlyAddRequestParams(address)); |
| final string = 'DELETE $address'; |
| r.headers.add(HttpHeaders.contentLengthHeader, string.length); |
| r.write(string); |
| final response = await r.close(); |
| response.listen((_) {}); |
| }); |
| requests.add(future); |
| } |
| |
| // HTTP GET |
| for (int i = 0; i < 10; ++i) { |
| final future = executeWithRandomDelay(() async { |
| final r = await client.getUrl(randomlyAddRequestParams(address)); |
| final response = await r.close(); |
| await response.drain(); |
| }); |
| requests.add(future); |
| } |
| // HTTP HEAD |
| for (int i = 0; i < 10; ++i) { |
| final future = executeWithRandomDelay(() async { |
| final r = await client.headUrl(randomlyAddRequestParams(address)); |
| await r.close(); |
| }); |
| requests.add(future); |
| } |
| |
| // HTTP CONNECT |
| for (int i = 0; i < 10; ++i) { |
| final future = executeWithRandomDelay(() async { |
| final r = |
| await client.openUrl('connect', randomlyAddRequestParams(address)); |
| await r.close(); |
| }); |
| requests.add(future); |
| } |
| |
| // HTTP PATCH |
| for (int i = 0; i < 10; ++i) { |
| final future = executeWithRandomDelay(() async { |
| final r = await client.patchUrl(randomlyAddRequestParams(address)); |
| final response = await r.close(); |
| response.listen(null); |
| }); |
| requests.add(future); |
| } |
| |
| // HTTP POST |
| for (int i = 0; i < 10; ++i) { |
| final future = executeWithRandomDelay(() async { |
| final r = await client.postUrl(randomlyAddRequestParams(address)); |
| r.add(Uint8List.fromList([0, 1, 2])); |
| await r.close(); |
| }); |
| requests.add(future); |
| } |
| |
| // HTTP PUT |
| for (int i = 0; i < 10; ++i) { |
| final future = executeWithRandomDelay(() async { |
| final r = await client.putUrl(randomlyAddRequestParams(address)); |
| await r.close(); |
| }); |
| requests.add(future); |
| } |
| |
| // Purposefully close server before some connections can be made to ensure |
| // that refused / interrupted connections correctly create finish timeline |
| // events. |
| await Future.delayed(Duration(milliseconds: serverShutdownDelayMs)); |
| await server.close(); |
| |
| // Ensure all requests complete before finishing. |
| await Future.wait(requests); |
| } |
| |
| bool isStartEvent(Map event) => (event['ph'] == 'b'); |
| bool isFinishEvent(Map event) => (event['ph'] == 'e'); |
| |
| bool hasCompletedEvents(List traceEvents) { |
| final events = <String, int>{}; |
| for (final event in traceEvents) { |
| final id = event['id']; |
| events.putIfAbsent(id, () => 0); |
| if (isStartEvent(event)) { |
| events[id] = events[id]! + 1; |
| } else if (isFinishEvent(event)) { |
| events[id] = events[id]! - 1; |
| } |
| } |
| bool valid = true; |
| events.forEach((id, count) { |
| if (count != 0) { |
| valid = false; |
| } |
| }); |
| return valid; |
| } |
| |
| List filterEventsByName(List traceEvents, String name) => |
| traceEvents.where((e) => e['name'].contains(name)).toList(); |
| |
| List filterEventsByIdAndName(List traceEvents, String id, String name) => |
| traceEvents |
| .where((e) => e['id'] == id && e['name'].contains(name)) |
| .toList(); |
| |
| void hasValidHttpConnections(List traceEvents) { |
| final events = filterEventsByName(traceEvents, 'HTTP Connection'); |
| expect(hasCompletedEvents(events), isTrue); |
| } |
| |
| void validateHttpStartEvent(Map event, String method) { |
| expect(event.containsKey('args'), isTrue); |
| final args = event['args']; |
| expect(args.containsKey('method'), isTrue); |
| expect(args['method'], method); |
| expect(args['filterKey'], 'HTTP/client'); |
| expect(args.containsKey('uri'), isTrue); |
| } |
| |
| void validateHttpFinishEvent(Map event) { |
| expect(event.containsKey('args'), isTrue); |
| final args = event['args']; |
| expect(args['filterKey'], 'HTTP/client'); |
| if (!args.containsKey('error')) { |
| expect(args.containsKey('requestHeaders'), isTrue); |
| expect(args['requestHeaders'] != null, isTrue); |
| expect(args.containsKey('compressionState'), isTrue); |
| expect(args.containsKey('connectionInfo'), isTrue); |
| expect(args.containsKey('contentLength'), isTrue); |
| expect(args.containsKey('cookies'), isTrue); |
| expect(args.containsKey('responseHeaders'), isTrue); |
| expect(args.containsKey('isRedirect'), isTrue); |
| expect(args.containsKey('persistentConnection'), isTrue); |
| expect(args.containsKey('reasonPhrase'), isTrue); |
| expect(args.containsKey('redirects'), isTrue); |
| expect(args.containsKey('statusCode'), isTrue); |
| // If proxyInfo is non-null, uri and port _must_ be non-null. |
| if (args.containsKey('proxyInfo')) { |
| final proxyInfo = args['proxyInfo']; |
| expect(proxyInfo.containsKey('uri'), isTrue); |
| expect(proxyInfo.containsKey('port'), isTrue); |
| } |
| } |
| } |
| |
| void hasValidHttpRequests(List traceEvents, String method) { |
| var events = filterEventsByName(traceEvents, 'HTTP CLIENT $method'); |
| for (final event in events) { |
| if (isStartEvent(event)) { |
| validateHttpStartEvent(event, method); |
| } else if (isFinishEvent(event)) { |
| validateHttpFinishEvent(event); |
| } else { |
| fail('unexpected event type: ${event["ph"]}'); |
| } |
| } |
| |
| // Check response body matches string stored in the map. |
| events = filterEventsByName(traceEvents, 'HTTP CLIENT response of $method'); |
| if (method == 'DELETE') { |
| // It called listen(). |
| expect(hasCompletedEvents(events), isTrue); |
| } |
| for (final event in events) { |
| // Each response will be associated with a request. |
| if (isFinishEvent(event)) { |
| continue; |
| } |
| final id = event['id']; |
| final data = filterEventsByIdAndName(traceEvents, id, 'Response body'); |
| if (data.length != 0) { |
| Expect.equals(1, data.length); |
| Expect.listEquals(utf8.encode(method), data[0]['args']['data']); |
| } |
| } |
| } |
| |
| void hasValidHttpCONNECTs(List traceEvents) => |
| hasValidHttpRequests(traceEvents, 'CONNECT'); |
| void hasValidHttpDELETEs(List traceEvents) => |
| hasValidHttpRequests(traceEvents, 'DELETE'); |
| void hasValidHttpGETs(List traceEvents) => |
| hasValidHttpRequests(traceEvents, 'GET'); |
| void hasValidHttpHEADs(List traceEvents) => |
| hasValidHttpRequests(traceEvents, 'HEAD'); |
| void hasValidHttpPATCHs(List traceEvents) => |
| hasValidHttpRequests(traceEvents, 'PATCH'); |
| void hasValidHttpPOSTs(List traceEvents) => |
| hasValidHttpRequests(traceEvents, 'POST'); |
| void hasValidHttpPUTs(List traceEvents) => |
| hasValidHttpRequests(traceEvents, 'PUT'); |
| |
| var tests = <IsolateTest>[ |
| (Isolate isolate) async { |
| final result = await isolate.vm.invokeRpcNoUpgrade('getVMTimeline', {}); |
| expect(result['type'], 'Timeline'); |
| expect(result.containsKey('traceEvents'), isTrue); |
| final traceEvents = result['traceEvents']; |
| expect(traceEvents.length > 0, isTrue); |
| hasValidHttpConnections(traceEvents); |
| hasValidHttpCONNECTs(traceEvents); |
| hasValidHttpDELETEs(traceEvents); |
| hasValidHttpGETs(traceEvents); |
| hasValidHttpHEADs(traceEvents); |
| hasValidHttpPATCHs(traceEvents); |
| hasValidHttpPOSTs(traceEvents); |
| hasValidHttpPUTs(traceEvents); |
| }, |
| ]; |
| |
| main(args) async => runIsolateTests(args, tests, testeeBefore: testMain); |