| // Copyright 2017 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" |
| "fmt" |
| "os" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "strings" |
| "testing" |
| |
| "github.com/google/pprof/internal/binutils" |
| "github.com/google/pprof/profile" |
| ) |
| |
| func TestWebList(t *testing.T) { |
| if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" { |
| t.Skip("weblist only tested on x86-64 linux") |
| } |
| |
| cpu := readProfile(filepath.Join("testdata", "sample.cpu"), t) |
| rpt := New(cpu, &Options{ |
| OutputFormat: WebList, |
| Symbol: regexp.MustCompile("busyLoop"), |
| SampleValue: func(v []int64) int64 { return v[1] }, |
| SampleUnit: cpu.SampleType[1].Unit, |
| }) |
| var buf bytes.Buffer |
| if err := Generate(&buf, rpt, &binutils.Binutils{}); err != nil { |
| t.Fatalf("could not generate weblist: %v", err) |
| } |
| output := buf.String() |
| |
| for _, expect := range []string{"func busyLoop", "call.*mapassign"} { |
| if match, _ := regexp.MatchString(expect, output); !match { |
| t.Errorf("weblist output does not contain '%s':\n%s", expect, output) |
| } |
| } |
| } |
| |
| func TestSourceSyntheticAddress(t *testing.T) { |
| testSourceMapping(t, true) |
| } |
| |
| func TestSourceMissingMapping(t *testing.T) { |
| testSourceMapping(t, false) |
| } |
| |
| // testSourceMapping checks that source info is found even when no applicable |
| // Mapping/objectFile exists. The locations used in the test are either zero |
| // (if zeroAddress is true), or non-zero (otherwise). |
| func testSourceMapping(t *testing.T, zeroAddress bool) { |
| nextAddr := uint64(0) |
| |
| makeLoc := func(name, fname string, line int64) *profile.Location { |
| if !zeroAddress { |
| nextAddr++ |
| } |
| return &profile.Location{ |
| Address: nextAddr, |
| Line: []profile.Line{ |
| { |
| Function: &profile.Function{Name: name, Filename: fname}, |
| Line: line, |
| }, |
| }, |
| } |
| } |
| |
| // Create profile that will need synthetic addresses since it has no mappings. |
| foo100 := makeLoc("foo", "foo.go", 100) |
| bar50 := makeLoc("bar", "bar.go", 50) |
| prof := &profile.Profile{ |
| Sample: []*profile.Sample{ |
| { |
| Value: []int64{9}, |
| Location: []*profile.Location{foo100, bar50}, |
| }, |
| { |
| Value: []int64{17}, |
| Location: []*profile.Location{bar50}, |
| }, |
| }, |
| } |
| rpt := &Report{ |
| prof: prof, |
| options: &Options{ |
| Symbol: regexp.MustCompile("foo|bar"), |
| SampleValue: func(s []int64) int64 { return s[0] }, |
| }, |
| formatValue: func(v int64) string { return fmt.Sprint(v) }, |
| } |
| |
| var out bytes.Buffer |
| err := PrintWebList(&out, rpt, nil, -1) |
| if err != nil { |
| t.Fatalf("PrintWebList returned unexpected error: %v", err) |
| } |
| got := out.String() |
| expect := regexp.MustCompile( |
| `(?s)` + // Allow "." to match newline |
| `bar\.go.* 50\b.* 17 +26 .*` + |
| `foo\.go.* 100\b.* 9 +9 `) |
| if !expect.MatchString(got) { |
| t.Errorf("expected regular expression %v does not match output:\n%s\n", expect, got) |
| } |
| } |
| |
| func TestOpenSourceFile(t *testing.T) { |
| tempdir, err := os.MkdirTemp("", "") |
| if err != nil { |
| t.Fatalf("failed to create temp dir: %v", err) |
| } |
| const lsep = string(filepath.ListSeparator) |
| for _, tc := range []struct { |
| desc string |
| searchPath string |
| trimPath string |
| fs []string |
| path string |
| wantPath string // If empty, error is wanted. |
| }{ |
| { |
| desc: "exact absolute path is found", |
| fs: []string{"foo/bar.cc"}, |
| path: "$dir/foo/bar.cc", |
| wantPath: "$dir/foo/bar.cc", |
| }, |
| { |
| desc: "exact relative path is found", |
| searchPath: "$dir", |
| fs: []string{"foo/bar.cc"}, |
| path: "foo/bar.cc", |
| wantPath: "$dir/foo/bar.cc", |
| }, |
| { |
| desc: "multiple search path", |
| searchPath: "some/path" + lsep + "$dir", |
| fs: []string{"foo/bar.cc"}, |
| path: "foo/bar.cc", |
| wantPath: "$dir/foo/bar.cc", |
| }, |
| { |
| desc: "relative path is found in parent dir", |
| searchPath: "$dir/foo/bar", |
| fs: []string{"bar.cc", "foo/bar/baz.cc"}, |
| path: "bar.cc", |
| wantPath: "$dir/bar.cc", |
| }, |
| { |
| desc: "trims configured prefix", |
| searchPath: "$dir", |
| trimPath: "some-path" + lsep + "/some/remote/path", |
| fs: []string{"my-project/foo/bar.cc"}, |
| path: "/some/remote/path/my-project/foo/bar.cc", |
| wantPath: "$dir/my-project/foo/bar.cc", |
| }, |
| { |
| desc: "trims heuristically", |
| searchPath: "$dir/my-project", |
| fs: []string{"my-project/foo/bar.cc"}, |
| path: "/some/remote/path/my-project/foo/bar.cc", |
| wantPath: "$dir/my-project/foo/bar.cc", |
| }, |
| { |
| desc: "error when not found", |
| path: "foo.cc", |
| }, |
| } { |
| t.Run(tc.desc, func(t *testing.T) { |
| defer func() { |
| if err := os.RemoveAll(tempdir); err != nil { |
| t.Fatalf("failed to remove dir %q: %v", tempdir, err) |
| } |
| }() |
| for _, f := range tc.fs { |
| path := filepath.Join(tempdir, filepath.FromSlash(f)) |
| dir := filepath.Dir(path) |
| if err := os.MkdirAll(dir, 0755); err != nil { |
| t.Fatalf("failed to create dir %q: %v", dir, err) |
| } |
| if err := os.WriteFile(path, nil, 0644); err != nil { |
| t.Fatalf("failed to create file %q: %v", path, err) |
| } |
| } |
| tc.searchPath = filepath.FromSlash(strings.Replace(tc.searchPath, "$dir", tempdir, -1)) |
| tc.path = filepath.FromSlash(strings.Replace(tc.path, "$dir", tempdir, 1)) |
| tc.wantPath = filepath.FromSlash(strings.Replace(tc.wantPath, "$dir", tempdir, 1)) |
| if file, err := openSourceFile(tc.path, tc.searchPath, tc.trimPath); err != nil && tc.wantPath != "" { |
| t.Errorf("openSourceFile(%q, %q, %q) = err %v, want path %q", tc.path, tc.searchPath, tc.trimPath, err, tc.wantPath) |
| } else if err == nil { |
| defer file.Close() |
| gotPath := file.Name() |
| if tc.wantPath == "" { |
| t.Errorf("openSourceFile(%q, %q, %q) = %q, want error", tc.path, tc.searchPath, tc.trimPath, gotPath) |
| } else if gotPath != tc.wantPath { |
| t.Errorf("openSourceFile(%q, %q, %q) = %q, want path %q", tc.path, tc.searchPath, tc.trimPath, gotPath, tc.wantPath) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestIndentation(t *testing.T) { |
| for _, c := range []struct { |
| str string |
| wantIndent int |
| }{ |
| {"", 0}, |
| {"foobar", 0}, |
| {" foo", 2}, |
| {"\tfoo", 8}, |
| {"\t foo", 9}, |
| {" \tfoo", 8}, |
| {" \tfoo", 8}, |
| {" \tfoo", 16}, |
| } { |
| if n := indentation(c.str); n != c.wantIndent { |
| t.Errorf("indentation(%v): got %d, want %d", c.str, n, c.wantIndent) |
| } |
| } |
| } |
| |
| func TestRightPad(t *testing.T) { |
| for _, c := range []struct { |
| pad int |
| in string |
| expect string |
| }{ |
| {0, "", ""}, |
| {4, "", " "}, |
| {4, "x", "x "}, |
| {4, "abcd", "abcd"}, // No padding because of overflow |
| {4, "abcde", "abcde"}, // No padding because of overflow |
| {10, "\tx", " x "}, |
| {10, "w\txy\tz", "w xy z"}, |
| {20, "w\txy\tz", "w xy z "}, |
| } { |
| out := rightPad(c.in, c.pad) |
| if out != c.expect { |
| t.Errorf("rightPad(%q, %d): got %q, want %q", c.in, c.pad, out, c.expect) |
| } |
| } |
| } |
| |
| func readProfile(fname string, t *testing.T) *profile.Profile { |
| file, err := os.Open(fname) |
| if err != nil { |
| t.Fatalf("%s: could not open profile: %v", fname, err) |
| } |
| defer file.Close() |
| p, err := profile.Parse(file) |
| if err != nil { |
| t.Fatalf("%s: could not parse profile: %v", fname, err) |
| } |
| |
| // Fix file names so they do not include absolute path names. |
| fix := func(s string) string { |
| const testdir = "/internal/report/" |
| pos := strings.Index(s, testdir) |
| if pos == -1 { |
| return s |
| } |
| return s[pos+len(testdir):] |
| } |
| for _, m := range p.Mapping { |
| m.File = fix(m.File) |
| } |
| for _, f := range p.Function { |
| f.Filename = fix(f.Filename) |
| } |
| |
| return p |
| } |