]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/vendor/.../pprof: update to 0f7d9ba1
authorDaniel Martí <mvdan@mvdan.cc>
Fri, 6 Apr 2018 21:38:48 +0000 (22:38 +0100)
committerBrad Fitzpatrick <bradfitz@golang.org>
Fri, 6 Apr 2018 23:03:41 +0000 (23:03 +0000)
In particular, to pull a few fixes that were causing some tests to be
flaky on our build dashboard.

Fixes #24405.
Fixes #24508.
Fixes #24611.

Change-Id: I713156ad11c924e4a4b603144d10395523d526ed
Reviewed-on: https://go-review.googlesource.com/105275
Run-TryBot: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
27 files changed:
src/cmd/vendor/github.com/google/pprof/driver/driver.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/addr2liner.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils_test.go
src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/build_mac.sh [new file with mode: 0755]
src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64 [new file with mode: 0755]
src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64.dSYM/Contents/Info.plist [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64.dSYM/Contents/Resources/DWARF/exe_mac_64 [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64 [new file with mode: 0755]
src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64.dSYM/Contents/Info.plist [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64.dSYM/Contents/Resources/DWARF/lib_mac_64 [new file with mode: 0644]
src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go
src/cmd/vendor/github.com/google/pprof/internal/driver/driver_test.go
src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go
src/cmd/vendor/github.com/google/pprof/internal/driver/fetch_test.go
src/cmd/vendor/github.com/google/pprof/internal/driver/flamegraph.go
src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go
src/cmd/vendor/github.com/google/pprof/internal/driver/options.go
src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go
src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go
src/cmd/vendor/github.com/google/pprof/internal/driver/webui_test.go
src/cmd/vendor/github.com/google/pprof/internal/elfexec/elfexec.go
src/cmd/vendor/github.com/google/pprof/internal/plugin/plugin.go
src/cmd/vendor/github.com/google/pprof/internal/proftest/proftest.go
src/cmd/vendor/github.com/google/pprof/internal/report/source.go
src/cmd/vendor/github.com/google/pprof/internal/report/source_test.go
src/cmd/vendor/github.com/google/pprof/third_party/svgpan/svgpan.go

index ee7d67d7843defdcb267b2c66e965b47b1556f28..3735d6ace990050e6194bb0f91dafe74fe47fc6d 100644 (file)
@@ -41,24 +41,36 @@ func (o *Options) internalOptions() *plugin.Options {
        if o.Sym != nil {
                sym = &internalSymbolizer{o.Sym}
        }
+       var httpServer func(args *plugin.HTTPServerArgs) error
+       if o.HTTPServer != nil {
+               httpServer = func(args *plugin.HTTPServerArgs) error {
+                       return o.HTTPServer(((*HTTPServerArgs)(args)))
+               }
+       }
        return &plugin.Options{
-               Writer:  o.Writer,
-               Flagset: o.Flagset,
-               Fetch:   o.Fetch,
-               Sym:     sym,
-               Obj:     obj,
-               UI:      o.UI,
+               Writer:     o.Writer,
+               Flagset:    o.Flagset,
+               Fetch:      o.Fetch,
+               Sym:        sym,
+               Obj:        obj,
+               UI:         o.UI,
+               HTTPServer: httpServer,
        }
 }
 
+// HTTPServerArgs contains arguments needed by an HTTP server that
+// is exporting a pprof web interface.
+type HTTPServerArgs plugin.HTTPServerArgs
+
 // Options groups all the optional plugins into pprof.
 type Options struct {
-       Writer  Writer
-       Flagset FlagSet
-       Fetch   Fetcher
-       Sym     Symbolizer
-       Obj     ObjTool
-       UI      UI
+       Writer     Writer
+       Flagset    FlagSet
+       Fetch      Fetcher
+       Sym        Symbolizer
+       Obj        ObjTool
+       UI         UI
+       HTTPServer func(*HTTPServerArgs) error
 }
 
 // Writer provides a mechanism to write data under a certain name,
@@ -206,6 +218,9 @@ type UI interface {
        // interactive terminal (as opposed to being redirected to a file).
        IsTerminal() bool
 
+       // WantBrowser indicates whether browser should be opened with the -http option.
+       WantBrowser() bool
+
        // SetAutoComplete instructs the UI to call complete(cmd) to obtain
        // the auto-completion of cmd, if the UI supports auto-completion at all.
        SetAutoComplete(complete func(string) string)
index 71e471b5d6ace9947aee35959ef07f28176d6906..c0661bf4aa9a3c640f6e3046b41075fffb340096 100644 (file)
@@ -41,9 +41,11 @@ type addr2Liner struct {
        rw   lineReaderWriter
        base uint64
 
-       // nm holds an NM based addr2Liner which can provide
-       // better full names compared to addr2line, which often drops
-       // namespaces etc. from the names it returns.
+       // nm holds an addr2Liner using nm tool. Certain versions of addr2line
+       // produce incomplete names due to
+       // https://sourceware.org/bugzilla/show_bug.cgi?id=17541. As a workaround,
+       // the names from nm are used when they look more complete. See addrInfo()
+       // code below for the exact heuristic.
        nm *addr2LinerNM
 }
 
@@ -215,17 +217,22 @@ func (d *addr2Liner) addrInfo(addr uint64) ([]plugin.Frame, error) {
                return nil, err
        }
 
-       // Get better name from nm if possible.
+       // Certain versions of addr2line produce incomplete names due to
+       // https://sourceware.org/bugzilla/show_bug.cgi?id=17541. Attempt to replace
+       // the name with a better one from nm.
        if len(stack) > 0 && d.nm != nil {
                nm, err := d.nm.addrInfo(addr)
                if err == nil && len(nm) > 0 {
-                       // Last entry in frame list should match since
-                       // it is non-inlined. As a simple heuristic,
-                       // we only switch to the nm-based name if it
-                       // is longer.
+                       // Last entry in frame list should match since it is non-inlined. As a
+                       // simple heuristic, we only switch to the nm-based name if it is longer
+                       // by 2 or more characters. We consider nm names that are longer by 1
+                       // character insignificant to avoid replacing foo with _foo on MacOS (for
+                       // unknown reasons read2line produces the former and nm produces the
+                       // latter on MacOS even though both tools are asked to produce mangled
+                       // names).
                        nmName := nm[len(nm)-1].Func
                        a2lName := stack[len(stack)-1].Func
-                       if len(nmName) > len(a2lName) {
+                       if len(nmName) > len(a2lName)+1 {
                                stack[len(stack)-1].Func = nmName
                        }
                }
index 390f952feb8f73de5ff60c997fcd077f4d15be30..12b6a5c4b262ee71efd02da1d44379acc85dac84 100644 (file)
@@ -81,6 +81,26 @@ func (bu *Binutils) update(fn func(r *binrep)) {
        bu.rep = r
 }
 
+// String returns string representation of the binutils state for debug logging.
+func (bu *Binutils) String() string {
+       r := bu.get()
+       var llvmSymbolizer, addr2line, nm, objdump string
+       if r.llvmSymbolizerFound {
+               llvmSymbolizer = r.llvmSymbolizer
+       }
+       if r.addr2lineFound {
+               addr2line = r.addr2line
+       }
+       if r.nmFound {
+               nm = r.nm
+       }
+       if r.objdumpFound {
+               objdump = r.objdump
+       }
+       return fmt.Sprintf("llvm-symbolizer=%q addr2line=%q nm=%q objdump=%q fast=%t",
+               llvmSymbolizer, addr2line, nm, objdump, r.fast)
+}
+
 // SetFastSymbolization sets a toggle that makes binutils use fast
 // symbolization (using nm), which is much faster than addr2line but
 // provides only symbol name information (no file/line).
@@ -111,6 +131,11 @@ func initTools(b *binrep, config string) {
        defaultPath := paths[""]
        b.llvmSymbolizer, b.llvmSymbolizerFound = findExe("llvm-symbolizer", append(paths["llvm-symbolizer"], defaultPath...))
        b.addr2line, b.addr2lineFound = findExe("addr2line", append(paths["addr2line"], defaultPath...))
+       if !b.addr2lineFound {
+               // On MacOS, brew installs addr2line under gaddr2line name, so search for
+               // that if the tool is not found by its default name.
+               b.addr2line, b.addr2lineFound = findExe("gaddr2line", append(paths["addr2line"], defaultPath...))
+       }
        b.nm, b.nmFound = findExe("nm", append(paths["nm"], defaultPath...))
        b.objdump, b.objdumpFound = findExe("objdump", append(paths["objdump"], defaultPath...))
 }
@@ -306,9 +331,9 @@ func (f *fileNM) SourceLine(addr uint64) ([]plugin.Frame, error) {
 }
 
 // fileAddr2Line implements the binutils.ObjFile interface, using
-// 'addr2line' to map addresses to symbols (with file/line number
-// information). It can be slow for large binaries with debug
-// information.
+// llvm-symbolizer, if that's available, or addr2line to map addresses to
+// symbols (with file/line number information). It can be slow for large
+// binaries with debug information.
 type fileAddr2Line struct {
        once sync.Once
        file
index 0317cf51265942a7aed54854966f833052e2af5c..d844ed7e4e7e0b4e4a0154c0a1cba46a8660d0ec 100644 (file)
@@ -265,8 +265,6 @@ func TestObjFile(t *testing.T) {
 func TestMachoFiles(t *testing.T) {
        skipUnlessDarwinAmd64(t)
 
-       t.Skip("Disabled because of issues with addr2line (see https://github.com/google/pprof/pull/313#issuecomment-364073010)")
-
        // Load `file`, pretending it was mapped at `start`. Then get the symbol
        // table. Check that it contains the symbol `sym` and that the address
        // `addr` gives the `expected` stack trace.
@@ -291,7 +289,7 @@ func TestMachoFiles(t *testing.T) {
                {"lib normal mapping", "lib_mac_64", 0, math.MaxUint64, 0,
                        0xfa0, "_bar",
                        []plugin.Frame{
-                               {Func: "bar", File: "/tmp/lib.c", Line: 6},
+                               {Func: "bar", File: "/tmp/lib.c", Line: 5},
                        }},
        } {
                t.Run(tc.desc, func(t *testing.T) {
@@ -300,6 +298,13 @@ func TestMachoFiles(t *testing.T) {
                        if err != nil {
                                t.Fatalf("Open: unexpected error %v", err)
                        }
+                       t.Logf("binutils: %v", bu)
+                       if runtime.GOOS == "darwin" && !bu.rep.addr2lineFound && !bu.rep.llvmSymbolizerFound {
+                               // On OSX user needs to install gaddr2line or llvm-symbolizer with
+                               // Homebrew, skip the test when the environment doesn't have it
+                               // installed.
+                               t.Skip("couldn't find addr2line or gaddr2line")
+                       }
                        defer f.Close()
                        syms, err := f.Symbols(nil, 0)
                        if err != nil {
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/build_mac.sh b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/build_mac.sh
new file mode 100755 (executable)
index 0000000..5ec98f3
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/bash -x
+
+# This is a script that generates the test MacOS executables in this directory.
+# It should be needed very rarely to run this script. It is mostly provided
+# as a future reference on how the original binary set was created.
+
+set -o errexit
+
+cat <<EOF >/tmp/hello.cc
+#include <stdio.h>
+
+int main() {
+  printf("Hello, world!\n");
+  return 0;
+}
+EOF
+
+cat <<EOF >/tmp/lib.c
+int foo() {
+  return 1;
+}
+
+int bar() {
+  return 2;
+}
+EOF
+
+cd $(dirname $0)
+rm -rf exe_mac_64* lib_mac_64*
+clang -g -o exe_mac_64 /tmp/hello.c
+clang -g -o lib_mac_64 -dynamiclib /tmp/lib.c
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64 b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64
new file mode 100755 (executable)
index 0000000..dba1ae1
Binary files /dev/null and b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64 differ
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64.dSYM/Contents/Info.plist b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64.dSYM/Contents/Info.plist
new file mode 100644 (file)
index 0000000..41ce537
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>CFBundleDevelopmentRegion</key>
+    <string>English</string>
+    <key>CFBundleIdentifier</key>
+    <string>com.apple.xcode.dsym.exe_mac_64</string>
+    <key>CFBundleInfoDictionaryVersion</key>
+    <string>6.0</string>
+    <key>CFBundlePackageType</key>
+    <string>dSYM</string>
+    <key>CFBundleSignature</key>
+    <string>????</string>
+    <key>CFBundleShortVersionString</key>
+    <string>1.0</string>
+    <key>CFBundleVersion</key>
+    <string>1</string>
+  </dict>
+</plist>
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64.dSYM/Contents/Resources/DWARF/exe_mac_64 b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64.dSYM/Contents/Resources/DWARF/exe_mac_64
new file mode 100644 (file)
index 0000000..2cb0e3b
Binary files /dev/null and b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_mac_64.dSYM/Contents/Resources/DWARF/exe_mac_64 differ
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64 b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64
new file mode 100755 (executable)
index 0000000..933a3f6
Binary files /dev/null and b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64 differ
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64.dSYM/Contents/Info.plist b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64.dSYM/Contents/Info.plist
new file mode 100644 (file)
index 0000000..409e4cf
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>CFBundleDevelopmentRegion</key>
+    <string>English</string>
+    <key>CFBundleIdentifier</key>
+    <string>com.apple.xcode.dsym.lib_mac_64</string>
+    <key>CFBundleInfoDictionaryVersion</key>
+    <string>6.0</string>
+    <key>CFBundlePackageType</key>
+    <string>dSYM</string>
+    <key>CFBundleSignature</key>
+    <string>????</string>
+    <key>CFBundleShortVersionString</key>
+    <string>1.0</string>
+    <key>CFBundleVersion</key>
+    <string>1</string>
+  </dict>
+</plist>
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64.dSYM/Contents/Resources/DWARF/lib_mac_64 b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64.dSYM/Contents/Resources/DWARF/lib_mac_64
new file mode 100644 (file)
index 0000000..e466c13
Binary files /dev/null and b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/lib_mac_64.dSYM/Contents/Resources/DWARF/lib_mac_64 differ
index c2b1cd082bc56c7c2ce70a65407a03fd353e1be4..3b7439fc9a4e7848448a1d6aa2ecae0a565cb390 100644 (file)
@@ -54,7 +54,7 @@ func PProf(eo *plugin.Options) error {
        }
 
        if src.HTTPHostport != "" {
-               return serveWebInterface(src.HTTPHostport, p, o, true)
+               return serveWebInterface(src.HTTPHostport, p, o)
        }
        return interactive(p, o)
 }
@@ -138,7 +138,7 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
 
        // Output to specified file.
        o.UI.PrintErr("Generating report in ", output)
-       out, err := os.Create(output)
+       out, err := o.Writer.Open(output)
        if err != nil {
                return err
        }
index 0604da911c32d19e1dbb8e78115fd294125167b3..06219eae644b9938af57bfc4e8144c83a310373e 100644 (file)
@@ -96,114 +96,118 @@ func TestParse(t *testing.T) {
        baseVars := pprofVariables
        defer func() { pprofVariables = baseVars }()
        for _, tc := range testcase {
-               // Reset the pprof variables before processing
-               pprofVariables = baseVars.makeCopy()
+               t.Run(tc.flags+":"+tc.source, func(t *testing.T) {
+                       // Reset the pprof variables before processing
+                       pprofVariables = baseVars.makeCopy()
 
-               f := baseFlags()
-               f.args = []string{tc.source}
+                       testUI := &proftest.TestUI{T: t, AllowRx: "Generating report in|Ignoring local file|expression matched no samples|Interpreted .* as range, not regexp"}
 
-               flags := strings.Split(tc.flags, ",")
+                       f := baseFlags()
+                       f.args = []string{tc.source}
 
-               // Skip the output format in the first flag, to output to a proto
-               addFlags(&f, flags[1:])
+                       flags := strings.Split(tc.flags, ",")
 
-               // Encode profile into a protobuf and decode it again.
-               protoTempFile, err := ioutil.TempFile("", "profile_proto")
-               if err != nil {
-                       t.Errorf("cannot create tempfile: %v", err)
-               }
-               defer os.Remove(protoTempFile.Name())
-               defer protoTempFile.Close()
-               f.strings["output"] = protoTempFile.Name()
-
-               if flags[0] == "topproto" {
-                       f.bools["proto"] = false
-                       f.bools["topproto"] = true
-               }
+                       // Skip the output format in the first flag, to output to a proto
+                       addFlags(&f, flags[1:])
 
-               // First pprof invocation to save the profile into a profile.proto.
-               o1 := setDefaults(nil)
-               o1.Flagset = f
-               o1.Fetch = testFetcher{}
-               o1.Sym = testSymbolizer{}
-               if err := PProf(o1); err != nil {
-                       t.Errorf("%s %q:  %v", tc.source, tc.flags, err)
-                       continue
-               }
-               // Reset the pprof variables after the proto invocation
-               pprofVariables = baseVars.makeCopy()
+                       // Encode profile into a protobuf and decode it again.
+                       protoTempFile, err := ioutil.TempFile("", "profile_proto")
+                       if err != nil {
+                               t.Errorf("cannot create tempfile: %v", err)
+                       }
+                       defer os.Remove(protoTempFile.Name())
+                       defer protoTempFile.Close()
+                       f.strings["output"] = protoTempFile.Name()
 
-               // Read the profile from the encoded protobuf
-               outputTempFile, err := ioutil.TempFile("", "profile_output")
-               if err != nil {
-                       t.Errorf("cannot create tempfile: %v", err)
-               }
-               defer os.Remove(outputTempFile.Name())
-               defer outputTempFile.Close()
-               f.strings["output"] = outputTempFile.Name()
-               f.args = []string{protoTempFile.Name()}
-
-               var solution string
-               // Apply the flags for the second pprof run, and identify name of
-               // the file containing expected results
-               if flags[0] == "topproto" {
-                       solution = solutionFilename(tc.source, &f)
-                       delete(f.bools, "topproto")
-                       f.bools["text"] = true
-               } else {
-                       delete(f.bools, "proto")
-                       addFlags(&f, flags[:1])
-                       solution = solutionFilename(tc.source, &f)
-               }
-               // The add_comment flag is not idempotent so only apply it on the first run.
-               delete(f.strings, "add_comment")
-
-               // Second pprof invocation to read the profile from profile.proto
-               // and generate a report.
-               o2 := setDefaults(nil)
-               o2.Flagset = f
-               o2.Sym = testSymbolizeDemangler{}
-               o2.Obj = new(mockObjTool)
-
-               if err := PProf(o2); err != nil {
-                       t.Errorf("%s: %v", tc.source, err)
-               }
-               b, err := ioutil.ReadFile(outputTempFile.Name())
-               if err != nil {
-                       t.Errorf("Failed to read profile %s: %v", outputTempFile.Name(), err)
-               }
+                       if flags[0] == "topproto" {
+                               f.bools["proto"] = false
+                               f.bools["topproto"] = true
+                       }
 
-               // Read data file with expected solution
-               solution = "testdata/" + solution
-               sbuf, err := ioutil.ReadFile(solution)
-               if err != nil {
-                       t.Errorf("reading solution file %s: %v", solution, err)
-                       continue
-               }
-               if runtime.GOOS == "windows" {
-                       sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte("testdata\\"), -1)
-                       sbuf = bytes.Replace(sbuf, []byte("/path/to/"), []byte("\\path\\to\\"), -1)
-               }
+                       // First pprof invocation to save the profile into a profile.proto.
+                       o1 := setDefaults(nil)
+                       o1.Flagset = f
+                       o1.Fetch = testFetcher{}
+                       o1.Sym = testSymbolizer{}
+                       o1.UI = testUI
+                       if err := PProf(o1); err != nil {
+                               t.Fatalf("%s %q:  %v", tc.source, tc.flags, err)
+                       }
+                       // Reset the pprof variables after the proto invocation
+                       pprofVariables = baseVars.makeCopy()
 
-               if flags[0] == "svg" {
-                       b = removeScripts(b)
-                       sbuf = removeScripts(sbuf)
-               }
+                       // Read the profile from the encoded protobuf
+                       outputTempFile, err := ioutil.TempFile("", "profile_output")
+                       if err != nil {
+                               t.Errorf("cannot create tempfile: %v", err)
+                       }
+                       defer os.Remove(outputTempFile.Name())
+                       defer outputTempFile.Close()
+                       f.strings["output"] = outputTempFile.Name()
+                       f.args = []string{protoTempFile.Name()}
+
+                       var solution string
+                       // Apply the flags for the second pprof run, and identify name of
+                       // the file containing expected results
+                       if flags[0] == "topproto" {
+                               solution = solutionFilename(tc.source, &f)
+                               delete(f.bools, "topproto")
+                               f.bools["text"] = true
+                       } else {
+                               delete(f.bools, "proto")
+                               addFlags(&f, flags[:1])
+                               solution = solutionFilename(tc.source, &f)
+                       }
+                       // The add_comment flag is not idempotent so only apply it on the first run.
+                       delete(f.strings, "add_comment")
+
+                       // Second pprof invocation to read the profile from profile.proto
+                       // and generate a report.
+                       o2 := setDefaults(nil)
+                       o2.Flagset = f
+                       o2.Sym = testSymbolizeDemangler{}
+                       o2.Obj = new(mockObjTool)
+                       o2.UI = testUI
+
+                       if err := PProf(o2); err != nil {
+                               t.Errorf("%s: %v", tc.source, err)
+                       }
+                       b, err := ioutil.ReadFile(outputTempFile.Name())
+                       if err != nil {
+                               t.Errorf("Failed to read profile %s: %v", outputTempFile.Name(), err)
+                       }
 
-               if string(b) != string(sbuf) {
-                       t.Errorf("diff %s %s", solution, tc.source)
-                       d, err := proftest.Diff(sbuf, b)
+                       // Read data file with expected solution
+                       solution = "testdata/" + solution
+                       sbuf, err := ioutil.ReadFile(solution)
                        if err != nil {
-                               t.Fatalf("diff %s %v", solution, err)
+                               t.Fatalf("reading solution file %s: %v", solution, err)
                        }
-                       t.Errorf("%s\n%s\n", solution, d)
-                       if *updateFlag {
-                               err := ioutil.WriteFile(solution, b, 0644)
+                       if runtime.GOOS == "windows" {
+                               sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte("testdata\\"), -1)
+                               sbuf = bytes.Replace(sbuf, []byte("/path/to/"), []byte("\\path\\to\\"), -1)
+                       }
+
+                       if flags[0] == "svg" {
+                               b = removeScripts(b)
+                               sbuf = removeScripts(sbuf)
+                       }
+
+                       if string(b) != string(sbuf) {
+                               t.Errorf("diff %s %s", solution, tc.source)
+                               d, err := proftest.Diff(sbuf, b)
                                if err != nil {
-                                       t.Errorf("failed to update the solution file %q: %v", solution, err)
+                                       t.Fatalf("diff %s %v", solution, err)
+                               }
+                               t.Errorf("%s\n%s\n", solution, d)
+                               if *updateFlag {
+                                       err := ioutil.WriteFile(solution, b, 0644)
+                                       if err != nil {
+                                               t.Errorf("failed to update the solution file %q: %v", solution, err)
+                                       }
                                }
                        }
-               }
+               })
        }
 }
 
index a929b0f790162a6b8c04c29a22adc8bcdfea5633..1b34e70beaa639a1359075bd409f37aea817a59e 100644 (file)
@@ -407,6 +407,7 @@ mapping:
                                if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil {
                                        fileNames = append(fileNames, matches...)
                                }
+                               fileNames = append(fileNames, filepath.Join(path, m.File, m.BuildID)) // perf path format
                        }
                        if m.File != "" {
                                // Try both the basename and the full path, to support the same directory
@@ -534,7 +535,8 @@ func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) {
                return nil, err
        }
        deferDeleteTempFile(profile.Name())
-       cmd := exec.Command("perf_to_profile", perfPath, profile.Name())
+       cmd := exec.Command("perf_to_profile", "-i", perfPath, "-o", profile.Name(), "-f")
+       cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
        if err := cmd.Run(); err != nil {
                profile.Close()
                return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err)
index c80a0dbc1d65ebbb19edd1465b3764cbc4167280..f15328bfaee91235efbd65bb7af3e69a7a7ed15b 100644 (file)
@@ -283,8 +283,10 @@ func TestFetchWithBase(t *testing.T) {
                        }
                        f.args = tc.sources
 
-                       o := setDefaults(nil)
-                       o.Flagset = f
+                       o := setDefaults(&plugin.Options{
+                               UI:      &proftest.TestUI{T: t, AllowRx: "Local symbolization failed|Some binary filenames not available"},
+                               Flagset: f,
+                       })
                        src, _, err := parseFlags(o)
 
                        if err != nil {
@@ -397,18 +399,6 @@ func TestHttpsInsecure(t *testing.T) {
        }()
        defer l.Close()
 
-       go func() {
-               deadline := time.Now().Add(5 * time.Second)
-               for time.Now().Before(deadline) {
-                       // Simulate a hotspot function. Spin in the inner loop for 100M iterations
-                       // to ensure we get most of the samples landed here rather than in the
-                       // library calls. We assume Go compiler won't elide the empty loop.
-                       for i := 0; i < 1e8; i++ {
-                       }
-                       runtime.Gosched()
-               }
-       }()
-
        outputTempFile, err := ioutil.TempFile("", "profile_output")
        if err != nil {
                t.Fatalf("Failed to create tempfile: %v", err)
@@ -416,7 +406,7 @@ func TestHttpsInsecure(t *testing.T) {
        defer os.Remove(outputTempFile.Name())
        defer outputTempFile.Close()
 
-       address := "https+insecure://" + l.Addr().String() + "/debug/pprof/profile"
+       address := "https+insecure://" + l.Addr().String() + "/debug/pprof/goroutine"
        s := &source{
                Sources:   []string{address},
                Seconds:   10,
@@ -435,31 +425,14 @@ func TestHttpsInsecure(t *testing.T) {
        if len(p.SampleType) == 0 {
                t.Fatalf("fetchProfiles(%s) got empty profile: len(p.SampleType)==0", address)
        }
-       switch runtime.GOOS {
-       case "plan9":
-               // CPU profiling is not supported on Plan9; see golang.org/issues/22564.
-               return
-       case "darwin":
-               if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" {
-                       // CPU profiling on iOS os not symbolized; see golang.org/issues/22612.
-                       return
-               }
-       }
        if len(p.Function) == 0 {
                t.Fatalf("fetchProfiles(%s) got non-symbolized profile: len(p.Function)==0", address)
        }
-       if err := checkProfileHasFunction(p, "TestHttpsInsecure"); !badSigprofOS[runtime.GOOS] && err != nil {
+       if err := checkProfileHasFunction(p, "TestHttpsInsecure"); err != nil {
                t.Fatalf("fetchProfiles(%s) %v", address, err)
        }
 }
 
-// Some operating systems don't trigger the profiling signal right.
-// See https://github.com/golang/go/issues/13841.
-var badSigprofOS = map[string]bool{
-       "darwin": true,
-       "netbsd": true,
-}
-
 func checkProfileHasFunction(p *profile.Profile, fname string) error {
        for _, f := range p.Function {
                if strings.Contains(f.Name, fname) {
index 10588d6262a09c682d583857c9f9f9f799cf44ff..29a41011bbabe8f60cf60887ebd77eca7f091405 100644 (file)
@@ -92,7 +92,7 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
                return
        }
 
-       ui.render(w, "/flamegraph", "flamegraph", rpt, errList, config.Labels, webArgs{
+       ui.render(w, "flamegraph", rpt, errList, config.Labels, webArgs{
                FlameGraph: template.JS(b),
                Nodes:      nodeArr,
        })
index b893697b622c6b61390dfee92e4aa07603251418..bebfbbec1ee1fe937ccb7756dc0e85e8d36ce4bb 100644 (file)
@@ -149,9 +149,14 @@ func greetings(p *profile.Profile, ui plugin.UI) {
        numLabelUnits := identifyNumLabelUnits(p, ui)
        ropt, err := reportOptions(p, numLabelUnits, pprofVariables)
        if err == nil {
-               ui.Print(strings.Join(report.ProfileLabels(report.New(p, ropt)), "\n"))
+               rpt := report.New(p, ropt)
+               ui.Print(strings.Join(report.ProfileLabels(rpt), "\n"))
+               if rpt.Total() == 0 && len(p.SampleType) > 1 {
+                       ui.Print(`No samples were found with the default sample value type.`)
+                       ui.Print(`Try "sample_index" command to analyze different sample values.`, "\n")
+               }
        }
-       ui.Print("Entering interactive mode (type \"help\" for commands, \"o\" for options)")
+       ui.Print(`Entering interactive mode (type "help" for commands, "o" for options)`)
 }
 
 // shortcuts represents composite commands that expand into a sequence
index cb20e948b4fcb827082bb3719f9638ad2993c471..34167d4bf5793dda4271f110aede3ea276dee2ff 100644 (file)
@@ -128,6 +128,10 @@ func (ui *stdUI) IsTerminal() bool {
        return false
 }
 
+func (ui *stdUI) WantBrowser() bool {
+       return true
+}
+
 func (ui *stdUI) SetAutoComplete(func(string) string) {
 }
 
index 5d2821cd426a6f96f8112cdaca02a0a10dc0d8c9..e9bc8720356456553486a3cf53594a0ecf7a4c89 100644 (file)
@@ -233,7 +233,7 @@ table tr td {
 {{define "header"}}
 <div class="header">
   <div class="title">
-    <h1><a href="/">pprof</a></h1>
+    <h1><a href="./">pprof</a></h1>
   </div>
 
   <div id="view" class="menu-item">
@@ -242,12 +242,12 @@ table tr td {
       <i class="downArrow"></i>
     </div>
     <div class="submenu">
-      <a title="{{.Help.top}}"  href="/top" id="topbtn">Top</a>
-      <a title="{{.Help.graph}}" href="/" id="graphbtn">Graph</a>
-      <a title="{{.Help.flamegraph}}" href="/flamegraph" id="flamegraph">Flame Graph</a>
-      <a title="{{.Help.peek}}" href="/peek" id="peek">Peek</a>
-      <a title="{{.Help.list}}" href="/source" id="list">Source</a>
-      <a title="{{.Help.disasm}}" href="/disasm" id="disasm">Disassemble</a>
+      <a title="{{.Help.top}}"  href="./top" id="topbtn">Top</a>
+      <a title="{{.Help.graph}}" href="./" id="graphbtn">Graph</a>
+      <a title="{{.Help.flamegraph}}" href="./flamegraph" id="flamegraph">Flame Graph</a>
+      <a title="{{.Help.peek}}" href="./peek" id="peek">Peek</a>
+      <a title="{{.Help.list}}" href="./source" id="list">Source</a>
+      <a title="{{.Help.disasm}}" href="./disasm" id="disasm">Disassemble</a>
     </div>
   </div>
 
@@ -257,12 +257,12 @@ table tr td {
       <i class="downArrow"></i>
     </div>
     <div class="submenu">
-      <a title="{{.Help.focus}}" href="{{.BaseURL}}" id="focus">Focus</a>
-      <a title="{{.Help.ignore}}" href="{{.BaseURL}}" id="ignore">Ignore</a>
-      <a title="{{.Help.hide}}" href="{{.BaseURL}}" id="hide">Hide</a>
-      <a title="{{.Help.show}}" href="{{.BaseURL}}" id="show">Show</a>
+      <a title="{{.Help.focus}}" href="?" id="focus">Focus</a>
+      <a title="{{.Help.ignore}}" href="?" id="ignore">Ignore</a>
+      <a title="{{.Help.hide}}" href="?" id="hide">Hide</a>
+      <a title="{{.Help.show}}" href="?" id="show">Show</a>
       <hr>
-      <a title="{{.Help.reset}}" href="{{.BaseURL}}">Reset</a>
+      <a title="{{.Help.reset}}" href="?">Reset</a>
     </div>
   </div>
 
@@ -295,7 +295,7 @@ table tr td {
     {{.HTMLBody}}
   </div>
   {{template "script" .}}
-  <script>viewer({{.BaseURL}}, {{.Nodes}});</script>
+  <script>viewer(new URL(window.location.href), {{.Nodes}});</script>
 </body>
 </html>
 {{end}}
@@ -597,7 +597,7 @@ function viewer(baseUrl, nodes) {
   function handleKey(e) {
     if (e.keyCode != 13) return;
     window.location.href =
-        updateUrl(new URL({{.BaseURL}}, window.location.href), 'f');
+        updateUrl(new URL(window.location.href), 'f');
     e.preventDefault();
   }
 
@@ -963,7 +963,7 @@ function viewer(baseUrl, nodes) {
       bindSort('namehdr', 'Name');
     }
 
-    viewer({{.BaseURL}}, {{.Nodes}});
+    viewer(new URL(window.location.href), {{.Nodes}});
     makeTopTable({{.Total}}, {{.Top}});
   </script>
 </body>
@@ -986,7 +986,7 @@ function viewer(baseUrl, nodes) {
     {{.HTMLBody}}
   </div>
   {{template "script" .}}
-  <script>viewer({{.BaseURL}}, null);</script>
+  <script>viewer(new URL(window.location.href), null);</script>
 </body>
 </html>
 {{end}}
@@ -1007,7 +1007,7 @@ function viewer(baseUrl, nodes) {
     </pre>
   </div>
   {{template "script" .}}
-  <script>viewer({{.BaseURL}}, null);</script>
+  <script>viewer(new URL(window.location.href), null);</script>
 </body>
 </html>
 {{end}}
@@ -1044,7 +1044,7 @@ function viewer(baseUrl, nodes) {
     <div id="flamegraphdetails" class="flamegraph-details"></div>
   </div>
   {{template "script" .}}
-  <script>viewer({{.BaseURL}}, {{.Nodes}});</script>
+  <script>viewer(new URL(window.location.href), {{.Nodes}});</script>
   <script>{{template "d3script" .}}</script>
   <script>{{template "d3tipscript" .}}</script>
   <script>{{template "d3flamegraphscript" .}}</script>
index 20d4e025f49e6fa8102f65c97ac34407b9887f99..89bc5586683a6fed947bf008b56e60c4ca910f19 100644 (file)
@@ -69,7 +69,6 @@ func (ec *errorCatcher) PrintErr(args ...interface{}) {
 
 // webArgs contains arguments passed to templates in webhtml.go.
 type webArgs struct {
-       BaseURL    string
        Title      string
        Errors     []string
        Total      int64
@@ -82,7 +81,7 @@ type webArgs struct {
        FlameGraph template.JS
 }
 
-func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, wantBrowser bool) error {
+func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) error {
        host, port, err := getHostAndPort(hostport)
        if err != nil {
                return err
@@ -117,7 +116,7 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, w
                },
        }
 
-       if wantBrowser {
+       if o.UI.WantBrowser() {
                go openBrowser("http://"+args.Hostport, o)
        }
        return server(args)
@@ -172,7 +171,15 @@ func defaultWebServer(args *plugin.HTTPServerArgs) error {
                }
                h.ServeHTTP(w, req)
        })
-       s := &http.Server{Handler: handler}
+
+       // We serve the ui at /ui/ and redirect there from the root. This is done
+       // to surface any problems with serving the ui at a non-root early. See:
+       //
+       // https://github.com/google/pprof/pull/348
+       mux := http.NewServeMux()
+       mux.Handle("/ui/", http.StripPrefix("/ui", handler))
+       mux.Handle("/", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect))
+       s := &http.Server{Handler: mux}
        return s.Serve(ln)
 }
 
@@ -248,11 +255,10 @@ func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request,
 }
 
 // render generates html using the named template based on the contents of data.
-func (ui *webInterface) render(w http.ResponseWriter, baseURL, tmpl string,
+func (ui *webInterface) render(w http.ResponseWriter, tmpl string,
        rpt *report.Report, errList, legend []string, data webArgs) {
        file := getFromLegend(legend, "File: ", "unknown")
        profile := getFromLegend(legend, "Type: ", "unknown")
-       data.BaseURL = baseURL
        data.Title = file + " " + profile
        data.Errors = errList
        data.Total = rpt.Total()
@@ -297,7 +303,7 @@ func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
                nodes = append(nodes, n.Info.Name)
        }
 
-       ui.render(w, "/", "graph", rpt, errList, legend, webArgs{
+       ui.render(w, "graph", rpt, errList, legend, webArgs{
                HTMLBody: template.HTML(string(svg)),
                Nodes:    nodes,
        })
@@ -332,7 +338,7 @@ func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) {
                nodes = append(nodes, item.Name)
        }
 
-       ui.render(w, "/top", "top", rpt, errList, legend, webArgs{
+       ui.render(w, "top", rpt, errList, legend, webArgs{
                Top:   top,
                Nodes: nodes,
        })
@@ -354,7 +360,7 @@ func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
        }
 
        legend := report.ProfileLabels(rpt)
-       ui.render(w, "/disasm", "plaintext", rpt, errList, legend, webArgs{
+       ui.render(w, "plaintext", rpt, errList, legend, webArgs{
                TextBody: out.String(),
        })
 
@@ -378,7 +384,7 @@ func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) {
        }
 
        legend := report.ProfileLabels(rpt)
-       ui.render(w, "/source", "sourcelisting", rpt, errList, legend, webArgs{
+       ui.render(w, "sourcelisting", rpt, errList, legend, webArgs{
                HTMLBody: template.HTML(body.String()),
        })
 }
@@ -399,7 +405,7 @@ func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
        }
 
        legend := report.ProfileLabels(rpt)
-       ui.render(w, "/peek", "plaintext", rpt, errList, legend, webArgs{
+       ui.render(w, "plaintext", rpt, errList, legend, webArgs{
                TextBody: out.String(),
        })
 }
index 424752fd1f9c692313b1243a607fbce05f3850d8..7e061699ceb49f1ba30edf50bbf0bda8683559c3 100644 (file)
@@ -28,6 +28,7 @@ import (
        "testing"
 
        "github.com/google/pprof/internal/plugin"
+       "github.com/google/pprof/internal/proftest"
        "github.com/google/pprof/profile"
 )
 
@@ -55,9 +56,9 @@ func TestWebInterface(t *testing.T) {
        // Start server and wait for it to be initialized
        go serveWebInterface("unused:1234", prof, &plugin.Options{
                Obj:        fakeObjTool{},
-               UI:         &stdUI{},
+               UI:         &proftest.TestUI{},
                HTTPServer: creator,
-       }, false)
+       })
        <-serverCreated
        defer server.Close()
 
index 7e42c88d145196adc72649100e2cc3b842e853d5..297bb24b1ce0efea09198e8220e134b8efb34e33 100644 (file)
@@ -218,7 +218,7 @@ func GetBase(fh *elf.FileHeader, loadSegment *elf.ProgHeader, stextOffset *uint6
                        // So the base should be:
                        if stextOffset != nil && (start%pageSize) == (*stextOffset%pageSize) {
                                // perf uses the address of _stext as start. Some tools may
-                               // adjust for this before calling GetBase, in which case the the page
+                               // adjust for this before calling GetBase, in which case the page
                                // alignment should be different from that of stextOffset.
                                return start - *stextOffset, nil
                        }
index e5878aed7046a0e52f9526d57c006f59260dab0c..00036560261461e96b4bb7e87da2154036019dbe 100644 (file)
@@ -192,6 +192,9 @@ type UI interface {
        // interactive terminal (as opposed to being redirected to a file).
        IsTerminal() bool
 
+       // WantBrowser indicates whether a browser should be opened with the -http option.
+       WantBrowser() bool
+
        // SetAutoComplete instructs the UI to call complete(cmd) to obtain
        // the auto-completion of cmd, if the UI supports auto-completion at all.
        SetAutoComplete(complete func(string) string)
index 03fac7e33077b6cf1e8b9edf86330f54275e43a8..5d3a19b4a1a7ccaf3e6b30586f99de9d67c86221 100644 (file)
@@ -122,7 +122,7 @@ func (ui *TestUI) PrintErr(args ...interface{}) {
        // implementation does. Without this Error() calls fmt.Sprintln() which
        // _always_ adds spaces between arguments, unlike fmt.Sprint() which only
        // adds them between arguments if neither is string.
-       ui.T.Error(fmt.Sprint(args...))
+       ui.T.Error("unexpected error: " + fmt.Sprint(args...))
 }
 
 // IsTerminal indicates if the UI is an interactive terminal.
@@ -130,6 +130,11 @@ func (ui *TestUI) IsTerminal() bool {
        return false
 }
 
+// WantBrowser indicates whether a browser should be opened with the -http option.
+func (ui *TestUI) WantBrowser() bool {
+       return false
+}
+
 // SetAutoComplete is not supported by the test UI.
 func (ui *TestUI) SetAutoComplete(_ func(string) string) {
 }
index 9a98f7460bf6185b203059aec5ebe2b9012a0b94..529583997360aea214832098eac7b3fb5e5bc6fb 100644 (file)
@@ -576,7 +576,7 @@ func openSourceFile(path, searchPath string) (*os.File, error) {
        }
 
        // Scan each component of the path
-       for _, dir := range strings.Split(searchPath, ":") {
+       for _, dir := range filepath.SplitList(searchPath) {
                // Search up for every parent of each possible path.
                for {
                        filename := filepath.Join(dir, path)
index d45d4c5815c9e795f8e23fa5c3eeb87dbd694e8c..682bfe0a1e338d6e48f9343281efdda9521b1fba 100644 (file)
@@ -2,6 +2,7 @@ package report
 
 import (
        "bytes"
+       "io/ioutil"
        "os"
        "path/filepath"
        "regexp"
@@ -38,6 +39,85 @@ func TestWebList(t *testing.T) {
        }
 }
 
+func TestOpenSourceFile(t *testing.T) {
+       tempdir, err := ioutil.TempDir("", "")
+       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
+               fs         []string
+               path       string
+               wantPath   string // If empty, error is wanted.
+       }{
+               {
+                       desc:     "exact absolute path is found",
+                       fs:       []string{"foo/bar.txt"},
+                       path:     "$dir/foo/bar.txt",
+                       wantPath: "$dir/foo/bar.txt",
+               },
+               {
+                       desc:       "exact relative path is found",
+                       searchPath: "$dir",
+                       fs:         []string{"foo/bar.txt"},
+                       path:       "foo/bar.txt",
+                       wantPath:   "$dir/foo/bar.txt",
+               },
+               {
+                       desc:       "multiple search path",
+                       searchPath: "some/path" + lsep + "$dir",
+                       fs:         []string{"foo/bar.txt"},
+                       path:       "foo/bar.txt",
+                       wantPath:   "$dir/foo/bar.txt",
+               },
+               {
+                       desc:       "relative path is found in parent dir",
+                       searchPath: "$dir/foo/bar",
+                       fs:         []string{"bar.txt", "foo/bar/baz.txt"},
+                       path:       "bar.txt",
+                       wantPath:   "$dir/bar.txt",
+               },
+               {
+                       desc: "error when not found",
+                       path: "foo.txt",
+               },
+       } {
+               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 := ioutil.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); err != nil && tc.wantPath != "" {
+                               t.Errorf("openSourceFile(%q, %q) = err %v, want path %q", tc.path, tc.searchPath, err, tc.wantPath)
+                       } else if err == nil {
+                               defer file.Close()
+                               gotPath := file.Name()
+                               if tc.wantPath == "" {
+                                       t.Errorf("openSourceFile(%q, %q) = %q, want error", tc.path, tc.searchPath, gotPath)
+                               } else if gotPath != tc.wantPath {
+                                       t.Errorf("openSourceFile(%q, %q) = %q, want path %q", tc.path, tc.searchPath, gotPath, tc.wantPath)
+                               }
+                       }
+               })
+       }
+}
+
 func TestIndentation(t *testing.T) {
        for _, c := range []struct {
                str        string
index e7639a388e3314c648b88e556c10f8495029e31a..6ca08adedb943fe51cbed0148af574911aac0065 100644 (file)
@@ -12,7 +12,7 @@ const JSSource = `
  * ======================
  *
  * Given an unique existing element with id "viewport" (or when missing, the
- * first g-element), including the the library into any SVG adds the following
+ * first g-element), including the library into any SVG adds the following
  * capabilities:
  *
  *  - Mouse panning