Better error message from ExpectFrame

Display the errors as diffs between the expected and wanted frame.

PiperOrigin-RevId: 308333271
This commit is contained in:
Eyal Soha
2020-04-24 15:04:03 -07:00
committed by gVisor bot
parent d5776be3fb
commit 3d860530a9
4 changed files with 375 additions and 46 deletions
+48 -44
View File
@@ -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)})
}
}
+245
View File
@@ -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)
}
}
})