1
2
3
4
5
6
7 package quic
8
9 import (
10 "bytes"
11 "encoding/hex"
12 "encoding/json"
13 "fmt"
14 "io"
15 "log/slog"
16 "reflect"
17 "testing"
18 "time"
19
20 "golang.org/x/net/quic/qlog"
21 )
22
23 func TestQLogHandshake(t *testing.T) {
24 testSides(t, "", func(t *testing.T, side connSide) {
25 qr := &qlogRecord{}
26 tc := newTestConn(t, side, qr.config)
27 tc.handshake()
28 tc.conn.Abort(nil)
29 tc.wantFrame("aborting connection generates CONN_CLOSE",
30 packetType1RTT, debugFrameConnectionCloseTransport{
31 code: errNo,
32 })
33 tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{})
34 tc.advanceToTimer()
35
36 var src, dst []byte
37 if side == clientSide {
38 src = testLocalConnID(0)
39 dst = testLocalConnID(-1)
40 } else {
41 src = testPeerConnID(-1)
42 dst = testPeerConnID(0)
43 }
44 qr.wantEvents(t, jsonEvent{
45 "name": "connectivity:connection_started",
46 "data": map[string]any{
47 "src_cid": hex.EncodeToString(src),
48 "dst_cid": hex.EncodeToString(dst),
49 },
50 }, jsonEvent{
51 "name": "connectivity:connection_closed",
52 "data": map[string]any{
53 "trigger": "clean",
54 },
55 })
56 })
57 }
58
59 func TestQLogPacketFrames(t *testing.T) {
60 qr := &qlogRecord{}
61 tc := newTestConn(t, clientSide, qr.config)
62 tc.handshake()
63 tc.conn.Abort(nil)
64 tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{})
65 tc.advanceToTimer()
66
67 qr.wantEvents(t, jsonEvent{
68 "name": "transport:packet_sent",
69 "data": map[string]any{
70 "header": map[string]any{
71 "packet_type": "initial",
72 "packet_number": 0,
73 "dcid": hex.EncodeToString(testLocalConnID(-1)),
74 "scid": hex.EncodeToString(testLocalConnID(0)),
75 },
76 "frames": []any{
77 map[string]any{"frame_type": "crypto"},
78 },
79 },
80 }, jsonEvent{
81 "name": "transport:packet_received",
82 "data": map[string]any{
83 "header": map[string]any{
84 "packet_type": "initial",
85 "packet_number": 0,
86 "dcid": hex.EncodeToString(testLocalConnID(0)),
87 "scid": hex.EncodeToString(testPeerConnID(0)),
88 },
89 "frames": []any{map[string]any{"frame_type": "crypto"}},
90 },
91 })
92 }
93
94 func TestQLogConnectionClosedTrigger(t *testing.T) {
95 for _, test := range []struct {
96 trigger string
97 connOpts []any
98 f func(*testConn)
99 }{{
100 trigger: "clean",
101 f: func(tc *testConn) {
102 tc.handshake()
103 tc.conn.Abort(nil)
104 },
105 }, {
106 trigger: "handshake_timeout",
107 connOpts: []any{
108 func(c *Config) {
109 c.HandshakeTimeout = 5 * time.Second
110 },
111 },
112 f: func(tc *testConn) {
113 tc.ignoreFrame(frameTypeCrypto)
114 tc.ignoreFrame(frameTypeAck)
115 tc.ignoreFrame(frameTypePing)
116 tc.advance(5 * time.Second)
117 },
118 }, {
119 trigger: "idle_timeout",
120 connOpts: []any{
121 func(c *Config) {
122 c.MaxIdleTimeout = 5 * time.Second
123 },
124 },
125 f: func(tc *testConn) {
126 tc.handshake()
127 tc.advance(5 * time.Second)
128 },
129 }, {
130 trigger: "error",
131 f: func(tc *testConn) {
132 tc.handshake()
133 tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{
134 code: errProtocolViolation,
135 })
136 tc.conn.Abort(nil)
137 },
138 }} {
139 t.Run(test.trigger, func(t *testing.T) {
140 qr := &qlogRecord{}
141 tc := newTestConn(t, clientSide, append(test.connOpts, qr.config)...)
142 test.f(tc)
143 fr, ptype := tc.readFrame()
144 switch fr := fr.(type) {
145 case debugFrameConnectionCloseTransport:
146 tc.writeFrames(ptype, fr)
147 case nil:
148 default:
149 t.Fatalf("unexpected frame: %v", fr)
150 }
151 tc.wantIdle("connection should be idle while closing")
152 tc.advance(5 * time.Second)
153 qr.wantEvents(t, jsonEvent{
154 "name": "connectivity:connection_closed",
155 "data": map[string]any{
156 "trigger": test.trigger,
157 },
158 })
159 })
160 }
161 }
162
163 func TestQLogRecovery(t *testing.T) {
164 qr := &qlogRecord{}
165 tc, s := newTestConnAndLocalStream(t, clientSide, uniStream,
166 permissiveTransportParameters, qr.config)
167
168
169 qr.ev = nil
170
171 data := make([]byte, 16)
172 s.Write(data)
173 s.CloseWrite()
174 tc.wantFrame("created stream 0",
175 packetType1RTT, debugFrameStream{
176 id: newStreamID(clientSide, uniStream, 0),
177 fin: true,
178 data: data,
179 })
180 tc.writeAckForAll()
181 tc.wantIdle("connection should be idle now")
182
183
184 qr.wantEvents(t, jsonEvent{
185 "name": "recovery:metrics_updated",
186 "data": map[string]any{
187 "bytes_in_flight": nil,
188 },
189 }, jsonEvent{
190 "name": "recovery:metrics_updated",
191 "data": map[string]any{
192 "bytes_in_flight": 0,
193 "congestion_window": nil,
194 "latest_rtt": nil,
195 "min_rtt": nil,
196 "rtt_variance": nil,
197 "smoothed_rtt": nil,
198 },
199 })
200 }
201
202 func TestQLogLoss(t *testing.T) {
203 qr := &qlogRecord{}
204 tc, s := newTestConnAndLocalStream(t, clientSide, uniStream,
205 permissiveTransportParameters, qr.config)
206
207
208 qr.ev = nil
209
210 data := make([]byte, 16)
211 s.Write(data)
212 s.CloseWrite()
213 tc.wantFrame("created stream 0",
214 packetType1RTT, debugFrameStream{
215 id: newStreamID(clientSide, uniStream, 0),
216 fin: true,
217 data: data,
218 })
219
220 const pto = false
221 tc.triggerLossOrPTO(packetType1RTT, pto)
222
223 qr.wantEvents(t, jsonEvent{
224 "name": "recovery:packet_lost",
225 "data": map[string]any{
226 "header": map[string]any{
227 "packet_number": nil,
228 "packet_type": "1RTT",
229 },
230 },
231 })
232 }
233
234 func TestQLogPacketDropped(t *testing.T) {
235 qr := &qlogRecord{}
236 tc := newTestConn(t, clientSide, permissiveTransportParameters, qr.config)
237 tc.handshake()
238
239
240 dgram := bytes.Join([][]byte{
241 {headerFormShort | fixedBit},
242 testLocalConnID(0),
243 make([]byte, 100),
244 []byte{1, 2, 3, 4},
245 }, nil)
246 tc.endpoint.write(&datagram{
247 b: dgram,
248 })
249
250 qr.wantEvents(t, jsonEvent{
251 "name": "connectivity:packet_dropped",
252 })
253 }
254
255 type nopCloseWriter struct {
256 io.Writer
257 }
258
259 func (nopCloseWriter) Close() error { return nil }
260
261 type jsonEvent map[string]any
262
263 func (j jsonEvent) String() string {
264 b, _ := json.MarshalIndent(j, "", " ")
265 return string(b)
266 }
267
268
269
270 func jsonPartialEqual(got, want any) (equal bool) {
271 cmpval := func(v any) any {
272
273 switch v := v.(type) {
274 case int:
275
276
277 return float64(v)
278 case jsonEvent:
279 return (map[string]any)(v)
280 case []jsonEvent:
281 s := []any{}
282 for _, e := range v {
283 s = append(s, e)
284 }
285 return s
286 }
287 return v
288 }
289 if want == nil {
290 return true
291 }
292 got = cmpval(got)
293 want = cmpval(want)
294 if reflect.TypeOf(got) != reflect.TypeOf(want) {
295 return false
296 }
297 switch w := want.(type) {
298 case map[string]any:
299
300 g := got.(map[string]any)
301 for k := range w {
302 if !jsonPartialEqual(g[k], w[k]) {
303 return false
304 }
305 }
306 case []any:
307
308
309 g := got.([]any)
310 for _, ge := range g {
311 if jsonPartialEqual(ge, w[0]) {
312 w = w[1:]
313 if len(w) == 0 {
314 return true
315 }
316 }
317 }
318 return false
319 default:
320 if !reflect.DeepEqual(got, want) {
321 return false
322 }
323 }
324 return true
325 }
326
327
328 type qlogRecord struct {
329 ev []jsonEvent
330 }
331
332 func (q *qlogRecord) Write(b []byte) (int, error) {
333
334 if len(b) < 1 || b[0] != 0x1e {
335 panic(fmt.Errorf("trace Write should start with record separator, got %q", string(b)))
336 }
337 var val map[string]any
338 if err := json.Unmarshal(b[1:], &val); err != nil {
339 panic(fmt.Errorf("log unmarshal failure: %v\n%v", err, string(b)))
340 }
341 q.ev = append(q.ev, val)
342 return len(b), nil
343 }
344
345 func (q *qlogRecord) Close() error { return nil }
346
347
348 func (q *qlogRecord) config(c *Config) {
349 c.QLogLogger = slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{
350 Level: QLogLevelFrame,
351 NewTrace: func(info qlog.TraceInfo) (io.WriteCloser, error) {
352 return q, nil
353 },
354 }))
355 }
356
357
358 func (q *qlogRecord) wantEvents(t *testing.T, want ...jsonEvent) {
359 t.Helper()
360 got := q.ev
361 if !jsonPartialEqual(got, want) {
362 t.Fatalf("got events:\n%v\n\nwant events:\n%v", got, want)
363 }
364 }
365
View as plain text