internal/lsp/regtest: simplify, consolidate, and document settings

Configuration of LSP settings within the regression test runner had
become a bit of a grab-bag: some were configured via explicit fields on
EditorConfig, some via the catch-all EditorConfig.Settings field, and
others via custom RunOption implementations.

Consolidate these fields as follows:
 - Add an EnvVars and Settings field, for configuring environment and
   LSP settings.
 - Eliminate the EditorConfig RunOption wrapper. RunOptions help build
   the config.
 - Remove RunOptions that just wrap a key-value settings pair. By
   definition settings are user-facing and cannot change without
   breaking compatibility. Therefore, our tests can and should set the
   exact string keys they are using.
 - Eliminate the unused SendPID option.

Also clean up some logic to change configuration.

For golang/go#39384

Change-Id: Id5d1614f139550cbc62db2bab1d1e1f545ad9393
Reviewed-on: https://go-review.googlesource.com/c/tools/+/416876
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
Robert Findley 2022-07-11 14:01:19 -04:00
parent 3db2cdc060
commit 6e6f3131ec
25 changed files with 205 additions and 305 deletions

View File

@ -93,14 +93,14 @@ func TestBenchmarkSymbols(t *testing.T) {
}
opts := benchmarkOptions(symbolOptions.workdir)
conf := EditorConfig{}
settings := make(Settings)
if symbolOptions.matcher != "" {
conf.SymbolMatcher = &symbolOptions.matcher
settings["symbolMatcher"] = symbolOptions.matcher
}
if symbolOptions.style != "" {
conf.SymbolStyle = &symbolOptions.style
settings["symbolStyle"] = symbolOptions.style
}
opts = append(opts, conf)
opts = append(opts, settings)
WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {
// We can't Await in this test, since we have disabled hooks. Instead, run
@ -200,9 +200,10 @@ func TestBenchmarkDidChange(t *testing.T) {
// Always run it in isolation since it measures global heap usage.
//
// Kubernetes example:
// $ go test -run=TestPrintMemStats -didchange_dir=$HOME/w/kubernetes
// TotalAlloc: 5766 MB
// HeapAlloc: 1984 MB
//
// $ go test -run=TestPrintMemStats -didchange_dir=$HOME/w/kubernetes
// TotalAlloc: 5766 MB
// HeapAlloc: 1984 MB
//
// Both figures exhibit variance of less than 1%.
func TestPrintMemStats(t *testing.T) {

View File

@ -63,9 +63,7 @@ const (
for _, test := range tests {
t.Run(test.label, func(t *testing.T) {
WithOptions(
EditorConfig{
CodeLenses: test.enabled,
},
Settings{"codelenses": test.enabled},
).Run(t, workspace, func(t *testing.T, env *Env) {
env.OpenFile("lib.go")
lens := env.CodeLens("lib.go")
@ -308,10 +306,11 @@ func main() {
}
`
WithOptions(
EditorConfig{
CodeLenses: map[string]bool{
Settings{
"codelenses": map[string]bool{
"gc_details": true,
}},
},
},
).Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.ExecuteCodeLensCommand("main.go", command.GCDetails)

View File

@ -529,7 +529,7 @@ func main() {
}
`
WithOptions(
EditorConfig{WindowsLineEndings: true},
WindowsLineEndings(),
).Run(t, src, func(t *testing.T, env *Env) {
// Trigger unimported completions for the example.com/blah package.
env.OpenFile("main.go")

View File

@ -21,11 +21,7 @@ func TestBugNotification(t *testing.T) {
// server.
WithOptions(
Modes(Singleton), // must be in-process to receive the bug report below
EditorConfig{
Settings: map[string]interface{}{
"showBugReports": true,
},
},
Settings{"showBugReports": true},
).Run(t, "", func(t *testing.T, env *Env) {
const desc = "got a bug"
bug.Report(desc, nil)

View File

@ -471,12 +471,11 @@ func _() {
}
`
WithOptions(
EditorConfig{
Env: map[string]string{
"GOPATH": "",
"GO111MODULE": "off",
},
}).Run(t, files, func(t *testing.T, env *Env) {
EnvVars{
"GOPATH": "",
"GO111MODULE": "off",
},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.Await(env.DiagnosticAtRegexp("main.go", "fmt"))
env.SaveBuffer("main.go")
@ -500,8 +499,9 @@ package x
var X = 0
`
editorConfig := EditorConfig{Env: map[string]string{"GOFLAGS": "-tags=foo"}}
WithOptions(editorConfig).Run(t, files, func(t *testing.T, env *Env) {
WithOptions(
EnvVars{"GOFLAGS": "-tags=foo"},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.OrganizeImports("main.go")
env.Await(EmptyDiagnostics("main.go"))
@ -573,9 +573,9 @@ hi mom
`
for _, go111module := range []string{"on", "off", ""} {
t.Run(fmt.Sprintf("GO111MODULE_%v", go111module), func(t *testing.T) {
WithOptions(EditorConfig{
Env: map[string]string{"GO111MODULE": go111module},
}).Run(t, files, func(t *testing.T, env *Env) {
WithOptions(
EnvVars{"GO111MODULE": go111module},
).Run(t, files, func(t *testing.T, env *Env) {
env.Await(
NoOutstandingWork(),
)
@ -605,11 +605,7 @@ func main() {
`
WithOptions(
InGOPATH(),
EditorConfig{
Env: map[string]string{
"GO111MODULE": "off",
},
},
EnvVars{"GO111MODULE": "off"},
).Run(t, collision, func(t *testing.T, env *Env) {
env.OpenFile("x/x.go")
env.Await(
@ -1236,7 +1232,7 @@ func main() {
})
WithOptions(
WorkspaceFolders("a"),
LimitWorkspaceScope(),
Settings{"expandWorkspaceToModule": false},
).Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("a/main.go")
env.Await(
@ -1267,11 +1263,7 @@ func main() {
`
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"staticcheck": true,
},
},
Settings{"staticcheck": true},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
var d protocol.PublishDiagnosticsParams
@ -1381,9 +1373,7 @@ func b(c bytes.Buffer) {
}
`
WithOptions(
EditorConfig{
AllExperiments: true,
},
Settings{"allExperiments": true},
).Run(t, mod, func(t *testing.T, env *Env) {
// Confirm that the setting doesn't cause any warnings.
env.Await(NoShowMessage())
@ -1495,11 +1485,7 @@ package foo_
WithOptions(
ProxyFiles(proxy),
InGOPATH(),
EditorConfig{
Env: map[string]string{
"GO111MODULE": "off",
},
},
EnvVars{"GO111MODULE": "off"},
).Run(t, contents, func(t *testing.T, env *Env) {
// Simulate typing character by character.
env.OpenFile("foo/foo_test.go")
@ -1698,9 +1684,7 @@ import (
t.Run("GOPATH", func(t *testing.T) {
WithOptions(
InGOPATH(),
EditorConfig{
Env: map[string]string{"GO111MODULE": "off"},
},
EnvVars{"GO111MODULE": "off"},
Modes(Singleton),
).Run(t, mod, func(t *testing.T, env *Env) {
env.Await(
@ -1729,11 +1713,7 @@ package b
t.Run("GO111MODULE="+go111module, func(t *testing.T) {
WithOptions(
Modes(Singleton),
EditorConfig{
Env: map[string]string{
"GO111MODULE": go111module,
},
},
EnvVars{"GO111MODULE": go111module},
).Run(t, modules, func(t *testing.T, env *Env) {
env.OpenFile("a/a.go")
env.OpenFile("b/go.mod")
@ -1750,11 +1730,7 @@ package b
t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) {
WithOptions(
Modes(Singleton),
EditorConfig{
Env: map[string]string{
"GO111MODULE": "auto",
},
},
EnvVars{"GO111MODULE": "auto"},
InGOPATH(),
).Run(t, modules, func(t *testing.T, env *Env) {
env.OpenFile("a/a.go")
@ -2026,9 +2002,7 @@ package a
func Hello() {}
`
WithOptions(
EditorConfig{
ExperimentalUseInvalidMetadata: true,
},
Settings{"experimentalUseInvalidMetadata": true},
Modes(Singleton),
).Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
@ -2082,9 +2056,7 @@ package main
func _() {}
`
WithOptions(
EditorConfig{
ExperimentalUseInvalidMetadata: true,
},
Settings{"experimentalUseInvalidMetadata": true},
// ExperimentalWorkspaceModule has a different failure mode for this
// case.
Modes(Singleton),

View File

@ -56,10 +56,8 @@ const (
for _, test := range tests {
t.Run(test.label, func(t *testing.T) {
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"hints": test.enabled,
},
Settings{
"hints": test.enabled,
},
).Run(t, workspace, func(t *testing.T, env *Env) {
env.OpenFile("lib.go")

View File

@ -9,7 +9,6 @@ import (
. "golang.org/x/tools/internal/lsp/regtest"
"golang.org/x/tools/internal/lsp/fake"
"golang.org/x/tools/internal/testenv"
)
@ -40,12 +39,11 @@ var FooErr = errors.New("foo")
env.DoneWithOpen(),
NoDiagnostics("a/a.go"),
)
cfg := &fake.EditorConfig{}
*cfg = env.Editor.Config
cfg := env.Editor.Config()
cfg.Settings = map[string]interface{}{
"staticcheck": true,
}
env.ChangeConfiguration(t, cfg)
env.ChangeConfiguration(cfg)
env.Await(
DiagnosticAt("a/a.go", 5, 4),
)
@ -70,11 +68,9 @@ import "errors"
var FooErr = errors.New("foo")
`
WithOptions(EditorConfig{
Settings: map[string]interface{}{
"staticcheck": true,
},
}).Run(t, files, func(t *testing.T, env *Env) {
WithOptions(
Settings{"staticcheck": true},
).Run(t, files, func(t *testing.T, env *Env) {
env.Await(ShownMessage("staticcheck is not supported"))
})
}

View File

@ -162,9 +162,7 @@ func main() {}
} {
t.Run(tt.importShortcut, func(t *testing.T) {
WithOptions(
EditorConfig{
ImportShortcut: tt.importShortcut,
},
Settings{"importShortcut": tt.importShortcut},
).Run(t, mod, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `"fmt"`))

View File

@ -352,10 +352,8 @@ const Bar = 42
`
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"gofumpt": true,
},
Settings{
"gofumpt": true,
},
).Run(t, input, func(t *testing.T, env *Env) {
env.OpenFile("foo.go")

View File

@ -153,9 +153,8 @@ var _, _ = x.X, y.Y
t.Fatal(err)
}
defer os.RemoveAll(modcache)
editorConfig := EditorConfig{Env: map[string]string{"GOMODCACHE": modcache}}
WithOptions(
editorConfig,
EnvVars{"GOMODCACHE": modcache},
ProxyFiles(proxy),
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("main.go")

View File

@ -75,7 +75,9 @@ const Hello = "Hello"
}
// Then change the environment to make these links private.
env.ChangeEnv(map[string]string{"GOPRIVATE": "import.test"})
cfg := env.Editor.Config()
cfg.Env = map[string]string{"GOPRIVATE": "import.test"}
env.ChangeConfiguration(cfg)
// Finally, verify that the links are gone.
content, _ = env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello"))

View File

@ -26,9 +26,7 @@ func main() {}
`
WithOptions(
Modes(Singleton),
EditorConfig{
AllExperiments: true,
},
Settings{"allExperiments": true},
).Run(t, src, func(t *testing.T, env *Env) {
params := &protocol.SemanticTokensParams{}
const badURI = "http://foo"

View File

@ -24,11 +24,7 @@ func main() {
`
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"directoryFilters": []string{""},
},
},
Settings{"directoryFilters": []string{""}},
).Run(t, src, func(t *testing.T, env *Env) {
// No need to do anything. Issue golang/go#51843 is triggered by the empty
// directory filter above.

View File

@ -30,7 +30,7 @@ func runShared(t *testing.T, testFunc func(env1 *Env, env2 *Env)) {
WithOptions(Modes(modes)).Run(t, sharedProgram, func(t *testing.T, env1 *Env) {
// Create a second test session connected to the same workspace and server
// as the first.
env2, cleanup := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config, true)
env2, cleanup := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config(), true)
defer cleanup()
env2.Await(InitialWorkspaceLoad)
testFunc(env1, env2)

View File

@ -60,11 +60,9 @@ func testGenerics[P *T, T any](p P) {
var FooErr error = errors.New("foo")
`
WithOptions(EditorConfig{
Settings: map[string]interface{}{
"staticcheck": true,
},
}).Run(t, files, func(t *testing.T, env *Env) {
WithOptions(
Settings{"staticcheck": true},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("a/a.go")
env.Await(
env.DiagnosticAtRegexpFromSource("a/a.go", "sort.Slice", "sortslice"),

View File

@ -72,9 +72,7 @@ const (
var symbolMatcher = string(source.SymbolFastFuzzy)
WithOptions(
EditorConfig{
SymbolMatcher: &symbolMatcher,
},
Settings{"symbolMatcher": symbolMatcher},
).Run(t, files, func(t *testing.T, env *Env) {
want := []string{
"Foo", // prefer exact segment matches first
@ -105,9 +103,7 @@ const (
var symbolMatcher = string(source.SymbolFastFuzzy)
WithOptions(
EditorConfig{
SymbolMatcher: &symbolMatcher,
},
Settings{"symbolMatcher": symbolMatcher},
).Run(t, files, func(t *testing.T, env *Env) {
compareSymbols(t, env.WorkspaceSymbol("ABC"), []string{"ABC", "AxxBxxCxx"})
compareSymbols(t, env.WorkspaceSymbol("'ABC"), []string{"ABC"})

View File

@ -740,11 +740,7 @@ func main() {
}
`
WithOptions(
EditorConfig{
Env: map[string]string{
"GOFLAGS": "-mod=readonly",
},
},
EnvVars{"GOFLAGS": "-mod=readonly"},
ProxyFiles(proxy),
Modes(Singleton),
).Run(t, mod, func(t *testing.T, env *Env) {
@ -830,9 +826,7 @@ func main() {
`
WithOptions(
ProxyFiles(workspaceProxy),
EditorConfig{
BuildFlags: []string{"-tags", "bob"},
},
Settings{"buildFlags": []string{"-tags", "bob"}},
).Run(t, mod, func(t *testing.T, env *Env) {
env.Await(
env.DiagnosticAtRegexp("main.go", `"example.com/blah"`),

View File

@ -35,11 +35,9 @@ go 1.17
{{end}}
`
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"templateExtensions": []string{"tmpl"},
"semanticTokens": true,
},
Settings{
"templateExtensions": []string{"tmpl"},
"semanticTokens": true,
},
).Run(t, files, func(t *testing.T, env *Env) {
var p protocol.SemanticTokensParams
@ -66,11 +64,9 @@ Hello {{}} <-- missing body
{{end}}
`
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"templateExtensions": []string{"tmpl"},
"semanticTokens": true,
},
Settings{
"templateExtensions": []string{"tmpl"},
"semanticTokens": true,
},
).Run(t, files, func(t *testing.T, env *Env) {
// TODO: can we move this diagnostic onto {{}}?
@ -112,11 +108,9 @@ B {{}} <-- missing body
`
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"templateExtensions": []string{"tmpl"},
},
DirectoryFilters: []string{"-b"},
Settings{
"directoryFilters": []string{"-b"},
"templateExtensions": []string{"tmpl"},
},
).Run(t, files, func(t *testing.T, env *Env) {
env.Await(
@ -184,10 +178,8 @@ go 1.12
`
WithOptions(
EditorConfig{
Settings: map[string]interface{}{
"templateExtensions": []string{"tmpl", "gotmpl"},
},
Settings{
"templateExtensions": []string{"tmpl", "gotmpl"},
},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("a.tmpl")

View File

@ -389,9 +389,9 @@ func _() {
package a
`
t.Run("close then delete", func(t *testing.T) {
WithOptions(EditorConfig{
VerboseOutput: true,
}).Run(t, pkg, func(t *testing.T, env *Env) {
WithOptions(
Settings{"verboseOutput": true},
).Run(t, pkg, func(t *testing.T, env *Env) {
env.OpenFile("a/a.go")
env.OpenFile("a/a_unneeded.go")
env.Await(
@ -424,7 +424,7 @@ package a
t.Run("delete then close", func(t *testing.T) {
WithOptions(
EditorConfig{VerboseOutput: true},
Settings{"verboseOutput": true},
).Run(t, pkg, func(t *testing.T, env *Env) {
env.OpenFile("a/a.go")
env.OpenFile("a/a_unneeded.go")
@ -620,11 +620,7 @@ func main() {
`
WithOptions(
InGOPATH(),
EditorConfig{
Env: map[string]string{
"GO111MODULE": "auto",
},
},
EnvVars{"GO111MODULE": "auto"},
Modes(Experimental), // module is in a subdirectory
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("foo/main.go")
@ -663,11 +659,7 @@ func main() {
`
WithOptions(
InGOPATH(),
EditorConfig{
Env: map[string]string{
"GO111MODULE": "auto",
},
},
EnvVars{"GO111MODULE": "auto"},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("foo/main.go")
env.RemoveWorkspaceFile("foo/go.mod")

View File

@ -1036,10 +1036,10 @@ package exclude
const _ = Nonexistant
`
cfg := EditorConfig{
DirectoryFilters: []string{"-exclude"},
}
WithOptions(cfg).Run(t, files, func(t *testing.T, env *Env) {
WithOptions(
Settings{"directoryFilters": []string{"-exclude"}},
).Run(t, files, func(t *testing.T, env *Env) {
env.Await(NoDiagnostics("exclude/x.go"))
})
}
@ -1064,10 +1064,9 @@ const _ = Nonexistant // should be ignored, since this is a non-workspace packag
const X = 1
`
cfg := EditorConfig{
DirectoryFilters: []string{"-exclude"},
}
WithOptions(cfg).Run(t, files, func(t *testing.T, env *Env) {
WithOptions(
Settings{"directoryFilters": []string{"-exclude"}},
).Run(t, files, func(t *testing.T, env *Env) {
env.Await(
NoDiagnostics("exclude/exclude.go"), // filtered out
NoDiagnostics("include/include.go"), // successfully builds
@ -1114,10 +1113,11 @@ go 1.12
-- exclude.com@v1.0.0/exclude.go --
package exclude
`
cfg := EditorConfig{
DirectoryFilters: []string{"-exclude"},
}
WithOptions(cfg, Modes(Experimental), ProxyFiles(proxy)).Run(t, files, func(t *testing.T, env *Env) {
WithOptions(
Modes(Experimental),
ProxyFiles(proxy),
Settings{"directoryFilters": []string{"-exclude"}},
).Run(t, files, func(t *testing.T, env *Env) {
env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`))
})
}
@ -1204,9 +1204,7 @@ go 1.12
package main
`
WithOptions(
EditorConfig{Env: map[string]string{
"GOPATH": filepath.FromSlash("$SANDBOX_WORKDIR/gopath"),
}},
EnvVars{"GOPATH": filepath.FromSlash("$SANDBOX_WORKDIR/gopath")},
Modes(Singleton),
).Run(t, mod, func(t *testing.T, env *Env) {
env.Await(

View File

@ -77,7 +77,7 @@ func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration
if item.Section != "gopls" {
continue
}
results[i] = c.editor.configuration()
results[i] = c.editor.settings()
}
return results, nil
}

View File

@ -25,7 +25,6 @@ import (
// Editor is a fake editor client. It keeps track of client state and can be
// used for writing LSP tests.
type Editor struct {
Config EditorConfig
// Server, client, and sandbox are concurrency safe and written only
// at construction time, so do not require synchronization.
@ -35,13 +34,10 @@ type Editor struct {
sandbox *Sandbox
defaultEnv map[string]string
// Since this editor is intended just for testing, we use very coarse
// locking.
mu sync.Mutex
// Editor state.
buffers map[string]buffer
// Capabilities / Options
serverCapabilities protocol.ServerCapabilities
mu sync.Mutex // guards config, buffers, serverCapabilities
config EditorConfig // editor configuration
buffers map[string]buffer // open buffers
serverCapabilities protocol.ServerCapabilities // capabilities / options
// Call metrics for the purpose of expectations. This is done in an ad-hoc
// manner for now. Perhaps in the future we should do something more
@ -77,21 +73,11 @@ func (b buffer) text() string {
//
// The zero value for EditorConfig should correspond to its defaults.
type EditorConfig struct {
Env map[string]string
BuildFlags []string
// CodeLenses is a map defining whether codelens are enabled, keyed by the
// codeLens command. CodeLenses which are not present in this map are left in
// their default state.
CodeLenses map[string]bool
// SymbolMatcher is the config associated with the "symbolMatcher" gopls
// config option.
SymbolMatcher, SymbolStyle *string
// LimitWorkspaceScope is true if the user does not want to expand their
// workspace scope to the entire module.
LimitWorkspaceScope bool
// Env holds environment variables to apply on top of the default editor
// environment. When applying these variables, the special string
// $SANDBOX_WORKDIR is replaced by the absolute path to the sandbox working
// directory.
Env map[string]string
// WorkspaceFolders is the workspace folders to configure on the LSP server,
// relative to the sandbox workdir.
@ -101,14 +87,6 @@ type EditorConfig struct {
// To explicitly send no workspace folders, use an empty (non-nil) slice.
WorkspaceFolders []string
// AllExperiments sets the "allExperiments" configuration, which enables
// all of gopls's opt-in settings.
AllExperiments bool
// Whether to send the current process ID, for testing data that is joined to
// the PID. This can only be set by one test.
SendPID bool
// Whether to edit files with windows line endings.
WindowsLineEndings bool
@ -120,14 +98,8 @@ type EditorConfig struct {
// "gotmpl" -> ".*tmpl"
FileAssociations map[string]string
// Settings holds arbitrary additional settings to apply to the gopls config.
// TODO(rfindley): replace existing EditorConfig fields with Settings.
// Settings holds user-provided configuration for the LSP server.
Settings map[string]interface{}
ImportShortcut string
DirectoryFilters []string
VerboseOutput bool
ExperimentalUseInvalidMetadata bool
}
// NewEditor Creates a new Editor.
@ -136,7 +108,7 @@ func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
buffers: make(map[string]buffer),
sandbox: sandbox,
defaultEnv: sandbox.GoEnv(),
Config: config,
config: config,
}
}
@ -155,7 +127,7 @@ func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHo
protocol.Handlers(
protocol.ClientHandler(e.client,
jsonrpc2.MethodNotFound)))
if err := e.initialize(ctx, e.Config.WorkspaceFolders); err != nil {
if err := e.initialize(ctx, e.config.WorkspaceFolders); err != nil {
return nil, err
}
e.sandbox.Workdir.AddWatcher(e.onFileChanges)
@ -213,65 +185,47 @@ func (e *Editor) Client() *Client {
return e.client
}
func (e *Editor) overlayEnv() map[string]string {
// settings builds the settings map for use in LSP settings
// RPCs.
func (e *Editor) settings() map[string]interface{} {
e.mu.Lock()
defer e.mu.Unlock()
env := make(map[string]string)
for k, v := range e.defaultEnv {
env[k] = v
}
for k, v := range e.config.Env {
env[k] = v
}
for k, v := range env {
v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename())
env[k] = v
}
for k, v := range e.Config.Env {
v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename())
env[k] = v
}
return env
}
func (e *Editor) configuration() map[string]interface{} {
config := map[string]interface{}{
settings := map[string]interface{}{
"env": env,
// Use verbose progress reporting so that regtests can assert on
// asynchronous operations being completed (such as diagnosing a snapshot).
"verboseWorkDoneProgress": true,
"env": e.overlayEnv(),
"expandWorkspaceToModule": !e.Config.LimitWorkspaceScope,
"completionBudget": "10s",
// Set a generous completion budget, so that tests don't flake because
// completions are too slow.
"completionBudget": "10s",
// Shorten the diagnostic delay to speed up test execution (else we'd add
// the default delay to each assertion about diagnostics)
"diagnosticsDelay": "10ms",
}
for k, v := range e.Config.Settings {
config[k] = v
for k, v := range e.config.Settings {
if k == "env" {
panic("must not provide env via the EditorConfig.Settings field: use the EditorConfig.Env field instead")
}
settings[k] = v
}
if e.Config.BuildFlags != nil {
config["buildFlags"] = e.Config.BuildFlags
}
if e.Config.DirectoryFilters != nil {
config["directoryFilters"] = e.Config.DirectoryFilters
}
if e.Config.ExperimentalUseInvalidMetadata {
config["experimentalUseInvalidMetadata"] = true
}
if e.Config.CodeLenses != nil {
config["codelenses"] = e.Config.CodeLenses
}
if e.Config.SymbolMatcher != nil {
config["symbolMatcher"] = *e.Config.SymbolMatcher
}
if e.Config.SymbolStyle != nil {
config["symbolStyle"] = *e.Config.SymbolStyle
}
if e.Config.AllExperiments {
config["allExperiments"] = true
}
if e.Config.VerboseOutput {
config["verboseOutput"] = true
}
if e.Config.ImportShortcut != "" {
config["importShortcut"] = e.Config.ImportShortcut
}
config["diagnosticsDelay"] = "10ms"
// ExperimentalWorkspaceModule is only set as a mode, not a configuration.
return config
return settings
}
func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) error {
@ -293,10 +247,7 @@ func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) erro
params.Capabilities.Window.WorkDoneProgress = true
// TODO: set client capabilities
params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated}
params.InitializationOptions = e.configuration()
if e.Config.SendPID {
params.ProcessID = int32(os.Getpid())
}
params.InitializationOptions = e.settings()
params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true
@ -397,20 +348,21 @@ func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
}
func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error {
e.mu.Lock()
defer e.mu.Unlock()
buf := buffer{
windowsLineEndings: e.Config.WindowsLineEndings,
windowsLineEndings: e.config.WindowsLineEndings,
version: 1,
path: path,
lines: lines(content),
dirty: dirty,
}
e.mu.Lock()
defer e.mu.Unlock()
e.buffers[path] = buf
item := protocol.TextDocumentItem{
URI: e.sandbox.Workdir.URI(buf.path),
LanguageID: e.languageID(buf.path),
LanguageID: languageID(buf.path, e.config.FileAssociations),
Version: int32(buf.version),
Text: buf.text(),
}
@ -436,9 +388,11 @@ var defaultFileAssociations = map[string]*regexp.Regexp{
"gotmpl": regexp.MustCompile(`^.*tmpl$`),
}
func (e *Editor) languageID(p string) string {
// languageID returns the language identifier for the path p given the user
// configured fileAssociations.
func languageID(p string, fileAssociations map[string]string) string {
base := path.Base(p)
for lang, re := range e.Config.FileAssociations {
for lang, re := range fileAssociations {
re := regexp.MustCompile(re)
if re.MatchString(base) {
return lang
@ -1205,6 +1159,30 @@ func (e *Editor) applyProtocolEdit(ctx context.Context, change protocol.TextDocu
return e.EditBuffer(ctx, path, fakeEdits)
}
// Config returns the current editor configuration.
func (e *Editor) Config() EditorConfig {
e.mu.Lock()
defer e.mu.Unlock()
return e.config
}
// ChangeConfiguration sets the new editor configuration, and if applicable
// sends a didChangeConfiguration notification.
//
// An error is returned if the change notification failed to send.
func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig) error {
e.mu.Lock()
e.config = newConfig
e.mu.Unlock() // don't hold e.mu during server calls
if e.Server != nil {
var params protocol.DidChangeConfigurationParams // empty: gopls ignores the Settings field
if err := e.Server.DidChangeConfiguration(ctx, &params); err != nil {
return err
}
}
return nil
}
// CodeAction executes a codeAction request on the server.
func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
if e.Server == nil {

View File

@ -111,7 +111,11 @@ type condition struct {
// NewEnv creates a new test environment using the given scratch environment
// and gopls server.
//
// The resulting func must be called to close the jsonrpc2 connection.
// The resulting cleanup func must be called to close the jsonrpc2 connection.
//
// TODO(rfindley): this function provides questionable value. Consider
// refactoring to move things like creating the server outside of this
// constructor.
func NewEnv(ctx context.Context, tb testing.TB, sandbox *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig, withHooks bool) (_ *Env, cleanup func()) {
tb.Helper()

View File

@ -133,17 +133,27 @@ func Options(hook func(*source.Options)) RunOption {
})
}
func SendPID() RunOption {
// WindowsLineEndings configures the editor to use windows line endings.
func WindowsLineEndings() RunOption {
return optionSetter(func(opts *runConfig) {
opts.editor.SendPID = true
opts.editor.WindowsLineEndings = true
})
}
// EditorConfig is a RunOption option that configured the regtest editor.
type EditorConfig fake.EditorConfig
// Settings is a RunOption that sets user-provided configuration for the LSP
// server.
//
// As a special case, the env setting must not be provided via Settings: use
// EnvVars instead.
type Settings map[string]interface{}
func (c EditorConfig) set(opts *runConfig) {
opts.editor = fake.EditorConfig(c)
func (s Settings) set(opts *runConfig) {
if opts.editor.Settings == nil {
opts.editor.Settings = make(map[string]interface{})
}
for k, v := range s {
opts.editor.Settings[k] = v
}
}
// WorkspaceFolders configures the workdir-relative workspace folders to send
@ -160,6 +170,20 @@ func WorkspaceFolders(relFolders ...string) RunOption {
})
}
// EnvVars sets environment variables for the LSP session. When applying these
// variables to the session, the special string $SANDBOX_WORKDIR is replaced by
// the absolute path to the sandbox working directory.
type EnvVars map[string]string
func (e EnvVars) set(opts *runConfig) {
if opts.editor.Env == nil {
opts.editor.Env = make(map[string]string)
}
for k, v := range e {
opts.editor.Env[k] = v
}
}
// InGOPATH configures the workspace working directory to be GOPATH, rather
// than a separate working directory for use with modules.
func InGOPATH() RunOption {
@ -212,13 +236,6 @@ func GOPROXY(goproxy string) RunOption {
})
}
// LimitWorkspaceScope sets the LimitWorkspaceScope configuration.
func LimitWorkspaceScope() RunOption {
return optionSetter(func(opts *runConfig) {
opts.editor.LimitWorkspaceScope = true
})
}
type TestFunc func(t *testing.T, env *Env)
// Run executes the test function in the default configured gopls execution

View File

@ -7,7 +7,6 @@ package regtest
import (
"encoding/json"
"path"
"testing"
"golang.org/x/tools/internal/lsp/command"
"golang.org/x/tools/internal/lsp/fake"
@ -427,31 +426,10 @@ func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []proto
return actions
}
func (e *Env) ChangeConfiguration(t *testing.T, config *fake.EditorConfig) {
e.Editor.Config = *config
if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &protocol.DidChangeConfigurationParams{
// gopls currently ignores the Settings field
}); err != nil {
t.Fatal(err)
}
}
// ChangeEnv modifies the editor environment and reconfigures the LSP client.
// TODO: extend this to "ChangeConfiguration", once we refactor the way editor
// configuration is defined.
func (e *Env) ChangeEnv(overlay map[string]string) {
// ChangeConfiguration updates the editor config, calling t.Fatal on any error.
func (e *Env) ChangeConfiguration(newConfig fake.EditorConfig) {
e.T.Helper()
// TODO: to be correct, this should probably be synchronized, but right now
// configuration is only ever modified synchronously in a regtest, so this
// correctness can wait for the previously mentioned refactoring.
if e.Editor.Config.Env == nil {
e.Editor.Config.Env = make(map[string]string)
}
for k, v := range overlay {
e.Editor.Config.Env[k] = v
}
var params protocol.DidChangeConfigurationParams
if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &params); err != nil {
if err := e.Editor.ChangeConfiguration(e.Ctx, newConfig); err != nil {
e.T.Fatal(err)
}
}