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