// Copyright 2014 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 driver
import (
"bytes"
"flag"
"fmt"
"net"
_ "net/http/pprof"
"os"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"testing"
"time"
"github.com/google/pprof/internal/plugin"
"github.com/google/pprof/internal/proftest"
"github.com/google/pprof/internal/symbolz"
"github.com/google/pprof/profile"
)
var updateFlag = flag.Bool("update", false, "Update the golden files")
func TestParse(t *testing.T) {
// Override weblist command to collect output in buffer
pprofCommands["weblist"].postProcess = nil
// Our mockObjTool.Open will always return success, causing
// driver.locateBinaries to "find" the binaries below in a non-existent
// directory. As a workaround, point the search path to the fake
// directory containing out fake binaries.
savePath := os.Getenv("PPROF_BINARY_PATH")
os.Setenv("PPROF_BINARY_PATH", "/path/to")
defer os.Setenv("PPROF_BINARY_PATH", savePath)
testcase := []struct {
flags, source string
}{
{"text,functions,flat", "cpu"},
{"text,functions,noinlines,flat", "cpu"},
{"text,filefunctions,noinlines,flat", "cpu"},
{"text,addresses,noinlines,flat", "cpu"},
{"tree,addresses,flat,nodecount=4", "cpusmall"},
{"text,functions,flat,nodecount=5,call_tree", "unknown"},
{"text,alloc_objects,flat", "heap_alloc"},
{"text,files,flat", "heap"},
{"text,files,flat,focus=[12]00,taghide=[X3]00", "heap"},
{"text,inuse_objects,flat", "heap"},
{"text,lines,cum,hide=line[X3]0", "cpu"},
{"text,lines,cum,show=[12]00", "cpu"},
{"text,lines,cum,hide=line[X3]0,focus=[12]00", "cpu"},
{"topproto,lines,cum,hide=mangled[X3]0", "cpu"},
{"topproto,lines", "cpu"},
{"tree,lines,cum,focus=[24]00", "heap"},
{"tree,relative_percentages,cum,focus=[24]00", "heap"},
{"tree,lines,cum,show_from=line2", "cpu"},
{"callgrind", "cpu"},
{"callgrind,call_tree", "cpu"},
{"callgrind", "heap"},
{"dot,functions,flat", "cpu"},
{"dot,functions,flat,call_tree", "cpu"},
{"dot,lines,flat,focus=[12]00", "heap"},
{"dot,unit=minimum", "heap_sizetags"},
{"dot,addresses,flat,ignore=[X3]002,focus=[X1]000", "contention"},
{"dot,files,cum", "contention"},
{"comments,add_comment=some-comment", "cpu"},
{"comments", "heap"},
{"tags", "cpu"},
{"tags,tagignore=tag[13],tagfocus=key[12]", "cpu"},
{"tags", "heap"},
{"tags,unit=bytes", "heap"},
{"traces", "cpu"},
{"traces,addresses", "cpu"},
{"traces", "heap_tags"},
{"dot,alloc_space,flat,focus=[234]00", "heap_alloc"},
{"dot,alloc_space,flat,tagshow=[2]00", "heap_alloc"},
{"dot,alloc_space,flat,hide=line.*1?23?", "heap_alloc"},
{"dot,inuse_space,flat,tagfocus=1mb:2gb", "heap"},
{"dot,inuse_space,flat,tagfocus=30kb:,tagignore=1mb:2mb", "heap"},
{"disasm=line[13],addresses,flat", "cpu"},
{"peek=line.*01", "cpu"},
{"weblist=line(1000|3000)$,addresses,flat", "cpu"},
{"tags,tagfocus=400kb:", "heap_request"},
{"tags,tagfocus=+400kb:", "heap_request"},
{"dot", "long_name_funcs"},
{"text", "long_name_funcs"},
}
baseConfig := currentConfig()
defer setCurrentConfig(baseConfig)
for _, tc := range testcase {
t.Run(tc.flags+":"+tc.source, func(t *testing.T) {
// Reset config before processing
setCurrentConfig(baseConfig)
testUI := &proftest.TestUI{T: t, AllowRx: "Generating report in|Ignoring local file|expression matched no samples|Interpreted .* as range, not regexp"}
f := baseFlags()
f.args = []string{tc.source}
flags := strings.Split(tc.flags, ",")
// Encode profile into a protobuf and decode it again.
protoTempFile, err := os.CreateTemp("", "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
f.bools["addresses"] = true
}
// First pprof invocation to save the profile into a profile.proto.
// Pass in flag set hen setting defaults, because otherwise default
// transport will try to add flags to the default flag set.
o1 := setDefaults(&plugin.Options{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 config after the proto invocation
setCurrentConfig(baseConfig)
// Read the profile from the encoded protobuf
outputTempFile, err := os.CreateTemp("", "profile_output")
if err != nil {
t.Errorf("cannot create tempfile: %v", err)
}
defer os.Remove(outputTempFile.Name())
defer outputTempFile.Close()
f = baseFlags()
f.strings["output"] = outputTempFile.Name()
f.args = []string{protoTempFile.Name()}
delete(f.bools, "proto")
addFlags(&f, flags)
solution := solutionFilename(tc.source, &f)
// Apply the flags for the second pprof run, and identify name of
// the file containing expected results
if flags[0] == "topproto" {
addFlags(&f, flags)
solution = solutionFilename(tc.source, &f)
delete(f.bools, "topproto")
f.bools["text"] = true
}
// Second pprof invocation to read the profile from profile.proto
// and generate a report.
// Pass in flag set hen setting defaults, because otherwise default
// transport will try to add flags to the default flag set.
o2 := setDefaults(&plugin.Options{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 := os.ReadFile(outputTempFile.Name())
if err != nil {
t.Errorf("Failed to read profile %s: %v", outputTempFile.Name(), err)
}
// Read data file with expected solution
solution = "testdata/" + solution
sbuf, err := os.ReadFile(solution)
if err != nil {
t.Fatalf("reading solution file %s: %v", solution, err)
}
if runtime.GOOS == "windows" {
if flags[0] == "dot" {
// The .dot test has the paths inside strings, so \ must be escaped.
sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte(`testdata\\`), -1)
sbuf = bytes.Replace(sbuf, []byte("/path/to/"), []byte(`\\path\\to\\`), -1)
} else {
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.Fatalf("diff %s %v", solution, err)
}
t.Errorf("%s\n%s\n", solution, d)
if *updateFlag {
err := os.WriteFile(solution, b, 0644)
if err != nil {
t.Errorf("failed to update the solution file %q: %v", solution, err)
}
}
}
})
}
}
// removeScripts removes pairs from its input
func removeScripts(in []byte) []byte {
beginMarker := []byte("