1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package graph
16
17 import (
18 "bytes"
19 "flag"
20 "fmt"
21 "os"
22 "path/filepath"
23 "reflect"
24 "strconv"
25 "strings"
26 "testing"
27
28 "github.com/google/pprof/internal/proftest"
29 )
30
31 var updateFlag = flag.Bool("update", false, "Update the golden files")
32
33 func 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
43 func TestComposeWithNodeAttributesAndZeroFlat(t *testing.T) {
44 g := baseGraph()
45 a, c := baseAttrsAndConfig()
46
47
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
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
67 func TestComposeWithTagsAndResidualEdge(t *testing.T) {
68 g := baseGraph()
69 a, c := baseAttrsAndConfig()
70
71
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
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
95 func TestComposeWithNestedTags(t *testing.T) {
96 g := baseGraph()
97 a, c := baseAttrsAndConfig()
98
99
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
120 func 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
130 func 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
141 func 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
153 func TestComposeWithCommentsWithNewlines(t *testing.T) {
154 g := baseGraph()
155 a, c := baseAttrsAndConfig()
156
157
158 c.Labels = []string{"comment line 1\ncomment line 2 \"unterminated double quote", `second comment "double quote"`}
159
160 var buf bytes.Buffer
161 ComposeDot(&buf, g, a, c)
162
163 compareGraphs(t, buf.Bytes(), "compose9.dot")
164 }
165
166 func baseGraph() *Graph {
167 src := &Node{
168 Info: NodeInfo{Name: "src"},
169 Flat: 10,
170 Cum: 25,
171 In: make(EdgeMap),
172 Out: make(EdgeMap),
173 LabelTags: make(TagMap),
174 NumericTags: make(map[string]TagMap),
175 }
176 dest := &Node{
177 Info: NodeInfo{Name: "dest"},
178 Flat: 15,
179 Cum: 25,
180 In: make(EdgeMap),
181 Out: make(EdgeMap),
182 LabelTags: make(TagMap),
183 NumericTags: make(map[string]TagMap),
184 }
185 edge := &Edge{
186 Src: src,
187 Dest: dest,
188 Weight: 10,
189 }
190 src.Out[dest] = edge
191 src.In[src] = edge
192 return &Graph{
193 Nodes: Nodes{
194 src,
195 dest,
196 },
197 }
198 }
199
200 func baseAttrsAndConfig() (*DotAttributes, *DotConfig) {
201 a := &DotAttributes{
202 Nodes: make(map[*Node]*DotNodeAttributes),
203 }
204 c := &DotConfig{
205 Title: "testtitle",
206 Labels: []string{"label1", "label2", `label3: "foo"`},
207 Total: 100,
208 FormatValue: func(v int64) string {
209 return strconv.FormatInt(v, 10)
210 },
211 }
212 return a, c
213 }
214
215 func compareGraphs(t *testing.T, got []byte, wantFile string) {
216 wantFile = filepath.Join("testdata", wantFile)
217 want, err := os.ReadFile(wantFile)
218 if err != nil {
219 t.Fatalf("error reading test file %s: %v", wantFile, err)
220 }
221
222 if string(got) != string(want) {
223 d, err := proftest.Diff(got, want)
224 if err != nil {
225 t.Fatalf("error finding diff: %v", err)
226 }
227 t.Errorf("Compose incorrectly wrote %s", string(d))
228 if *updateFlag {
229 err := os.WriteFile(wantFile, got, 0644)
230 if err != nil {
231 t.Errorf("failed to update the golden file %q: %v", wantFile, err)
232 }
233 }
234 }
235 }
236
237 func TestNodeletCountCapping(t *testing.T) {
238 labelTags := make(TagMap)
239 for i := 0; i < 10; i++ {
240 name := fmt.Sprintf("tag-%d", i)
241 labelTags[name] = &Tag{
242 Name: name,
243 Flat: 10,
244 Cum: 10,
245 }
246 }
247 numTags := make(TagMap)
248 for i := 0; i < 10; i++ {
249 name := fmt.Sprintf("num-tag-%d", i)
250 numTags[name] = &Tag{
251 Name: name,
252 Unit: "mb",
253 Value: 16,
254 Flat: 10,
255 Cum: 10,
256 }
257 }
258 node1 := &Node{
259 Info: NodeInfo{Name: "node1-with-tags"},
260 Flat: 10,
261 Cum: 10,
262 NumericTags: map[string]TagMap{"": numTags},
263 LabelTags: labelTags,
264 }
265 node2 := &Node{
266 Info: NodeInfo{Name: "node2"},
267 Flat: 15,
268 Cum: 15,
269 }
270 node3 := &Node{
271 Info: NodeInfo{Name: "node3"},
272 Flat: 15,
273 Cum: 15,
274 }
275 g := &Graph{
276 Nodes: Nodes{
277 node1,
278 node2,
279 node3,
280 },
281 }
282 for n := 1; n <= 3; n++ {
283 input := maxNodelets + n
284 if got, want := len(g.SelectTopNodes(input, true)), n; got != want {
285 t.Errorf("SelectTopNodes(%d): got %d nodes, want %d", input, got, want)
286 }
287 }
288 }
289
290 func TestMultilinePrintableName(t *testing.T) {
291 ni := &NodeInfo{
292 Name: "test1.test2::test3",
293 File: "src/file.cc",
294 Address: 123,
295 Lineno: 999,
296 }
297
298 want := fmt.Sprintf(`%016x\ntest1\ntest2\ntest3\nfile.cc:999\n`, 123)
299 if got := multilinePrintableName(ni); got != want {
300 t.Errorf("multilinePrintableName(%#v) == %q, want %q", ni, got, want)
301 }
302 }
303
304 func TestTagCollapse(t *testing.T) {
305
306 makeTag := func(name, unit string, value, flat, cum int64) *Tag {
307 return &Tag{name, unit, value, flat, 0, cum, 0}
308 }
309
310 tagSource := []*Tag{
311 makeTag("12mb", "mb", 12, 100, 100),
312 makeTag("1kb", "kb", 1, 1, 1),
313 makeTag("1mb", "mb", 1, 1000, 1000),
314 makeTag("2048mb", "mb", 2048, 1000, 1000),
315 makeTag("1b", "b", 1, 100, 100),
316 makeTag("2b", "b", 2, 100, 100),
317 makeTag("7b", "b", 7, 100, 100),
318 }
319
320 tagWant := [][]*Tag{
321 {
322 makeTag("1B..2GB", "", 0, 2401, 2401),
323 },
324 {
325 makeTag("2GB", "", 0, 1000, 1000),
326 makeTag("1B..12MB", "", 0, 1401, 1401),
327 },
328 {
329 makeTag("2GB", "", 0, 1000, 1000),
330 makeTag("12MB", "", 0, 100, 100),
331 makeTag("1B..1MB", "", 0, 1301, 1301),
332 },
333 {
334 makeTag("2GB", "", 0, 1000, 1000),
335 makeTag("1MB", "", 0, 1000, 1000),
336 makeTag("2B..1kB", "", 0, 201, 201),
337 makeTag("1B", "", 0, 100, 100),
338 makeTag("12MB", "", 0, 100, 100),
339 },
340 }
341
342 for _, tc := range tagWant {
343 var got, want []*Tag
344 b := builder{nil, &DotAttributes{}, &DotConfig{}}
345 got = b.collapsedTags(tagSource, len(tc), true)
346 want = SortTags(tc, true)
347
348 if !reflect.DeepEqual(got, want) {
349 t.Errorf("collapse to %d, got:\n%v\nwant:\n%v", len(tc), tagString(got), tagString(want))
350 }
351 }
352 }
353
354 func TestEscapeForDot(t *testing.T) {
355 for _, tc := range []struct {
356 desc string
357 input []string
358 want []string
359 }{
360 {
361 desc: "with multiple doubles quotes",
362 input: []string{`label: "foo" and "bar"`},
363 want: []string{`label: \"foo\" and \"bar\"`},
364 },
365 {
366 desc: "with graphviz center line character",
367 input: []string{"label: foo \n bar"},
368 want: []string{`label: foo \l bar`},
369 },
370 {
371 desc: "with two backslashes",
372 input: []string{`label: \\`},
373 want: []string{`label: \\\\`},
374 },
375 {
376 desc: "with two double quotes together",
377 input: []string{`label: ""`},
378 want: []string{`label: \"\"`},
379 },
380 {
381 desc: "with multiple labels",
382 input: []string{`label1: "foo"`, `label2: "bar"`},
383 want: []string{`label1: \"foo\"`, `label2: \"bar\"`},
384 },
385 } {
386 t.Run(tc.desc, func(t *testing.T) {
387 if got := escapeAllForDot(tc.input); !reflect.DeepEqual(got, tc.want) {
388 t.Errorf("escapeAllForDot(%s) = %s, want %s", tc.input, got, tc.want)
389 }
390 })
391 }
392 }
393
394 func tagString(t []*Tag) string {
395 var ret []string
396 for _, s := range t {
397 ret = append(ret, fmt.Sprintln(s))
398 }
399 return strings.Join(ret, ":")
400 }
401
View as plain text