// 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"), ), ) }) }