mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Better error message from ExpectFrame
Display the errors as diffs between the expected and wanted frame. PiperOrigin-RevId: 308333271
This commit is contained in:
@@ -21,7 +21,6 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -66,14 +65,16 @@ func pickPort() (int, uint16, error) {
|
||||
|
||||
// layerState stores the state of a layer of a connection.
|
||||
type layerState interface {
|
||||
// outgoing returns an outgoing layer to be sent in a frame.
|
||||
// outgoing returns an outgoing layer to be sent in a frame. It should not
|
||||
// update layerState, that is done in layerState.sent.
|
||||
outgoing() Layer
|
||||
|
||||
// incoming creates an expected Layer for comparing against a received Layer.
|
||||
// Because the expectation can depend on values in the received Layer, it is
|
||||
// an input to incoming. For example, the ACK number needs to be checked in a
|
||||
// TCP packet but only if the ACK flag is set in the received packet. The
|
||||
// calles takes ownership of the returned Layer.
|
||||
// TCP packet but only if the ACK flag is set in the received packet. It
|
||||
// should not update layerState, that is done in layerState.received. The
|
||||
// caller takes ownership of the returned Layer.
|
||||
incoming(received Layer) Layer
|
||||
|
||||
// sent updates the layerState based on the Layer that was sent. The input is
|
||||
@@ -363,44 +364,33 @@ type Connection struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
// match tries to match each Layer in received against the incoming filter. If
|
||||
// received is longer than layerStates then that may still count as a match. The
|
||||
// reverse is never a match. override overrides the default matchers for each
|
||||
// Layer.
|
||||
// Returns the default incoming frame against which to match. If received is
|
||||
// longer than layerStates then that may still count as a match. The reverse is
|
||||
// never a match and nil is returned.
|
||||
func (conn *Connection) incoming(received Layers) Layers {
|
||||
if len(received) < len(conn.layerStates) {
|
||||
return nil
|
||||
}
|
||||
in := Layers{}
|
||||
for i, s := range conn.layerStates {
|
||||
toMatch := s.incoming(received[i])
|
||||
if toMatch == nil {
|
||||
return nil
|
||||
}
|
||||
in = append(in, toMatch)
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
func (conn *Connection) match(override, received Layers) bool {
|
||||
var layersToMatch int
|
||||
if len(override) < len(conn.layerStates) {
|
||||
layersToMatch = len(conn.layerStates)
|
||||
} else {
|
||||
layersToMatch = len(override)
|
||||
toMatch := conn.incoming(received)
|
||||
if toMatch == nil {
|
||||
return false // Not enough layers in gotLayers for matching.
|
||||
}
|
||||
if len(received) < layersToMatch {
|
||||
return false
|
||||
if err := toMatch.merge(override); err != nil {
|
||||
return false // Failing to merge is not matching.
|
||||
}
|
||||
for i := 0; i < layersToMatch; i++ {
|
||||
var toMatch Layer
|
||||
if i < len(conn.layerStates) {
|
||||
s := conn.layerStates[i]
|
||||
toMatch = s.incoming(received[i])
|
||||
if toMatch == nil {
|
||||
return false
|
||||
}
|
||||
if i < len(override) {
|
||||
if err := toMatch.merge(override[i]); err != nil {
|
||||
conn.t.Fatalf("failed to merge: %s", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toMatch = override[i]
|
||||
if toMatch == nil {
|
||||
conn.t.Fatalf("expect the overriding layers to be non-nil")
|
||||
}
|
||||
}
|
||||
if !toMatch.match(received[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return toMatch.match(received)
|
||||
}
|
||||
|
||||
// Close frees associated resources held by the Connection.
|
||||
@@ -470,6 +460,16 @@ func (conn *Connection) recvFrame(timeout time.Duration) Layers {
|
||||
return parse(parseEther, b)
|
||||
}
|
||||
|
||||
// layersError stores the Layers that we got and the Layers that we wanted to
|
||||
// match.
|
||||
type layersError struct {
|
||||
got, want Layers
|
||||
}
|
||||
|
||||
func (e *layersError) Error() string {
|
||||
return e.got.diff(e.want)
|
||||
}
|
||||
|
||||
// Expect a frame with the final layerStates layer matching the provided Layer
|
||||
// within the timeout specified. If it doesn't arrive in time, it returns nil.
|
||||
func (conn *Connection) Expect(layer Layer, timeout time.Duration) (Layer, error) {
|
||||
@@ -485,21 +485,25 @@ func (conn *Connection) Expect(layer Layer, timeout time.Duration) (Layer, error
|
||||
return gotFrame[len(conn.layerStates)-1], nil
|
||||
}
|
||||
conn.t.Fatal("the received frame should be at least as long as the expected layers")
|
||||
return nil, fmt.Errorf("the received frame should be at least as long as the expected layers")
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// ExpectFrame expects a frame that matches the provided Layers within the
|
||||
// timeout specified. If it doesn't arrive in time, it returns nil.
|
||||
// timeout specified. If one arrives in time, the Layers is returned without an
|
||||
// error. If it doesn't arrive in time, it returns nil and error is non-nil.
|
||||
func (conn *Connection) ExpectFrame(layers Layers, timeout time.Duration) (Layers, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
var allLayers []string
|
||||
var errs error
|
||||
for {
|
||||
var gotLayers Layers
|
||||
if timeout = time.Until(deadline); timeout > 0 {
|
||||
gotLayers = conn.recvFrame(timeout)
|
||||
}
|
||||
if gotLayers == nil {
|
||||
return nil, fmt.Errorf("got %d packets:\n%s", len(allLayers), strings.Join(allLayers, "\n"))
|
||||
if errs == nil {
|
||||
return nil, fmt.Errorf("got no frames matching %v during %s", layers, timeout)
|
||||
}
|
||||
return nil, fmt.Errorf("got no frames matching %v during %s: got %w", layers, timeout, errs)
|
||||
}
|
||||
if conn.match(layers, gotLayers) {
|
||||
for i, s := range conn.layerStates {
|
||||
@@ -509,7 +513,7 @@ func (conn *Connection) ExpectFrame(layers Layers, timeout time.Duration) (Layer
|
||||
}
|
||||
return gotLayers, nil
|
||||
}
|
||||
allLayers = append(allLayers, fmt.Sprintf("%s", gotLayers))
|
||||
errs = multierr.Combine(errs, &layersError{got: gotLayers, want: conn.incoming(gotLayers)})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"go.uber.org/multierr"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
@@ -720,3 +721,247 @@ func (ls *Layers) match(other Layers) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// layerDiff stores the diffs for each field along with the label for the Layer.
|
||||
// If rows is nil, that means that there was no diff.
|
||||
type layerDiff struct {
|
||||
label string
|
||||
rows []layerDiffRow
|
||||
}
|
||||
|
||||
// layerDiffRow stores the fields and corresponding values for two got and want
|
||||
// layers. If the value was nil then the string stored is the empty string.
|
||||
type layerDiffRow struct {
|
||||
field, got, want string
|
||||
}
|
||||
|
||||
// diffLayer extracts all differing fields between two layers.
|
||||
func diffLayer(got, want Layer) []layerDiffRow {
|
||||
vGot := reflect.ValueOf(got).Elem()
|
||||
vWant := reflect.ValueOf(want).Elem()
|
||||
if vGot.Type() != vWant.Type() {
|
||||
return nil
|
||||
}
|
||||
t := vGot.Type()
|
||||
var result []layerDiffRow
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
t := t.Field(i)
|
||||
if t.Anonymous {
|
||||
// Ignore the LayerBase in the Layer struct.
|
||||
continue
|
||||
}
|
||||
vGot := vGot.Field(i)
|
||||
vWant := vWant.Field(i)
|
||||
gotString := ""
|
||||
if !vGot.IsNil() {
|
||||
gotString = fmt.Sprint(reflect.Indirect(vGot))
|
||||
}
|
||||
wantString := ""
|
||||
if !vWant.IsNil() {
|
||||
wantString = fmt.Sprint(reflect.Indirect(vWant))
|
||||
}
|
||||
result = append(result, layerDiffRow{t.Name, gotString, wantString})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// layerType returns a concise string describing the type of the Layer, like
|
||||
// "TCP", or "IPv6".
|
||||
func layerType(l Layer) string {
|
||||
return reflect.TypeOf(l).Elem().Name()
|
||||
}
|
||||
|
||||
// diff compares Layers and returns a representation of the difference. Each
|
||||
// Layer in the Layers is pairwise compared. If an element in either is nil, it
|
||||
// is considered a match with the other Layer. If two Layers have differing
|
||||
// types, they don't match regardless of the contents. If two Layers have the
|
||||
// same type then the fields in the Layer are pairwise compared. Fields that are
|
||||
// nil always match. Two non-nil fields only match if they point to equal
|
||||
// values. diff returns an empty string if and only if *ls and other match.
|
||||
func (ls *Layers) diff(other Layers) string {
|
||||
var allDiffs []layerDiff
|
||||
// Check the cases where one list is longer than the other, where one or both
|
||||
// elements are nil, where the sides have different types, and where the sides
|
||||
// have the same type.
|
||||
for i := 0; i < len(*ls) || i < len(other); i++ {
|
||||
if i >= len(*ls) {
|
||||
// Matching ls against other where other is longer than ls. missing
|
||||
// matches everything so we just include a label without any rows. Having
|
||||
// no rows is a sign that there was no diff.
|
||||
allDiffs = append(allDiffs, layerDiff{
|
||||
label: "missing matches " + layerType(other[i]),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if i >= len(other) {
|
||||
// Matching ls against other where ls is longer than other. missing
|
||||
// matches everything so we just include a label without any rows. Having
|
||||
// no rows is a sign that there was no diff.
|
||||
allDiffs = append(allDiffs, layerDiff{
|
||||
label: layerType((*ls)[i]) + " matches missing",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (*ls)[i] == nil && other[i] == nil {
|
||||
// Matching ls against other where both elements are nil. nil matches
|
||||
// everything so we just include a label without any rows. Having no rows
|
||||
// is a sign that there was no diff.
|
||||
allDiffs = append(allDiffs, layerDiff{
|
||||
label: "nil matches nil",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (*ls)[i] == nil {
|
||||
// Matching ls against other where the element in ls is nil. nil matches
|
||||
// everything so we just include a label without any rows. Having no rows
|
||||
// is a sign that there was no diff.
|
||||
allDiffs = append(allDiffs, layerDiff{
|
||||
label: "nil matches " + layerType(other[i]),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if other[i] == nil {
|
||||
// Matching ls against other where the element in other is nil. nil
|
||||
// matches everything so we just include a label without any rows. Having
|
||||
// no rows is a sign that there was no diff.
|
||||
allDiffs = append(allDiffs, layerDiff{
|
||||
label: layerType((*ls)[i]) + " matches nil",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if reflect.TypeOf((*ls)[i]) == reflect.TypeOf(other[i]) {
|
||||
// Matching ls against other where both elements have the same type. Match
|
||||
// each field pairwise and only report a diff if there is a mismatch,
|
||||
// which is only when both sides are non-nil and have differring values.
|
||||
diff := diffLayer((*ls)[i], other[i])
|
||||
var layerDiffRows []layerDiffRow
|
||||
for _, d := range diff {
|
||||
if d.got == "" || d.want == "" || d.got == d.want {
|
||||
continue
|
||||
}
|
||||
layerDiffRows = append(layerDiffRows, layerDiffRow{
|
||||
d.field,
|
||||
d.got,
|
||||
d.want,
|
||||
})
|
||||
}
|
||||
if len(layerDiffRows) > 0 {
|
||||
allDiffs = append(allDiffs, layerDiff{
|
||||
label: layerType((*ls)[i]),
|
||||
rows: layerDiffRows,
|
||||
})
|
||||
} else {
|
||||
allDiffs = append(allDiffs, layerDiff{
|
||||
label: layerType((*ls)[i]) + " matches " + layerType(other[i]),
|
||||
// Having no rows is a sign that there was no diff.
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Neither side is nil and the types are different, so we'll display one
|
||||
// side then the other.
|
||||
allDiffs = append(allDiffs, layerDiff{
|
||||
label: layerType((*ls)[i]) + " doesn't match " + layerType(other[i]),
|
||||
})
|
||||
diff := diffLayer((*ls)[i], (*ls)[i])
|
||||
layerDiffRows := []layerDiffRow{}
|
||||
for _, d := range diff {
|
||||
if len(d.got) == 0 {
|
||||
continue
|
||||
}
|
||||
layerDiffRows = append(layerDiffRows, layerDiffRow{
|
||||
d.field,
|
||||
d.got,
|
||||
"",
|
||||
})
|
||||
}
|
||||
allDiffs = append(allDiffs, layerDiff{
|
||||
label: layerType((*ls)[i]),
|
||||
rows: layerDiffRows,
|
||||
})
|
||||
|
||||
layerDiffRows = []layerDiffRow{}
|
||||
diff = diffLayer(other[i], other[i])
|
||||
for _, d := range diff {
|
||||
if len(d.want) == 0 {
|
||||
continue
|
||||
}
|
||||
layerDiffRows = append(layerDiffRows, layerDiffRow{
|
||||
d.field,
|
||||
"",
|
||||
d.want,
|
||||
})
|
||||
}
|
||||
allDiffs = append(allDiffs, layerDiff{
|
||||
label: layerType(other[i]),
|
||||
rows: layerDiffRows,
|
||||
})
|
||||
}
|
||||
|
||||
output := ""
|
||||
// These are for output formatting.
|
||||
maxLabelLen, maxFieldLen, maxGotLen, maxWantLen := 0, 0, 0, 0
|
||||
foundOne := false
|
||||
for _, l := range allDiffs {
|
||||
if len(l.label) > maxLabelLen && len(l.rows) > 0 {
|
||||
maxLabelLen = len(l.label)
|
||||
}
|
||||
if l.rows != nil {
|
||||
foundOne = true
|
||||
}
|
||||
for _, r := range l.rows {
|
||||
if len(r.field) > maxFieldLen {
|
||||
maxFieldLen = len(r.field)
|
||||
}
|
||||
if l := len(fmt.Sprint(r.got)); l > maxGotLen {
|
||||
maxGotLen = l
|
||||
}
|
||||
if l := len(fmt.Sprint(r.want)); l > maxWantLen {
|
||||
maxWantLen = l
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundOne {
|
||||
return ""
|
||||
}
|
||||
for _, l := range allDiffs {
|
||||
if len(l.rows) == 0 {
|
||||
output += "(" + l.label + ")\n"
|
||||
continue
|
||||
}
|
||||
for i, r := range l.rows {
|
||||
var label string
|
||||
if i == 0 {
|
||||
label = l.label + ":"
|
||||
}
|
||||
output += fmt.Sprintf(
|
||||
"%*s %*s %*v %*v\n",
|
||||
maxLabelLen+1, label,
|
||||
maxFieldLen+1, r.field+":",
|
||||
maxGotLen, r.got,
|
||||
maxWantLen, r.want,
|
||||
)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// merge merges the other Layers into ls. If the other Layers is longer, those
|
||||
// additional Layer structs are added to ls. The errors from merging are
|
||||
// collected and returned.
|
||||
func (ls *Layers) merge(other Layers) error {
|
||||
var errs error
|
||||
for i, o := range other {
|
||||
if i < len(*ls) {
|
||||
errs = multierr.Combine(errs, (*ls)[i].merge(o))
|
||||
} else {
|
||||
*ls = append(*ls, o)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -313,3 +313,83 @@ func TestConnectionMatch(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLayersDiff(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
x, y Layers
|
||||
want string
|
||||
}{
|
||||
{
|
||||
Layers{&Ether{Type: NetworkProtocolNumber(12)}, &TCP{DataOffset: Uint8(5), SeqNum: Uint32(5)}},
|
||||
Layers{&Ether{Type: NetworkProtocolNumber(13)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
|
||||
"Ether: Type: 12 13\n" +
|
||||
" TCP: SeqNum: 5 6\n" +
|
||||
" DataOffset: 5 7\n",
|
||||
},
|
||||
{
|
||||
Layers{&Ether{Type: NetworkProtocolNumber(12)}, &UDP{SrcPort: Uint16(123)}},
|
||||
Layers{&Ether{Type: NetworkProtocolNumber(13)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
|
||||
"Ether: Type: 12 13\n" +
|
||||
"(UDP doesn't match TCP)\n" +
|
||||
" UDP: SrcPort: 123 \n" +
|
||||
" TCP: SeqNum: 6\n" +
|
||||
" DataOffset: 7\n",
|
||||
},
|
||||
{
|
||||
Layers{&UDP{SrcPort: Uint16(123)}},
|
||||
Layers{&Ether{Type: NetworkProtocolNumber(13)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
|
||||
"(UDP doesn't match Ether)\n" +
|
||||
" UDP: SrcPort: 123 \n" +
|
||||
"Ether: Type: 13\n" +
|
||||
"(missing matches TCP)\n",
|
||||
},
|
||||
{
|
||||
Layers{nil, &UDP{SrcPort: Uint16(123)}},
|
||||
Layers{&Ether{Type: NetworkProtocolNumber(13)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
|
||||
"(nil matches Ether)\n" +
|
||||
"(UDP doesn't match TCP)\n" +
|
||||
"UDP: SrcPort: 123 \n" +
|
||||
"TCP: SeqNum: 6\n" +
|
||||
" DataOffset: 7\n",
|
||||
},
|
||||
{
|
||||
Layers{&Ether{Type: NetworkProtocolNumber(13)}, &IPv4{IHL: Uint8(4)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
|
||||
Layers{&Ether{Type: NetworkProtocolNumber(13)}, &IPv4{IHL: Uint8(6)}, &TCP{DataOffset: Uint8(7), SeqNum: Uint32(6)}},
|
||||
"(Ether matches Ether)\n" +
|
||||
"IPv4: IHL: 4 6\n" +
|
||||
"(TCP matches TCP)\n",
|
||||
},
|
||||
{
|
||||
Layers{&Payload{Bytes: []byte("foo")}},
|
||||
Layers{&Payload{Bytes: []byte("bar")}},
|
||||
"Payload: Bytes: [102 111 111] [98 97 114]\n",
|
||||
},
|
||||
{
|
||||
Layers{&Payload{Bytes: []byte("")}},
|
||||
Layers{&Payload{}},
|
||||
"",
|
||||
},
|
||||
{
|
||||
Layers{&Payload{Bytes: []byte("")}},
|
||||
Layers{&Payload{Bytes: []byte("")}},
|
||||
"",
|
||||
},
|
||||
{
|
||||
Layers{&UDP{}},
|
||||
Layers{&TCP{}},
|
||||
"(UDP doesn't match TCP)\n" +
|
||||
"(UDP)\n" +
|
||||
"(TCP)\n",
|
||||
},
|
||||
} {
|
||||
if got := tt.x.diff(tt.y); got != tt.want {
|
||||
t.Errorf("%s.diff(%s) = %q, want %q", tt.x, tt.y, got, tt.want)
|
||||
}
|
||||
if tt.x.match(tt.y) != (tt.x.diff(tt.y) == "") {
|
||||
t.Errorf("match and diff of %s and %s disagree", tt.x, tt.y)
|
||||
}
|
||||
if tt.y.match(tt.x) != (tt.y.diff(tt.x) == "") {
|
||||
t.Errorf("match and diff of %s and %s disagree", tt.y, tt.x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@ func TestFinWait2Timeout(t *testing.T) {
|
||||
t.Fatalf("expected a RST packet within a second but got none: %s", err)
|
||||
}
|
||||
} else {
|
||||
if _, err := conn.Expect(tb.TCP{Flags: tb.Uint8(header.TCPFlagRst)}, 10*time.Second); err == nil {
|
||||
t.Fatalf("expected no RST packets within ten seconds but got one: %s", err)
|
||||
if got, err := conn.Expect(tb.TCP{Flags: tb.Uint8(header.TCPFlagRst)}, 10*time.Second); got != nil || err == nil {
|
||||
t.Fatalf("expected no RST packets within ten seconds but got one: %s", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user