blob: e0e3ce1becaec002f2c915debec0a6aa9dd3da02 [file] [log] [blame]
// 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 (
"go/ast"
"go/constant"
"go/types"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/frob"
"golang.org/x/tools/gopls/internal/util/safetoken"
)
// An Index records the test set of a package.
type Index struct {
pkg gobPackage
}
// Decode decodes the given gob-encoded data as an Index.
func Decode(data []byte) *Index {
var pkg gobPackage
packageCodec.Decode(data, &pkg)
return &Index{pkg}
}
// Encode encodes the receiver as gob-encoded data.
func (index *Index) Encode() []byte {
return packageCodec.Encode(index.pkg)
}
func (index *Index) All() []Result {
var results []Result
for _, file := range index.pkg.Files {
for _, test := range file.Tests {
results = append(results, test.result())
}
}
return results
}
// A Result reports a test function
type Result struct {
Location protocol.Location // location of the test
Name string // name of the test
}
// NewIndex returns a new index of method-set information for all
// package-level types in the specified package.
func NewIndex(files []*parsego.File, info *types.Info) *Index {
b := &indexBuilder{
fileIndex: make(map[protocol.DocumentURI]int),
subNames: make(map[string]int),
visited: make(map[*types.Func]bool),
}
return b.build(files, info)
}
// build adds to the index all tests of the specified package.
func (b *indexBuilder) build(files []*parsego.File, info *types.Info) *Index {
for _, file := range files {
if !strings.HasSuffix(file.Tok.Name(), "_test.go") {
continue
}
for _, decl := range file.File.Decls {
decl, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
obj, ok := info.ObjectOf(decl.Name).(*types.Func)
if !ok || !obj.Exported() {
continue
}
// error.Error has empty Position, PkgPath, and ObjectPath.
if obj.Pkg() == nil {
continue
}
isTest, isExample := isTestOrExample(obj)
if !isTest && !isExample {
continue
}
var t gobTest
t.Name = decl.Name.Name
t.Location.URI = file.URI
t.Location.Range, _ = file.NodeRange(decl)
i, ok := b.fileIndex[t.Location.URI]
if !ok {
i = len(b.Files)
b.Files = append(b.Files, gobFile{})
b.fileIndex[t.Location.URI] = i
}
b.Files[i].Tests = append(b.Files[i].Tests, t)
b.visited[obj] = true
// Check for subtests
if isTest {
b.Files[i].Tests = append(b.Files[i].Tests, b.findSubtests(t, decl.Type, decl.Body, file, files, info)...)
}
}
}
return &Index{pkg: b.gobPackage}
}
func (b *indexBuilder) findSubtests(parent gobTest, typ *ast.FuncType, body *ast.BlockStmt, file *parsego.File, files []*parsego.File, info *types.Info) []gobTest {
if body == nil {
return nil
}
// If the [testing.T] parameter is unnamed, the func cannot call
// [testing.T.Run] and thus cannot create any subtests
if len(typ.Params.List[0].Names) == 0 {
return nil
}
// This "can't fail" because testKind should guarantee that the function has
// one parameter and the check above guarantees that parameter is named
param := info.ObjectOf(typ.Params.List[0].Names[0])
// Find statements of form t.Run(name, func(...) {...}) where t is the
// parameter of the enclosing test function.
var tests []gobTest
for _, stmt := range body.List {
expr, ok := stmt.(*ast.ExprStmt)
if !ok {
continue
}
call, ok := expr.X.(*ast.CallExpr)
if !ok || len(call.Args) != 2 {
continue
}
fun, ok := call.Fun.(*ast.SelectorExpr)
if !ok || fun.Sel.Name != "Run" {
continue
}
recv, ok := fun.X.(*ast.Ident)
if !ok || info.ObjectOf(recv) != param {
continue
}
sig, ok := info.TypeOf(call.Args[1]).(*types.Signature)
if !ok {
continue
}
if _, ok := testKind(sig); !ok {
continue // subtest has wrong signature
}
val := info.Types[call.Args[0]].Value // may be zero
if val == nil || val.Kind() != constant.String {
continue
}
var t gobTest
t.Name = b.uniqueName(parent.Name, rewrite(constant.StringVal(val)))
t.Location.URI = file.URI
t.Location.Range, _ = file.NodeRange(call)
tests = append(tests, t)
fn, typ, body := findFunc(files, info, body, call.Args[1])
if typ == nil {
continue
}
// Function literals don't have an associated object
if fn == nil {
tests = append(tests, b.findSubtests(t, typ, body, file, files, info)...)
continue
}
// Never recurse if the second argument is a top-level test function
if isTest, _ := isTestOrExample(fn); isTest {
continue
}
// Don't recurse into functions that have already been visited
if b.visited[fn] {
continue
}
b.visited[fn] = true
tests = append(tests, b.findSubtests(t, typ, body, file, files, info)...)
}
return tests
}
// findFunc finds the type and body of the given expr, which may be a function
// literal or reference to a declared function. If the expression is a declared
// function, findFunc returns its [types.Func]. If the expression is a function
// literal, findFunc returns nil for the first return value. If no function is
// found, findFunc returns (nil, nil, nil).
func findFunc(files []*parsego.File, info *types.Info, body *ast.BlockStmt, expr ast.Expr) (*types.Func, *ast.FuncType, *ast.BlockStmt) {
var obj types.Object
switch arg := expr.(type) {
case *ast.FuncLit:
return nil, arg.Type, arg.Body
case *ast.Ident:
obj = info.ObjectOf(arg)
if obj == nil {
return nil, nil, nil
}
case *ast.SelectorExpr:
// Look for methods within the current package. We will not handle
// imported functions and methods for now, as that would require access
// to the source of other packages and would be substantially more
// complex. However, those cases should be rare.
sel, ok := info.Selections[arg]
if !ok {
return nil, nil, nil
}
obj = sel.Obj()
default:
return nil, nil, nil
}
if v, ok := obj.(*types.Var); ok {
// TODO: Handle vars. This could handled by walking over the body (and
// the file), but that doesn't account for assignment. If the variable
// is assigned multiple times, we could easily get the wrong one.
_, _ = v, body
return nil, nil, nil
}
for _, file := range files {
// Skip files that don't contain the object (there should only be a
// single file that _does_ contain it)
if _, err := safetoken.Offset(file.Tok, obj.Pos()); err != nil {
continue
}
for _, decl := range file.File.Decls {
decl, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
if info.ObjectOf(decl.Name) == obj {
return obj.(*types.Func), decl.Type, decl.Body
}
}
}
return nil, nil, nil
}
// isTestOrExample reports whether the given func is a testing func or an
// example func (or neither). isTestOrExample returns (true, false) for testing
// funcs, (false, true) for example funcs, and (false, false) otherwise.
func isTestOrExample(fn *types.Func) (isTest, isExample bool) {
sig := fn.Type().(*types.Signature)
if sig.Params().Len() == 0 &&
sig.Results().Len() == 0 {
return false, isTestName(fn.Name(), "Example")
}
kind, ok := testKind(sig)
if !ok {
return false, false
}
switch kind.Name() {
case "T":
return isTestName(fn.Name(), "Test"), false
case "B":
return isTestName(fn.Name(), "Benchmark"), false
case "F":
return isTestName(fn.Name(), "Fuzz"), false
default:
return false, false // "can't happen" (see testKind)
}
}
// isTestName reports whether name is a valid test name for the test kind
// indicated by the given prefix ("Test", "Benchmark", etc.).
//
// Adapted from go/analysis/passes/tests.
func isTestName(name, prefix string) bool {
suffix, ok := strings.CutPrefix(name, prefix)
if !ok {
return false
}
if len(suffix) == 0 {
// "Test" is ok.
return true
}
r, _ := utf8.DecodeRuneInString(suffix)
return !unicode.IsLower(r)
}
// testKind returns the parameter type TypeName of a test, benchmark, or fuzz
// function (one of testing.[TBF]).
func testKind(sig *types.Signature) (*types.TypeName, bool) {
if sig.Params().Len() != 1 ||
sig.Results().Len() != 0 {
return nil, false
}
ptr, ok := sig.Params().At(0).Type().(*types.Pointer)
if !ok {
return nil, false
}
named, ok := ptr.Elem().(*types.Named)
if !ok || named.Obj().Pkg() == nil || named.Obj().Pkg().Path() != "testing" {
return nil, false
}
switch named.Obj().Name() {
case "T", "B", "F":
return named.Obj(), true
}
return nil, false
}
// An indexBuilder builds an index for a single package.
type indexBuilder struct {
gobPackage
fileIndex map[protocol.DocumentURI]int
subNames map[string]int
visited map[*types.Func]bool
}
// -- serial format of index --
// (The name says gob but in fact we use frob.)
var packageCodec = frob.CodecFor[gobPackage]()
// A gobPackage records the test set of each package-level type for a single package.
type gobPackage struct {
Files []gobFile
}
type gobFile struct {
Tests []gobTest
}
// A gobTest records the name, type, and position of a single test.
type gobTest struct {
Location protocol.Location // location of the test
Name string // name of the test
}
func (t *gobTest) result() Result {
return Result(*t)
}