aboutsummaryrefslogtreecommitdiffstats
path: root/test/recipes/70-test_quic_multistream_data/verify-qlog.py
blob: ae11007f3ea784d77ba8db27deb2851c5efc1542 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#!/usr/bin/env python3
#
# Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
#
# Licensed under the Apache License 2.0 (the "License").  You may not use
# this file except in compliance with the License.  You can obtain a copy
# in the file LICENSE in the source distribution or at
# https://www.openssl.org/source/license.html
import sys, os, os.path, glob, json, re

re_version = re.compile(r'''^OpenSSL/[0-9]+\.[0-9]\.[0-9](-[^ ]+)? ([^)]+)''')

class Unexpected(Exception):
    def __init__(self, filename, msg):
        Exception.__init__(self, f"file {repr(filename)}: {msg}")

class Malformed(Exception):
    def __init__(self, line, msg):
        Exception.__init__(self, f"{line}: {msg}")

event_type_counts = {}
frame_type_counts = {}

def load_file(filename):
    objs = []
    with open(filename, 'r') as fi:
        for line in fi:
            if line[0] != '\x1e':
                raise Unexpected(filename, "expected JSON-SEQ leader")

            line = line[1:]
            try:
                objs.append(json.loads(line))
            except:
                fi.seek(0)
                fdata = fi.read()
                print(fdata)
                raise Malformed(line, "Malformed json input")
    return objs

def check_header(filename, hdr):
    if not 'qlog_format' in hdr:
        raise Unexpected(filename, "must have qlog_format in header line")

    if not 'qlog_version' in hdr:
        raise Unexpected(filename, "must have qlog_version in header line")

    if not 'trace' in hdr:
        raise Unexpected(filename, "must have trace in header line")

    hdr_trace = hdr["trace"]
    if not 'common_fields' in hdr_trace:
        raise Unexpected(filename, "must have common_fields in header line")

    if not 'vantage_point' in hdr_trace:
        raise Unexpected(filename, "must have vantage_point in header line")

    if hdr_trace["vantage_point"].get('type') not in ('client', 'server'):
        raise Unexpected(filename, "unexpected vantage_point")

    vp_name = hdr_trace["vantage_point"].get('name')
    if type(vp_name) != str:
        raise Unexpected(filename, "expected vantage_point name")

    if not re_version.match(vp_name):
        raise Unexpected(filename, "expected correct vantage_point format")

    hdr_common_fields = hdr_trace["common_fields"]
    if hdr_common_fields.get("time_format") != "delta":
        raise Unexpected(filename, "must have expected time_format")

    if hdr_common_fields.get("protocol_type") != ["QUIC"]:
        raise Unexpected(filename, "must have expected protocol_type")

    if hdr["qlog_format"] != "JSON-SEQ":
        raise Unexpected(filename, "unexpected qlog_format")

    if hdr["qlog_version"] != "0.3":
        raise Unexpected(filename, "unexpected qlog_version")

def check_event(filename, event):
    name = event.get("name")

    if type(name) != str:
        raise Unexpected(filename, "expected event to have name")

    event_type_counts.setdefault(name, 0)
    event_type_counts[name] += 1

    if type(event.get("time")) != int:
        raise Unexpected(filename, "expected event to have time")

    data = event.get('data')
    if type(data) != dict:
        raise Unexpected(filename, "expected event to have data")

    if "qlog_format" in event:
        raise Unexpected(filename, "event must not be header line")

    if name in ('transport:packet_sent', 'transport:packet_received'):
        check_packet_header(filename, event, data.get('header'))

        datagram_id = data.get('datagram_id')
        if type(datagram_id) != int:
            raise Unexpected(filename, "datagram ID must be integer")

        for frame in data.get('frames', []):
            check_frame(filename, event, frame)

def check_packet_header(filename, event, header):
    if type(header) != dict:
        raise Unexpected(filename, "expected object for packet header")

    # packet type -> has frames?
    packet_types = {
            'version_negotiation': False,
            'retry': False,
            'initial': True,
            'handshake': True,
            '0RTT': True,
            '1RTT': True,
    }

    data = event['data']
    packet_type = header.get('packet_type')
    if packet_type not in packet_types:
        raise Unexpected(filename, f"unexpected packet type: {packet_type}")

    if type(header.get('dcid')) != str:
        raise Unexpected(filename, "expected packet event to have DCID")
    if packet_type != '1RTT' and type(header.get('scid')) != str:
        raise Unexpected(filename, "expected packet event to have SCID")

    if type(data.get('datagram_id')) != int:
        raise Unexpected(filename, "expected packet event to have datagram ID")

    if packet_types[packet_type]:
        if type(header.get('packet_number')) != int:
            raise Unexpected(filename, f"expected packet event to have packet number")
        if type(data.get('frames')) != list:
            raise Unexpected(filename, "expected packet event to have frames")

def check_frame(filename, event, frame):
    frame_type = frame.get('frame_type')
    if type(frame_type) != str:
        raise Unexpected(filename, "frame must have frame_type field")

    frame_type_counts.setdefault(event['name'], {})
    counts = frame_type_counts[event['name']]

    counts.setdefault(frame_type, 0)
    counts[frame_type] += 1

def check_file(filename):
    objs = load_file(filename)
    if len(objs) < 2:
        raise Unexpected(filename, "must have at least two objects")

    check_header(filename, objs[0])
    for event in objs[1:]:
        check_event(filename, event)

def run():
    num_files = 0

    # Check each file for validity.
    qlogdir = os.environ['QLOGDIR']
    for filename in glob.glob(os.path.join(qlogdir, '*.sqlog')):
        check_file(filename)
        num_files += 1

    # Check that all supported events were generated.
    required_events = (
        "transport:parameters_set",
        "connectivity:connection_state_updated",
        "connectivity:connection_started",
        "transport:packet_sent",
        "transport:packet_received",
        "connectivity:connection_closed"
    )

    if num_files < 300:
        raise Unexpected(qlogdir, f"unexpectedly few output files: {num_files}")

    for required_event in required_events:
        count = event_type_counts.get(required_event, 0)
        if count < 100:
            raise Unexpected(qlogdir, f"unexpectedly low count of event '{required_event}': got {count}")

    # For each direction, ensure that at least one of the tests we run generated
    # a given frame type.
    required_frame_types = (
        "padding",
        "ping",
        "ack",

        "crypto",
        "handshake_done",
        "connection_close",

        "path_challenge",
        "path_response",

        "stream",
        "reset_stream",
        "stop_sending",

        "new_connection_id",
        "retire_connection_id",

        "max_streams",
        "streams_blocked",

        "max_stream_data",
        "stream_data_blocked",

        "max_data",
        "data_blocked",

        "new_token",
    )

    for required_frame_type in required_frame_types:
        sent_count = frame_type_counts.get('transport:packet_sent', {}).get(required_frame_type, 0)
        if sent_count < 1:
            raise Unexpected(qlogdir, f"unexpectedly did not send any '{required_frame_type}' frames")

        received_count = frame_type_counts.get('transport:packet_received', {}).get(required_frame_type, 0)
        if received_count < 1:
            raise Unexpected(qlogdir, f"unexpectedly did not receive any '{required_frame_type}' frames")

    return 0

if __name__ == '__main__':
    sys.exit(run())