blob: 9ef471946a27b69cf3fd1386e73d8b8998d79c12 [file] [log] [blame]
// 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);