1 // Copyright 2017 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 15 package driver 16 17 import ( 18 "fmt" 19 "io/ioutil" 20 "net" 21 "net/http" 22 "net/http/httptest" 23 "net/url" 24 "os/exec" 25 "regexp" 26 "sync" 27 "testing" 28 "time" 29 30 "runtime" 31 32 "github.com/google/pprof/internal/plugin" 33 "github.com/google/pprof/profile" 34 ) 35 36 func TestWebInterface(t *testing.T) { 37 // This test starts a web browser in a background goroutine 38 // after a 500ms delay. Sometimes the test exits before it 39 // can run the browser, but sometimes the browser does open. 40 // That's obviously unacceptable. 41 defer time.Sleep(2 * time.Second) // to see the browser open 42 t.Skip("golang.org/issue/22651") 43 44 if runtime.GOOS == "nacl" { 45 t.Skip("test assumes tcp available") 46 } 47 48 prof := makeFakeProfile() 49 50 // Custom http server creator 51 var server *httptest.Server 52 serverCreated := make(chan bool) 53 creator := func(a *plugin.HTTPServerArgs) error { 54 server = httptest.NewServer(http.HandlerFunc( 55 func(w http.ResponseWriter, r *http.Request) { 56 if h := a.Handlers[r.URL.Path]; h != nil { 57 h.ServeHTTP(w, r) 58 } 59 })) 60 serverCreated <- true 61 return nil 62 } 63 64 // Start server and wait for it to be initialized 65 go serveWebInterface("unused:1234", prof, &plugin.Options{ 66 Obj: fakeObjTool{}, 67 UI: &stdUI{}, 68 HTTPServer: creator, 69 }) 70 <-serverCreated 71 defer server.Close() 72 73 haveDot := false 74 if _, err := exec.LookPath("dot"); err == nil { 75 haveDot = true 76 } 77 78 type testCase struct { 79 path string 80 want []string 81 needDot bool 82 } 83 testcases := []testCase{ 84 {"/", []string{"F1", "F2", "F3", "testbin", "cpu"}, true}, 85 {"/top", []string{`"Name":"F2","InlineLabel":"","Flat":200,"Cum":300,"FlatFormat":"200ms","CumFormat":"300ms"}`}, false}, 86 {"/source?f=" + url.QueryEscape("F[12]"), 87 []string{"F1", "F2", "300ms +line1"}, false}, 88 {"/peek?f=" + url.QueryEscape("F[12]"), 89 []string{"300ms.*F1", "200ms.*300ms.*F2"}, false}, 90 {"/disasm?f=" + url.QueryEscape("F[12]"), 91 []string{"f1:asm", "f2:asm"}, false}, 92 } 93 for _, c := range testcases { 94 if c.needDot && !haveDot { 95 t.Log("skipping", c.path, "since dot (graphviz) does not seem to be installed") 96 continue 97 } 98 99 res, err := http.Get(server.URL + c.path) 100 if err != nil { 101 t.Error("could not fetch", c.path, err) 102 continue 103 } 104 data, err := ioutil.ReadAll(res.Body) 105 if err != nil { 106 t.Error("could not read response", c.path, err) 107 continue 108 } 109 result := string(data) 110 for _, w := range c.want { 111 if match, _ := regexp.MatchString(w, result); !match { 112 t.Errorf("response for %s does not match "+ 113 "expected pattern '%s'; "+ 114 "actual result:\n%s", c.path, w, result) 115 } 116 } 117 } 118 119 // Also fetch all the test case URLs in parallel to test thread 120 // safety when run under the race detector. 121 var wg sync.WaitGroup 122 for _, c := range testcases { 123 if c.needDot && !haveDot { 124 continue 125 } 126 path := server.URL + c.path 127 for count := 0; count < 2; count++ { 128 wg.Add(1) 129 go func() { 130 http.Get(path) 131 wg.Done() 132 }() 133 } 134 } 135 wg.Wait() 136 137 time.Sleep(5 * time.Second) 138 } 139 140 // Implement fake object file support. 141 142 const addrBase = 0x1000 143 const fakeSource = "testdata/file1000.src" 144 145 type fakeObj struct{} 146 147 func (f fakeObj) Close() error { return nil } 148 func (f fakeObj) Name() string { return "testbin" } 149 func (f fakeObj) Base() uint64 { return 0 } 150 func (f fakeObj) BuildID() string { return "" } 151 func (f fakeObj) SourceLine(addr uint64) ([]plugin.Frame, error) { 152 return nil, fmt.Errorf("SourceLine unimplemented") 153 } 154 func (f fakeObj) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) { 155 return []*plugin.Sym{ 156 {[]string{"F1"}, fakeSource, addrBase, addrBase + 10}, 157 {[]string{"F2"}, fakeSource, addrBase + 10, addrBase + 20}, 158 {[]string{"F3"}, fakeSource, addrBase + 20, addrBase + 30}, 159 }, nil 160 } 161 162 type fakeObjTool struct{} 163 164 func (obj fakeObjTool) Open(file string, start, limit, offset uint64) (plugin.ObjFile, error) { 165 return fakeObj{}, nil 166 } 167 168 func (obj fakeObjTool) Disasm(file string, start, end uint64) ([]plugin.Inst, error) { 169 return []plugin.Inst{ 170 {Addr: addrBase + 0, Text: "f1:asm", Function: "F1"}, 171 {Addr: addrBase + 10, Text: "f2:asm", Function: "F2"}, 172 {Addr: addrBase + 20, Text: "d3:asm", Function: "F3"}, 173 }, nil 174 } 175 176 func makeFakeProfile() *profile.Profile { 177 // Three functions: F1, F2, F3 with three lines, 11, 22, 33. 178 funcs := []*profile.Function{ 179 {ID: 1, Name: "F1", Filename: fakeSource, StartLine: 3}, 180 {ID: 2, Name: "F2", Filename: fakeSource, StartLine: 5}, 181 {ID: 3, Name: "F3", Filename: fakeSource, StartLine: 7}, 182 } 183 lines := []profile.Line{ 184 {Function: funcs[0], Line: 11}, 185 {Function: funcs[1], Line: 22}, 186 {Function: funcs[2], Line: 33}, 187 } 188 mapping := []*profile.Mapping{ 189 { 190 ID: 1, 191 Start: addrBase, 192 Limit: addrBase + 10, 193 Offset: 0, 194 File: "testbin", 195 HasFunctions: true, 196 HasFilenames: true, 197 HasLineNumbers: true, 198 }, 199 } 200 201 // Three interesting addresses: base+{10,20,30} 202 locs := []*profile.Location{ 203 {ID: 1, Address: addrBase + 10, Line: lines[0:1], Mapping: mapping[0]}, 204 {ID: 2, Address: addrBase + 20, Line: lines[1:2], Mapping: mapping[0]}, 205 {ID: 3, Address: addrBase + 30, Line: lines[2:3], Mapping: mapping[0]}, 206 } 207 208 // Two stack traces. 209 return &profile.Profile{ 210 PeriodType: &profile.ValueType{Type: "cpu", Unit: "milliseconds"}, 211 Period: 1, 212 DurationNanos: 10e9, 213 SampleType: []*profile.ValueType{ 214 {Type: "cpu", Unit: "milliseconds"}, 215 }, 216 Sample: []*profile.Sample{ 217 { 218 Location: []*profile.Location{locs[2], locs[1], locs[0]}, 219 Value: []int64{100}, 220 }, 221 { 222 Location: []*profile.Location{locs[1], locs[0]}, 223 Value: []int64{200}, 224 }, 225 }, 226 Location: locs, 227 Function: funcs, 228 Mapping: mapping, 229 } 230 } 231 232 func TestIsLocalHost(t *testing.T) { 233 for _, s := range []string{"localhost:10000", "[::1]:10000", "127.0.0.1:10000"} { 234 host, _, err := net.SplitHostPort(s) 235 if err != nil { 236 t.Error("unexpected error when splitting", s) 237 continue 238 } 239 if !isLocalhost(host) { 240 t.Errorf("host %s from %s not considered local", host, s) 241 } 242 } 243 } 244