...

Source file src/github.com/google/pprof/internal/report/report_test.go

Documentation: github.com/google/pprof/internal/report

     1  // Copyright 2014 Google Inc. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package report
    16  
    17  import (
    18  	"bytes"
    19  	"os"
    20  	"path/filepath"
    21  	"regexp"
    22  	"runtime"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/google/pprof/internal/binutils"
    27  	"github.com/google/pprof/internal/graph"
    28  	"github.com/google/pprof/internal/proftest"
    29  	"github.com/google/pprof/profile"
    30  )
    31  
    32  type testcase struct {
    33  	rpt  *Report
    34  	want string
    35  }
    36  
    37  func TestSource(t *testing.T) {
    38  	const path = "testdata/"
    39  
    40  	sampleValue1 := func(v []int64) int64 {
    41  		return v[1]
    42  	}
    43  
    44  	for _, tc := range []testcase{
    45  		{
    46  			rpt: New(
    47  				testProfile.Copy(),
    48  				&Options{
    49  					OutputFormat: List,
    50  					Symbol:       regexp.MustCompile(`.`),
    51  					TrimPath:     "/some/path",
    52  
    53  					SampleValue: sampleValue1,
    54  					SampleUnit:  testProfile.SampleType[1].Unit,
    55  				},
    56  			),
    57  			want: path + "source.rpt",
    58  		},
    59  		{
    60  			rpt: New(
    61  				testProfile.Copy(),
    62  				&Options{
    63  					OutputFormat: Dot,
    64  					CallTree:     true,
    65  					Symbol:       regexp.MustCompile(`.`),
    66  					TrimPath:     "/some/path",
    67  
    68  					SampleValue: sampleValue1,
    69  					SampleUnit:  testProfile.SampleType[1].Unit,
    70  				},
    71  			),
    72  			want: path + "source.dot",
    73  		},
    74  	} {
    75  		var b bytes.Buffer
    76  		if err := Generate(&b, tc.rpt, &binutils.Binutils{}); err != nil {
    77  			t.Fatalf("%s: %v", tc.want, err)
    78  		}
    79  
    80  		gold, err := os.ReadFile(tc.want)
    81  		if err != nil {
    82  			t.Fatalf("%s: %v", tc.want, err)
    83  		}
    84  		if runtime.GOOS == "windows" {
    85  			if tc.rpt.options.OutputFormat == Dot {
    86  				// The .dot test has the paths inside strings, so \ must be escaped.
    87  				gold = bytes.Replace(gold, []byte("testdata/"), []byte(`testdata\\`), -1)
    88  			} else {
    89  				gold = bytes.Replace(gold, []byte("testdata/"), []byte(`testdata\`), -1)
    90  			}
    91  		}
    92  		if string(b.String()) != string(gold) {
    93  			d, err := proftest.Diff(gold, b.Bytes())
    94  			if err != nil {
    95  				t.Fatalf("%s: %v", "source", err)
    96  			}
    97  			t.Error("source" + "\n" + string(d) + "\n" + "gold:\n" + tc.want)
    98  		}
    99  	}
   100  }
   101  
   102  // TestFilter ensures that commands with a regexp filter argument return an
   103  // error if there are no results.
   104  func TestFilter(t *testing.T) {
   105  	const filter = "doesNotExist"
   106  
   107  	tests := []struct {
   108  		name   string
   109  		format int
   110  	}{
   111  		{
   112  			name:   "list",
   113  			format: List,
   114  		},
   115  		{
   116  			name:   "disasm",
   117  			format: Dis,
   118  		},
   119  		{
   120  			// N.B. Tree with a Symbol is "peek".
   121  			name:   "peek",
   122  			format: Tree,
   123  		},
   124  	}
   125  	for _, tc := range tests {
   126  		t.Run(tc.name, func(t *testing.T) {
   127  			rpt := New(testProfile.Copy(), &Options{
   128  				OutputFormat: tc.format,
   129  				Symbol:       regexp.MustCompile(filter),
   130  				SampleValue:  func(v []int64) int64 { return v[1] },
   131  				SampleUnit:   testProfile.SampleType[1].Unit,
   132  			})
   133  
   134  			var buf bytes.Buffer
   135  			err := Generate(&buf, rpt, &binutils.Binutils{})
   136  			if err == nil {
   137  				t.Fatalf("Generate got nil, want error; buf = %s", buf.String())
   138  			}
   139  			if !strings.Contains(err.Error(), filter) {
   140  				t.Errorf("Error got %v, want it to contain %q", err, filter)
   141  			}
   142  		})
   143  	}
   144  }
   145  
   146  // testM contains mappings for fake profiles used in tests.
   147  var testM = []*profile.Mapping{
   148  	{
   149  		ID:              1,
   150  		HasFunctions:    true,
   151  		HasFilenames:    true,
   152  		HasLineNumbers:  true,
   153  		HasInlineFrames: true,
   154  	},
   155  }
   156  
   157  // testF contains functions for fake profiles used in tests.
   158  var testF = []*profile.Function{
   159  	{
   160  		ID:       1,
   161  		Name:     "main",
   162  		Filename: "testdata/source1",
   163  	},
   164  	{
   165  		ID:       2,
   166  		Name:     "foo",
   167  		Filename: "testdata/source1",
   168  	},
   169  	{
   170  		ID:       3,
   171  		Name:     "bar",
   172  		Filename: "testdata/source1",
   173  	},
   174  	{
   175  		ID:       4,
   176  		Name:     "tee",
   177  		Filename: "/some/path/testdata/source2",
   178  	},
   179  }
   180  
   181  // testL contains locations for fake profiles used in tests.
   182  var testL = []*profile.Location{
   183  	{
   184  		ID:      1,
   185  		Mapping: testM[0],
   186  		Line: []profile.Line{
   187  			{
   188  				Function: testF[0],
   189  				Line:     2,
   190  				Column:   2,
   191  			},
   192  		},
   193  	},
   194  	{
   195  		ID:      2,
   196  		Mapping: testM[0],
   197  		Line: []profile.Line{
   198  			{
   199  				Function: testF[1],
   200  				Line:     4,
   201  				Column:   4,
   202  			},
   203  		},
   204  	},
   205  	{
   206  		ID:      3,
   207  		Mapping: testM[0],
   208  		Line: []profile.Line{
   209  			{
   210  				Function: testF[2],
   211  				Line:     10,
   212  			},
   213  		},
   214  	},
   215  	{
   216  		ID:      4,
   217  		Mapping: testM[0],
   218  		Line: []profile.Line{
   219  			{
   220  				Function: testF[3],
   221  				Line:     2,
   222  			},
   223  		},
   224  	},
   225  	{
   226  		ID:      5,
   227  		Mapping: testM[0],
   228  		Line: []profile.Line{
   229  			{
   230  				Function: testF[3],
   231  				Line:     8,
   232  			},
   233  		},
   234  	},
   235  	{
   236  		ID:      6,
   237  		Mapping: testM[0],
   238  		Line: []profile.Line{
   239  			{
   240  				Function: testF[3],
   241  				Line:     7,
   242  			},
   243  			{
   244  				Function: testF[2],
   245  				Line:     6,
   246  			},
   247  		},
   248  	},
   249  }
   250  
   251  // testSample returns a profile sample with specified value and stack.
   252  // Note: callees come first in sample stacks.
   253  func testSample(value int64, locs ...*profile.Location) *profile.Sample {
   254  	return &profile.Sample{
   255  		Value:    []int64{value},
   256  		Location: locs,
   257  	}
   258  }
   259  
   260  // makeTestProfile returns a profile with specified samples that uses testL/testF/testM
   261  // (defined in report_test.go).
   262  func makeTestProfile(samples ...*profile.Sample) *profile.Profile {
   263  	return &profile.Profile{
   264  		SampleType: []*profile.ValueType{{Type: "samples", Unit: "count"}},
   265  		Sample:     samples,
   266  		Location:   testL,
   267  		Function:   testF,
   268  		Mapping:    testM,
   269  	}
   270  }
   271  
   272  // testProfile contains a fake profile used in tests.
   273  // Various report methods modify profiles so tests should operate on testProfile.Copy().
   274  var testProfile = &profile.Profile{
   275  	PeriodType:    &profile.ValueType{Type: "cpu", Unit: "millisecond"},
   276  	Period:        10,
   277  	DurationNanos: 10e9,
   278  	SampleType: []*profile.ValueType{
   279  		{Type: "samples", Unit: "count"},
   280  		{Type: "cpu", Unit: "cycles"},
   281  	},
   282  	Sample: []*profile.Sample{
   283  		{
   284  			Location: []*profile.Location{testL[0]},
   285  			Value:    []int64{1, 1},
   286  		},
   287  		{
   288  			Location: []*profile.Location{testL[2], testL[1], testL[0]},
   289  			Value:    []int64{1, 10},
   290  		},
   291  		{
   292  			Location: []*profile.Location{testL[4], testL[2], testL[0]},
   293  			Value:    []int64{1, 100},
   294  		},
   295  		{
   296  			Location: []*profile.Location{testL[3], testL[0]},
   297  			Value:    []int64{1, 1000},
   298  		},
   299  		{
   300  			Location: []*profile.Location{testL[4], testL[3], testL[0]},
   301  			Value:    []int64{1, 10000},
   302  		},
   303  	},
   304  	Location: testL,
   305  	Function: testF,
   306  	Mapping:  testM,
   307  }
   308  
   309  func TestDisambiguation(t *testing.T) {
   310  	parent1 := &graph.Node{Info: graph.NodeInfo{Name: "parent1"}}
   311  	parent2 := &graph.Node{Info: graph.NodeInfo{Name: "parent2"}}
   312  	child1 := &graph.Node{Info: graph.NodeInfo{Name: "child"}, Function: parent1}
   313  	child2 := &graph.Node{Info: graph.NodeInfo{Name: "child"}, Function: parent2}
   314  	child3 := &graph.Node{Info: graph.NodeInfo{Name: "child"}, Function: parent1}
   315  	sibling := &graph.Node{Info: graph.NodeInfo{Name: "sibling"}, Function: parent1}
   316  
   317  	n := []*graph.Node{parent1, parent2, child1, child2, child3, sibling}
   318  
   319  	wanted := map[*graph.Node]string{
   320  		parent1: "parent1",
   321  		parent2: "parent2",
   322  		child1:  "child [1/2]",
   323  		child2:  "child [2/2]",
   324  		child3:  "child [1/2]",
   325  		sibling: "sibling",
   326  	}
   327  
   328  	g := &graph.Graph{Nodes: n}
   329  
   330  	names := getDisambiguatedNames(g)
   331  
   332  	for node, want := range wanted {
   333  		if got := names[node]; got != want {
   334  			t.Errorf("name %s, got %s, want %s", node.Info.Name, got, want)
   335  		}
   336  	}
   337  }
   338  
   339  func TestFunctionMap(t *testing.T) {
   340  
   341  	fm := make(functionMap)
   342  	nodes := []graph.NodeInfo{
   343  		{Name: "fun1"},
   344  		{Name: "fun2", File: "filename"},
   345  		{Name: "fun1"},
   346  		{Name: "fun2", File: "filename2"},
   347  	}
   348  
   349  	want := []struct {
   350  		wantFunction profile.Function
   351  		wantAdded    bool
   352  	}{
   353  		{profile.Function{ID: 1, Name: "fun1"}, true},
   354  		{profile.Function{ID: 2, Name: "fun2", Filename: "filename"}, true},
   355  		{profile.Function{ID: 1, Name: "fun1"}, false},
   356  		{profile.Function{ID: 3, Name: "fun2", Filename: "filename2"}, true},
   357  	}
   358  
   359  	for i, tc := range nodes {
   360  		gotFunc, gotAdded := fm.findOrAdd(tc)
   361  		if got, want := gotFunc, want[i].wantFunction; *got != want {
   362  			t.Errorf("%d: got %v, want %v", i, got, want)
   363  		}
   364  		if got, want := gotAdded, want[i].wantAdded; got != want {
   365  			t.Errorf("%d: got %v, want %v", i, got, want)
   366  		}
   367  	}
   368  }
   369  
   370  func TestLegendActiveFilters(t *testing.T) {
   371  	activeFilterInput := []string{
   372  		"focus=123|456|789|101112|131415|161718|192021|222324|252627|282930|313233|343536|363738|acbdefghijklmnop",
   373  		"show=short filter",
   374  	}
   375  	expectedLegendActiveFilter := []string{
   376  		"Active filters:",
   377  		"   focus=123|456|789|101112|131415|161718|192021|222324|252627|282930|313233|343536…",
   378  		"   show=short filter",
   379  	}
   380  	legendActiveFilter := legendActiveFilters(activeFilterInput)
   381  	if len(legendActiveFilter) != len(expectedLegendActiveFilter) {
   382  		t.Errorf("wanted length %v got length %v", len(expectedLegendActiveFilter), len(legendActiveFilter))
   383  	}
   384  	for i := range legendActiveFilter {
   385  		if legendActiveFilter[i] != expectedLegendActiveFilter[i] {
   386  			t.Errorf("%d: want \"%v\", got \"%v\"", i, expectedLegendActiveFilter[i], legendActiveFilter[i])
   387  		}
   388  	}
   389  }
   390  
   391  func TestComputeTotal(t *testing.T) {
   392  	p1 := testProfile.Copy()
   393  	p1.Sample = []*profile.Sample{
   394  		{
   395  			Location: []*profile.Location{testL[0]},
   396  			Value:    []int64{1, 1},
   397  		},
   398  		{
   399  			Location: []*profile.Location{testL[2], testL[1], testL[0]},
   400  			Value:    []int64{1, 10},
   401  		},
   402  		{
   403  			Location: []*profile.Location{testL[4], testL[2], testL[0]},
   404  			Value:    []int64{1, 100},
   405  		},
   406  	}
   407  
   408  	p2 := testProfile.Copy()
   409  	p2.Sample = []*profile.Sample{
   410  		{
   411  			Location: []*profile.Location{testL[0]},
   412  			Value:    []int64{1, 1},
   413  		},
   414  		{
   415  			Location: []*profile.Location{testL[2], testL[1], testL[0]},
   416  			Value:    []int64{1, -10},
   417  		},
   418  		{
   419  			Location: []*profile.Location{testL[4], testL[2], testL[0]},
   420  			Value:    []int64{1, 100},
   421  		},
   422  	}
   423  
   424  	p3 := testProfile.Copy()
   425  	p3.Sample = []*profile.Sample{
   426  		{
   427  			Location: []*profile.Location{testL[0]},
   428  			Value:    []int64{10000, 1},
   429  		},
   430  		{
   431  			Location: []*profile.Location{testL[2], testL[1], testL[0]},
   432  			Value:    []int64{-10, 3},
   433  			Label:    map[string][]string{"pprof::base": {"true"}},
   434  		},
   435  		{
   436  			Location: []*profile.Location{testL[2], testL[1], testL[0]},
   437  			Value:    []int64{1000, -10},
   438  		},
   439  		{
   440  			Location: []*profile.Location{testL[2], testL[1], testL[0]},
   441  			Value:    []int64{-9000, 3},
   442  			Label:    map[string][]string{"pprof::base": {"true"}},
   443  		},
   444  		{
   445  			Location: []*profile.Location{testL[2], testL[1], testL[0]},
   446  			Value:    []int64{-1, 3},
   447  			Label:    map[string][]string{"pprof::base": {"true"}},
   448  		},
   449  		{
   450  			Location: []*profile.Location{testL[4], testL[2], testL[0]},
   451  			Value:    []int64{100, 100},
   452  		},
   453  		{
   454  			Location: []*profile.Location{testL[2], testL[1], testL[0]},
   455  			Value:    []int64{100, 3},
   456  			Label:    map[string][]string{"pprof::base": {"true"}},
   457  		},
   458  	}
   459  
   460  	testcases := []struct {
   461  		desc           string
   462  		prof           *profile.Profile
   463  		value, meanDiv func(v []int64) int64
   464  		wantTotal      int64
   465  	}{
   466  		{
   467  			desc: "no diff base, all positive values, index 1",
   468  			prof: p1,
   469  			value: func(v []int64) int64 {
   470  				return v[0]
   471  			},
   472  			wantTotal: 3,
   473  		},
   474  		{
   475  			desc: "no diff base, all positive values, index 2",
   476  			prof: p1,
   477  			value: func(v []int64) int64 {
   478  				return v[1]
   479  			},
   480  			wantTotal: 111,
   481  		},
   482  		{
   483  			desc: "no diff base, some negative values",
   484  			prof: p2,
   485  			value: func(v []int64) int64 {
   486  				return v[1]
   487  			},
   488  			wantTotal: 111,
   489  		},
   490  		{
   491  			desc: "diff base, some negative values",
   492  			prof: p3,
   493  			value: func(v []int64) int64 {
   494  				return v[0]
   495  			},
   496  			wantTotal: 9111,
   497  		},
   498  	}
   499  
   500  	for _, tc := range testcases {
   501  		t.Run(tc.desc, func(t *testing.T) {
   502  			if gotTotal := computeTotal(tc.prof, tc.value, tc.meanDiv); gotTotal != tc.wantTotal {
   503  				t.Errorf("got total %d, want %v", gotTotal, tc.wantTotal)
   504  			}
   505  		})
   506  	}
   507  }
   508  
   509  func TestPrintAssemblyErrorMessage(t *testing.T) {
   510  	profile := readProfile(filepath.Join("testdata", "sample.cpu"), t)
   511  
   512  	for _, tc := range []struct {
   513  		desc   string
   514  		symbol string
   515  		want   string
   516  	}{
   517  		{
   518  			desc:   "no matched symbol in binary",
   519  			symbol: "symbol-not-exist",
   520  			want:   "no matches found for regexp symbol-not-exist",
   521  		},
   522  		{
   523  			desc:   "no matched address in binary",
   524  			symbol: "0xffffaaaa",
   525  			want:   "no matches found for address 0xffffaaaa",
   526  		},
   527  		{
   528  			desc:   "matched address in binary but not in the profile",
   529  			symbol: "0x400000",
   530  			want:   "address 0x400000 found in binary, but the corresponding symbols do not have samples in the profile",
   531  		},
   532  	} {
   533  		rpt := New(
   534  			profile.Copy(),
   535  			&Options{
   536  				OutputFormat: List,
   537  				Symbol:       regexp.MustCompile(tc.symbol),
   538  				SampleValue: func(v []int64) int64 {
   539  					return v[1]
   540  				},
   541  				SampleUnit: profile.SampleType[1].Unit,
   542  			},
   543  		)
   544  
   545  		if err := PrintAssembly(os.Stdout, rpt, &binutils.Binutils{}, -1); err == nil || err.Error() != tc.want {
   546  			t.Errorf(`Got "%v", want %q`, err, tc.want)
   547  		}
   548  	}
   549  }
   550  

View as plain text