From 4b0dd19c9273af13053b3bc27ea254835f0d63a1 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 12 Dec 2025 22:10:16 +0100 Subject: [PATCH] internal/report: use better heuristic for locating the source code for profile functions The current heuristic for locating the source code for profile functions doesn't work in the following cases when running pprof with default options: 1. It cannot find source code for standard Go packages. Users need to specify -source_path=`go env GOROOT` as a workaround. 2. It cannot find source code for external modules. Users need to specify -source_path=`go env GOMODCACHE` as a workaround. But this workaround stops working if external module names contain uppercase chars. For example, github.com/FooBar/Baz, because Go stores sources for such module names into $GOMODCACHE/github.com/!foo!bar/!baz directory according to https://go.dev/ref/mod#module-cache , while pprof is unaware of such a conversion. 3. It cannot find source code at the vendor directory. Users need to specify -source_path=`pwd`/vendor as a workaround. But this workaround doesn't work for vendored modules, since their names contain module version in the form module_name@version, while the source code for the module_name is stored inside vendor/module_name directory. 4. It cannot find source code for profiles obtained from Go executables built with -trimpath command-line flag, if their sources are put in the directory with the name other than the module name. This significantly reduces pprof usability, since it is very hard to inspect sources with `list` command. Users have to create horrible hacks with -source_path and -trim_path command-line flags such as https://github.com/VictoriaMetrics/VictoriaLogs/pull/893/files , but these hacks do not cover properly all the cases mentioned above. This commit covers all the cases mentioned above, so pprof properly detects source code with default settings, without the need to specify -trim_path and -source_path command-line flags. --- go.mod | 1 + go.sum | 2 + internal/report/report.go | 50 ++++++- internal/report/source.go | 232 ++++++++++++++++++++++----------- internal/report/source_test.go | 142 +++++++++++++++----- internal/report/stacks.go | 2 +- 6 files changed, 319 insertions(+), 110 deletions(-) diff --git a/go.mod b/go.mod index db65a4d9..4ad72c6f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.9 require ( github.com/chzyer/readline v1.5.1 github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b + golang.org/x/mod v0.31.0 ) require golang.org/x/sys v0.32.0 // indirect diff --git a/go.sum b/go.sum index fb7c0ef5..35f7a014 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA= github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/internal/report/report.go b/internal/report/report.go index ad8b84bf..04826b31 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -20,8 +20,10 @@ import ( "fmt" "io" "net/url" + "os" "path/filepath" "regexp" + "runtime" "sort" "strconv" "strings" @@ -84,6 +86,28 @@ type Options struct { IntelSyntax bool // Whether or not to print assembly in Intel syntax. } +func (o *Options) sourcePaths() []string { + sourcePaths := filepath.SplitList(o.SourcePath) + if len(sourcePaths) == 0 { + // Search for source files in the current directory by default. + cwd, err := os.Getwd() + if err != nil { + cwd = "." + } + sourcePaths = []string{cwd} + } + + // Always search for source files in $GOROOT/src (aka standard packages) + // as a fallback. + sourcePaths = append(sourcePaths, filepath.Join(runtime.GOROOT(), "src")) + + return sourcePaths +} + +func (o *Options) trimPaths() []string { + return filepath.SplitList(o.TrimPath) +} + // Generate generates a report as directed by the Report. func Generate(w io.Writer, rpt *Report, obj plugin.ObjTool) error { o := rpt.options @@ -240,9 +264,12 @@ func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph { // Clean up file paths using heuristics. prof := rpt.prof + sourcePaths := o.sourcePaths() + trimPaths := o.trimPaths() for _, f := range prof.Function { - f.Filename = trimPath(f.Filename, o.TrimPath, o.SourcePath) + f.Filename = sourceFilename(f.Filename, sourcePaths, trimPaths) } + // Removes all numeric tags except for the bytes tag prior // to making graph. // TODO: modify to select first numeric tag if no bytes tag @@ -293,6 +320,27 @@ func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph { return graph.New(rpt.prof, gopt) } +// sourceFilename returns filename path to the given path. +func sourceFilename(path string, sourcePaths, trimPaths []string) string { + f := tryOpenSourceFile(path, sourcePaths, trimPaths) + if f == nil { + return path + } + filename := f.Name() + _ = f.Close() + + // Trim current directory from filename for simpler readability + wd, err := os.Getwd() + if err == nil { + if !strings.HasSuffix(wd, string(filepath.Separator)) { + wd += string(filepath.Separator) + } + filename = strings.TrimPrefix(filename, wd) + } + + return filename +} + // printProto writes the incoming proto via the writer w. // If the divide_by option has been specified, samples are scaled appropriately. func printProto(w io.Writer, rpt *Report) error { diff --git a/internal/report/source.go b/internal/report/source.go index f17952fa..828021aa 100644 --- a/internal/report/source.go +++ b/internal/report/source.go @@ -34,6 +34,7 @@ import ( "github.com/google/pprof/internal/measurement" "github.com/google/pprof/internal/plugin" "github.com/google/pprof/profile" + "golang.org/x/mod/module" ) // printSource prints an annotated source listing, include all @@ -63,15 +64,7 @@ func printSource(w io.Writer, rpt *Report) error { return fmt.Errorf("no matches found for regexp: %s", o.Symbol) } - sourcePath := o.SourcePath - if sourcePath == "" { - wd, err := os.Getwd() - if err != nil { - return fmt.Errorf("could not stat current dir: %v", err) - } - sourcePath = wd - } - reader := newSourceReader(sourcePath, o.TrimPath) + reader := newSourceReader(o) fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total)) for _, fn := range functions { @@ -100,11 +93,12 @@ func printSource(w io.Writer, rpt *Report) error { // Print each file associated with this function. for _, fl := range sourceFiles { - filename := fl.Info.File - fns := fileNodes[filename] + path := fl.Info.File + fns := fileNodes[path] flatSum, cumSum := fns.Sum() - fnodes, _, err := getSourceFromFile(filename, reader, fns, 0, 0) + fnodes, _, err := getSourceFromFile(path, reader, fns, 0, 0) + filename := sourceFilename(path, reader.sourcePaths, reader.trimPaths) fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, filename) fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", rpt.formatValue(flatSum), rpt.formatValue(cumSum), @@ -241,15 +235,7 @@ type WebListCall struct { // MakeWebList returns an annotated source listing of rpt. // rpt.prof should contain inlined call info. func MakeWebList(rpt *Report, obj plugin.ObjTool, maxFiles int) (WebListData, error) { - sourcePath := rpt.options.SourcePath - if sourcePath == "" { - wd, err := os.Getwd() - if err != nil { - return WebListData{}, fmt.Errorf("could not stat current dir: %v", err) - } - sourcePath = wd - } - sp := newSourcePrinter(rpt, obj, sourcePath) + sp := newSourcePrinter(rpt, obj) if len(sp.interest) == 0 { return WebListData{}, fmt.Errorf("no matches found for regexp: %s", rpt.options.Symbol) } @@ -257,9 +243,9 @@ func MakeWebList(rpt *Report, obj plugin.ObjTool, maxFiles int) (WebListData, er return sp.generate(maxFiles, rpt), nil } -func newSourcePrinter(rpt *Report, obj plugin.ObjTool, sourcePath string) *sourcePrinter { +func newSourcePrinter(rpt *Report, obj plugin.ObjTool) *sourcePrinter { sp := &sourcePrinter{ - reader: newSourceReader(sourcePath, rpt.options.TrimPath), + reader: newSourceReader(rpt.options), synth: newSynthCode(rpt.prof.Mapping), objectTool: obj, objects: map[string]plugin.ObjFile{}, @@ -941,12 +927,11 @@ func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start // sourceReader provides access to source code with caching of file contents. type sourceReader struct { - // searchPath is a filepath.ListSeparator-separated list of directories where - // source files should be searched. - searchPath string + // sourcePaths is a list of directories where source files should be searched. + sourcePaths []string - // trimPath is a filepath.ListSeparator-separated list of paths to trim. - trimPath string + // trimPaths is a list of path prefixes to trim. + trimPaths []string // files maps from path name to a list of lines. // files[*][0] is unused since line numbering starts at 1. @@ -957,12 +942,12 @@ type sourceReader struct { errors map[string]error } -func newSourceReader(searchPath, trimPath string) *sourceReader { +func newSourceReader(options *Options) *sourceReader { return &sourceReader{ - searchPath, - trimPath, - make(map[string][]string), - make(map[string]error), + sourcePaths: options.sourcePaths(), + trimPaths: options.trimPaths(), + files: make(map[string][]string), + errors: make(map[string]error), } } @@ -976,8 +961,9 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) { if !ok { // Read and cache file contents. lines = []string{""} // Skip 0th line - f, err := openSourceFile(path, reader.searchPath, reader.trimPath) - if err != nil { + f := tryOpenSourceFile(path, reader.sourcePaths, reader.trimPaths) + if f == nil { + err := fmt.Errorf("could not find %s at %s", path, strings.Join(reader.sourcePaths, string(filepath.ListSeparator))) reader.errors[path] = err } else { s := bufio.NewScanner(f) @@ -985,7 +971,7 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) { lines = append(lines, s.Text()) } f.Close() - if s.Err() != nil { + if err := s.Err(); err != nil { reader.errors[path] = err } } @@ -997,62 +983,89 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) { return lines[lineno], true } -// openSourceFile opens a source file from a name encoded in a profile. File -// names in a profile after can be relative paths, so search them in each of -// the paths in searchPath and their parents. In case the profile contains -// absolute paths, additional paths may be configured to trim from the source -// paths in the profile. This effectively turns the path into a relative path -// searching it using searchPath as usual). -func openSourceFile(path, searchPath, trim string) (*os.File, error) { - path = trimPath(path, trim, searchPath) - // If file is still absolute, require file to exist. +// tryOpenSourceFile opens a source file from the path encoded in a profile. +func tryOpenSourceFile(path string, sourcePaths, trimPaths []string) *os.File { + path = trimPathPrefix(path, trimPaths) + if filepath.IsAbs(path) { - f, err := os.Open(path) - return f, err + if f := tryOpenFile(path); f != nil { + return f + } } - // Scan each component of the path. - for _, dir := range filepath.SplitList(searchPath) { - // Search up for every parent of each possible path. - for { + + vendoredPath := getVendoredPath(path) + + for _, dir := range sourcePaths { + if !filepath.IsAbs(path) { + // Try opening the path at dir. filename := filepath.Join(dir, path) - if f, err := os.Open(filename); err == nil { - return f, nil + if f := tryOpenFile(filename); f != nil { + return f } - parent := filepath.Dir(dir) - if parent == dir { + + // Try opening the path at dir/vendor. + filename = filepath.Join(dir, "vendor", vendoredPath) + if f := tryOpenFile(filename); f != nil { + return f + } + + } + + // The path may contain arbitrary prefix, which doesn't match the dir. + // Try reading the file at the dir with trimmed path prefixes. + pathWithoutVolume := strings.TrimPrefix(path, filepath.VolumeName(path)) + pathTrimmed := strings.TrimPrefix(pathWithoutVolume, string(filepath.Separator)) + for { + filename := filepath.Join(dir, pathTrimmed) + if f := tryOpenFile(filename); f != nil { + return f + } + + n := strings.IndexByte(pathTrimmed, filepath.Separator) + if n < 0 { break } - dir = parent + pathTrimmed = pathTrimmed[n+1:] } } - return nil, fmt.Errorf("could not find file %s on path %s", path, searchPath) -} - -// trimPath cleans up a path by removing prefixes that are commonly -// found on profiles plus configured prefixes. -// TODO(aalexand): Consider optimizing out the redundant work done in this -// function if it proves to matter. -func trimPath(path, trimPath, searchPath string) string { - // Keep path variable intact as it's used below to form the return value. - sPath, searchPath := filepath.ToSlash(path), filepath.ToSlash(searchPath) - if trimPath == "" { - // If the trim path is not configured, try to guess it heuristically: - // search for basename of each search path in the original path and, if - // found, strip everything up to and including the basename. So, for - // example, given original path "/some/remote/path/my-project/foo/bar.c" - // and search path "/my/local/path/my-project" the heuristic will return - // "/my/local/path/my-project/foo/bar.c". - for _, dir := range filepath.SplitList(searchPath) { - want := "/" + filepath.Base(dir) + "/" - if found := strings.Index(sPath, want); found != -1 { - return path[found+len(want):] + if !filepath.IsAbs(path) { + // Try opening the path at $GOMODCACHE + if modcacheDir := getGomodCacheDir(); modcacheDir != "" { + modcachePath := getGomodPath(path) + filename := filepath.Join(modcacheDir, modcachePath) + if f := tryOpenFile(filename); f != nil { + return f } } } - // Trim configured trim prefixes. - trimPaths := append(filepath.SplitList(filepath.ToSlash(trimPath)), "/proc/self/cwd/./", "/proc/self/cwd/") + + return nil +} + +// getGomodCacheDir returns GOMODCACHE value. +// +// See https://go.dev/ref/mod#module-cache . +func getGomodCacheDir() string { + if v, ok := os.LookupEnv("GOMODCACHE"); ok { + return v + } + if v, ok := os.LookupEnv("GOPATH"); ok { + return filepath.Join(v, "pkg", "mod") + } + if v, ok := os.LookupEnv("HOME"); ok { + return filepath.Join(v, "go", "pkg", "mod") + } + return "" +} + +// trimPathPrefix cleans up a path by removing trimPaths prefixes. +func trimPathPrefix(path string, trimPaths []string) string { + // Keep path variable intact as it's used below to form the return value. + sPath := filepath.ToSlash(path) + for _, trimPath := range trimPaths { + trimPath = filepath.ToSlash(trimPath) if !strings.HasSuffix(trimPath, "/") { trimPath += "/" } @@ -1063,6 +1076,67 @@ func trimPath(path, trimPath, searchPath string) string { return path } +// getGomodPath returns the path under GOMODCACHE for the given path. +func getGomodPath(path string) string { + n := strings.IndexByte(path, '@') + if n < 0 { + return path + } + gomodPath := path[:n] + + tail := "" + gomodVersion := path[n+1:] + if n := strings.IndexByte(gomodVersion, filepath.Separator); n >= 0 { + tail = gomodVersion[n:] + gomodVersion = gomodVersion[:n] + } + + gomodPathEscaped, err := module.EscapePath(gomodPath) + if err != nil { + gomodPathEscaped = gomodPath + } + gomodVersionEscaped, err := module.EscapeVersion(gomodVersion) + if err != nil { + gomodVersionEscaped = gomodVersion + } + return gomodPathEscaped + "@" + gomodVersionEscaped + tail +} + +// getVendoredPath strips @v... from the repo@v.../package path. +func getVendoredPath(path string) string { + n := strings.IndexByte(path, '@') + if n < 0 { + return path + } + prefix := path[:n] + suffix := path[n+1:] + n = strings.IndexByte(suffix, filepath.Separator) + if n < 0 { + return prefix + } + return prefix + suffix[n:] +} + +func tryOpenFile(filename string) *os.File { + if filename == "" { + return nil + } + f, err := os.Open(filename) + if err != nil { + return nil + } + stat, err := f.Stat() + if err != nil { + _ = f.Close() + return nil + } + if stat.IsDir() { + _ = f.Close() + return nil + } + return f +} + func indentation(line string) int { column := 0 for _, c := range line { diff --git a/internal/report/source_test.go b/internal/report/source_test.go index afd166b3..9cccabb7 100644 --- a/internal/report/source_test.go +++ b/internal/report/source_test.go @@ -120,7 +120,7 @@ func testSourceMapping(t *testing.T, zeroAddress bool) { } } -func TestOpenSourceFile(t *testing.T) { +func TestSourceFilename(t *testing.T) { tempdir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("failed to create temp dir: %v", err) @@ -128,57 +128,144 @@ func TestOpenSourceFile(t *testing.T) { const lsep = string(filepath.ListSeparator) for _, tc := range []struct { desc string - searchPath string + sourcePath string trimPath string fs []string path string wantPath string // If empty, error is wanted. }{ { - desc: "exact absolute path is found", + desc: "exact absolute path", fs: []string{"foo/bar.cc"}, path: "$dir/foo/bar.cc", wantPath: "$dir/foo/bar.cc", }, { - desc: "exact relative path is found", - searchPath: "$dir", + desc: "exact absolute path not found", + fs: []string{"abc.cc"}, + path: "/aaa/foo/bar.cc", + wantPath: "/aaa/foo/bar.cc", + }, + { + desc: "exact relative path", + sourcePath: "$dir", fs: []string{"foo/bar.cc"}, path: "foo/bar.cc", wantPath: "$dir/foo/bar.cc", }, + { + desc: "exact relative path not found", + sourcePath: "$dir", + fs: []string{"baz.cc"}, + path: "foo/bar.cc", + wantPath: "foo/bar.cc", + }, + { + desc: "exact relative path in vendor", + sourcePath: "$dir", + fs: []string{"vendor/foo/bar.cc"}, + path: "foo/bar.cc", + wantPath: "$dir/vendor/foo/bar.cc", + }, + { + desc: "exact relative path in vendor not found", + sourcePath: "$dir", + fs: []string{"vendor/bar.cc"}, + path: "foo/bar.cc", + wantPath: "foo/bar.cc", + }, + { + desc: "exact relative path with module version in vendor", + sourcePath: "$dir", + fs: []string{"vendor/foo/bar.cc"}, + path: "foo@v1.2.3/bar.cc", + wantPath: "$dir/vendor/foo/bar.cc", + }, + { + desc: "exact relative path with module version in vendor not found", + sourcePath: "$dir", + fs: []string{"vendor/bar.cc"}, + path: "foo@v1.2.3/bar.cc", + wantPath: "foo@v1.2.3/bar.cc", + }, { desc: "multiple search path", - searchPath: "some/path" + lsep + "$dir", + sourcePath: "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", + sourcePath: "$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", + desc: "trims configured prefix not found", + sourcePath: "$dir", + trimPath: "/some/remote/path", + fs: []string{"my-project/foo/baz.cc"}, + path: "/some/remote/path/my-project/foo/bar.cc", + wantPath: "/some/remote/path/my-project/foo/bar.cc", + }, + { + desc: "heuristic trims different prefixes from relative path", + sourcePath: "$dir", + fs: []string{"bar.cc"}, + path: "github.com/x/foo/bar.cc", + wantPath: "$dir/bar.cc", + }, + { + desc: "heuristic trims different prefixes from relative path not found", + sourcePath: "$dir", + fs: []string{"baz.cc"}, + path: "github.com/x/foo/bar.cc", + wantPath: "github.com/x/foo/bar.cc", + }, + { + desc: "heuristic trims different prefixes from absolute path", + sourcePath: "$dir", + fs: []string{"foo/bar.cc"}, + path: "/x/foo/bar.cc", + wantPath: "$dir/foo/bar.cc", + }, + { + desc: "heuristic trims different prefixes from absolute path not found", + sourcePath: "$dir", + fs: []string{"foo/baz.cc"}, + path: "/x/foo/bar.cc", + wantPath: "/x/foo/bar.cc", + }, + { + desc: "heuristic trims same directory", + sourcePath: "$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", + desc: "heuristic trims same directory not found", + sourcePath: "$dir/my-project", + fs: []string{"my-project/foo/baz.cc"}, + path: "/some/remote/path/my-project/foo/bar.cc", + wantPath: "/some/remote/path/my-project/foo/bar.cc", + }, + { + desc: "heuristic trims different directory", + sourcePath: "$dir/my-local-project", + fs: []string{"my-local-project/foo/bar.cc"}, + path: "/some/remote/path/my-project/foo/bar.cc", + wantPath: "$dir/my-local-project/foo/bar.cc", + }, + { + desc: "heuristic trims different directory not found", + sourcePath: "$dir/my-local-project", + fs: []string{"my-local-project/foo/baz.cc"}, + path: "/some/remote/path/my-project/foo/bar.cc", + wantPath: "/some/remote/path/my-project/foo/bar.cc", }, } { t.Run(tc.desc, func(t *testing.T) { @@ -197,19 +284,16 @@ func TestOpenSourceFile(t *testing.T) { t.Fatalf("failed to create file %q: %v", path, err) } } - tc.searchPath = filepath.FromSlash(strings.Replace(tc.searchPath, "$dir", tempdir, -1)) + tc.sourcePath = filepath.FromSlash(strings.Replace(tc.sourcePath, "$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) - } + + sourcePaths := filepath.SplitList(tc.sourcePath) + trimPaths := filepath.SplitList(tc.trimPath) + + filename := sourceFilename(tc.path, sourcePaths, trimPaths) + if filename != tc.wantPath { + t.Errorf("sourceFilename(%q, %q, %q) = %q, want %q", tc.path, tc.sourcePath, tc.trimPath, filename, tc.wantPath) } }) } diff --git a/internal/report/stacks.go b/internal/report/stacks.go index dbf20beb..67e69f62 100644 --- a/internal/report/stacks.go +++ b/internal/report/stacks.go @@ -127,7 +127,7 @@ func (s *StackSet) makeInitialStacks(rpt *Report) { return i } - fileName := trimPath(fn.Filename, rpt.options.TrimPath, rpt.options.SourcePath) + fileName := sourceFilename(fn.Filename, rpt.options.sourcePaths(), rpt.options.trimPaths()) x := StackSource{ FileName: fileName, Inlined: inlined,