golang.org/x/net@v0.25.1-0.20240516223405-c87a5b62e243/quic/qlog_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build go1.21
     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() // let the conn finish draining
    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() // let the conn finish draining
    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) // long enough for the drain timer to expire
   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  	// Ignore events from the handshake.
   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  	// Don't check the contents of fields, but verify that recovery metrics are logged.
   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  	// Ignore events from the handshake.
   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  	// A garbage-filled datagram with a DCID matching this connection.
   240  	dgram := bytes.Join([][]byte{
   241  		{headerFormShort | fixedBit},
   242  		testLocalConnID(0),
   243  		make([]byte, 100),
   244  		[]byte{1, 2, 3, 4}, // random data, to avoid this looking like a stateless reset
   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  // jsonPartialEqual compares two JSON structures.
   269  // It ignores fields not set in want (see below for specifics).
   270  func jsonPartialEqual(got, want any) (equal bool) {
   271  	cmpval := func(v any) any {
   272  		// Map certain types to a common representation.
   273  		switch v := v.(type) {
   274  		case int:
   275  			// JSON uses float64s rather than ints for numbers.
   276  			// Map int->float64 so we can use integers in expectations.
   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 // match anything
   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  		// JSON object: Every field in want must match a field in got.
   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  		// JSON slice: Every field in want must match a field in got, in order.
   308  		// So want=[2,4] matches got=[1,2,3,4] but not [4,2].
   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  // A qlogRecord records events.
   328  type qlogRecord struct {
   329  	ev []jsonEvent
   330  }
   331  
   332  func (q *qlogRecord) Write(b []byte) (int, error) {
   333  	// This relies on the property that the Handler always makes one Write call per event.
   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  // config may be passed to newTestConn to configure the conn to use this logger.
   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  // wantEvents checks that every event in want occurs in the order specified.
   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  }