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 driver
16
17import (
18	"fmt"
19	"math/rand"
20	"strings"
21	"testing"
22
23	"github.com/google/pprof/internal/plugin"
24	"github.com/google/pprof/internal/proftest"
25	"github.com/google/pprof/internal/report"
26	"github.com/google/pprof/internal/transport"
27	"github.com/google/pprof/profile"
28)
29
30func TestShell(t *testing.T) {
31	p := &profile.Profile{}
32	generateReportWrapper = checkValue
33	defer func() { generateReportWrapper = generateReport }()
34
35	// Use test commands and variables to exercise interactive processing
36	var savedCommands commands
37	savedCommands, pprofCommands = pprofCommands, testCommands
38	defer func() { pprofCommands = savedCommands }()
39
40	savedConfig := currentConfig()
41	defer setCurrentConfig(savedConfig)
42
43	shortcuts1, scScript1 := makeShortcuts(interleave(script, 2), 1)
44	shortcuts2, scScript2 := makeShortcuts(interleave(script, 1), 2)
45
46	var testcases = []struct {
47		name              string
48		input             []string
49		shortcuts         shortcuts
50		allowRx           string
51		numAllowRxMatches int
52		propagateError    bool
53	}{
54		{"Random interleave of independent scripts 1", interleave(script, 0), pprofShortcuts, "", 0, false},
55		{"Random interleave of independent scripts 2", interleave(script, 1), pprofShortcuts, "", 0, false},
56		{"Random interleave of independent scripts with shortcuts 1", scScript1, shortcuts1, "", 0, false},
57		{"Random interleave of independent scripts with shortcuts 2", scScript2, shortcuts2, "", 0, false},
58		{"Group with invalid value", []string{"sort=this"}, pprofShortcuts, `invalid "sort" value`, 1, false},
59		{"No special value provided for the option", []string{"sample_index"}, pprofShortcuts, `please specify a value, e.g. sample_index=<val>`, 1, false},
60		{"No string value provided for the option", []string{"focus"}, pprofShortcuts, `please specify a value, e.g. focus=<val>`, 1, false},
61		{"No float value provided for the option", []string{"divide_by"}, pprofShortcuts, `please specify a value, e.g. divide_by=<val>`, 1, false},
62		{"Helpful input format reminder", []string{"sample_index 0"}, pprofShortcuts, `did you mean: sample_index=0`, 1, false},
63		{"Verify propagation of IO errors", []string{"**error**"}, pprofShortcuts, "", 0, true},
64	}
65
66	o := setDefaults(&plugin.Options{HTTPTransport: transport.New(nil)})
67	for _, tc := range testcases {
68		t.Run(tc.name, func(t *testing.T) {
69			setCurrentConfig(savedConfig)
70			pprofShortcuts = tc.shortcuts
71			ui := &proftest.TestUI{
72				T:       t,
73				Input:   tc.input,
74				AllowRx: tc.allowRx,
75			}
76			o.UI = ui
77
78			err := interactive(p, o)
79			if (tc.propagateError && err == nil) || (!tc.propagateError && err != nil) {
80				t.Errorf("%s: %v", tc.name, err)
81			}
82
83			// Confirm error message written out once.
84			if tc.numAllowRxMatches != ui.NumAllowRxMatches {
85				t.Errorf("want error message to be printed %d time(s), got %d",
86					tc.numAllowRxMatches, ui.NumAllowRxMatches)
87			}
88		})
89	}
90}
91
92var testCommands = commands{
93	"check": &command{report.Raw, nil, nil, true, "", ""},
94}
95
96// script contains sequences of commands to be executed for testing. Commands
97// are split by semicolon and interleaved randomly, so they must be
98// independent from each other.
99var script = []string{
100	"call_tree=true;call_tree=false;check call_tree=false;call_tree=yes;check call_tree=true",
101	"mean=1;check mean=true;mean=n;check mean=false",
102	"nodecount=-1;nodecount=-2;check nodecount=-2;nodecount=999999;check nodecount=999999",
103	"nodefraction=-1;nodefraction=-2.5;check nodefraction=-2.5;nodefraction=0.0001;check nodefraction=0.0001",
104	"focus=one;focus=two;check focus=two",
105	"flat=true;check sort=flat;cum=1;check sort=cum",
106}
107
108func makeShortcuts(input []string, seed int) (shortcuts, []string) {
109	rand.Seed(int64(seed))
110
111	s := shortcuts{}
112	var output, chunk []string
113	for _, l := range input {
114		chunk = append(chunk, l)
115		switch rand.Intn(3) {
116		case 0:
117			// Create a macro for commands in 'chunk'.
118			macro := fmt.Sprintf("alias%d", len(s))
119			s[macro] = chunk
120			output = append(output, macro)
121			chunk = nil
122		case 1:
123			// Append commands in 'chunk' by themselves.
124			output = append(output, chunk...)
125			chunk = nil
126		case 2:
127			// Accumulate commands into 'chunk'
128		}
129	}
130	output = append(output, chunk...)
131	return s, output
132}
133
134func checkValue(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) error {
135	if len(cmd) != 2 {
136		return fmt.Errorf("expected len(cmd)==2, got %v", cmd)
137	}
138
139	input := cmd[1]
140	args := strings.SplitN(input, "=", 2)
141	if len(args) == 0 {
142		return fmt.Errorf("unexpected empty input")
143	}
144	name, value := args[0], ""
145	if len(args) == 2 {
146		value = args[1]
147	}
148
149	f, ok := configFieldMap[name]
150	if !ok {
151		return fmt.Errorf("Could not find variable named %s", name)
152	}
153
154	if got := cfg.get(f); got != value {
155		return fmt.Errorf("Variable %s, want %s, got %s", name, value, got)
156	}
157	return nil
158}
159
160func interleave(input []string, seed int) []string {
161	var inputs [][]string
162	for _, s := range input {
163		inputs = append(inputs, strings.Split(s, ";"))
164	}
165	rand.Seed(int64(seed))
166	var output []string
167	for len(inputs) > 0 {
168		next := rand.Intn(len(inputs))
169		output = append(output, inputs[next][0])
170		if tail := inputs[next][1:]; len(tail) > 0 {
171			inputs[next] = tail
172		} else {
173			inputs = append(inputs[:next], inputs[next+1:]...)
174		}
175	}
176	return output
177}
178
179func TestInteractiveCommands(t *testing.T) {
180	type interactiveTestcase struct {
181		input string
182		want  map[string]string
183	}
184
185	testcases := []interactiveTestcase{
186		{
187			"top 10 --cum focus1 -ignore focus2",
188			map[string]string{
189				"granularity": "functions",
190				"nodecount":   "10",
191				"sort":        "cum",
192				"focus":       "focus1|focus2",
193				"ignore":      "ignore",
194			},
195		},
196		{
197			"top10 --cum focus1 -ignore focus2",
198			map[string]string{
199				"granularity": "functions",
200				"nodecount":   "10",
201				"sort":        "cum",
202				"focus":       "focus1|focus2",
203				"ignore":      "ignore",
204			},
205		},
206		{
207			"dot",
208			map[string]string{
209				"granularity": "functions",
210				"nodecount":   "80",
211				"sort":        "flat",
212			},
213		},
214		{
215			"tags   -ignore1 -ignore2 focus1 >out",
216			map[string]string{
217				"granularity": "functions",
218				"nodecount":   "80",
219				"sort":        "flat",
220				"output":      "out",
221				"tagfocus":    "focus1",
222				"tagignore":   "ignore1|ignore2",
223			},
224		},
225		{
226			"weblist  find -test",
227			map[string]string{
228				"granularity": "addresses",
229				"noinlines":   "false",
230				"nodecount":   "0",
231				"sort":        "flat",
232				"ignore":      "test",
233			},
234		},
235		{
236			"callgrind   fun -ignore  >out",
237			map[string]string{
238				"granularity": "addresses",
239				"nodecount":   "0",
240				"sort":        "flat",
241				"output":      "out",
242			},
243		},
244		{
245			"999",
246			nil, // Error
247		},
248	}
249
250	for _, tc := range testcases {
251		cmd, cfg, err := parseCommandLine(strings.Fields(tc.input))
252		if tc.want == nil && err != nil {
253			// Error expected
254			continue
255		}
256		if err != nil {
257			t.Errorf("failed on %q: %v", tc.input, err)
258			continue
259		}
260
261		// Get report output format
262		c := pprofCommands[cmd[0]]
263		if c == nil {
264			t.Fatalf("unexpected nil command")
265		}
266		cfg = applyCommandOverrides(cmd[0], c.format, cfg)
267
268		for n, want := range tc.want {
269			if got := cfg.get(configFieldMap[n]); got != want {
270				t.Errorf("failed on %q, cmd=%q, %s got %s, want %s", tc.input, cmd, n, got, want)
271			}
272		}
273	}
274}
275