internal/lsp/regtest: add regression tests for on-disk file changes

There are still many more cases to check, but this is a good starting
point. A few tests have skips in them because I encountered bugs, which
I plan to go back and fix.

Change-Id: I0b7bbeb632d38c09d6bdb1f4866d81a1690d6ca7
Reviewed-on: https://go-review.googlesource.com/c/tools/+/238917
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/fake/workdir.go b/internal/lsp/fake/workdir.go
index 464e441..67a6978 100644
--- a/internal/lsp/fake/workdir.go
+++ b/internal/lsp/fake/workdir.go
@@ -153,12 +153,36 @@
 	}
 }
 
+// WriteFiles writes the text file content to workdir-relative paths.
+// It batches notifications rather than sending them consecutively.
+func (w *Workdir) WriteFiles(ctx context.Context, files map[string]string) error {
+	var evts []FileEvent
+	for filename, content := range files {
+		evt, err := w.writeFile(ctx, filename, content)
+		if err != nil {
+			return err
+		}
+		evts = append(evts, evt)
+	}
+	w.sendEvents(ctx, evts)
+	return nil
+}
+
 // WriteFile writes text file content to a workdir-relative path.
 func (w *Workdir) WriteFile(ctx context.Context, path, content string) error {
+	evt, err := w.writeFile(ctx, path, content)
+	if err != nil {
+		return err
+	}
+	w.sendEvents(ctx, []FileEvent{evt})
+	return nil
+}
+
+func (w *Workdir) writeFile(ctx context.Context, path, content string) (FileEvent, error) {
 	fp := w.filePath(path)
 	_, err := os.Stat(fp)
 	if err != nil && !os.IsNotExist(err) {
-		return fmt.Errorf("checking if %q exists: %w", path, err)
+		return FileEvent{}, fmt.Errorf("checking if %q exists: %w", path, err)
 	}
 	var changeType protocol.FileChangeType
 	if os.IsNotExist(err) {
@@ -167,17 +191,15 @@
 		changeType = protocol.Changed
 	}
 	if err := w.writeFileData(path, content); err != nil {
-		return err
+		return FileEvent{}, err
 	}
-	evts := []FileEvent{{
+	return FileEvent{
 		Path: path,
 		ProtocolEvent: protocol.FileEvent{
 			URI:  w.URI(path),
 			Type: changeType,
 		},
-	}}
-	w.sendEvents(ctx, evts)
-	return nil
+	}, nil
 }
 
 func (w *Workdir) writeFileData(path string, content string) error {
diff --git a/internal/lsp/regtest/env.go b/internal/lsp/regtest/env.go
index d86fe27..b1ef95c 100644
--- a/internal/lsp/regtest/env.go
+++ b/internal/lsp/regtest/env.go
@@ -438,22 +438,11 @@
 // NoErrorLogs asserts that the client has not received any log messages of
 // error severity.
 func NoErrorLogs() LogExpectation {
-	check := func(msgs []*protocol.LogMessageParams) (Verdict, interface{}) {
-		for _, msg := range msgs {
-			if msg.Type == protocol.Error {
-				return Unmeetable, nil
-			}
-		}
-		return Met, nil
-	}
-	return LogExpectation{
-		check:       check,
-		description: "no errors have been logged",
-	}
+	return NoLogMatching(protocol.Error, "")
 }
 
 // LogMatching asserts that the client has received a log message
-// matching of type typ matching the regexp re.
+// of type typ matching the regexp re.
 func LogMatching(typ protocol.MessageType, re string) LogExpectation {
 	rec, err := regexp.Compile(re)
 	if err != nil {
@@ -473,6 +462,35 @@
 	}
 }
 
+// NoLogMatching asserts that the client has not received a log message
+// of type typ matching the regexp re. If re is an empty string, any log
+// message is considered a match.
+func NoLogMatching(typ protocol.MessageType, re string) LogExpectation {
+	var r *regexp.Regexp
+	if re != "" {
+		var err error
+		r, err = regexp.Compile(re)
+		if err != nil {
+			panic(err)
+		}
+	}
+	check := func(msgs []*protocol.LogMessageParams) (Verdict, interface{}) {
+		for _, msg := range msgs {
+			if msg.Type != typ {
+				continue
+			}
+			if r == nil || r.Match([]byte(msg.Message)) {
+				return Unmeetable, nil
+			}
+		}
+		return Met, nil
+	}
+	return LogExpectation{
+		check:       check,
+		description: fmt.Sprintf("no log message matching %q", re),
+	}
+}
+
 // A DiagnosticExpectation is a condition that must be met by the current set
 // of diagnostics for a file.
 type DiagnosticExpectation struct {
diff --git a/internal/lsp/regtest/watch_test.go b/internal/lsp/regtest/watch_test.go
new file mode 100644
index 0000000..99eee4d
--- /dev/null
+++ b/internal/lsp/regtest/watch_test.go
@@ -0,0 +1,416 @@
+// 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/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(
+				NoDiagnostics("a/a.go"),
+			)
+			env.WriteWorkspaceFile("b/b.go", newMethod)
+			env.Await(
+				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(
+				NoDiagnostics("a/a.go"),
+			)
+			env.WriteWorkspaceFile("a/a.go", implementation)
+			env.Await(
+				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(
+				NoDiagnostics("a/a.go"),
+			)
+			env.WriteWorkspaceFiles(map[string]string{
+				"a/a.go": implementation,
+				"b/b.go": newMethod,
+			})
+			env.Await(
+				NoDiagnostics("a/a.go"),
+				NoDiagnostics("a/a.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"),
+			)
+		})
+	})
+
+}
diff --git a/internal/lsp/regtest/wrappers.go b/internal/lsp/regtest/wrappers.go
index 4ed36dd..2f37462 100644
--- a/internal/lsp/regtest/wrappers.go
+++ b/internal/lsp/regtest/wrappers.go
@@ -43,6 +43,15 @@
 	}
 }
 
+// WriteWorkspaceFiles deletes a file on disk but does nothing in the
+// editor. It calls t.Fatal on any error.
+func (e *Env) WriteWorkspaceFiles(files map[string]string) {
+	e.T.Helper()
+	if err := e.Sandbox.Workdir.WriteFiles(e.Ctx, files); err != nil {
+		e.T.Fatal(err)
+	}
+}
+
 // OpenFile opens a file in the editor, calling t.Fatal on any error.
 func (e *Env) OpenFile(name string) {
 	e.T.Helper()