Skip to content

Commit

Permalink
improve tests
Browse files Browse the repository at this point in the history
* TestUnmarshal & TestMarshal now share their test cases
* the test cases are expanded with more complex structs
* added TestUnmarshalErrors to verify faulty input
* added BenchmarkUnmarshal
  • Loading branch information
earwin committed Oct 16, 2024
1 parent e03d557 commit 54f6adf
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 167 deletions.
156 changes: 148 additions & 8 deletions data_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,156 @@
package tlv

import "strconv"
import (
"encoding/hex"
)

type BinaryMarshalTest struct {
Value int
func unhex(s string) []byte {
if b, err := hex.DecodeString(s); err != nil {
panic(err)
} else {
return b
}
}

func (mt *BinaryMarshalTest) MarshalBinary() (data []byte, err error) {
return []byte(strconv.Itoa(mt.Value)), nil
// TODO: Primitive valued TLVs don't seem to exist?
// Why are we handling the marshal/unmarshal for them at all?

type tints struct {
I8 int8 `tlv:"101"`
I16 int16 `tlv:"102"`
I32 int32 `tlv:"103"`
I64 int64 `tlv:"104"`
}

type tuints struct {
UI8 uint8 `tlv:"105"`
UI16 uint16 `tlv:"106"`
UI32 uint32 `tlv:"107"`
UI64 uint64 `tlv:"108"`
}

type tstrings struct {
S string `tlv:"109"`
B []byte `tlv:"110"`
}

func (mt *BinaryMarshalTest) UnmarshalBinary(data []byte) (err error) {
mt.Value, err = strconv.Atoi(string(data))
return err
//type field struct {
// Value customMarshal `tlv:"111"`
//}

type tslice struct {
Grouped []customMarshal `tlv:"112"`
}

type tpointer struct {
Optional *customMarshal `tlv:"113"`
}

type tsliceofpointers struct {
Grouped []*customMarshal `tlv:"114"`
}

type tchonky struct {
I *tints `tlv:"115"`
S *tslice `tlv:"116"`
R *tpointer `tlv:"117"`
SR *tsliceofpointers `tlv:"118"`
}

type customMarshal struct{ Value []byte }

func (mt *customMarshal) MarshalBinary() ([]byte, error) { return mt.Value, nil }

func (mt *customMarshal) UnmarshalBinary(data []byte) error { mt.Value = data; return nil }

var cases = []struct {
name string
decoded any
encoded []byte
}{
{
name: "integers",
decoded: tints{
I8: 0x7A,
I16: -0x7AFE,
I32: 0x7AFEDEAD,
I64: -0x7AFEDEAD7AFEDEAD,
},
encoded: unhex("006500017A006600028502006700047AFEDEAD006800088501215285012153"),
},
{
name: "unsigned integers",
decoded: tuints{
UI8: 0xCA,
UI16: 0xCAFE,
UI32: 0xCAFEDEAD,
UI64: 0xCAFEDEADCAFEDEAD,
},
encoded: unhex("00690001CA006A0002CAFE006B0004CAFEDEAD006C0008CAFEDEADCAFEDEAD"),
},
{
name: "strings",
decoded: tstrings{
S: "Hello, World!",
B: []byte("Byebye, World!"),
},
encoded: unhex("006D000D48656C6C6F2C20576F726C6421006E000E4279656279652C20576F726C6421"),
},
//{
// name: "custom",
// decoded: customMarshal{
// Value: "Uh, hi?",
// },
// encoded: unhex("55682C2068693F"),
//},
//{
// name: "field",
// decoded: field{
// Value: customMarshal{Value: "Hello, World!"},
// },
// encoded: unhex("006F000648656C6C6F3F"),
//},
{
name: "slice",
decoded: tslice{Grouped: []customMarshal{
{Value: []byte("Hello")},
{Value: []byte("Free5GC")},
{Value: []byte("World")},
}},
encoded: unhex("0070000548656C6C6F007000074672656535474300700005576F726C64"),
},
{
name: "pointer",
decoded: tpointer{Optional: &customMarshal{Value: []byte("Hello?")}},
encoded: unhex("0071000648656C6C6F3F"),
},
{
name: "slice of pointers",
decoded: tsliceofpointers{Grouped: []*customMarshal{
{Value: []byte("Hello?")},
{Value: []byte("Free5GC?")},
{Value: []byte("World?")},
}},
encoded: unhex("0072000648656C6C6F3F00720008467265653547433F00720006576F726C643F"),
},
{
name: "chonky",
decoded: tchonky{
I: &tints{I8: 1, I16: 2, I32: 3, I64: 4},
S: &tslice{Grouped: []customMarshal{
{Value: []byte("Far")},
{Value: []byte("Over")},
{Value: []byte("The")},
}},
R: &tpointer{Optional: &customMarshal{Value: []byte("Misty?")}},
SR: &tsliceofpointers{Grouped: []*customMarshal{
{Value: []byte("Mountains?")},
{Value: []byte("Cold?")},
{Value: []byte("To?")},
}},
},
encoded: unhex("0073001F00650001010066000200020067000400000003006800080000000000000004" +
"0074001600700003466172007000044F766572007000035468650075000A007100064D697374793F0076" +
"001E0072000A4D6F756E7461696E733F00720005436F6C643F00720003546F3F"),
},
}
193 changes: 114 additions & 79 deletions decode_test.go
Original file line number Diff line number Diff line change
@@ -1,102 +1,137 @@
package tlv

import (
"encoding/hex"
"errors"
"reflect"
"testing"

"github.com/stretchr/testify/require"
)

func TestUnmarshal(t *testing.T) {
testCases := []struct {
name string
instance interface{}
inputHex string
moreCases := append(cases, []struct {
name string
decoded any
encoded []byte
}{
{
name: "int64",
instance: struct {
Value int64 `tlv:"30"`
}{Value: 32324},
inputHex: "001E00080000000000007E44",
name: "unexpected tlvs",
decoded: tpointer{Optional: &customMarshal{Value: []byte("Hello?")}},
encoded: unhex("DEAD0002CAFE0071000648656C6C6F3FCAFE0002DEAD"),
},
{
name: "int32",
instance: struct {
Value int32 `tlv:"31"`
}{Value: 32324},
inputHex: "001F000400007E44",
},
{
name: "int16",
instance: struct {
Value int16 `tlv:"32"`
}{Value: 32324},
inputHex: "002000027E44",
},
{
name: "int8",
instance: struct {
Value int8 `tlv:"33"`
}{Value: 68},
inputHex: "0021000144",
},
{
name: "slice of struct",
instance: struct {
List []struct {
Name []byte `tlv:"20"`
Sequence uint16 `tlv:"40"`
} `tlv:"80"`
}{
List: []struct {
Name []byte `tlv:"20"`
Sequence uint16 `tlv:"40"`
}{
{Name: []byte("Hello"), Sequence: 1},
{Name: []byte("World"), Sequence: 2},
{Name: []byte("free5gc"), Sequence: 3},
},
},
inputHex: "0050000F0014000548656C6C6F002800020001" +
"0050000F00140005576F726C64002800020002" +
"005000110014000766726565356763002800020003",
},
{
name: "slice of binary",
instance: struct {
List []BinaryMarshalTest `tlv:"123"`
}{
List: []BinaryMarshalTest{
{
Value: 1100,
},
{
Value: 1200,
},
{
Value: 3244,
},
},
},
inputHex: "007B000431313030007B000431323030007B000433323434",
},
}
}...)

for _, tc := range testCases {
for _, tc := range moreCases {
t.Run(tc.name, func(t *testing.T) {
instanceType := reflect.TypeOf(tc.instance)
// create a new empty instance of the same type
instanceType := reflect.TypeOf(tc.decoded)
testInstance := reflect.New(instanceType).Interface()

buf, err := hex.DecodeString(tc.inputHex)
require.NoError(t, err)
err = Unmarshal(buf, testInstance)
err := Unmarshal(tc.encoded, testInstance)
require.NoError(t, err)

// dereference the interface
// dereference and compare to original
testInstance = reflect.ValueOf(testInstance).Elem().Interface()
require.Equal(t, tc.instance, testInstance)
require.Equal(t, tc.decoded, testInstance)
})
}
}

func BenchmarkUnmarshal(b *testing.B) {
b.ReportAllocs()

for i := 0; i < b.N; i++ {
decoded := reflect.New(reflect.TypeOf(cases[i%len(cases)].decoded)).Interface()
if err := Unmarshal(cases[i%len(cases)].encoded, decoded); err != nil {
b.Error(err)
}
}
}

type tBrokenUnmarshal struct{}

func (t tBrokenUnmarshal) UnmarshalBinary(_ []byte) error { return errors.New("irreparably broken") }

type tOkayish struct {
V *customMarshal `tlv:"1"`
}
type tSliceUnsupported struct {
V []chan any `tlv:"1"`
}
type tMalformedTLV1 struct {
V *customMarshal `tlv:"NaN"`
}
type tMalformedTLV2 struct {
V *customMarshal `tlv:"65555"`
}
type tNested struct {
V1 *tNested1 `tlv:"1"`
}
type tNested1 struct {
V2 []*tNested2 `tlv:"2"`
}
type tNested2 struct {
V3 *tBrokenUnmarshal `tlv:"3"`
}

func TestUnmarshalErrors(t *testing.T) {
bytesOkayish := unhex("00010001FFFFFF")

cases := []struct {
name string
target any
bytes []byte
}{
{name: "eof value", target: &tOkayish{}, bytes: unhex("FFFFFFFF")},
{name: "unsupported slice type", target: &tSliceUnsupported{}, bytes: bytesOkayish},
{name: "field unexported", target: &struct{ v customMarshal }{}, bytes: bytesOkayish},
{name: "field missing tlv", target: &struct{ V customMarshal }{}, bytes: bytesOkayish},
{name: "field malformed tlv 1", target: &tMalformedTLV1{}, bytes: bytesOkayish},
{name: "field malformed tlv 2", target: &tMalformedTLV2{}, bytes: bytesOkayish},
{name: "nested error", target: &tNested{}, bytes: unhex("000100090002000500030001FF")},
//{name: "unexpected tlv", target: &tOkayish{}, bytes: unhex("DEAD0001FF")},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := Unmarshal(tc.bytes, tc.target)
t.Log(err)
require.Error(t, err)
})
}

panicCases := []struct {
name string
target any
bytes []byte
}{
{name: "nil", target: nil, bytes: bytesOkayish},
{name: "non-pointer", target: "not a pointer", bytes: bytesOkayish},
}

for _, tc := range panicCases {
t.Run(tc.name, func(t *testing.T) {
require.Panics(t, func() {
_ = Unmarshal(tc.bytes, tc.target)
})
})
}

// swallows an error, prints to console for some
swallowedCases := []struct {
name string
target any
bytes []byte
}{
{name: "eof tag", target: &tOkayish{}, bytes: unhex("FF")},
{name: "eof len", target: &tOkayish{}, bytes: unhex("FFFFFF")},
{name: "unsupported type", target: ref(make(chan any)), bytes: bytesOkayish},
}

for _, tc := range swallowedCases {
t.Run(tc.name, func(t *testing.T) {
err := Unmarshal(tc.bytes, tc.target)
require.NoError(t, err)
})
}
}
Loading

0 comments on commit 54f6adf

Please sign in to comment.