1// Copyright 2019 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
5// Package cmdtest contains the test suite for the command line behavior of gopls.
6package cmdtest
7
8import (
9	"bytes"
10	"context"
11	"fmt"
12	"io"
13	"os"
14	"path/filepath"
15	"strconv"
16	"strings"
17	"sync"
18	"testing"
19
20	"golang.org/x/tools/go/packages/packagestest"
21	"golang.org/x/tools/internal/lsp/cmd"
22	"golang.org/x/tools/internal/lsp/protocol"
23	"golang.org/x/tools/internal/lsp/source"
24	"golang.org/x/tools/internal/lsp/tests"
25	"golang.org/x/tools/internal/span"
26	"golang.org/x/tools/internal/tool"
27)
28
29type runner struct {
30	exporter    packagestest.Exporter
31	data        *tests.Data
32	ctx         context.Context
33	options     func(*source.Options)
34	normalizers []normalizer
35	remote      string
36}
37
38type normalizer struct {
39	path     string
40	slashed  string
41	escaped  string
42	fragment string
43}
44
45func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Context, remote string, options func(*source.Options)) *runner {
46	r := &runner{
47		exporter:    exporter,
48		data:        data,
49		ctx:         ctx,
50		options:     options,
51		normalizers: make([]normalizer, 0, len(data.Exported.Modules)),
52		remote:      remote,
53	}
54	// build the path normalizing patterns
55	for _, m := range data.Exported.Modules {
56		for fragment := range m.Files {
57			n := normalizer{
58				path:     data.Exported.File(m.Name, fragment),
59				fragment: fragment,
60			}
61			if n.slashed = filepath.ToSlash(n.path); n.slashed == n.path {
62				n.slashed = ""
63			}
64			quoted := strconv.Quote(n.path)
65			if n.escaped = quoted[1 : len(quoted)-1]; n.escaped == n.path {
66				n.escaped = ""
67			}
68			r.normalizers = append(r.normalizers, n)
69		}
70	}
71	return r
72}
73
74func (r *runner) CodeLens(t *testing.T, spn span.Span, want []protocol.CodeLens) {
75	//TODO: add command line completions tests when it works
76}
77
78func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
79	//TODO: add command line completions tests when it works
80}
81
82func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) {
83	//TODO: add command line completions tests when it works
84}
85
86func (r *runner) UnimportedCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
87	//TODO: add command line completions tests when it works
88}
89
90func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
91	//TODO: add command line completions tests when it works
92}
93
94func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
95	//TODO: add command line completions tests when it works
96}
97
98func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
99	//TODO: add command line completions tests when it works
100}
101
102func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) {
103	//TODO: add command line completions tests when it works
104}
105
106func (r *runner) WorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) {
107	//TODO: add command line workspace symbol tests when it works
108}
109
110func (r *runner) FuzzyWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) {
111	//TODO: add command line workspace symbol tests when it works
112}
113
114func (r *runner) CaseSensitiveWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) {
115	//TODO: add command line workspace symbol tests when it works
116}
117
118func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) {
119	rStdout, wStdout, err := os.Pipe()
120	if err != nil {
121		t.Fatal(err)
122	}
123	oldStdout := os.Stdout
124	rStderr, wStderr, err := os.Pipe()
125	if err != nil {
126		t.Fatal(err)
127	}
128	oldStderr := os.Stderr
129	stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
130	var wg sync.WaitGroup
131	wg.Add(2)
132	go func() {
133		io.Copy(stdout, rStdout)
134		wg.Done()
135	}()
136	go func() {
137		io.Copy(stderr, rStderr)
138		wg.Done()
139	}()
140	os.Stdout, os.Stderr = wStdout, wStderr
141	app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Exported.Config.Env, r.options)
142	remote := r.remote
143	err = tool.Run(tests.Context(t),
144		app,
145		append([]string{fmt.Sprintf("-remote=internal@%s", remote)}, args...))
146	if err != nil {
147		fmt.Fprint(os.Stderr, err)
148	}
149	wStdout.Close()
150	wStderr.Close()
151	wg.Wait()
152	os.Stdout, os.Stderr = oldStdout, oldStderr
153	rStdout.Close()
154	rStderr.Close()
155	return stdout.String(), stderr.String()
156}
157
158// NormalizeGoplsCmd runs the gopls command and normalizes its output.
159func (r *runner) NormalizeGoplsCmd(t testing.TB, args ...string) (string, string) {
160	stdout, stderr := r.runGoplsCmd(t, args...)
161	return r.Normalize(stdout), r.Normalize(stderr)
162}
163
164// NormalizePrefix normalizes a single path at the front of the input string.
165func (r *runner) NormalizePrefix(s string) string {
166	for _, n := range r.normalizers {
167		if t := strings.TrimPrefix(s, n.path); t != s {
168			return n.fragment + t
169		}
170		if t := strings.TrimPrefix(s, n.slashed); t != s {
171			return n.fragment + t
172		}
173		if t := strings.TrimPrefix(s, n.escaped); t != s {
174			return n.fragment + t
175		}
176	}
177	return s
178}
179
180// Normalize replaces all paths present in s with just the fragment portion
181// this is used to make golden files not depend on the temporary paths of the files
182func (r *runner) Normalize(s string) string {
183	type entry struct {
184		path     string
185		index    int
186		fragment string
187	}
188	match := make([]entry, 0, len(r.normalizers))
189	// collect the initial state of all the matchers
190	for _, n := range r.normalizers {
191		index := strings.Index(s, n.path)
192		if index >= 0 {
193			match = append(match, entry{n.path, index, n.fragment})
194		}
195		if n.slashed != "" {
196			index := strings.Index(s, n.slashed)
197			if index >= 0 {
198				match = append(match, entry{n.slashed, index, n.fragment})
199			}
200		}
201		if n.escaped != "" {
202			index := strings.Index(s, n.escaped)
203			if index >= 0 {
204				match = append(match, entry{n.escaped, index, n.fragment})
205			}
206		}
207	}
208	// result should be the same or shorter than the input
209	buf := bytes.NewBuffer(make([]byte, 0, len(s)))
210	last := 0
211	for {
212		// find the nearest path match to the start of the buffer
213		next := -1
214		nearest := len(s)
215		for i, c := range match {
216			if c.index >= 0 && nearest > c.index {
217				nearest = c.index
218				next = i
219			}
220		}
221		// if there are no matches, we copy the rest of the string and are done
222		if next < 0 {
223			buf.WriteString(s[last:])
224			return buf.String()
225		}
226		// we have a match
227		n := &match[next]
228		// copy up to the start of the match
229		buf.WriteString(s[last:n.index])
230		// skip over the filename
231		last = n.index + len(n.path)
232		// add in the fragment instead
233		buf.WriteString(n.fragment)
234		// see what the next match for this path is
235		n.index = strings.Index(s[last:], n.path)
236		if n.index >= 0 {
237			n.index += last
238		}
239	}
240}
241