blob: b24163d1e29432bfa918087001f6d7e363c07a66 [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package pgo
import (
"bytes"
"encoding/binary"
"fmt"
"reflect"
"strings"
"testing"
)
// equal returns an error if got and want are not equal.
func equal(got, want *Profile) error {
if got.TotalWeight != want.TotalWeight {
return fmt.Errorf("got.TotalWeight %d != want.TotalWeight %d", got.TotalWeight, want.TotalWeight)
}
if !reflect.DeepEqual(got.NamedEdgeMap.ByWeight, want.NamedEdgeMap.ByWeight) {
return fmt.Errorf("got.NamedEdgeMap.ByWeight != want.NamedEdgeMap.ByWeight\ngot = %+v\nwant = %+v", got.NamedEdgeMap.ByWeight, want.NamedEdgeMap.ByWeight)
}
if !reflect.DeepEqual(got.NamedEdgeMap.Weight, want.NamedEdgeMap.Weight) {
return fmt.Errorf("got.NamedEdgeMap.Weight != want.NamedEdgeMap.Weight\ngot = %+v\nwant = %+v", got.NamedEdgeMap.Weight, want.NamedEdgeMap.Weight)
}
return nil
}
func testRoundTrip(t *testing.T, d *Profile) []byte {
var buf bytes.Buffer
n, err := d.WriteTo(&buf)
if err != nil {
t.Fatalf("WriteTo got err %v want nil", err)
}
if n != int64(buf.Len()) {
t.Errorf("WriteTo got n %d want %d", n, int64(buf.Len()))
}
b := buf.Bytes()
got, err := FromSerialized(&buf)
if err != nil {
t.Fatalf("processSerialized got err %v want nil", err)
}
if err := equal(got, d); err != nil {
t.Errorf("processSerialized output does not match input: %v", err)
}
return b
}
func TestEmpty(t *testing.T) {
d := emptyProfile()
b := testRoundTrip(t, d)
// Contents should consist of only a header.
if string(b) != serializationHeader {
t.Errorf("WriteTo got %q want %q", string(b), serializationHeader)
}
}
func TestRoundTrip(t *testing.T) {
d := &Profile{
TotalWeight: 3,
NamedEdgeMap: NamedEdgeMap{
ByWeight: []NamedCallEdge{
{
CallerName: "a",
CalleeName: "b",
CallSiteOffset: 14,
},
{
CallerName: "c",
CalleeName: "d",
CallSiteOffset: 15,
},
},
Weight: map[NamedCallEdge]int64{
{
CallerName: "a",
CalleeName: "b",
CallSiteOffset: 14,
}: 2,
{
CallerName: "c",
CalleeName: "d",
CallSiteOffset: 15,
}: 1,
},
},
}
testRoundTrip(t, d)
}
func constructFuzzProfile(t *testing.T, b []byte) *Profile {
// The fuzzer can't construct an arbitrary structure, so instead we
// consume bytes from b to act as our edge data.
r := bytes.NewReader(b)
consumeString := func() (string, bool) {
// First byte: how many bytes to read for this string? We only
// use a byte to avoid making humongous strings.
length, err := r.ReadByte()
if err != nil {
return "", false
}
if length == 0 {
return "", false
}
b := make([]byte, length)
_, err = r.Read(b)
if err != nil {
return "", false
}
return string(b), true
}
consumeInt64 := func() (int64, bool) {
b := make([]byte, 8)
_, err := r.Read(b)
if err != nil {
return 0, false
}
return int64(binary.LittleEndian.Uint64(b)), true
}
d := emptyProfile()
for {
caller, ok := consumeString()
if !ok {
break
}
if strings.ContainsAny(caller, " \r\n") {
t.Skip("caller contains space or newline")
}
callee, ok := consumeString()
if !ok {
break
}
if strings.ContainsAny(callee, " \r\n") {
t.Skip("callee contains space or newline")
}
line, ok := consumeInt64()
if !ok {
break
}
weight, ok := consumeInt64()
if !ok {
break
}
edge := NamedCallEdge{
CallerName: caller,
CalleeName: callee,
CallSiteOffset: int(line),
}
if _, ok := d.NamedEdgeMap.Weight[edge]; ok {
t.Skip("duplicate edge")
}
d.NamedEdgeMap.Weight[edge] = weight
d.TotalWeight += weight
}
byWeight := make([]NamedCallEdge, 0, len(d.NamedEdgeMap.Weight))
for namedEdge := range d.NamedEdgeMap.Weight {
byWeight = append(byWeight, namedEdge)
}
sortByWeight(byWeight, d.NamedEdgeMap.Weight)
d.NamedEdgeMap.ByWeight = byWeight
return d
}
func FuzzRoundTrip(f *testing.F) {
f.Add([]byte("")) // empty profile
f.Fuzz(func(t *testing.T, b []byte) {
d := constructFuzzProfile(t, b)
testRoundTrip(t, d)
})
}