blob: 119f5d16af74d4b3cfebee28f83f92a0af9c07f7 [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.21
package quic
import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log/slog"
"reflect"
"testing"
"time"
"golang.org/x/net/internal/quic/qlog"
)
func TestQLogHandshake(t *testing.T) {
testSides(t, "", func(t *testing.T, side connSide) {
qr := &qlogRecord{}
tc := newTestConn(t, side, qr.config)
tc.handshake()
tc.conn.Abort(nil)
tc.wantFrame("aborting connection generates CONN_CLOSE",
packetType1RTT, debugFrameConnectionCloseTransport{
code: errNo,
})
tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{})
tc.advanceToTimer() // let the conn finish draining
var src, dst []byte
if side == clientSide {
src = testLocalConnID(0)
dst = testLocalConnID(-1)
} else {
src = testPeerConnID(-1)
dst = testPeerConnID(0)
}
qr.wantEvents(t, jsonEvent{
"name": "connectivity:connection_started",
"data": map[string]any{
"src_cid": hex.EncodeToString(src),
"dst_cid": hex.EncodeToString(dst),
},
}, jsonEvent{
"name": "connectivity:connection_closed",
"data": map[string]any{
"trigger": "clean",
},
})
})
}
func TestQLogConnectionClosedTrigger(t *testing.T) {
for _, test := range []struct {
trigger string
connOpts []any
f func(*testConn)
}{{
trigger: "clean",
f: func(tc *testConn) {
tc.handshake()
tc.conn.Abort(nil)
},
}, {
trigger: "handshake_timeout",
connOpts: []any{
func(c *Config) {
c.HandshakeTimeout = 5 * time.Second
},
},
f: func(tc *testConn) {
tc.ignoreFrame(frameTypeCrypto)
tc.ignoreFrame(frameTypeAck)
tc.ignoreFrame(frameTypePing)
tc.advance(5 * time.Second)
},
}, {
trigger: "idle_timeout",
connOpts: []any{
func(c *Config) {
c.MaxIdleTimeout = 5 * time.Second
},
},
f: func(tc *testConn) {
tc.handshake()
tc.advance(5 * time.Second)
},
}, {
trigger: "error",
f: func(tc *testConn) {
tc.handshake()
tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{
code: errProtocolViolation,
})
tc.conn.Abort(nil)
},
}} {
t.Run(test.trigger, func(t *testing.T) {
qr := &qlogRecord{}
tc := newTestConn(t, clientSide, append(test.connOpts, qr.config)...)
test.f(tc)
fr, ptype := tc.readFrame()
switch fr := fr.(type) {
case debugFrameConnectionCloseTransport:
tc.writeFrames(ptype, fr)
case nil:
default:
t.Fatalf("unexpected frame: %v", fr)
}
tc.wantIdle("connection should be idle while closing")
tc.advance(5 * time.Second) // long enough for the drain timer to expire
qr.wantEvents(t, jsonEvent{
"name": "connectivity:connection_closed",
"data": map[string]any{
"trigger": test.trigger,
},
})
})
}
}
type nopCloseWriter struct {
io.Writer
}
func (nopCloseWriter) Close() error { return nil }
type jsonEvent map[string]any
func (j jsonEvent) String() string {
b, _ := json.MarshalIndent(j, "", " ")
return string(b)
}
// eventPartialEqual verifies that every field set in want matches the corresponding field in got.
// It ignores additional fields in got.
func eventPartialEqual(got, want jsonEvent) bool {
for k := range want {
ge, gok := got[k].(map[string]any)
we, wok := want[k].(map[string]any)
if gok && wok {
if !eventPartialEqual(ge, we) {
return false
}
} else {
if !reflect.DeepEqual(got[k], want[k]) {
return false
}
}
}
return true
}
// A qlogRecord records events.
type qlogRecord struct {
ev []jsonEvent
}
func (q *qlogRecord) Write(b []byte) (int, error) {
// This relies on the property that the Handler always makes one Write call per event.
if len(b) < 1 || b[0] != 0x1e {
panic(fmt.Errorf("trace Write should start with record separator, got %q", string(b)))
}
var val map[string]any
if err := json.Unmarshal(b[1:], &val); err != nil {
panic(fmt.Errorf("log unmarshal failure: %v\n%v", err, string(b)))
}
q.ev = append(q.ev, val)
return len(b), nil
}
func (q *qlogRecord) Close() error { return nil }
// config may be passed to newTestConn to configure the conn to use this logger.
func (q *qlogRecord) config(c *Config) {
c.QLogLogger = slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{
NewTrace: func(info qlog.TraceInfo) (io.WriteCloser, error) {
return q, nil
},
}))
}
// wantEvents checks that every event in want occurs in the order specified.
func (q *qlogRecord) wantEvents(t *testing.T, want ...jsonEvent) {
t.Helper()
got := q.ev
unseen := want
for _, g := range got {
if eventPartialEqual(g, unseen[0]) {
unseen = unseen[1:]
if len(unseen) == 0 {
return
}
}
}
t.Fatalf("got events:\n%v\n\nwant events:\n%v", got, want)
}