internal/lsp: switch golden files to use txtar

I was about to add some more tests and it caused a huge number of golden files,
which was hard to deal with. Now all the golden files are packed into a single
.golden archive in the txtar format.
I also changed the tagging key for hover results to use the marker name rather
than the line and column, as it makes it more stable against test data changes.

Change-Id: Iaa1f54ab55a41d380db67b9f6f928fa7a52d9a5e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/174877
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Ian Cottrell 2019-05-01 19:16:03 -04:00
parent 5658f888b3
commit 490d13020f
35 changed files with 180 additions and 84 deletions

View File

@ -5,9 +5,7 @@
package cmd_test package cmd_test
import ( import (
"bytes"
"context" "context"
"io/ioutil"
"os/exec" "os/exec"
"regexp" "regexp"
"strings" "strings"
@ -33,13 +31,11 @@ func (r *runner) Format(t *testing.T, data tests.Formats) {
t.Fatal(err) t.Fatal(err)
} }
args := append(mode, filename) args := append(mode, filename)
expect := string(r.data.Golden(tag, filename, func(golden string) error { expect := string(r.data.Golden(tag, filename, func() ([]byte, error) {
cmd := exec.Command("gofmt", args...) cmd := exec.Command("gofmt", args...)
buf := &bytes.Buffer{} contents, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files
cmd.Stdout = buf contents = []byte(r.normalizePaths(fixFileHeader(string(contents))))
cmd.Run() // ignore error, sometimes we have intentionally ungofmt-able files return contents, nil
contents := r.normalizePaths(fixFileHeader(buf.String()))
return ioutil.WriteFile(golden, []byte(contents), 0666)
})) }))
if expect == "" { if expect == "" {
//TODO: our error handling differs, for now just skip unformattable files //TODO: our error handling differs, for now just skip unformattable files

View File

@ -9,8 +9,6 @@ import (
"context" "context"
"fmt" "fmt"
"go/token" "go/token"
"io/ioutil"
"os"
"os/exec" "os/exec"
"sort" "sort"
"strings" "strings"
@ -290,16 +288,10 @@ func (r *runner) Format(t *testing.T, data tests.Formats) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
gofmted := string(r.data.Golden("gofmt", filename, func(golden string) error { gofmted := string(r.data.Golden("gofmt", filename, func() ([]byte, error) {
cmd := exec.Command("gofmt", filename) cmd := exec.Command("gofmt", filename)
stdout, err := os.Create(golden) out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files
if err != nil { return out, nil
return err
}
defer stdout.Close()
cmd.Stdout = stdout
cmd.Run() // ignore error, sometimes we have intentionally ungofmt-able files
return nil
})) }))
edits, err := r.server.Formatting(context.Background(), &protocol.DocumentFormattingParams{ edits, err := r.server.Formatting(context.Background(), &protocol.DocumentFormattingParams{
@ -365,13 +357,13 @@ func (r *runner) Definition(t *testing.T, data tests.Definitions) {
t.Errorf("for %v got %v want %v", d.Src, def, d.Def) t.Errorf("for %v got %v want %v", d.Src, def, d.Def)
} }
if hover != nil { if hover != nil {
tag := fmt.Sprintf("hover-%d-%d", d.Def.Start().Line(), d.Def.Start().Column()) tag := fmt.Sprintf("%s-hover", d.Name)
filename, err := d.Def.URI().Filename() filename, err := d.Src.URI().Filename()
if err != nil { if err != nil {
t.Fatalf("failed for %v: %v", d.Def, err) t.Fatalf("failed for %v: %v", d.Def, err)
} }
expectHover := string(r.data.Golden(tag, filename, func(golden string) error { expectHover := string(r.data.Golden(tag, filename, func() ([]byte, error) {
return ioutil.WriteFile(golden, []byte(hover.Contents.Value), 0666) return []byte(hover.Contents.Value), nil
})) }))
if hover.Contents.Value != expectHover { if hover.Contents.Value != expectHover {
t.Errorf("for %v got %q want %q", d.Src, hover.Contents.Value, expectHover) t.Errorf("for %v got %q want %q", d.Src, hover.Contents.Value, expectHover)

4
internal/lsp/reset_golden.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
find ./internal/lsp/ -name *.golden -delete
go test ./internal/lsp/ ./internal/lsp/cmd -golden

View File

@ -1,3 +1,25 @@
-- gofmt --
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("")
}
-- gofmt-d --
--- format/bad_format.go.orig --- format/bad_format.go.orig
+++ format/bad_format.go +++ format/bad_format.go
@@ -1,16 +1,13 @@ @@ -1,16 +1,13 @@
@ -18,3 +40,4 @@
var x int //@diag("x", "LSP", "x declared but not used") var x int //@diag("x", "LSP", "x declared but not used")
} }

View File

@ -1,19 +0,0 @@
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("")
}

View File

@ -1,3 +1,4 @@
-- gofmt --
package format //@format("package") package format //@format("package")
import ( import (
@ -7,3 +8,6 @@ import (
func goodbye() { func goodbye() {
log.Printf("byeeeee") log.Printf("byeeeee")
} }
-- gofmt-d --

View File

@ -1,3 +1,8 @@
-- gofmt --
package format //@format("package")
func _() {}
-- gofmt-d --
--- format/newline_format.go.orig --- format/newline_format.go.orig
+++ format/newline_format.go +++ format/newline_format.go
@@ -1,2 +1,2 @@ @@ -1,2 +1,2 @@
@ -5,3 +10,4 @@
-func _() {} -func _() {}
\ No newline at end of file \ No newline at end of file
+func _() {} +func _() {}

View File

@ -1,2 +0,0 @@
package format //@format("package")
func _() {}

View File

@ -1,6 +1,11 @@
-- gofmt --
package format //@format("package")
-- gofmt-d --
--- format/one_line.go.orig --- format/one_line.go.orig
+++ format/one_line.go +++ format/one_line.go
@@ -1 +1 @@ @@ -1 +1 @@
-package format //@format("package") -package format //@format("package")
\ No newline at end of file \ No newline at end of file
+package format //@format("package") +package format //@format("package")

View File

@ -1 +0,0 @@
package format //@format("package")

View File

@ -0,0 +1,6 @@
-- Random-hover --
func Random() int
-- Random2-hover --
func Random2(y int) int
-- err-hover --
var err error

View File

@ -1 +0,0 @@
var err error

View File

@ -1 +0,0 @@
type a.A string

View File

@ -1 +0,0 @@
func a.Stuff()

View File

@ -0,0 +1,6 @@
-- PosSum-hover --
func (*Pos).Sum() int
-- PosX-hover --
field x int
-- RandomParamY-hover --
var y int

View File

@ -1 +0,0 @@
field x int

View File

@ -1 +0,0 @@
func (*Pos).Sum() int

View File

@ -1 +0,0 @@
func Random() int

View File

@ -1 +0,0 @@
var y int

View File

@ -1 +0,0 @@
func Random2(y int) int

View File

@ -0,0 +1,16 @@
-- A-hover --
type a.A string
-- S1-hover --
type S1 struct{F1 int; S2; a.A}
-- S1F1-hover --
field F1 int
-- S1S2-hover --
field S2 S2
-- S2-hover --
type S2 struct{F1 string; F2 int; *a.A}
-- S2F1-hover --
field F1 string
-- S2F2-hover --
field F2 int
-- Stuff-hover --
func a.Stuff()

View File

@ -1 +0,0 @@
type S2 struct{F1 string; F2 int; *a.A}

View File

@ -1 +0,0 @@
field F1 string

View File

@ -1 +0,0 @@
field F2 int

View File

@ -1 +0,0 @@
type S1 struct{F1 int; S2; a.A}

View File

@ -1 +0,0 @@
field F1 int

View File

@ -1 +0,0 @@
field S2 S2

View File

@ -0,0 +1,4 @@
-- S1-hover --
type S1 struct{F1 int; S2; a.A}
-- S1F1-hover --
field F1 int

View File

@ -0,0 +1,2 @@
-- myUnclosedIf-hover --
var myUnclosedIf string

View File

@ -1 +0,0 @@
var myUnclosedIf string

View File

@ -0,0 +1,4 @@
-- gofmt --
-- gofmt-d --

View File

@ -12,9 +12,9 @@ import (
"go/token" "go/token"
"io/ioutil" "io/ioutil"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sort"
"strings" "strings"
"testing" "testing"
@ -22,6 +22,7 @@ import (
"golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/txtar"
) )
// We hardcode the expected number of test cases to ensure that all tests // We hardcode the expected number of test cases to ensure that all tests
@ -40,10 +41,10 @@ const (
) )
const ( const (
overlayFile = ".overlay" overlayFileSuffix = ".overlay"
goldenFile = ".golden" goldenFileSuffix = ".golden"
inFile = ".in" inFileSuffix = ".in"
testModule = "golang.org/x/tools/internal/lsp" testModule = "golang.org/x/tools/internal/lsp"
) )
var updateGolden = flag.Bool("golden", false, "Update golden files") var updateGolden = flag.Bool("golden", false, "Update golden files")
@ -78,6 +79,7 @@ type Data struct {
t testing.TB t testing.TB
fragments map[string]string fragments map[string]string
dir string dir string
golden map[string]*Golden
} }
type Tests interface { type Tests interface {
@ -92,6 +94,7 @@ type Tests interface {
} }
type Definition struct { type Definition struct {
Name string
Src span.Span Src span.Span
IsType bool IsType bool
Flags string Flags string
@ -110,6 +113,12 @@ type Link struct {
Target string Target string
} }
type Golden struct {
Filename string
Archive *txtar.Archive
Modified bool
}
func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data { func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
t.Helper() t.Helper()
@ -128,19 +137,29 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
t: t, t: t,
dir: dir, dir: dir,
fragments: map[string]string{}, fragments: map[string]string{},
golden: map[string]*Golden{},
} }
files := packagestest.MustCopyFileTree(dir) files := packagestest.MustCopyFileTree(dir)
overlays := map[string][]byte{} overlays := map[string][]byte{}
for fragment, operation := range files { for fragment, operation := range files {
if strings.Contains(fragment, goldenFile) { if trimmed := strings.TrimSuffix(fragment, goldenFileSuffix); trimmed != fragment {
delete(files, fragment) delete(files, fragment)
} else if trimmed := strings.TrimSuffix(fragment, inFile); trimmed != fragment { goldFile := filepath.Join(dir, fragment)
archive, err := txtar.ParseFile(goldFile)
if err != nil {
t.Fatalf("could not read golden file %v: %v", fragment, err)
}
data.golden[trimmed] = &Golden{
Filename: goldFile,
Archive: archive,
}
} else if trimmed := strings.TrimSuffix(fragment, inFileSuffix); trimmed != fragment {
delete(files, fragment) delete(files, fragment)
files[trimmed] = operation files[trimmed] = operation
} else if index := strings.Index(fragment, overlayFile); index >= 0 { } else if index := strings.Index(fragment, overlayFileSuffix); index >= 0 {
delete(files, fragment) delete(files, fragment)
partial := fragment[:index] + fragment[index+len(overlayFile):] partial := fragment[:index] + fragment[index+len(overlayFileSuffix):]
contents, err := ioutil.ReadFile(filepath.Join(dir, fragment)) contents, err := ioutil.ReadFile(filepath.Join(dir, fragment))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -200,6 +219,12 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
symbols[i].Children = children symbols[i].Children = children
} }
} }
// run a second pass to collect names for some entries.
if err := data.Exported.Expect(map[string]interface{}{
"godef": data.collectDefinitionNames,
}); err != nil {
t.Fatal(err)
}
return data return data
} }
@ -287,9 +312,23 @@ func Run(t *testing.T, tests Tests, data *Data) {
} }
tests.Link(t, data.Links) tests.Link(t, data.Links)
}) })
if *updateGolden {
for _, golden := range data.golden {
if !golden.Modified {
continue
}
sort.Slice(golden.Archive.Files, func(i, j int) bool {
return golden.Archive.Files[i].Name < golden.Archive.Files[j].Name
})
if err := ioutil.WriteFile(golden.Filename, txtar.Format(golden.Archive), 0666); err != nil {
t.Fatal(err)
}
}
}
} }
func (data *Data) Golden(tag string, target string, update func(golden string) error) []byte { func (data *Data) Golden(tag string, target string, update func() ([]byte, error)) []byte {
data.t.Helper() data.t.Helper()
fragment, found := data.fragments[target] fragment, found := data.fragments[target]
if !found { if !found {
@ -298,24 +337,44 @@ func (data *Data) Golden(tag string, target string, update func(golden string) e
} }
fragment = target fragment = target
} }
dir, file := path.Split(fragment) golden := data.golden[fragment]
prefix, suffix := file, "" if golden == nil {
// we deliberately use the first . not the last if !*updateGolden {
if dot := strings.IndexRune(file, '.'); dot >= 0 { data.t.Fatalf("could not find golden file %v: %v", fragment, tag)
prefix = file[:dot] }
suffix = file[dot:] golden = &Golden{
Filename: filepath.Join(data.dir, fragment+goldenFileSuffix),
Archive: &txtar.Archive{},
Modified: true,
}
data.golden[fragment] = golden
} }
golden := path.Join(data.dir, dir, prefix) + "." + tag + goldenFile + suffix var file *txtar.File
if *updateGolden { for i := range golden.Archive.Files {
if err := update(golden); err != nil { f := &golden.Archive.Files[i]
data.t.Fatalf("could not update golden file %v: %v", golden, err) if f.Name == tag {
file = f
break
} }
} }
contents, err := ioutil.ReadFile(golden) if *updateGolden {
if err != nil { if file == nil {
data.t.Fatalf("could not read golden file %v: %v", golden, err) golden.Archive.Files = append(golden.Archive.Files, txtar.File{
Name: tag,
})
file = &golden.Archive.Files[len(golden.Archive.Files)-1]
}
contents, err := update()
if err != nil {
data.t.Fatalf("could not update golden file %v: %v", fragment, err)
}
file.Data = append(contents, '\n') // add trailing \n for txtar
golden.Modified = true
} }
return contents if file == nil {
data.t.Fatalf("could not find golden contents %v: %v", fragment, tag)
}
return file.Data[:len(file.Data)-1] // drop the trailing \n
} }
func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg string) { func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg string) {
@ -371,6 +430,12 @@ func (data *Data) collectTypeDefinitions(src, target span.Span) {
} }
} }
func (data *Data) collectDefinitionNames(src span.Span, name string) {
d := data.Definitions[src]
d.Name = name
data.Definitions[src] = d
}
func (data *Data) collectHighlights(name string, rng span.Span) { func (data *Data) collectHighlights(name string, rng span.Span) {
data.Highlights[name] = append(data.Highlights[name], rng) data.Highlights[name] = append(data.Highlights[name], rng)
} }