// 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) }) }