blob: e9f6779bf8fc3545755b7e267627cc8989f22b73 [file] [edit]
// Copyright (c) 2026, 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:io';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/test_helper.dart';
/// Generates WebSocket timeline activity by establishing a connection,
/// exchanging a text message, and closing the connection.
Future<void> testeeMain(List<String> args) async {
HttpClient.enableTimelineLogging = true;
final server = await HttpServer.bind(
InternetAddress.loopbackIPv4,
0,
);
server.transform(WebSocketTransformer()).listen((socket) {
socket.listen((message) {
socket.add(message);
});
});
final client = await WebSocket.connect(
'ws://${server.address.host}:${server.port}',
);
client.add('hello');
await client.first;
await client.close(
WebSocketStatus.normalClosure,
'bye',
);
await server.close();
}
class ContainsTimelineEvent extends CustomMatcher {
final String name;
final Map<String, dynamic>? expectedArgs;
ContainsTimelineEvent(
this.name, {
this.expectedArgs,
}) : super(
'timeline event "$name"',
'timeline event',
anything,
);
@override
Object? featureValueOf(dynamic actual) {
if (actual is! List<TimelineEvent>) {
return null;
}
for (final timelineEvent in actual) {
final event = timelineEvent.json!;
if (event['name'] != name) {
continue;
}
if (expectedArgs == null) {
return event;
}
final actualArgs = event['args'];
if (actualArgs is! Map) {
throw StateError(
'Expected timeline event args to be a Map, '
'got ${actualArgs.runtimeType}',
);
}
final matches = expectedArgs!.entries.every(
(entry) => actualArgs[entry.key] == entry.value,
);
if (matches) {
return event;
}
}
return null;
}
}
/// Returns a matcher that verifies the timeline contains an event
/// with the given name and optional argument subset.
Matcher containsTimelineEvent(
String name, {
Map<String, dynamic>? expectedArgs,
}) {
return ContainsTimelineEvent(
name,
expectedArgs: expectedArgs,
);
}
/// Binary frame events are not verified separately because they follow the
/// same instrumentation path as text frames.
///
/// Ping and Pong events are not verified here because they are emitted by
/// timer-driven keep alive logic, making their occurrence timing-dependent.
final tests = <IsolateTest>[
(VmService service, IsolateRef isolateRef) async {
final timeline = await service.getVMTimeline();
final traceEvents = timeline.traceEvents!;
expect(traceEvents, isNotEmpty);
expect(
traceEvents,
containsTimelineEvent(
'WebSocket.Connect',
),
);
expect(
traceEvents,
containsTimelineEvent(
'WebSocket.Send',
expectedArgs: {
'direction': 'out',
'opcode': 'text',
},
),
);
expect(
traceEvents,
containsTimelineEvent(
'WebSocket.Receive',
expectedArgs: {
'direction': 'in',
'opcode': 'text',
},
),
);
expect(
traceEvents,
containsTimelineEvent(
'WebSocket.Close',
expectedArgs: {
'direction': 'out',
'closeCode': WebSocketStatus.normalClosure,
'reason': 'bye',
},
),
);
},
];
void main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'verify_websocket_timeline_test.dart',
testeeMain: testeeMain,
extraArgs: ['--complete-timeline'],
);