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 }