1// Copyright 2014 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 15package graph 16 17import ( 18 "bytes" 19 "flag" 20 "fmt" 21 "io/ioutil" 22 "path/filepath" 23 "reflect" 24 "strconv" 25 "strings" 26 "testing" 27 28 "github.com/google/pprof/internal/proftest" 29) 30 31var updateFlag = flag.Bool("update", false, "Update the golden files") 32 33func TestComposeWithStandardGraph(t *testing.T) { 34 g := baseGraph() 35 a, c := baseAttrsAndConfig() 36 37 var buf bytes.Buffer 38 ComposeDot(&buf, g, a, c) 39 40 compareGraphs(t, buf.Bytes(), "compose1.dot") 41} 42 43func TestComposeWithNodeAttributesAndZeroFlat(t *testing.T) { 44 g := baseGraph() 45 a, c := baseAttrsAndConfig() 46 47 // Set NodeAttributes for Node 1. 48 a.Nodes[g.Nodes[0]] = &DotNodeAttributes{ 49 Shape: "folder", 50 Bold: true, 51 Peripheries: 2, 52 URL: "www.google.com", 53 Formatter: func(ni *NodeInfo) string { 54 return strings.ToUpper(ni.Name) 55 }, 56 } 57 58 // Set Flat value to zero on Node 2. 59 g.Nodes[1].Flat = 0 60 61 var buf bytes.Buffer 62 ComposeDot(&buf, g, a, c) 63 64 compareGraphs(t, buf.Bytes(), "compose2.dot") 65} 66 67func TestComposeWithTagsAndResidualEdge(t *testing.T) { 68 g := baseGraph() 69 a, c := baseAttrsAndConfig() 70 71 // Add tags to Node 1. 72 g.Nodes[0].LabelTags["a"] = &Tag{ 73 Name: "tag1", 74 Cum: 10, 75 Flat: 10, 76 } 77 g.Nodes[0].NumericTags[""] = TagMap{ 78 "b": &Tag{ 79 Name: "tag2", 80 Cum: 20, 81 Flat: 20, 82 Unit: "ms", 83 }, 84 } 85 86 // Set edge to be Residual. 87 g.Nodes[0].Out[g.Nodes[1]].Residual = true 88 89 var buf bytes.Buffer 90 ComposeDot(&buf, g, a, c) 91 92 compareGraphs(t, buf.Bytes(), "compose3.dot") 93} 94 95func TestComposeWithNestedTags(t *testing.T) { 96 g := baseGraph() 97 a, c := baseAttrsAndConfig() 98 99 // Add tags to Node 1. 100 g.Nodes[0].LabelTags["tag1"] = &Tag{ 101 Name: "tag1", 102 Cum: 10, 103 Flat: 10, 104 } 105 g.Nodes[0].NumericTags["tag1"] = TagMap{ 106 "tag2": &Tag{ 107 Name: "tag2", 108 Cum: 20, 109 Flat: 20, 110 Unit: "ms", 111 }, 112 } 113 114 var buf bytes.Buffer 115 ComposeDot(&buf, g, a, c) 116 117 compareGraphs(t, buf.Bytes(), "compose5.dot") 118} 119 120func TestComposeWithEmptyGraph(t *testing.T) { 121 g := &Graph{} 122 a, c := baseAttrsAndConfig() 123 124 var buf bytes.Buffer 125 ComposeDot(&buf, g, a, c) 126 127 compareGraphs(t, buf.Bytes(), "compose4.dot") 128} 129 130func TestComposeWithStandardGraphAndURL(t *testing.T) { 131 g := baseGraph() 132 a, c := baseAttrsAndConfig() 133 c.LegendURL = "http://example.com" 134 135 var buf bytes.Buffer 136 ComposeDot(&buf, g, a, c) 137 138 compareGraphs(t, buf.Bytes(), "compose6.dot") 139} 140 141func TestComposeWithNamesThatNeedEscaping(t *testing.T) { 142 g := baseGraph() 143 a, c := baseAttrsAndConfig() 144 g.Nodes[0].Info = NodeInfo{Name: `var"src"`} 145 g.Nodes[1].Info = NodeInfo{Name: `var"#dest#"`} 146 147 var buf bytes.Buffer 148 ComposeDot(&buf, g, a, c) 149 150 compareGraphs(t, buf.Bytes(), "compose7.dot") 151} 152 153func baseGraph() *Graph { 154 src := &Node{ 155 Info: NodeInfo{Name: "src"}, 156 Flat: 10, 157 Cum: 25, 158 In: make(EdgeMap), 159 Out: make(EdgeMap), 160 LabelTags: make(TagMap), 161 NumericTags: make(map[string]TagMap), 162 } 163 dest := &Node{ 164 Info: NodeInfo{Name: "dest"}, 165 Flat: 15, 166 Cum: 25, 167 In: make(EdgeMap), 168 Out: make(EdgeMap), 169 LabelTags: make(TagMap), 170 NumericTags: make(map[string]TagMap), 171 } 172 edge := &Edge{ 173 Src: src, 174 Dest: dest, 175 Weight: 10, 176 } 177 src.Out[dest] = edge 178 src.In[src] = edge 179 return &Graph{ 180 Nodes: Nodes{ 181 src, 182 dest, 183 }, 184 } 185} 186 187func baseAttrsAndConfig() (*DotAttributes, *DotConfig) { 188 a := &DotAttributes{ 189 Nodes: make(map[*Node]*DotNodeAttributes), 190 } 191 c := &DotConfig{ 192 Title: "testtitle", 193 Labels: []string{"label1", "label2", `label3: "foo"`}, 194 Total: 100, 195 FormatValue: func(v int64) string { 196 return strconv.FormatInt(v, 10) 197 }, 198 } 199 return a, c 200} 201 202func compareGraphs(t *testing.T, got []byte, wantFile string) { 203 wantFile = filepath.Join("testdata", wantFile) 204 want, err := ioutil.ReadFile(wantFile) 205 if err != nil { 206 t.Fatalf("error reading test file %s: %v", wantFile, err) 207 } 208 209 if string(got) != string(want) { 210 d, err := proftest.Diff(got, want) 211 if err != nil { 212 t.Fatalf("error finding diff: %v", err) 213 } 214 t.Errorf("Compose incorrectly wrote %s", string(d)) 215 if *updateFlag { 216 err := ioutil.WriteFile(wantFile, got, 0644) 217 if err != nil { 218 t.Errorf("failed to update the golden file %q: %v", wantFile, err) 219 } 220 } 221 } 222} 223 224func TestNodeletCountCapping(t *testing.T) { 225 labelTags := make(TagMap) 226 for i := 0; i < 10; i++ { 227 name := fmt.Sprintf("tag-%d", i) 228 labelTags[name] = &Tag{ 229 Name: name, 230 Flat: 10, 231 Cum: 10, 232 } 233 } 234 numTags := make(TagMap) 235 for i := 0; i < 10; i++ { 236 name := fmt.Sprintf("num-tag-%d", i) 237 numTags[name] = &Tag{ 238 Name: name, 239 Unit: "mb", 240 Value: 16, 241 Flat: 10, 242 Cum: 10, 243 } 244 } 245 node1 := &Node{ 246 Info: NodeInfo{Name: "node1-with-tags"}, 247 Flat: 10, 248 Cum: 10, 249 NumericTags: map[string]TagMap{"": numTags}, 250 LabelTags: labelTags, 251 } 252 node2 := &Node{ 253 Info: NodeInfo{Name: "node2"}, 254 Flat: 15, 255 Cum: 15, 256 } 257 node3 := &Node{ 258 Info: NodeInfo{Name: "node3"}, 259 Flat: 15, 260 Cum: 15, 261 } 262 g := &Graph{ 263 Nodes: Nodes{ 264 node1, 265 node2, 266 node3, 267 }, 268 } 269 for n := 1; n <= 3; n++ { 270 input := maxNodelets + n 271 if got, want := len(g.SelectTopNodes(input, true)), n; got != want { 272 t.Errorf("SelectTopNodes(%d): got %d nodes, want %d", input, got, want) 273 } 274 } 275} 276 277func TestMultilinePrintableName(t *testing.T) { 278 ni := &NodeInfo{ 279 Name: "test1.test2::test3", 280 File: "src/file.cc", 281 Address: 123, 282 Lineno: 999, 283 } 284 285 want := fmt.Sprintf(`%016x\ntest1\ntest2\ntest3\nfile.cc:999\n`, 123) 286 if got := multilinePrintableName(ni); got != want { 287 t.Errorf("multilinePrintableName(%#v) == %q, want %q", ni, got, want) 288 } 289} 290 291func TestTagCollapse(t *testing.T) { 292 293 makeTag := func(name, unit string, value, flat, cum int64) *Tag { 294 return &Tag{name, unit, value, flat, 0, cum, 0} 295 } 296 297 tagSource := []*Tag{ 298 makeTag("12mb", "mb", 12, 100, 100), 299 makeTag("1kb", "kb", 1, 1, 1), 300 makeTag("1mb", "mb", 1, 1000, 1000), 301 makeTag("2048mb", "mb", 2048, 1000, 1000), 302 makeTag("1b", "b", 1, 100, 100), 303 makeTag("2b", "b", 2, 100, 100), 304 makeTag("7b", "b", 7, 100, 100), 305 } 306 307 tagWant := [][]*Tag{ 308 { 309 makeTag("1B..2GB", "", 0, 2401, 2401), 310 }, 311 { 312 makeTag("2GB", "", 0, 1000, 1000), 313 makeTag("1B..12MB", "", 0, 1401, 1401), 314 }, 315 { 316 makeTag("2GB", "", 0, 1000, 1000), 317 makeTag("12MB", "", 0, 100, 100), 318 makeTag("1B..1MB", "", 0, 1301, 1301), 319 }, 320 { 321 makeTag("2GB", "", 0, 1000, 1000), 322 makeTag("1MB", "", 0, 1000, 1000), 323 makeTag("2B..1kB", "", 0, 201, 201), 324 makeTag("1B", "", 0, 100, 100), 325 makeTag("12MB", "", 0, 100, 100), 326 }, 327 } 328 329 for _, tc := range tagWant { 330 var got, want []*Tag 331 b := builder{nil, &DotAttributes{}, &DotConfig{}} 332 got = b.collapsedTags(tagSource, len(tc), true) 333 want = SortTags(tc, true) 334 335 if !reflect.DeepEqual(got, want) { 336 t.Errorf("collapse to %d, got:\n%v\nwant:\n%v", len(tc), tagString(got), tagString(want)) 337 } 338 } 339} 340 341func TestEscapeForDot(t *testing.T) { 342 for _, tc := range []struct { 343 desc string 344 input []string 345 want []string 346 }{ 347 { 348 desc: "with multiple doubles quotes", 349 input: []string{`label: "foo" and "bar"`}, 350 want: []string{`label: \"foo\" and \"bar\"`}, 351 }, 352 { 353 desc: "with graphviz center line character", 354 input: []string{"label: foo \n bar"}, 355 want: []string{`label: foo \l bar`}, 356 }, 357 { 358 desc: "with two backslashes", 359 input: []string{`label: \\`}, 360 want: []string{`label: \\\\`}, 361 }, 362 { 363 desc: "with two double quotes together", 364 input: []string{`label: ""`}, 365 want: []string{`label: \"\"`}, 366 }, 367 { 368 desc: "with multiple labels", 369 input: []string{`label1: "foo"`, `label2: "bar"`}, 370 want: []string{`label1: \"foo\"`, `label2: \"bar\"`}, 371 }, 372 } { 373 t.Run(tc.desc, func(t *testing.T) { 374 if got := escapeAllForDot(tc.input); !reflect.DeepEqual(got, tc.want) { 375 t.Errorf("escapeAllForDot(%s) = %s, want %s", tc.input, got, tc.want) 376 } 377 }) 378 } 379} 380 381func tagString(t []*Tag) string { 382 var ret []string 383 for _, s := range t { 384 ret = append(ret, fmt.Sprintln(s)) 385 } 386 return strings.Join(ret, ":") 387} 388