1// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package regtest
6
7import (
8	"flag"
9	"fmt"
10	"runtime"
11	"strings"
12	"testing"
13
14	"golang.org/x/tools/internal/lsp/fake"
15)
16
17// dummyCompletionFunction to test manually configured completion using CLI.
18func dummyCompletionFunction() { const s = "placeholder"; fmt.Printf("%s", s) }
19
20type completionBenchOptions struct {
21	workdir, file, locationRegexp string
22	printResults                  bool
23	// hook to run edits before initial completion, not supported for manually
24	// configured completions.
25	preCompletionEdits func(*Env)
26}
27
28var completionOptions = completionBenchOptions{}
29
30func init() {
31	flag.StringVar(&completionOptions.workdir, "completion_workdir", "", "directory to run completion benchmarks in")
32	flag.StringVar(&completionOptions.file, "completion_file", "", "relative path to the file to complete in")
33	flag.StringVar(&completionOptions.locationRegexp, "completion_regexp", "", "regexp location to complete at")
34	flag.BoolVar(&completionOptions.printResults, "completion_print_results", false, "whether to print completion results")
35}
36
37func benchmarkCompletion(options completionBenchOptions, t *testing.T) {
38	if completionOptions.workdir == "" {
39		t.Skip("-completion_workdir not configured, skipping benchmark")
40	}
41
42	opts := stressTestOptions(options.workdir)
43
44	// Completion gives bad results if IWL is not yet complete, so we must await
45	// it first (and therefore need hooks).
46	opts = append(opts, SkipHooks(false))
47
48	withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {
49		env.OpenFile(options.file)
50
51		// Run edits required for this completion.
52		if options.preCompletionEdits != nil {
53			options.preCompletionEdits(env)
54		}
55
56		// Add a comment as a marker at the start of the file, we'll replace
57		// this in every iteration to trigger type checking and hence emulate
58		// a more real world scenario.
59		env.EditBuffer(options.file, fake.Edit{Text: "// 0\n"})
60
61		// Run a completion to make sure the system is warm.
62		pos := env.RegexpSearch(options.file, options.locationRegexp)
63		completions := env.Completion(options.file, pos)
64
65		if options.printResults {
66			fmt.Println("Results:")
67			for i := 0; i < len(completions.Items); i++ {
68				fmt.Printf("\t%d. %v\n", i, completions.Items[i])
69			}
70		}
71
72		results := testing.Benchmark(func(b *testing.B) {
73			for i := 0; i < b.N; i++ {
74				b.StopTimer()
75				env.RegexpReplace(options.file, `\/\/ \d*`, fmt.Sprintf("// %d", i))
76
77				// explicitly garbage collect since we don't want to count this
78				// time in completion benchmarks.
79				if i%10 == 0 {
80					runtime.GC()
81				}
82				b.StartTimer()
83
84				env.Completion(options.file, pos)
85			}
86		})
87
88		printBenchmarkResults(results)
89	})
90}
91
92// endPosInBuffer returns the position for last character in the buffer for
93// the given file.
94func endPosInBuffer(env *Env, name string) fake.Pos {
95	buffer := env.Editor.BufferText(name)
96	lines := strings.Split(buffer, "\n")
97	numLines := len(lines)
98
99	return fake.Pos{
100		Line:   numLines - 1,
101		Column: len([]rune(lines[numLines-1])),
102	}
103}
104
105// Benchmark completion at a specified file and location. When no CLI options
106// are specified, this test is skipped.
107// To Run (from x/tools/gopls) against the dummy function above:
108// 	go test -v ./internal/regtest -run=TestBenchmarkConfiguredCompletion
109// 	-completion_workdir="$HOME/Developer/tools"
110// 	-completion_file="gopls/internal/regtest/completion_bench_test.go"
111// 	-completion_regexp="dummyCompletionFunction.*fmt\.Printf\(\"%s\", s(\))"
112func TestBenchmarkConfiguredCompletion(t *testing.T) {
113	benchmarkCompletion(completionOptions, t)
114}
115
116// To run (from x/tools/gopls):
117// 	go test -v ./internal/regtest -run TestBenchmark<>Completion
118//	-completion_workdir="$HOME/Developer/tools"
119// where <> is one of the tests below. completion_workdir should be path to
120// x/tools on your system.
121
122// Benchmark struct completion in tools codebase.
123func TestBenchmarkStructCompletion(t *testing.T) {
124	file := "internal/lsp/cache/session.go"
125
126	preCompletionEdits := func(env *Env) {
127		env.OpenFile(file)
128		originalBuffer := env.Editor.BufferText(file)
129		env.EditBuffer(file, fake.Edit{
130			End:  endPosInBuffer(env, file),
131			Text: originalBuffer + "\nvar testVariable map[string]bool = Session{}.\n",
132		})
133	}
134
135	benchmarkCompletion(completionBenchOptions{
136		workdir:            completionOptions.workdir,
137		file:               file,
138		locationRegexp:     `var testVariable map\[string\]bool = Session{}(\.)`,
139		preCompletionEdits: preCompletionEdits,
140		printResults:       completionOptions.printResults,
141	}, t)
142}
143
144// Benchmark import completion in tools codebase.
145func TestBenchmarkImportCompletion(t *testing.T) {
146	benchmarkCompletion(completionBenchOptions{
147		workdir:        completionOptions.workdir,
148		file:           "internal/lsp/source/completion/completion.go",
149		locationRegexp: `go\/()`,
150		printResults:   completionOptions.printResults,
151	}, t)
152}
153
154// Benchmark slice completion in tools codebase.
155func TestBenchmarkSliceCompletion(t *testing.T) {
156	file := "internal/lsp/cache/session.go"
157
158	preCompletionEdits := func(env *Env) {
159		env.OpenFile(file)
160		originalBuffer := env.Editor.BufferText(file)
161		env.EditBuffer(file, fake.Edit{
162			End:  endPosInBuffer(env, file),
163			Text: originalBuffer + "\nvar testVariable []byte = \n",
164		})
165	}
166
167	benchmarkCompletion(completionBenchOptions{
168		workdir:            completionOptions.workdir,
169		file:               file,
170		locationRegexp:     `var testVariable \[\]byte (=)`,
171		preCompletionEdits: preCompletionEdits,
172		printResults:       completionOptions.printResults,
173	}, t)
174}
175
176// Benchmark deep completion in function call in tools codebase.
177func TestBenchmarkFuncDeepCompletion(t *testing.T) {
178	file := "internal/lsp/source/completion/completion.go"
179	fileContent := `
180func (c *completer) _() {
181	c.inference.kindMatches(c.)
182}
183`
184	preCompletionEdits := func(env *Env) {
185		env.OpenFile(file)
186		originalBuffer := env.Editor.BufferText(file)
187		env.EditBuffer(file, fake.Edit{
188			End:  endPosInBuffer(env, file),
189			Text: originalBuffer + fileContent,
190		})
191	}
192
193	benchmarkCompletion(completionBenchOptions{
194		workdir:            completionOptions.workdir,
195		file:               file,
196		locationRegexp:     `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
197		preCompletionEdits: preCompletionEdits,
198		printResults:       completionOptions.printResults,
199	}, t)
200}
201