internal/lsp: adding golden file support to the test harness

Also convert the format tests to use it. This means that the build bots no
longer need to run gofmt.

Change-Id: I5cb9d239183b17d81fdb00b38e9099d224c07e6a
Reviewed-on: https://go-review.googlesource.com/c/tools/+/172973
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/testdata/format/bad_format.gofmt.golden.go b/internal/lsp/testdata/format/bad_format.gofmt.golden.go
new file mode 100644
index 0000000..919b2d2
--- /dev/null
+++ b/internal/lsp/testdata/format/bad_format.gofmt.golden.go
@@ -0,0 +1,19 @@
+package format //@format("package")
+
+import (
+	"fmt"
+	"log"
+	"runtime"
+)
+
+func hello() {
+
+	var x int //@diag("x", "LSP", "x declared but not used")
+}
+
+func hi() {
+	runtime.GOROOT()
+	fmt.Printf("")
+
+	log.Printf("")
+}
diff --git a/internal/lsp/testdata/format/good_format.gofmt.golden.go b/internal/lsp/testdata/format/good_format.gofmt.golden.go
new file mode 100644
index 0000000..01cb161
--- /dev/null
+++ b/internal/lsp/testdata/format/good_format.gofmt.golden.go
@@ -0,0 +1,9 @@
+package format //@format("package")
+
+import (
+	"log"
+)
+
+func goodbye() {
+	log.Printf("byeeeee")
+}
diff --git a/internal/lsp/testdata/format/newline_format.gofmt.golden.go b/internal/lsp/testdata/format/newline_format.gofmt.golden.go
new file mode 100644
index 0000000..29459ac
--- /dev/null
+++ b/internal/lsp/testdata/format/newline_format.gofmt.golden.go
@@ -0,0 +1,2 @@
+package format //@format("package")
+func _()       {}
diff --git a/internal/lsp/testdata/noparse_format/noparse_format.gofmt.golden.go b/internal/lsp/testdata/noparse_format/noparse_format.gofmt.golden.go
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/internal/lsp/testdata/noparse_format/noparse_format.gofmt.golden.go
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 10012c8..d4a8960 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -5,13 +5,15 @@
 package tests
 
 import (
-	"bytes"
 	"context"
+	"flag"
 	"go/ast"
 	"go/parser"
 	"go/token"
 	"io/ioutil"
+	"os"
 	"os/exec"
+	"path"
 	"path/filepath"
 	"runtime"
 	"strings"
@@ -36,6 +38,15 @@
 	ExpectedSignaturesCount      = 19
 )
 
+const (
+	overlayFile = ".overlay"
+	goldenFile  = ".golden"
+	inFile      = ".in"
+	testModule  = "golang.org/x/tools/internal/lsp"
+)
+
+var updateGolden = flag.Bool("golden", false, "Update golden files")
+
 type Diagnostics map[span.URI][]source.Diagnostic
 type CompletionItems map[token.Pos]*source.CompletionItem
 type Completions map[span.Span][]token.Pos
@@ -58,6 +69,10 @@
 	Symbols         Symbols
 	symbolsChildren SymbolsChildren
 	Signatures      Signatures
+
+	t         testing.TB
+	fragments map[string]string
+	dir       string
 }
 
 type Tests interface {
@@ -91,19 +106,23 @@
 		Symbols:         make(Symbols),
 		symbolsChildren: make(SymbolsChildren),
 		Signatures:      make(Signatures),
+
+		t:         t,
+		dir:       dir,
+		fragments: map[string]string{},
 	}
 
 	files := packagestest.MustCopyFileTree(dir)
 	overlays := map[string][]byte{}
 	for fragment, operation := range files {
-		if trimmed := strings.TrimSuffix(fragment, ".in"); trimmed != fragment {
+		if strings.Contains(fragment, goldenFile) {
+			delete(files, fragment)
+		} else if trimmed := strings.TrimSuffix(fragment, inFile); trimmed != fragment {
 			delete(files, fragment)
 			files[trimmed] = operation
-		}
-		const overlay = ".overlay"
-		if index := strings.Index(fragment, overlay); index >= 0 {
+		} else if index := strings.Index(fragment, overlayFile); index >= 0 {
 			delete(files, fragment)
-			partial := fragment[:index] + fragment[index+len(overlay):]
+			partial := fragment[:index] + fragment[index+len(overlayFile):]
 			contents, err := ioutil.ReadFile(filepath.Join(dir, fragment))
 			if err != nil {
 				t.Fatal(err)
@@ -113,12 +132,16 @@
 	}
 	modules := []packagestest.Module{
 		{
-			Name:    "golang.org/x/tools/internal/lsp",
+			Name:    testModule,
 			Files:   files,
 			Overlay: overlays,
 		},
 	}
 	data.Exported = packagestest.Export(t, exporter, modules)
+	for fragment, _ := range files {
+		filename := data.Exported.File(testModule, fragment)
+		data.fragments[filename] = fragment
+	}
 
 	// Merge the exported.Config with the view.Config.
 	data.Config = *data.Exported.Config
@@ -231,6 +254,35 @@
 	})
 }
 
+func (data *Data) Golden(tag string, target string, update func(golden string) error) []byte {
+	data.t.Helper()
+	fragment, found := data.fragments[target]
+	if !found {
+		if filepath.IsAbs(target) {
+			data.t.Fatalf("invalid golden file fragment %v", target)
+		}
+		fragment = target
+	}
+	dir, file := path.Split(fragment)
+	prefix, suffix := file, ""
+	// we deliberately use the first . not the last
+	if dot := strings.IndexRune(file, '.'); dot >= 0 {
+		prefix = file[:dot]
+		suffix = file[dot:]
+	}
+	golden := path.Join(data.dir, dir, prefix) + "." + tag + goldenFile + suffix
+	if *updateGolden {
+		if err := update(golden); err != nil {
+			data.t.Fatalf("could not update golden file %v: %v", golden, err)
+		}
+	}
+	contents, err := ioutil.ReadFile(golden)
+	if err != nil {
+		data.t.Fatalf("could not read golden file %v: %v", golden, err)
+	}
+	return contents
+}
+
 func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg string) {
 	if _, ok := data.Diagnostics[spn.URI()]; !ok {
 		data.Diagnostics[spn.URI()] = []source.Diagnostic{}
@@ -266,11 +318,17 @@
 }
 
 func (data *Data) collectFormats(pos token.Position) {
-	cmd := exec.Command("gofmt", pos.Filename)
-	stdout := bytes.NewBuffer(nil)
-	cmd.Stdout = stdout
-	cmd.Run() // ignore error, sometimes we have intentionally ungofmt-able files
-	data.Formats[pos.Filename] = stdout.String()
+	data.Formats[pos.Filename] = string(data.Golden("gofmt", pos.Filename, func(golden string) error {
+		cmd := exec.Command("gofmt", pos.Filename)
+		stdout, err := os.Create(golden)
+		if err != nil {
+			return err
+		}
+		defer stdout.Close()
+		cmd.Stdout = stdout
+		cmd.Run() // ignore error, sometimes we have intentionally ungofmt-able files
+		return nil
+	}))
 }
 
 func (data *Data) collectDefinitions(src, target span.Span) {