diff options
author | Eyal Soha <eyalsoha@google.com> | 2020-04-24 15:02:33 -0700 |
---|---|---|
committer | gVisor bot <gvisor-bot@google.com> | 2020-04-24 15:04:03 -0700 |
commit | 3d860530a904004aea5bc95e6331b3b11cec1877 (patch) | |
tree | 8eccdfef2a5d51f4b94324558978c33b2d6ddbb5 /test/packetimpact | |
parent | d5776be3fbcc9e71c449b7b41786929734ce47e2 (diff) |
Better error message from ExpectFrame
Display the errors as diffs between the expected and wanted frame.
PiperOrigin-RevId: 308333271
Diffstat (limited to 'test/packetimpact')
-rw-r--r-- | test/packetimpact/testbench/connections.go | 94 | ||||
-rw-r--r-- | test/packetimpact/testbench/layers.go | 245 | ||||
-rw-r--r-- | test/packetimpact/testbench/layers_test.go | 80 | ||||
-rw-r--r-- | test/packetimpact/tests/fin_wait2_timeout_test.go | 4 |
4 files changed, 376 insertions, 47 deletions
diff --git a/test/packetimpact/testbench/connections.go b/test/packetimpact/testbench/connections.go index 952a717e0..42a90a859 100644 --- a/test/packetimpact/testbench/connections.go +++ b/test/packetimpact/testbench/connections.go @@ -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. -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) - } - if len(received) < layersToMatch { - return false - } - 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 +// 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 { + toMatch := conn.incoming(received) + if toMatch == nil { + return false // Not enough layers in gotLayers for matching. } - return true + if err := toMatch.merge(override); err != nil { + return false // Failing to merge is not matching. + } + 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)}) } } diff --git a/test/packetimpact/testbench/layers.go b/test/packetimpact/testbench/layers.go index 01e99567d..2cbbbb318 100644 --- a/test/packetimpact/testbench/layers.go +++ b/test/packetimpact/testbench/layers.go @@ -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 +} diff --git a/test/packetimpact/testbench/layers_test.go b/test/packetimpact/testbench/layers_test.go index f07ec5eb2..96f72de5b 100644 --- a/test/packetimpact/testbench/layers_test.go +++ b/test/packetimpact/testbench/layers_test.go @@ -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) + } + } +} diff --git a/test/packetimpact/tests/fin_wait2_timeout_test.go b/test/packetimpact/tests/fin_wait2_timeout_test.go index b98594f94..99dc77f9a 100644 --- a/test/packetimpact/tests/fin_wait2_timeout_test.go +++ b/test/packetimpact/tests/fin_wait2_timeout_test.go @@ -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) } } }) |