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