github.com/cilium/ebpf@v0.15.1-0.20240517100537-8079b37aa138/internal/sysenc/marshal_test.go (about)

     1  package sysenc
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/binary"
     6  	"fmt"
     7  	"math"
     8  	"reflect"
     9  	"testing"
    10  
    11  	"github.com/go-quicktest/qt"
    12  	"github.com/google/go-cmp/cmp/cmpopts"
    13  
    14  	"github.com/cilium/ebpf/internal"
    15  )
    16  
    17  type testcase struct {
    18  	new        func() any
    19  	zeroAllocs bool
    20  }
    21  
    22  type struc struct {
    23  	A uint64
    24  	B uint32
    25  }
    26  
    27  type explicitPad struct {
    28  	_ uint32
    29  }
    30  
    31  func testcases() []testcase {
    32  	return []testcase{
    33  		{func() any { return new([1]uint64) }, true},
    34  		{func() any { return new(int16) }, true},
    35  		{func() any { return new(uint16) }, true},
    36  		{func() any { return new(int32) }, true},
    37  		{func() any { return new(uint32) }, true},
    38  		{func() any { return new(int64) }, true},
    39  		{func() any { return new(uint64) }, true},
    40  		{func() any { return make([]byte, 9) }, true},
    41  		{func() any { return new(explicitPad) }, true},
    42  		{func() any { return make([]explicitPad, 0) }, false},
    43  		{func() any { return make([]explicitPad, 1) }, false},
    44  		{func() any { return make([]explicitPad, 2) }, false},
    45  		{func() any { return new(struc) }, false},
    46  		{func() any { return make([]struc, 0) }, false},
    47  		{func() any { return make([]struc, 1) }, false},
    48  		{func() any { return make([]struc, 2) }, false},
    49  		{func() any { return int16(math.MaxInt16) }, false},
    50  		{func() any { return uint16(math.MaxUint16) }, false},
    51  		{func() any { return int32(math.MaxInt32) }, false},
    52  		{func() any { return uint32(math.MaxUint32) }, false},
    53  		{func() any { return int64(math.MaxInt64) }, false},
    54  		{func() any { return uint64(math.MaxUint64) }, false},
    55  		{func() any { return struc{math.MaxUint64, math.MaxUint32} }, false},
    56  	}
    57  }
    58  
    59  func TestMarshal(t *testing.T) {
    60  	for _, test := range testcases() {
    61  		value := test.new()
    62  		t.Run(fmt.Sprintf("%T", value), func(t *testing.T) {
    63  			var want bytes.Buffer
    64  			if err := binary.Write(&want, internal.NativeEndian, value); err != nil {
    65  				t.Fatal(err)
    66  			}
    67  
    68  			have := make([]byte, want.Len())
    69  			buf, err := Marshal(value, binary.Size(value))
    70  			if err != nil {
    71  				t.Fatal(err)
    72  			}
    73  			qt.Assert(t, qt.Equals(buf.CopyTo(have), want.Len()))
    74  			qt.Assert(t, qt.CmpEquals(have, want.Bytes(), cmpopts.EquateEmpty()))
    75  		})
    76  	}
    77  }
    78  
    79  func TestMarshalAllocations(t *testing.T) {
    80  	allocationsPerMarshal := func(t *testing.T, data any) float64 {
    81  		size := binary.Size(data)
    82  		return testing.AllocsPerRun(5, func() {
    83  			_, err := Marshal(data, size)
    84  			if err != nil {
    85  				t.Fatal(err)
    86  			}
    87  		})
    88  	}
    89  
    90  	for _, test := range testcases() {
    91  		if !test.zeroAllocs {
    92  			continue
    93  		}
    94  
    95  		value := test.new()
    96  		t.Run(fmt.Sprintf("%T", value), func(t *testing.T) {
    97  			qt.Assert(t, qt.Equals(allocationsPerMarshal(t, value), 0))
    98  		})
    99  	}
   100  }
   101  
   102  func TestUnmarshal(t *testing.T) {
   103  	for _, test := range testcases() {
   104  		value := test.new()
   105  		if !canUnmarshalInto(value) {
   106  			continue
   107  		}
   108  
   109  		t.Run(fmt.Sprintf("%T", value), func(t *testing.T) {
   110  			want := test.new()
   111  			buf := randomiseValue(t, want)
   112  
   113  			qt.Assert(t, qt.IsNil(Unmarshal(value, buf)))
   114  			qt.Assert(t, qt.DeepEquals(value, want))
   115  		})
   116  	}
   117  }
   118  
   119  func TestUnmarshalAllocations(t *testing.T) {
   120  	allocationsPerUnmarshal := func(t *testing.T, data any, buf []byte) float64 {
   121  		return testing.AllocsPerRun(5, func() {
   122  			err := Unmarshal(data, buf)
   123  			if err != nil {
   124  				t.Fatal(err)
   125  			}
   126  		})
   127  	}
   128  
   129  	for _, test := range testcases() {
   130  		if !test.zeroAllocs {
   131  			continue
   132  		}
   133  
   134  		value := test.new()
   135  		if !canUnmarshalInto(value) {
   136  			continue
   137  		}
   138  
   139  		t.Run(fmt.Sprintf("%T", value), func(t *testing.T) {
   140  			buf := make([]byte, binary.Size(value))
   141  			qt.Assert(t, qt.Equals(allocationsPerUnmarshal(t, value, buf), 0))
   142  		})
   143  	}
   144  }
   145  
   146  func TestUnsafeBackingMemory(t *testing.T) {
   147  	marshalNative := func(t *testing.T, data any) []byte {
   148  		t.Helper()
   149  
   150  		var buf bytes.Buffer
   151  		qt.Assert(t, qt.IsNil(binary.Write(&buf, internal.NativeEndian, data)))
   152  		return buf.Bytes()
   153  	}
   154  
   155  	for _, test := range []struct {
   156  		name  string
   157  		value any
   158  	}{
   159  		{
   160  			"slice",
   161  			[]uint32{1, 2},
   162  		},
   163  		{
   164  			"pointer to slice",
   165  			&[]uint32{2},
   166  		},
   167  		{
   168  			"pointer to array",
   169  			&[2]uint64{},
   170  		},
   171  		{
   172  			"pointer to int64",
   173  			new(int64),
   174  		},
   175  		{
   176  			"pointer to struct",
   177  			&struct {
   178  				A, B uint16
   179  				C    uint32
   180  			}{},
   181  		},
   182  		{
   183  			"struct with explicit padding",
   184  			&struct{ _ uint64 }{},
   185  		},
   186  	} {
   187  		t.Run("valid: "+test.name, func(t *testing.T) {
   188  			want := marshalNative(t, test.value)
   189  			have := unsafeBackingMemory(test.value)
   190  			qt.Assert(t, qt.DeepEquals(have, want))
   191  		})
   192  	}
   193  
   194  	for _, test := range []struct {
   195  		name  string
   196  		value any
   197  	}{
   198  		{
   199  			"nil",
   200  			nil,
   201  		},
   202  		{
   203  			"nil slice",
   204  			([]byte)(nil),
   205  		},
   206  		{
   207  			"nil pointer",
   208  			(*uint64)(nil),
   209  		},
   210  		{
   211  			"nil pointer to slice",
   212  			(*[]uint32)(nil),
   213  		},
   214  		{
   215  			"nil pointer to array",
   216  			(*[2]uint64)(nil),
   217  		},
   218  		{
   219  			"unexported field",
   220  			&struct{ a uint64 }{},
   221  		},
   222  		{
   223  			"struct containing pointer",
   224  			&struct{ A *uint64 }{},
   225  		},
   226  		{
   227  			"struct with trailing padding",
   228  			&struc{},
   229  		},
   230  		{
   231  			"struct with interspersed padding",
   232  			&struct {
   233  				B uint32
   234  				A uint64
   235  			}{},
   236  		},
   237  		{
   238  			"padding between slice entries",
   239  			&[]struc{{}},
   240  		},
   241  		{
   242  			"padding between array entries",
   243  			&[2]struc{},
   244  		},
   245  	} {
   246  		t.Run("invalid: "+test.name, func(t *testing.T) {
   247  			qt.Assert(t, qt.IsNil(unsafeBackingMemory(test.value)))
   248  		})
   249  	}
   250  }
   251  
   252  func BenchmarkMarshal(b *testing.B) {
   253  	for _, test := range testcases() {
   254  		value := test.new()
   255  		b.Run(fmt.Sprintf("%T", value), func(b *testing.B) {
   256  			size := binary.Size(value)
   257  			b.ResetTimer()
   258  			b.ReportAllocs()
   259  			for i := 0; i < b.N; i++ {
   260  				_, _ = Marshal(value, size)
   261  			}
   262  		})
   263  	}
   264  }
   265  
   266  func BenchmarkUnmarshal(b *testing.B) {
   267  	for _, test := range testcases() {
   268  		value := test.new()
   269  		if !canUnmarshalInto(value) {
   270  			continue
   271  		}
   272  
   273  		b.Run(fmt.Sprintf("%T", value), func(b *testing.B) {
   274  			size := binary.Size(value)
   275  			buf := make([]byte, size)
   276  			b.ResetTimer()
   277  			b.ReportAllocs()
   278  			for i := 0; i < b.N; i++ {
   279  				_ = Unmarshal(value, buf)
   280  			}
   281  		})
   282  	}
   283  }
   284  
   285  func randomiseValue(tb testing.TB, value any) []byte {
   286  	tb.Helper()
   287  
   288  	size := binary.Size(value)
   289  	if size == -1 {
   290  		tb.Fatalf("Can't unmarshal into %T", value)
   291  	}
   292  
   293  	buf := make([]byte, size)
   294  	for i := range buf {
   295  		buf[i] = byte(i)
   296  	}
   297  
   298  	err := binary.Read(bytes.NewReader(buf), internal.NativeEndian, value)
   299  	qt.Assert(tb, qt.IsNil(err))
   300  
   301  	return buf
   302  }
   303  
   304  func canUnmarshalInto(data any) bool {
   305  	kind := reflect.TypeOf(data).Kind()
   306  	return kind == reflect.Slice || kind == reflect.Pointer
   307  }