1// Copyright 2017 Google LLC
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
15package profiler
16
17import (
18	"bytes"
19	"testing"
20
21	"cloud.google.com/go/internal/testutil"
22	"github.com/google/go-cmp/cmp/cmpopts"
23	"github.com/google/pprof/profile"
24)
25
26type fakeFunc struct {
27	name   string
28	file   string
29	lineno int
30}
31
32func (f *fakeFunc) Name() string {
33	return f.name
34}
35func (f *fakeFunc) FileLine(_ uintptr) (string, int) {
36	return f.file, f.lineno
37}
38
39var cmpOpt = cmpopts.IgnoreUnexported(profile.Profile{}, profile.Function{},
40	profile.Line{}, profile.Location{}, profile.Sample{}, profile.ValueType{})
41
42// TestRuntimeFunctionTrimming tests if symbolize trims runtime functions as intended.
43func TestRuntimeFunctionTrimming(t *testing.T) {
44	fakeFuncMap := map[uintptr]*fakeFunc{
45		0x10: {"runtime.goexit", "runtime.go", 10},
46		0x20: {"runtime.other", "runtime.go", 20},
47		0x30: {"foo", "foo.go", 30},
48		0x40: {"bar", "bar.go", 40},
49	}
50	backupFuncForPC := funcForPC
51	funcForPC = func(pc uintptr) function {
52		return fakeFuncMap[pc]
53	}
54	defer func() {
55		funcForPC = backupFuncForPC
56	}()
57	testLoc := []*profile.Location{
58		{ID: 1, Address: 0x10},
59		{ID: 2, Address: 0x20},
60		{ID: 3, Address: 0x30},
61		{ID: 4, Address: 0x40},
62	}
63	testProfile := &profile.Profile{
64		Sample: []*profile.Sample{
65			{Location: []*profile.Location{testLoc[0], testLoc[1], testLoc[3], testLoc[2]}},
66			{Location: []*profile.Location{testLoc[1], testLoc[3], testLoc[2]}},
67			{Location: []*profile.Location{testLoc[3], testLoc[2], testLoc[1]}},
68			{Location: []*profile.Location{testLoc[3], testLoc[2], testLoc[0]}},
69			{Location: []*profile.Location{testLoc[0], testLoc[1], testLoc[3], testLoc[0]}},
70			{Location: []*profile.Location{testLoc[1], testLoc[0]}},
71		},
72		Location: testLoc,
73	}
74	testProfiles := make([]*profile.Profile, 2)
75	testProfiles[0] = testProfile.Copy()
76	testProfiles[1] = testProfile.Copy()
77	// Test case for CPU profile.
78	testProfiles[0].PeriodType = &profile.ValueType{Type: "cpu", Unit: "nanoseconds"}
79	// Test case for heap profile.
80	testProfiles[1].PeriodType = &profile.ValueType{Type: "space", Unit: "bytes"}
81	wantFunc := []*profile.Function{
82		{ID: 1, Name: "runtime.goexit", SystemName: "runtime.goexit", Filename: "runtime.go"},
83		{ID: 2, Name: "runtime.other", SystemName: "runtime.other", Filename: "runtime.go"},
84		{ID: 3, Name: "foo", SystemName: "foo", Filename: "foo.go"},
85		{ID: 4, Name: "bar", SystemName: "bar", Filename: "bar.go"},
86	}
87	wantLoc := []*profile.Location{
88		{ID: 1, Address: 0x10, Line: []profile.Line{{Function: wantFunc[0], Line: 10}}},
89		{ID: 2, Address: 0x20, Line: []profile.Line{{Function: wantFunc[1], Line: 20}}},
90		{ID: 3, Address: 0x30, Line: []profile.Line{{Function: wantFunc[2], Line: 30}}},
91		{ID: 4, Address: 0x40, Line: []profile.Line{{Function: wantFunc[3], Line: 40}}},
92	}
93	wantProfiles := []*profile.Profile{
94		{
95			PeriodType: &profile.ValueType{Type: "cpu", Unit: "nanoseconds"},
96			Sample: []*profile.Sample{
97				{Location: []*profile.Location{wantLoc[1], wantLoc[3], wantLoc[2]}},
98				{Location: []*profile.Location{wantLoc[1], wantLoc[3], wantLoc[2]}},
99				{Location: []*profile.Location{wantLoc[3], wantLoc[2], wantLoc[1]}},
100				{Location: []*profile.Location{wantLoc[3], wantLoc[2]}},
101				{Location: []*profile.Location{wantLoc[1], wantLoc[3]}},
102				{Location: []*profile.Location{wantLoc[1]}},
103			},
104			Location: wantLoc,
105			Function: wantFunc,
106		},
107		{
108			PeriodType: &profile.ValueType{Type: "space", Unit: "bytes"},
109			Sample: []*profile.Sample{
110				{Location: []*profile.Location{wantLoc[3], wantLoc[2]}},
111				{Location: []*profile.Location{wantLoc[3], wantLoc[2]}},
112				{Location: []*profile.Location{wantLoc[3], wantLoc[2], wantLoc[1]}},
113				{Location: []*profile.Location{wantLoc[3], wantLoc[2]}},
114				{Location: []*profile.Location{wantLoc[3]}},
115				{Location: []*profile.Location{wantLoc[0]}},
116			},
117			Location: wantLoc,
118			Function: wantFunc,
119		},
120	}
121	for i := 0; i < 2; i++ {
122		symbolize(testProfiles[i])
123		if !testutil.Equal(testProfiles[i], wantProfiles[i], cmpOpt) {
124			t.Errorf("incorrect trimming (testcase = %d): got {%v}, want {%v}", i, testProfiles[i], wantProfiles[i])
125		}
126	}
127}
128
129// TestParseAndSymbolize tests if parseAndSymbolize parses and symbolizes
130// profiles as intended.
131func TestParseAndSymbolize(t *testing.T) {
132	fakeFuncMap := map[uintptr]*fakeFunc{
133		0x10: {"foo", "foo.go", 10},
134		0x20: {"bar", "bar.go", 20},
135	}
136	backupFuncForPC := funcForPC
137	funcForPC = func(pc uintptr) function {
138		return fakeFuncMap[pc]
139	}
140	defer func() {
141		funcForPC = backupFuncForPC
142	}()
143
144	testLoc := []*profile.Location{
145		{ID: 1, Address: 0x10},
146		{ID: 2, Address: 0x20},
147	}
148	testProfile := &profile.Profile{
149		SampleType: []*profile.ValueType{
150			{Type: "cpu", Unit: "nanoseconds"},
151		},
152		PeriodType: &profile.ValueType{Type: "cpu", Unit: "nanoseconds"},
153		Sample: []*profile.Sample{
154			{Location: []*profile.Location{testLoc[0], testLoc[1]}, Value: []int64{1}},
155			{Location: []*profile.Location{testLoc[1]}, Value: []int64{1}},
156		},
157		Location: testLoc,
158	}
159	testProfiles := make([]*profile.Profile, 2)
160	testProfiles[0] = testProfile.Copy()
161	testProfiles[1] = testProfile.Copy()
162
163	wantFunc := []*profile.Function{
164		{ID: 1, Name: "foo", SystemName: "foo", Filename: "foo.go"},
165		{ID: 2, Name: "bar", SystemName: "bar", Filename: "bar.go"},
166	}
167	wantLoc := []*profile.Location{
168		{ID: 1, Address: 0x10, Line: []profile.Line{{Function: wantFunc[0], Line: 10}}},
169		{ID: 2, Address: 0x20, Line: []profile.Line{{Function: wantFunc[1], Line: 20}}},
170	}
171	wantProfile := &profile.Profile{
172		SampleType: []*profile.ValueType{
173			{Type: "cpu", Unit: "nanoseconds"},
174		},
175		PeriodType: &profile.ValueType{Type: "cpu", Unit: "nanoseconds"},
176		Sample: []*profile.Sample{
177			{Location: []*profile.Location{wantLoc[0], wantLoc[1]}, Value: []int64{1}},
178			{Location: []*profile.Location{wantLoc[1]}, Value: []int64{1}},
179		},
180		Location: wantLoc,
181		Function: wantFunc,
182	}
183
184	// Profile already symbolized.
185	testProfiles[1].Location = []*profile.Location{
186		{ID: 1, Address: 0x10, Line: []profile.Line{{Function: wantFunc[0], Line: 10}}},
187		{ID: 2, Address: 0x20, Line: []profile.Line{{Function: wantFunc[1], Line: 20}}},
188	}
189	testProfiles[1].Function = []*profile.Function{
190		{ID: 1, Name: "foo", SystemName: "foo", Filename: "foo.go"},
191		{ID: 2, Name: "bar", SystemName: "bar", Filename: "bar.go"},
192	}
193	for i := 0; i < 2; i++ {
194		var prof bytes.Buffer
195		testProfiles[i].Write(&prof)
196
197		parseAndSymbolize(&prof)
198		gotProfile, err := profile.ParseData(prof.Bytes())
199		if err != nil {
200			t.Errorf("parsing symbolized profile (testcase = %d) got err: %v, want no error", i, err)
201		}
202		if !testutil.Equal(gotProfile, wantProfile, cmpOpt) {
203			t.Errorf("incorrect symbolization (testcase = %d): got {%v}, want {%v}", i, gotProfile, wantProfile)
204		}
205	}
206}
207
208func TestIsSymbolizedGoVersion(t *testing.T) {
209	for _, tc := range []struct {
210		input string
211		want  bool
212	}{
213		{"go1.9beta2", true},
214		{"go1.9", true},
215		{"go1.9.1", true},
216		{"go1.10", true},
217		{"go1.10.1", true},
218		{"go2.0", true},
219		{"go3.1", true},
220		{"go1.8", false},
221		{"go1.8.1", false},
222		{"go1.7", false},
223		{"devel ", false},
224	} {
225		if got := isSymbolizedGoVersion(tc.input); got != tc.want {
226			t.Errorf("isSymbolizedGoVersion(%v) got %v, want %v", tc.input, got, tc.want)
227		}
228	}
229}
230