// Copyright 2014 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package report import ( "bytes" "os" "path/filepath" "regexp" "runtime" "strings" "testing" "github.com/google/pprof/internal/binutils" "github.com/google/pprof/internal/graph" "github.com/google/pprof/internal/proftest" "github.com/google/pprof/profile" ) type testcase struct { rpt *Report want string } func TestSource(t *testing.T) { const path = "testdata/" sampleValue1 := func(v []int64) int64 { return v[1] } for _, tc := range []testcase{ { rpt: New( testProfile.Copy(), &Options{ OutputFormat: List, Symbol: regexp.MustCompile(`.`), TrimPath: "/some/path", SampleValue: sampleValue1, SampleUnit: testProfile.SampleType[1].Unit, }, ), want: path + "source.rpt", }, { rpt: New( testProfile.Copy(), &Options{ OutputFormat: Dot, CallTree: true, Symbol: regexp.MustCompile(`.`), TrimPath: "/some/path", SampleValue: sampleValue1, SampleUnit: testProfile.SampleType[1].Unit, }, ), want: path + "source.dot", }, } { var b bytes.Buffer if err := Generate(&b, tc.rpt, &binutils.Binutils{}); err != nil { t.Fatalf("%s: %v", tc.want, err) } gold, err := os.ReadFile(tc.want) if err != nil { t.Fatalf("%s: %v", tc.want, err) } if runtime.GOOS == "windows" { if tc.rpt.options.OutputFormat == Dot { // The .dot test has the paths inside strings, so \ must be escaped. gold = bytes.Replace(gold, []byte("testdata/"), []byte(`testdata\\`), -1) } else { gold = bytes.Replace(gold, []byte("testdata/"), []byte(`testdata\`), -1) } } if string(b.String()) != string(gold) { d, err := proftest.Diff(gold, b.Bytes()) if err != nil { t.Fatalf("%s: %v", "source", err) } t.Error("source" + "\n" + string(d) + "\n" + "gold:\n" + tc.want) } } } // TestFilter ensures that commands with a regexp filter argument return an // error if there are no results. func TestFilter(t *testing.T) { const filter = "doesNotExist" tests := []struct { name string format int }{ { name: "list", format: List, }, { name: "disasm", format: Dis, }, { // N.B. Tree with a Symbol is "peek". name: "peek", format: Tree, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { rpt := New(testProfile.Copy(), &Options{ OutputFormat: tc.format, Symbol: regexp.MustCompile(filter), SampleValue: func(v []int64) int64 { return v[1] }, SampleUnit: testProfile.SampleType[1].Unit, }) var buf bytes.Buffer err := Generate(&buf, rpt, &binutils.Binutils{}) if err == nil { t.Fatalf("Generate got nil, want error; buf = %s", buf.String()) } if !strings.Contains(err.Error(), filter) { t.Errorf("Error got %v, want it to contain %q", err, filter) } }) } } // testM contains mappings for fake profiles used in tests. var testM = []*profile.Mapping{ { ID: 1, HasFunctions: true, HasFilenames: true, HasLineNumbers: true, HasInlineFrames: true, }, } // testF contains functions for fake profiles used in tests. var testF = []*profile.Function{ { ID: 1, Name: "main", Filename: "testdata/source1", }, { ID: 2, Name: "foo", Filename: "testdata/source1", }, { ID: 3, Name: "bar", Filename: "testdata/source1", }, { ID: 4, Name: "tee", Filename: "/some/path/testdata/source2", }, } // testL contains locations for fake profiles used in tests. var testL = []*profile.Location{ { ID: 1, Mapping: testM[0], Line: []profile.Line{ { Function: testF[0], Line: 2, Column: 2, }, }, }, { ID: 2, Mapping: testM[0], Line: []profile.Line{ { Function: testF[1], Line: 4, Column: 4, }, }, }, { ID: 3, Mapping: testM[0], Line: []profile.Line{ { Function: testF[2], Line: 10, }, }, }, { ID: 4, Mapping: testM[0], Line: []profile.Line{ { Function: testF[3], Line: 2, }, }, }, { ID: 5, Mapping: testM[0], Line: []profile.Line{ { Function: testF[3], Line: 8, }, }, }, { ID: 6, Mapping: testM[0], Line: []profile.Line{ { Function: testF[3], Line: 7, }, { Function: testF[2], Line: 6, }, }, }, } // testSample returns a profile sample with specified value and stack. // Note: callees come first in sample stacks. func testSample(value int64, locs ...*profile.Location) *profile.Sample { return &profile.Sample{ Value: []int64{value}, Location: locs, } } // makeTestProfile returns a profile with specified samples that uses testL/testF/testM // (defined in report_test.go). func makeTestProfile(samples ...*profile.Sample) *profile.Profile { return &profile.Profile{ SampleType: []*profile.ValueType{{Type: "samples", Unit: "count"}}, Sample: samples, Location: testL, Function: testF, Mapping: testM, } } // testProfile contains a fake profile used in tests. // Various report methods modify profiles so tests should operate on testProfile.Copy(). var testProfile = &profile.Profile{ PeriodType: &profile.ValueType{Type: "cpu", Unit: "millisecond"}, Period: 10, DurationNanos: 10e9, SampleType: []*profile.ValueType{ {Type: "samples", Unit: "count"}, {Type: "cpu", Unit: "cycles"}, }, Sample: []*profile.Sample{ { Location: []*profile.Location{testL[0]}, Value: []int64{1, 1}, }, { Location: []*profile.Location{testL[2], testL[1], testL[0]}, Value: []int64{1, 10}, }, { Location: []*profile.Location{testL[4], testL[2], testL[0]}, Value: []int64{1, 100}, }, { Location: []*profile.Location{testL[3], testL[0]}, Value: []int64{1, 1000}, }, { Location: []*profile.Location{testL[4], testL[3], testL[0]}, Value: []int64{1, 10000}, }, }, Location: testL, Function: testF, Mapping: testM, } func TestDisambiguation(t *testing.T) { parent1 := &graph.Node{Info: graph.NodeInfo{Name: "parent1"}} parent2 := &graph.Node{Info: graph.NodeInfo{Name: "parent2"}} child1 := &graph.Node{Info: graph.NodeInfo{Name: "child"}, Function: parent1} child2 := &graph.Node{Info: graph.NodeInfo{Name: "child"}, Function: parent2} child3 := &graph.Node{Info: graph.NodeInfo{Name: "child"}, Function: parent1} sibling := &graph.Node{Info: graph.NodeInfo{Name: "sibling"}, Function: parent1} n := []*graph.Node{parent1, parent2, child1, child2, child3, sibling} wanted := map[*graph.Node]string{ parent1: "parent1", parent2: "parent2", child1: "child [1/2]", child2: "child [2/2]", child3: "child [1/2]", sibling: "sibling", } g := &graph.Graph{Nodes: n} names := getDisambiguatedNames(g) for node, want := range wanted { if got := names[node]; got != want { t.Errorf("name %s, got %s, want %s", node.Info.Name, got, want) } } } func TestFunctionMap(t *testing.T) { fm := make(functionMap) nodes := []graph.NodeInfo{ {Name: "fun1"}, {Name: "fun2", File: "filename"}, {Name: "fun1"}, {Name: "fun2", File: "filename2"}, } want := []struct { wantFunction profile.Function wantAdded bool }{ {profile.Function{ID: 1, Name: "fun1"}, true}, {profile.Function{ID: 2, Name: "fun2", Filename: "filename"}, true}, {profile.Function{ID: 1, Name: "fun1"}, false}, {profile.Function{ID: 3, Name: "fun2", Filename: "filename2"}, true}, } for i, tc := range nodes { gotFunc, gotAdded := fm.findOrAdd(tc) if got, want := gotFunc, want[i].wantFunction; *got != want { t.Errorf("%d: got %v, want %v", i, got, want) } if got, want := gotAdded, want[i].wantAdded; got != want { t.Errorf("%d: got %v, want %v", i, got, want) } } } func TestLegendActiveFilters(t *testing.T) { activeFilterInput := []string{ "focus=123|456|789|101112|131415|161718|192021|222324|252627|282930|313233|343536|363738|acbdefghijklmnop", "show=short filter", } expectedLegendActiveFilter := []string{ "Active filters:", " focus=123|456|789|101112|131415|161718|192021|222324|252627|282930|313233|343536…", " show=short filter", } legendActiveFilter := legendActiveFilters(activeFilterInput) if len(legendActiveFilter) != len(expectedLegendActiveFilter) { t.Errorf("wanted length %v got length %v", len(expectedLegendActiveFilter), len(legendActiveFilter)) } for i := range legendActiveFilter { if legendActiveFilter[i] != expectedLegendActiveFilter[i] { t.Errorf("%d: want \"%v\", got \"%v\"", i, expectedLegendActiveFilter[i], legendActiveFilter[i]) } } } func TestComputeTotal(t *testing.T) { p1 := testProfile.Copy() p1.Sample = []*profile.Sample{ { Location: []*profile.Location{testL[0]}, Value: []int64{1, 1}, }, { Location: []*profile.Location{testL[2], testL[1], testL[0]}, Value: []int64{1, 10}, }, { Location: []*profile.Location{testL[4], testL[2], testL[0]}, Value: []int64{1, 100}, }, } p2 := testProfile.Copy() p2.Sample = []*profile.Sample{ { Location: []*profile.Location{testL[0]}, Value: []int64{1, 1}, }, { Location: []*profile.Location{testL[2], testL[1], testL[0]}, Value: []int64{1, -10}, }, { Location: []*profile.Location{testL[4], testL[2], testL[0]}, Value: []int64{1, 100}, }, } p3 := testProfile.Copy() p3.Sample = []*profile.Sample{ { Location: []*profile.Location{testL[0]}, Value: []int64{10000, 1}, }, { Location: []*profile.Location{testL[2], testL[1], testL[0]}, Value: []int64{-10, 3}, Label: map[string][]string{"pprof::base": {"true"}}, }, { Location: []*profile.Location{testL[2], testL[1], testL[0]}, Value: []int64{1000, -10}, }, { Location: []*profile.Location{testL[2], testL[1], testL[0]}, Value: []int64{-9000, 3}, Label: map[string][]string{"pprof::base": {"true"}}, }, { Location: []*profile.Location{testL[2], testL[1], testL[0]}, Value: []int64{-1, 3}, Label: map[string][]string{"pprof::base": {"true"}}, }, { Location: []*profile.Location{testL[4], testL[2], testL[0]}, Value: []int64{100, 100}, }, { Location: []*profile.Location{testL[2], testL[1], testL[0]}, Value: []int64{100, 3}, Label: map[string][]string{"pprof::base": {"true"}}, }, } testcases := []struct { desc string prof *profile.Profile value, meanDiv func(v []int64) int64 wantTotal int64 }{ { desc: "no diff base, all positive values, index 1", prof: p1, value: func(v []int64) int64 { return v[0] }, wantTotal: 3, }, { desc: "no diff base, all positive values, index 2", prof: p1, value: func(v []int64) int64 { return v[1] }, wantTotal: 111, }, { desc: "no diff base, some negative values", prof: p2, value: func(v []int64) int64 { return v[1] }, wantTotal: 111, }, { desc: "diff base, some negative values", prof: p3, value: func(v []int64) int64 { return v[0] }, wantTotal: 9111, }, } for _, tc := range testcases { t.Run(tc.desc, func(t *testing.T) { if gotTotal := computeTotal(tc.prof, tc.value, tc.meanDiv); gotTotal != tc.wantTotal { t.Errorf("got total %d, want %v", gotTotal, tc.wantTotal) } }) } } func TestPrintAssemblyErrorMessage(t *testing.T) { profile := readProfile(filepath.Join("testdata", "sample.cpu"), t) for _, tc := range []struct { desc string symbol string want string }{ { desc: "no matched symbol in binary", symbol: "symbol-not-exist", want: "no matches found for regexp symbol-not-exist", }, { desc: "no matched address in binary", symbol: "0xffffaaaa", want: "no matches found for address 0xffffaaaa", }, { desc: "matched address in binary but not in the profile", symbol: "0x400000", want: "address 0x400000 found in binary, but the corresponding symbols do not have samples in the profile", }, } { rpt := New( profile.Copy(), &Options{ OutputFormat: List, Symbol: regexp.MustCompile(tc.symbol), SampleValue: func(v []int64) int64 { return v[1] }, SampleUnit: profile.SampleType[1].Unit, }, ) if err := PrintAssembly(os.Stdout, rpt, &binutils.Binutils{}, -1); err == nil || err.Error() != tc.want { t.Errorf(`Got "%v", want %q`, err, tc.want) } } }