blob: 46ed48ae97befb66abd8e7b8fe6a213f73c0b5ae [file] [log] [blame] [edit]
// 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")
// Insert a trivial edit so that we don't automatically update the buffer
// (see CL 267577).
env.EditBuffer("a/a.go", fake.NewEdit(0, 0, 0, 0, " "))
env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`)
env.Await(
OnceMet(
CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
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) {
withOptions(EditorConfig{
VerboseOutput: true,
}).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) {
withOptions(EditorConfig{
VerboseOutput: true,
}).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"),
),
)
})
}