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
15package driver
16
17import (
18	"fmt"
19	"io/ioutil"
20	"net/http"
21	"net/url"
22	"os"
23	"path/filepath"
24	"reflect"
25	"regexp"
26	"runtime"
27	"testing"
28	"time"
29
30	"github.com/google/pprof/internal/plugin"
31	"github.com/google/pprof/internal/proftest"
32	"github.com/google/pprof/profile"
33)
34
35func TestSymbolizationPath(t *testing.T) {
36	if runtime.GOOS == "windows" {
37		t.Skip("test assumes Unix paths")
38	}
39
40	// Save environment variables to restore after test
41	saveHome := os.Getenv(homeEnv())
42	savePath := os.Getenv("PPROF_BINARY_PATH")
43
44	tempdir, err := ioutil.TempDir("", "home")
45	if err != nil {
46		t.Fatal("creating temp dir: ", err)
47	}
48	defer os.RemoveAll(tempdir)
49	os.MkdirAll(filepath.Join(tempdir, "pprof", "binaries", "abcde10001"), 0700)
50	os.Create(filepath.Join(tempdir, "pprof", "binaries", "abcde10001", "binary"))
51
52	obj := testObj{tempdir}
53	os.Setenv(homeEnv(), tempdir)
54	for _, tc := range []struct {
55		env, file, buildID, want string
56		msgCount                 int
57	}{
58		{"", "/usr/bin/binary", "", "/usr/bin/binary", 0},
59		{"", "/usr/bin/binary", "fedcb10000", "/usr/bin/binary", 0},
60		{"/usr", "/bin/binary", "", "/usr/bin/binary", 0},
61		{"", "/prod/path/binary", "abcde10001", filepath.Join(tempdir, "pprof/binaries/abcde10001/binary"), 0},
62		{"/alternate/architecture", "/usr/bin/binary", "", "/alternate/architecture/binary", 0},
63		{"/alternate/architecture", "/usr/bin/binary", "abcde10001", "/alternate/architecture/binary", 0},
64		{"/nowhere:/alternate/architecture", "/usr/bin/binary", "fedcb10000", "/usr/bin/binary", 1},
65		{"/nowhere:/alternate/architecture", "/usr/bin/binary", "abcde10002", "/usr/bin/binary", 1},
66	} {
67		os.Setenv("PPROF_BINARY_PATH", tc.env)
68		p := &profile.Profile{
69			Mapping: []*profile.Mapping{
70				{
71					File:    tc.file,
72					BuildID: tc.buildID,
73				},
74			},
75		}
76		s := &source{}
77		locateBinaries(p, s, obj, &proftest.TestUI{T: t, Ignore: tc.msgCount})
78		if file := p.Mapping[0].File; file != tc.want {
79			t.Errorf("%s:%s:%s, want %s, got %s", tc.env, tc.file, tc.buildID, tc.want, file)
80		}
81	}
82	os.Setenv(homeEnv(), saveHome)
83	os.Setenv("PPROF_BINARY_PATH", savePath)
84}
85
86func TestCollectMappingSources(t *testing.T) {
87	const startAddress uint64 = 0x40000
88	const url = "http://example.com"
89	for _, tc := range []struct {
90		file, buildID string
91		want          plugin.MappingSources
92	}{
93		{"/usr/bin/binary", "buildId", mappingSources("buildId", url, startAddress)},
94		{"/usr/bin/binary", "", mappingSources("/usr/bin/binary", url, startAddress)},
95		{"", "", mappingSources(url, url, startAddress)},
96	} {
97		p := &profile.Profile{
98			Mapping: []*profile.Mapping{
99				{
100					File:    tc.file,
101					BuildID: tc.buildID,
102					Start:   startAddress,
103				},
104			},
105		}
106		got := collectMappingSources(p, url)
107		if !reflect.DeepEqual(got, tc.want) {
108			t.Errorf("%s:%s, want %v, got %v", tc.file, tc.buildID, tc.want, got)
109		}
110	}
111}
112
113func TestUnsourceMappings(t *testing.T) {
114	for _, tc := range []struct {
115		file, buildID, want string
116	}{
117		{"/usr/bin/binary", "buildId", "/usr/bin/binary"},
118		{"http://example.com", "", ""},
119	} {
120		p := &profile.Profile{
121			Mapping: []*profile.Mapping{
122				{
123					File:    tc.file,
124					BuildID: tc.buildID,
125				},
126			},
127		}
128		unsourceMappings(p)
129		if got := p.Mapping[0].File; got != tc.want {
130			t.Errorf("%s:%s, want %s, got %s", tc.file, tc.buildID, tc.want, got)
131		}
132	}
133}
134
135type testObj struct {
136	home string
137}
138
139func (o testObj) Open(file string, start, limit, offset uint64) (plugin.ObjFile, error) {
140	switch file {
141	case "/alternate/architecture/binary":
142		return testFile{file, "abcde10001"}, nil
143	case "/usr/bin/binary":
144		return testFile{file, "fedcb10000"}, nil
145	case filepath.Join(o.home, "pprof/binaries/abcde10001/binary"):
146		return testFile{file, "abcde10001"}, nil
147	}
148	return nil, fmt.Errorf("not found: %s", file)
149}
150func (testObj) Demangler(_ string) func(names []string) (map[string]string, error) {
151	return func(names []string) (map[string]string, error) { return nil, nil }
152}
153func (testObj) Disasm(file string, start, end uint64) ([]plugin.Inst, error) { return nil, nil }
154
155type testFile struct{ name, buildID string }
156
157func (f testFile) Name() string                                               { return f.name }
158func (testFile) Base() uint64                                                 { return 0 }
159func (f testFile) BuildID() string                                            { return f.buildID }
160func (testFile) SourceLine(addr uint64) ([]plugin.Frame, error)               { return nil, nil }
161func (testFile) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) { return nil, nil }
162func (testFile) Close() error                                                 { return nil }
163
164func TestFetch(t *testing.T) {
165	const path = "testdata/"
166
167	// Intercept http.Get calls from HTTPFetcher.
168	httpGet = stubHTTPGet
169
170	type testcase struct {
171		source, execName string
172	}
173
174	for _, tc := range []testcase{
175		{path + "go.crc32.cpu", ""},
176		{path + "go.nomappings.crash", "/bin/gotest.exe"},
177		{"http://localhost/profile?file=cppbench.cpu", ""},
178	} {
179		p, _, _, err := grabProfile(&source{ExecName: tc.execName}, tc.source, 0, nil, testObj{}, &proftest.TestUI{T: t})
180		if err != nil {
181			t.Fatalf("%s: %s", tc.source, err)
182		}
183		if len(p.Sample) == 0 {
184			t.Errorf("%s: want non-zero samples", tc.source)
185		}
186		if e := tc.execName; e != "" {
187			switch {
188			case len(p.Mapping) == 0 || p.Mapping[0] == nil:
189				t.Errorf("%s: want mapping[0].execName == %s, got no mappings", tc.source, e)
190			case p.Mapping[0].File != e:
191				t.Errorf("%s: want mapping[0].execName == %s, got %s", tc.source, e, p.Mapping[0].File)
192			}
193		}
194	}
195}
196
197// mappingSources creates MappingSources map with a single item.
198func mappingSources(key, source string, start uint64) plugin.MappingSources {
199	return plugin.MappingSources{
200		key: []struct {
201			Source string
202			Start  uint64
203		}{
204			{Source: source, Start: start},
205		},
206	}
207}
208
209// stubHTTPGet intercepts a call to http.Get and rewrites it to use
210// "file://" to get the profile directly from a file.
211func stubHTTPGet(source string, _ time.Duration) (*http.Response, error) {
212	url, err := url.Parse(source)
213	if err != nil {
214		return nil, err
215	}
216
217	values := url.Query()
218	file := values.Get("file")
219
220	if file == "" {
221		return nil, fmt.Errorf("want .../file?profile, got %s", source)
222	}
223
224	t := &http.Transport{}
225	t.RegisterProtocol("file", http.NewFileTransport(http.Dir("testdata/")))
226
227	c := &http.Client{Transport: t}
228	return c.Get("file:///" + file)
229}
230