| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package regtest |
| |
| import ( |
| "testing" |
| |
| "golang.org/x/tools/internal/lsp" |
| "golang.org/x/tools/internal/lsp/fake" |
| "golang.org/x/tools/internal/lsp/protocol" |
| ) |
| |
| func TestEditFile(t *testing.T) { |
| const pkg = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.14 |
| -- a/a.go -- |
| package a |
| |
| func _() { |
| var x int |
| } |
| ` |
| // Edit the file when it's *not open* in the workspace, and check that |
| // diagnostics are updated. |
| t.Run("unopened", func(t *testing.T) { |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.Await( |
| env.DiagnosticAtRegexp("a/a.go", "x"), |
| ) |
| env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) |
| env.Await( |
| EmptyDiagnostics("a/a.go"), |
| ) |
| }) |
| }) |
| |
| // Edit the file when it *is open* in the workspace, and check that |
| // diagnostics are *not* updated. |
| t.Run("opened", func(t *testing.T) { |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.OpenFile("a/a.go") |
| env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) |
| env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) |
| env.Await( |
| env.DiagnosticAtRegexp("a/a.go", "x"), |
| ) |
| }) |
| }) |
| } |
| |
| // Edit a dependency on disk and expect a new diagnostic. |
| func TestEditDependency(t *testing.T) { |
| const pkg = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.14 |
| -- b/b.go -- |
| package b |
| |
| func B() int { return 0 } |
| -- a/a.go -- |
| package a |
| |
| import ( |
| "mod.com/b" |
| ) |
| |
| func _() { |
| _ = b.B() |
| } |
| ` |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.OpenFile("a/a.go") |
| env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) |
| env.WriteWorkspaceFile("b/b.go", `package b; func B() {};`) |
| env.Await( |
| env.DiagnosticAtRegexp("a/a.go", "b.B"), |
| ) |
| }) |
| } |
| |
| // Edit both the current file and one of its dependencies on disk and |
| // expect diagnostic changes. |
| func TestEditFileAndDependency(t *testing.T) { |
| const pkg = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.14 |
| -- b/b.go -- |
| package b |
| |
| func B() int { return 0 } |
| -- a/a.go -- |
| package a |
| |
| import ( |
| "mod.com/b" |
| ) |
| |
| func _() { |
| var x int |
| _ = b.B() |
| } |
| ` |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.Await( |
| env.DiagnosticAtRegexp("a/a.go", "x"), |
| ) |
| env.WriteWorkspaceFiles(map[string]string{ |
| "b/b.go": `package b; func B() {};`, |
| "a/a.go": `package a |
| |
| import "mod.com/b" |
| |
| func _() { |
| b.B() |
| }`, |
| }) |
| env.Await( |
| EmptyDiagnostics("a/a.go"), |
| NoDiagnostics("b/b.go"), |
| ) |
| }) |
| } |
| |
| // Delete a dependency and expect a new diagnostic. |
| func TestDeleteDependency(t *testing.T) { |
| const pkg = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.14 |
| -- b/b.go -- |
| package b |
| |
| func B() int { return 0 } |
| -- a/a.go -- |
| package a |
| |
| import ( |
| "mod.com/b" |
| ) |
| |
| func _() { |
| _ = b.B() |
| } |
| ` |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.OpenFile("a/a.go") |
| env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)) |
| env.RemoveWorkspaceFile("b/b.go") |
| env.Await( |
| env.DiagnosticAtRegexp("a/a.go", "\"mod.com/b\""), |
| ) |
| }) |
| } |
| |
| // Create a dependency on disk and expect the diagnostic to go away. |
| func TestCreateDependency(t *testing.T) { |
| const missing = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.14 |
| -- b/b.go -- |
| package b |
| |
| func B() int { return 0 } |
| -- a/a.go -- |
| package a |
| |
| import ( |
| "mod.com/c" |
| ) |
| |
| func _() { |
| c.C() |
| } |
| ` |
| runner.Run(t, missing, func(t *testing.T, env *Env) { |
| t.Skipf("the initial workspace load fails and never retries") |
| |
| env.Await( |
| env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""), |
| ) |
| env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) |
| env.Await( |
| EmptyDiagnostics("c/c.go"), |
| ) |
| }) |
| } |
| |
| // Create a new dependency and add it to the file on disk. |
| // This is similar to what might happen if you switch branches. |
| func TestCreateAndAddDependency(t *testing.T) { |
| const original = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.14 |
| -- a/a.go -- |
| package a |
| |
| func _() {} |
| ` |
| runner.Run(t, original, func(t *testing.T, env *Env) { |
| env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) |
| env.WriteWorkspaceFile("a/a.go", `package a; import "mod.com/c"; func _() { c.C() }`) |
| env.Await( |
| NoDiagnostics("a/a.go"), |
| ) |
| }) |
| } |
| |
| // Create a new file that defines a new symbol, in the same package. |
| func TestCreateFile(t *testing.T) { |
| const pkg = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.14 |
| -- a/a.go -- |
| package a |
| |
| func _() { |
| hello() |
| } |
| ` |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.Await( |
| env.DiagnosticAtRegexp("a/a.go", "hello"), |
| ) |
| env.WriteWorkspaceFile("a/a2.go", `package a; func hello() {};`) |
| env.Await( |
| EmptyDiagnostics("a/a.go"), |
| ) |
| }) |
| } |
| |
| // Add a new method to an interface and implement it. |
| // Inspired by the structure of internal/lsp/source and internal/lsp/cache. |
| func TestCreateImplementation(t *testing.T) { |
| const pkg = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.14 |
| -- b/b.go -- |
| package b |
| |
| type B interface{ |
| Hello() string |
| } |
| |
| func SayHello(bee B) { |
| println(bee.Hello()) |
| } |
| -- a/a.go -- |
| package a |
| |
| import "mod.com/b" |
| |
| type X struct {} |
| |
| func (_ X) Hello() string { |
| return "" |
| } |
| |
| func _() { |
| x := X{} |
| b.SayHello(x) |
| } |
| ` |
| const newMethod = `package b |
| type B interface{ |
| Hello() string |
| Bye() string |
| } |
| |
| func SayHello(bee B) { |
| println(bee.Hello()) |
| }` |
| const implementation = `package a |
| |
| import "mod.com/b" |
| |
| type X struct {} |
| |
| func (_ X) Hello() string { |
| return "" |
| } |
| |
| func (_ X) Bye() string { |
| return "" |
| } |
| |
| func _() { |
| x := X{} |
| b.SayHello(x) |
| }` |
| |
| // Add the new method before the implementation. Expect diagnostics. |
| t.Run("method before implementation", func(t *testing.T) { |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.Await( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1), |
| ) |
| env.WriteWorkspaceFile("b/b.go", newMethod) |
| env.Await( |
| OnceMet( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), |
| DiagnosticAt("a/a.go", 12, 12), |
| ), |
| ) |
| env.WriteWorkspaceFile("a/a.go", implementation) |
| env.Await( |
| EmptyDiagnostics("a/a.go"), |
| ) |
| }) |
| }) |
| // Add the new implementation before the new method. Expect no diagnostics. |
| t.Run("implementation before method", func(t *testing.T) { |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.Await( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1), |
| ) |
| env.WriteWorkspaceFile("a/a.go", implementation) |
| env.Await( |
| OnceMet( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), |
| NoDiagnostics("a/a.go"), |
| ), |
| ) |
| env.WriteWorkspaceFile("b/b.go", newMethod) |
| env.Await( |
| NoDiagnostics("a/a.go"), |
| ) |
| }) |
| }) |
| // Add both simultaneously. Expect no diagnostics. |
| t.Run("implementation and method simultaneously", func(t *testing.T) { |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.Await( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1), |
| ) |
| env.WriteWorkspaceFiles(map[string]string{ |
| "a/a.go": implementation, |
| "b/b.go": newMethod, |
| }) |
| env.Await( |
| OnceMet( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), |
| NoDiagnostics("a/a.go"), |
| ), |
| NoDiagnostics("b/b.go"), |
| ) |
| }) |
| }) |
| } |
| |
| // Tests golang/go#38498. Delete a file and then force a reload. |
| // Assert that we no longer try to load the file. |
| func TestDeleteFiles(t *testing.T) { |
| const pkg = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.14 |
| -- a/a.go -- |
| package a |
| |
| func _() { |
| var _ int |
| } |
| -- a/a_unneeded.go -- |
| package a |
| ` |
| t.Run("close then delete", func(t *testing.T) { |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.OpenFile("a/a.go") |
| env.OpenFile("a/a_unneeded.go") |
| env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 2)) |
| |
| // Close and delete the open file, mimicking what an editor would do. |
| env.CloseBuffer("a/a_unneeded.go") |
| env.RemoveWorkspaceFile("a/a_unneeded.go") |
| env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")") |
| env.Await( |
| env.DiagnosticAtRegexp("a/a.go", "fmt"), |
| ) |
| env.SaveBuffer("a/a.go") |
| env.Await( |
| NoLogMatching(protocol.Info, "a_unneeded.go"), |
| EmptyDiagnostics("a/a.go"), |
| ) |
| }) |
| }) |
| |
| t.Run("delete then close", func(t *testing.T) { |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.OpenFile("a/a.go") |
| env.OpenFile("a/a_unneeded.go") |
| env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 2)) |
| |
| // Delete and then close the file. |
| env.CloseBuffer("a/a_unneeded.go") |
| env.RemoveWorkspaceFile("a/a_unneeded.go") |
| env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")") |
| env.Await( |
| env.DiagnosticAtRegexp("a/a.go", "fmt"), |
| ) |
| env.SaveBuffer("a/a.go") |
| env.Await( |
| NoLogMatching(protocol.Info, "a_unneeded.go"), |
| EmptyDiagnostics("a/a.go"), |
| ) |
| }) |
| }) |
| } |
| |
| // This change reproduces the behavior of switching branches, with multiple |
| // files being created and deleted. The key change here is the movement of a |
| // symbol from one file to another in a given package through a deletion and |
| // creation. To reproduce an issue with metadata invalidation in batched |
| // changes, the last change in the batch is an on-disk file change that doesn't |
| // require metadata invalidation. |
| func TestMoveSymbol(t *testing.T) { |
| const pkg = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.14 |
| -- main.go -- |
| package main |
| |
| import "mod.com/a" |
| |
| func main() { |
| var x int |
| x = a.Hello |
| println(x) |
| } |
| -- a/a1.go -- |
| package a |
| |
| var Hello int |
| -- a/a2.go -- |
| package a |
| |
| func _() {} |
| ` |
| runner.Run(t, pkg, func(t *testing.T, env *Env) { |
| env.Await( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1), |
| ) |
| env.ChangeFilesOnDisk([]fake.FileEvent{ |
| { |
| Path: "a/a3.go", |
| Content: `package a |
| |
| var Hello int |
| `, |
| ProtocolEvent: protocol.FileEvent{ |
| URI: env.Sandbox.Workdir.URI("a/a3.go"), |
| Type: protocol.Created, |
| }, |
| }, |
| { |
| Path: "a/a1.go", |
| ProtocolEvent: protocol.FileEvent{ |
| URI: env.Sandbox.Workdir.URI("a/a1.go"), |
| Type: protocol.Deleted, |
| }, |
| }, |
| { |
| Path: "a/a2.go", |
| Content: `package a; func _() {};`, |
| ProtocolEvent: protocol.FileEvent{ |
| URI: env.Sandbox.Workdir.URI("a/a2.go"), |
| Type: protocol.Changed, |
| }, |
| }, |
| }) |
| env.Await( |
| OnceMet( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), |
| NoDiagnostics("main.go"), |
| ), |
| ) |
| }) |
| } |
| |
| // Reproduce golang/go#40456. |
| func TestChangeVersion(t *testing.T) { |
| const proxy = ` |
| -- example.com@v1.2.3/go.mod -- |
| module example.com |
| |
| go 1.12 |
| -- example.com@v1.2.3/blah/blah.go -- |
| package blah |
| |
| const Name = "Blah" |
| |
| func X(x int) {} |
| -- example.com@v1.2.2/go.mod -- |
| module example.com |
| |
| go 1.12 |
| -- example.com@v1.2.2/blah/blah.go -- |
| package blah |
| |
| const Name = "Blah" |
| |
| func X() {} |
| -- random.org@v1.2.3/go.mod -- |
| module random.org |
| |
| go 1.12 |
| -- random.org@v1.2.3/blah/blah.go -- |
| package hello |
| |
| const Name = "Hello" |
| ` |
| const mod = ` |
| -- go.mod -- |
| module mod.com |
| |
| go 1.12 |
| |
| require example.com v1.2.2 |
| -- main.go -- |
| package main |
| |
| import "example.com/blah" |
| |
| func main() { |
| blah.X() |
| } |
| ` |
| withOptions(WithProxyFiles(proxy)).run(t, mod, func(t *testing.T, env *Env) { |
| env.Await( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1), |
| ) |
| env.WriteWorkspaceFiles(map[string]string{ |
| "go.mod": `module mod.com |
| |
| go 1.12 |
| |
| require example.com v1.2.3 |
| `, |
| "main.go": `package main |
| |
| import ( |
| "example.com/blah" |
| ) |
| |
| func main() { |
| blah.X(1) |
| } |
| `, |
| }) |
| env.Await( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), |
| NoDiagnostics("main.go"), |
| ) |
| }) |
| } |
| |
| // Reproduces golang/go#37069. |
| func TestSwitchFromGOPATHToModules(t *testing.T) { |
| t.Skipf("golang/go#37069 is not yet resolved.") |
| |
| const files = ` |
| -- foo/blah/blah.go -- |
| package blah |
| |
| const Name = "" |
| -- foo/main.go -- |
| package main |
| |
| import "blah" |
| |
| func main() { |
| _ = blah.Name |
| } |
| ` |
| withOptions(InGOPATH()).run(t, files, func(t *testing.T, env *Env) { |
| env.OpenFile("foo/main.go") |
| env.Await( |
| OnceMet( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1), |
| env.DiagnosticAtRegexp("foo/main.go", `"blah"`), |
| ), |
| ) |
| if err := env.Sandbox.RunGoCommand(env.Ctx, "foo", "mod", []string{"init", "mod.com"}); err != nil { |
| t.Fatal(err) |
| } |
| env.Await( |
| OnceMet( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1), |
| env.DiagnosticAtRegexp("foo/main.go", `"blah`), |
| ), |
| ) |
| env.RegexpReplace("foo/main.go", `"blah"`, `"mod.com/blah"`) |
| env.Await( |
| OnceMet( |
| CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1), |
| NoDiagnostics("foo/main.go"), |
| ), |
| ) |
| }) |
| } |