--- /dev/null
+// 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"
+ "golang.org/x/tools/internal/testenv"
+)
+
+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.Skip("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.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.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.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(
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 2),
+ LogMatching(protocol.Info, "a_unneeded.go", 1),
+ ),
+ )
+
+ // 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(
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
+ // There should only be one log message containing
+ // a_unneeded.go, from the initial workspace load, which we
+ // check for earlier. If there are more, there's a bug.
+ LogMatching(protocol.Info, "a_unneeded.go", 1),
+ ),
+ 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(
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 2),
+ LogMatching(protocol.Info, "a_unneeded.go", 1),
+ ),
+ )
+
+ // Delete and then close the file.
+ env.RemoveWorkspaceFile("a/a_unneeded.go")
+ env.CloseBuffer("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(
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
+ // There should only be one log message containing
+ // a_unneeded.go, from the initial workspace load, which we
+ // check for earlier. If there are more, there's a bug.
+ LogMatching(protocol.Info, "a_unneeded.go", 1),
+ ),
+ 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.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.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#40340.
+func TestSwitchFromGOPATHToModules(t *testing.T) {
+ testenv.NeedsGo1Point(t, 13)
+
+ const files = `
+-- foo/blah/blah.go --
+package blah
+
+const Name = ""
+-- foo/main.go --
+package main
+
+import "blah"
+
+func main() {
+ _ = blah.Name
+}
+`
+ withOptions(
+ InGOPATH(),
+ WithModes(Experimental), // module is in a subdirectory
+ ).run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("foo/main.go")
+ env.Await(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(
+ EmptyDiagnostics("foo/main.go"),
+ )
+ })
+}
+
+// Reproduces golang/go#40487.
+func TestSwitchFromModulesToGOPATH(t *testing.T) {
+ testenv.NeedsGo1Point(t, 13)
+
+ const files = `
+-- foo/go.mod --
+module mod.com
+
+go 1.14
+-- foo/blah/blah.go --
+package blah
+
+const Name = ""
+-- foo/main.go --
+package main
+
+import "mod.com/blah"
+
+func main() {
+ _ = blah.Name
+}
+`
+ withOptions(
+ InGOPATH(),
+ ).run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("foo/main.go")
+ env.RemoveWorkspaceFile("foo/go.mod")
+ env.Await(
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+ env.DiagnosticAtRegexp("foo/main.go", `"mod.com/blah"`),
+ ),
+ )
+ env.RegexpReplace("foo/main.go", `"mod.com/blah"`, `"foo/blah"`)
+ env.Await(
+ EmptyDiagnostics("foo/main.go"),
+ )
+ })
+}
+
+func TestNewSymbolInTestVariant(t *testing.T) {
+ const files = `
+-- go.mod --
+module mod.com
+
+go 1.12
+-- a/a.go --
+package a
+
+func bob() {}
+-- a/a_test.go --
+package a
+
+import "testing"
+
+func TestBob(t *testing.T) {
+ bob()
+}
+`
+ run(t, files, func(t *testing.T, env *Env) {
+ // Add a new symbol to the package under test and use it in the test
+ // variant. Expect no diagnostics.
+ env.WriteWorkspaceFiles(map[string]string{
+ "a/a.go": `package a
+
+func bob() {}
+func george() {}
+`,
+ "a/a_test.go": `package a
+
+import "testing"
+
+func TestAll(t *testing.T) {
+ bob()
+ george()
+}
+`,
+ })
+ env.Await(
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+ NoDiagnostics("a/a.go"),
+ ),
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+ NoDiagnostics("a/a_test.go"),
+ ),
+ )
+ // Now, add a new file to the test variant and use its symbol in the
+ // original test file. Expect no diagnostics.
+ env.WriteWorkspaceFiles(map[string]string{
+ "a/a_test.go": `package a
+
+import "testing"
+
+func TestAll(t *testing.T) {
+ bob()
+ george()
+ hi()
+}
+`,
+ "a/a2_test.go": `package a
+
+import "testing"
+
+func hi() {}
+
+func TestSomething(t *testing.T) {}
+`,
+ })
+ env.Await(
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2),
+ NoDiagnostics("a/a_test.go"),
+ ),
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2),
+ NoDiagnostics("a/a2_test.go"),
+ ),
+ )
+ })
+}