1
2
3
4
5
6
7 package quic
8
9 import (
10 "encoding/hex"
11 "encoding/json"
12 "fmt"
13 "io"
14 "log/slog"
15 "reflect"
16 "testing"
17 "time"
18
19 "golang.org/x/net/internal/quic/qlog"
20 )
21
22 func TestQLogHandshake(t *testing.T) {
23 testSides(t, "", func(t *testing.T, side connSide) {
24 qr := &qlogRecord{}
25 tc := newTestConn(t, side, qr.config)
26 tc.handshake()
27 tc.conn.Abort(nil)
28 tc.wantFrame("aborting connection generates CONN_CLOSE",
29 packetType1RTT, debugFrameConnectionCloseTransport{
30 code: errNo,
31 })
32 tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{})
33 tc.advanceToTimer()
34
35 var src, dst []byte
36 if side == clientSide {
37 src = testLocalConnID(0)
38 dst = testLocalConnID(-1)
39 } else {
40 src = testPeerConnID(-1)
41 dst = testPeerConnID(0)
42 }
43 qr.wantEvents(t, jsonEvent{
44 "name": "connectivity:connection_started",
45 "data": map[string]any{
46 "src_cid": hex.EncodeToString(src),
47 "dst_cid": hex.EncodeToString(dst),
48 },
49 }, jsonEvent{
50 "name": "connectivity:connection_closed",
51 "data": map[string]any{
52 "trigger": "clean",
53 },
54 })
55 })
56 }
57
58 func TestQLogPacketFrames(t *testing.T) {
59 qr := &qlogRecord{}
60 tc := newTestConn(t, clientSide, qr.config)
61 tc.handshake()
62 tc.conn.Abort(nil)
63 tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{})
64 tc.advanceToTimer()
65
66 qr.wantEvents(t, jsonEvent{
67 "name": "transport:packet_sent",
68 "data": map[string]any{
69 "header": map[string]any{
70 "packet_type": "initial",
71 "packet_number": 0,
72 "dcid": hex.EncodeToString(testLocalConnID(-1)),
73 "scid": hex.EncodeToString(testLocalConnID(0)),
74 },
75 "frames": []any{
76 map[string]any{"frame_type": "crypto"},
77 },
78 },
79 }, jsonEvent{
80 "name": "transport:packet_received",
81 "data": map[string]any{
82 "header": map[string]any{
83 "packet_type": "initial",
84 "packet_number": 0,
85 "dcid": hex.EncodeToString(testLocalConnID(0)),
86 "scid": hex.EncodeToString(testPeerConnID(0)),
87 },
88 "frames": []any{map[string]any{"frame_type": "crypto"}},
89 },
90 })
91 }
92
93 func TestQLogConnectionClosedTrigger(t *testing.T) {
94 for _, test := range []struct {
95 trigger string
96 connOpts []any
97 f func(*testConn)
98 }{{
99 trigger: "clean",
100 f: func(tc *testConn) {
101 tc.handshake()
102 tc.conn.Abort(nil)
103 },
104 }, {
105 trigger: "handshake_timeout",
106 connOpts: []any{
107 func(c *Config) {
108 c.HandshakeTimeout = 5 * time.Second
109 },
110 },
111 f: func(tc *testConn) {
112 tc.ignoreFrame(frameTypeCrypto)
113 tc.ignoreFrame(frameTypeAck)
114 tc.ignoreFrame(frameTypePing)
115 tc.advance(5 * time.Second)
116 },
117 }, {
118 trigger: "idle_timeout",
119 connOpts: []any{
120 func(c *Config) {
121 c.MaxIdleTimeout = 5 * time.Second
122 },
123 },
124 f: func(tc *testConn) {
125 tc.handshake()
126 tc.advance(5 * time.Second)
127 },
128 }, {
129 trigger: "error",
130 f: func(tc *testConn) {
131 tc.handshake()
132 tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{
133 code: errProtocolViolation,
134 })
135 tc.conn.Abort(nil)
136 },
137 }} {
138 t.Run(test.trigger, func(t *testing.T) {
139 qr := &qlogRecord{}
140 tc := newTestConn(t, clientSide, append(test.connOpts, qr.config)...)
141 test.f(tc)
142 fr, ptype := tc.readFrame()
143 switch fr := fr.(type) {
144 case debugFrameConnectionCloseTransport:
145 tc.writeFrames(ptype, fr)
146 case nil:
147 default:
148 t.Fatalf("unexpected frame: %v", fr)
149 }
150 tc.wantIdle("connection should be idle while closing")
151 tc.advance(5 * time.Second)
152 qr.wantEvents(t, jsonEvent{
153 "name": "connectivity:connection_closed",
154 "data": map[string]any{
155 "trigger": test.trigger,
156 },
157 })
158 })
159 }
160 }
161
162 type nopCloseWriter struct {
163 io.Writer
164 }
165
166 func (nopCloseWriter) Close() error { return nil }
167
168 type jsonEvent map[string]any
169
170 func (j jsonEvent) String() string {
171 b, _ := json.MarshalIndent(j, "", " ")
172 return string(b)
173 }
174
175
176
177 func jsonPartialEqual(got, want any) (equal bool) {
178 cmpval := func(v any) any {
179
180 switch v := v.(type) {
181 case int:
182
183
184 return float64(v)
185 case jsonEvent:
186 return (map[string]any)(v)
187 case []jsonEvent:
188 s := []any{}
189 for _, e := range v {
190 s = append(s, e)
191 }
192 return s
193 }
194 return v
195 }
196 got = cmpval(got)
197 want = cmpval(want)
198 if reflect.TypeOf(got) != reflect.TypeOf(want) {
199 return false
200 }
201 switch w := want.(type) {
202 case nil:
203
204 case map[string]any:
205
206 g := got.(map[string]any)
207 for k := range w {
208 if !jsonPartialEqual(g[k], w[k]) {
209 return false
210 }
211 }
212 case []any:
213
214
215 g := got.([]any)
216 for _, ge := range g {
217 if jsonPartialEqual(ge, w[0]) {
218 w = w[1:]
219 if len(w) == 0 {
220 return true
221 }
222 }
223 }
224 return false
225 default:
226 if !reflect.DeepEqual(got, want) {
227 return false
228 }
229 }
230 return true
231 }
232
233
234 type qlogRecord struct {
235 ev []jsonEvent
236 }
237
238 func (q *qlogRecord) Write(b []byte) (int, error) {
239
240 if len(b) < 1 || b[0] != 0x1e {
241 panic(fmt.Errorf("trace Write should start with record separator, got %q", string(b)))
242 }
243 var val map[string]any
244 if err := json.Unmarshal(b[1:], &val); err != nil {
245 panic(fmt.Errorf("log unmarshal failure: %v\n%v", err, string(b)))
246 }
247 q.ev = append(q.ev, val)
248 return len(b), nil
249 }
250
251 func (q *qlogRecord) Close() error { return nil }
252
253
254 func (q *qlogRecord) config(c *Config) {
255 c.QLogLogger = slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{
256 Level: QLogLevelFrame,
257 NewTrace: func(info qlog.TraceInfo) (io.WriteCloser, error) {
258 return q, nil
259 },
260 }))
261 }
262
263
264 func (q *qlogRecord) wantEvents(t *testing.T, want ...jsonEvent) {
265 t.Helper()
266 got := q.ev
267 if !jsonPartialEqual(got, want) {
268 t.Fatalf("got events:\n%v\n\nwant events:\n%v", got, want)
269 }
270 }
271
View as plain text