Skip to content

Commit

Permalink
gopls/internal: test discovery
Browse files Browse the repository at this point in the history
Implements test discovery. Tests are discovered as part of the type
checking process, at the same time as method sets and xrefs, and cached.
Does not implement the Modules command.

Adds static detection of simple subtests. This provides a framework for
static analysis of subtests but intentionally does not support more than
the most trivial case in order to minimize the complexity of this CL.

Fixes golang/go#59445. Updates golang/go#59445, golang/vscode-go#1602,
golang/vscode-go#2445.

Change-Id: Ief497977da09a1e07831e6c5f3b7d28d6874fd9f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/548675
Reviewed-by: Alan Donovan <[email protected]>
Reviewed-by: Robert Findley <[email protected]>
Auto-Submit: Robert Findley <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
  • Loading branch information
firelizzard18 authored and gopherbot committed Sep 10, 2024
1 parent 8ba9169 commit dc4c525
Show file tree
Hide file tree
Showing 11 changed files with 1,025 additions and 56 deletions.
1 change: 1 addition & 0 deletions gopls/internal/cache/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ func storePackageResults(ctx context.Context, ph *packageHandle, p *Package) {
toCache := map[string][]byte{
xrefsKind: p.pkg.xrefs(),
methodSetsKind: p.pkg.methodsets().Encode(),
testsKind: p.pkg.tests().Encode(),
diagnosticsKind: encodeDiagnostics(p.pkg.diagnostics),
}

Expand Down
11 changes: 11 additions & 0 deletions gopls/internal/cache/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/cache/methodsets"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/cache/testfuncs"
"golang.org/x/tools/gopls/internal/cache/xrefs"
"golang.org/x/tools/gopls/internal/protocol"
)
Expand Down Expand Up @@ -60,6 +61,9 @@ type syntaxPackage struct {

methodsetsOnce sync.Once
_methodsets *methodsets.Index // only used by the methodsets method

testsOnce sync.Once
_tests *testfuncs.Index // only used by the tests method
}

func (p *syntaxPackage) xrefs() []byte {
Expand All @@ -76,6 +80,13 @@ func (p *syntaxPackage) methodsets() *methodsets.Index {
return p._methodsets
}

func (p *syntaxPackage) tests() *testfuncs.Index {
p.testsOnce.Do(func() {
p._tests = testfuncs.NewIndex(p.compiledGoFiles, p.typesInfo)
})
return p._tests
}

func (p *Package) String() string { return string(p.metadata.ID) }

func (p *Package) Metadata() *metadata.Package { return p.metadata }
Expand Down
28 changes: 28 additions & 0 deletions gopls/internal/cache/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/cache/methodsets"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/cache/testfuncs"
"golang.org/x/tools/gopls/internal/cache/typerefs"
"golang.org/x/tools/gopls/internal/cache/xrefs"
"golang.org/x/tools/gopls/internal/file"
Expand Down Expand Up @@ -572,6 +573,7 @@ func (s *Snapshot) Overlays() []*overlay {
const (
xrefsKind = "xrefs"
methodSetsKind = "methodsets"
testsKind = "tests"
exportDataKind = "export"
diagnosticsKind = "diagnostics"
typerefsKind = "typerefs"
Expand Down Expand Up @@ -673,6 +675,32 @@ func (s *Snapshot) MethodSets(ctx context.Context, ids ...PackageID) ([]*methods
return indexes, s.forEachPackage(ctx, ids, pre, post)
}

// Tests returns test-set indexes for the specified packages. There is a
// one-to-one correspondence between ID and Index.
//
// If these indexes cannot be loaded from cache, the requested packages may be
// type-checked.
func (s *Snapshot) Tests(ctx context.Context, ids ...PackageID) ([]*testfuncs.Index, error) {
ctx, done := event.Start(ctx, "cache.snapshot.Tests")
defer done()

indexes := make([]*testfuncs.Index, len(ids))
pre := func(i int, ph *packageHandle) bool {
data, err := filecache.Get(testsKind, ph.key)
if err == nil { // hit
indexes[i] = testfuncs.Decode(data)
return false
} else if err != filecache.ErrNotFound {
event.Error(ctx, "reading tests from filecache", err)
}
return true
}
post := func(i int, pkg *Package) {
indexes[i] = pkg.pkg.tests()
}
return indexes, s.forEachPackage(ctx, ids, pre, post)
}

// MetadataForFile returns a new slice containing metadata for each
// package containing the Go file identified by uri, ordered by the
// number of CompiledGoFiles (i.e. "narrowest" to "widest" package),
Expand Down
116 changes: 116 additions & 0 deletions gopls/internal/cache/testfuncs/match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2024 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 testfuncs

import (
"fmt"
"strconv"
"strings"
)

// The functions in this file are copies of those from the testing package.
//
// https://cs.opensource.google/go/go/+/refs/tags/go1.22.5:src/testing/match.go

// uniqueName creates a unique name for the given parent and subname by affixing
// it with one or more counts, if necessary.
func (b *indexBuilder) uniqueName(parent, subname string) string {
base := parent + "/" + subname

for {
n := b.subNames[base]
if n < 0 {
panic("subtest count overflow")
}
b.subNames[base] = n + 1

if n == 0 && subname != "" {
prefix, nn := parseSubtestNumber(base)
if len(prefix) < len(base) && nn < b.subNames[prefix] {
// This test is explicitly named like "parent/subname#NN",
// and #NN was already used for the NNth occurrence of "parent/subname".
// Loop to add a disambiguating suffix.
continue
}
return base
}

name := fmt.Sprintf("%s#%02d", base, n)
if b.subNames[name] != 0 {
// This is the nth occurrence of base, but the name "parent/subname#NN"
// collides with the first occurrence of a subtest *explicitly* named
// "parent/subname#NN". Try the next number.
continue
}

return name
}
}

// parseSubtestNumber splits a subtest name into a "#%02d"-formatted int
// suffix (if present), and a prefix preceding that suffix (always).
func parseSubtestNumber(s string) (prefix string, nn int) {
i := strings.LastIndex(s, "#")
if i < 0 {
return s, 0
}

prefix, suffix := s[:i], s[i+1:]
if len(suffix) < 2 || (len(suffix) > 2 && suffix[0] == '0') {
// Even if suffix is numeric, it is not a possible output of a "%02" format
// string: it has either too few digits or too many leading zeroes.
return s, 0
}
if suffix == "00" {
if !strings.HasSuffix(prefix, "/") {
// We only use "#00" as a suffix for subtests named with the empty
// string — it isn't a valid suffix if the subtest name is non-empty.
return s, 0
}
}

n, err := strconv.ParseInt(suffix, 10, 32)
if err != nil || n < 0 {
return s, 0
}
return prefix, int(n)
}

// rewrite rewrites a subname to having only printable characters and no white
// space.
func rewrite(s string) string {
b := []byte{}
for _, r := range s {
switch {
case isSpace(r):
b = append(b, '_')
case !strconv.IsPrint(r):
s := strconv.QuoteRune(r)
b = append(b, s[1:len(s)-1]...)
default:
b = append(b, string(r)...)
}
}
return string(b)
}

func isSpace(r rune) bool {
if r < 0x2000 {
switch r {
// Note: not the same as Unicode Z class.
case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0, 0x1680:
return true
}
} else {
if r <= 0x200a {
return true
}
switch r {
case 0x2028, 0x2029, 0x202f, 0x205f, 0x3000:
return true
}
}
return false
}
Loading

0 comments on commit dc4c525

Please sign in to comment.