Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a go_test directive to enable generating go_test targets per _test.go file #1597

Merged
merged 11 commits into from
Sep 6, 2023
9 changes: 9 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,15 @@ The following directives are recognized:
| ``proto_library`` rules. If there are any pre-generated Go files, they will be treated as |
| regular Go files. |
+---------------------------------------------------+----------------------------------------+
| :direc:`# gazelle:go_test mode` | ``default`` |
+---------------------------------------------------+----------------------------------------+
| Tells Gazelle how to generate rules for _test.go files. Valid values are: |
| |
| * ``default``: One ``go_test`` rule will be generated whose ``srcs`` includes |
| all ``_test.go`` files in the directory. |
| * ``file``: A distinct ``go_test`` rule will be generated for each ``_test.go`` file in the|
| package directory. |
+---------------------------------------------------+----------------------------------------+
| :direc:`# gazelle:go_grpc_compilers` | ``@io_bazel_rules_go//proto:go_grpc`` |
+---------------------------------------------------+----------------------------------------+
| The protocol buffers compiler(s) to use for building go bindings for gRPC. |
Expand Down
49 changes: 48 additions & 1 deletion language/go/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"errors"
"flag"
"fmt"
"github.com/bazelbuild/bazel-gazelle/internal/module"
"go/build"
"io/ioutil"
"log"
Expand All @@ -30,6 +29,8 @@ import (
"strconv"
"strings"

"github.com/bazelbuild/bazel-gazelle/internal/module"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

nit

"github.com/bazelbuild/bazel-gazelle/config"
gzflag "github.com/bazelbuild/bazel-gazelle/flag"
"github.com/bazelbuild/bazel-gazelle/internal/version"
Expand Down Expand Up @@ -126,18 +127,55 @@ type goConfig struct {
// in internal packages.
submodules []moduleRepo

// testMode determines how go_test targets are generated.
testMode testMode

// buildDirectives, buildExternalAttr, buildExtraArgsAttr,
// buildFileGenerationAttr, buildFileNamesAttr, buildFileProtoModeAttr and
// buildTagsAttr are attributes for go_repository rules, set on the command
// line.
buildDirectivesAttr, buildExternalAttr, buildExtraArgsAttr, buildFileGenerationAttr, buildFileNamesAttr, buildFileProtoModeAttr, buildTagsAttr string
}

// testMode determines how go_test rules are generated.
type testMode int

const (
// defaultTestMode generates a go_test for the primary package in a directory.
defaultTestMode = iota

// fileTestMode generates a go_test for each Go test file.
fileTestMode
)

var (
defaultGoProtoCompilers = []string{"@io_bazel_rules_go//proto:go_proto"}
defaultGoGrpcCompilers = []string{"@io_bazel_rules_go//proto:go_grpc"}
)

func (m testMode) String() string {
switch m {
case defaultTestMode:
return "default"
case fileTestMode:
return "file"
default:
log.Panicf("unknown mode %d", m)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't panic on a String(), since this is mostly used for fmt.Sprintf(. Instead, let's just say "unknown"

return ""
}
}

func testModeFromString(s string) (testMode, error) {
switch s {
case "default":
return defaultTestMode, nil
case "file":
return fileTestMode, nil
default:
return 0, fmt.Errorf("unrecognized go_test mode: %q", s)
}
}

func newGoConfig() *goConfig {
gc := &goConfig{
goProtoCompilers: defaultGoProtoCompilers,
Expand Down Expand Up @@ -351,6 +389,7 @@ func (*goLang) KnownDirectives() []string {
"go_naming_convention",
"go_naming_convention_external",
"go_proto_compilers",
"go_test",
"go_visibility",
"importmap_prefix",
"prefix",
Expand Down Expand Up @@ -592,6 +631,14 @@ Update io_bazel_rules_go to a newer version in your WORKSPACE file.`
gc.goProtoCompilers = splitValue(d.Value)
}

case "go_test":
mode, err := testModeFromString(d.Value)
if err != nil {
log.Print(err)
continue
}
gc.testMode = mode

case "go_visibility":
gc.goVisibility = append(gc.goVisibility, strings.TrimSpace(d.Value))

Expand Down
57 changes: 41 additions & 16 deletions language/go/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,8 @@ func (gl *goLang) GenerateRules(args language.GenerateArgs) language.GenerateRes
g.maybePublishToolLib(r, pkg)
rules = append(rules, r)
}
rules = append(rules,
g.generateBin(pkg, libName),
g.generateTest(pkg, libName))
rules = append(rules, g.generateBin(pkg, libName))
rules = append(rules, g.generateTests(pkg, libName)...)
}

for _, r := range rules {
Expand Down Expand Up @@ -517,22 +516,48 @@ func (g *generator) generateBin(pkg *goPackage, library string) *rule.Rule {
return goBinary
}

func (g *generator) generateTest(pkg *goPackage, library string) *rule.Rule {
func (g *generator) generateTests(pkg *goPackage, library string) []*rule.Rule {
gc := getGoConfig(g.c)
name := testNameByConvention(gc.goNamingConvention, pkg.importPath)
goTest := rule.NewRule("go_test", name)
if !pkg.test.sources.hasGo() {
return goTest // empty
}
var embed string
if pkg.test.hasInternalTest {
embed = library
tests := pkg.tests
if len(tests) == 0 && gc.testMode == defaultTestMode {
tests = []goTarget{goTarget{}}
}
var name func(goTarget) string
switch gc.testMode {
case defaultTestMode:
name = func(goTarget) string {
return testNameByConvention(gc.goNamingConvention, pkg.importPath)
}
case fileTestMode:
name = func(test goTarget) string {
if test.sources.hasGo() {
if srcs := test.sources.buildFlat(); len(srcs) == 1 {
return testNameFromSingleSource(srcs[0])
}
}
return testNameByConvention(gc.goNamingConvention, pkg.importPath)
}
}
g.setCommonAttrs(goTest, pkg.rel, nil, pkg.test, embed)
if pkg.hasTestdata {
goTest.SetAttr("data", rule.GlobValue{Patterns: []string{"testdata/**"}})
var res []*rule.Rule
for i, test := range tests {
goTest := rule.NewRule("go_test", name(test))
hasGo := test.sources.hasGo()
if hasGo || i == 0 {
res = append(res, goTest)
if !hasGo {
continue
}
}
var embed string
if test.hasInternalTest {
embed = library
}
g.setCommonAttrs(goTest, pkg.rel, nil, test, embed)
if pkg.hasTestdata {
goTest.SetAttr("data", rule.GlobValue{Patterns: []string{"testdata/**"}})
}
}
return goTest
return res
}

// maybePublishToolLib makes the given go_library rule public if needed for nogo.
Expand Down
53 changes: 44 additions & 9 deletions language/go/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ import (
// goPackage contains metadata for a set of .go and .proto files that can be
// used to generate Go rules.
type goPackage struct {
name, dir, rel string
library, binary, test goTarget
proto protoTarget
hasTestdata bool
importPath string
name, dir, rel string
library, binary goTarget
tests []goTarget
proto protoTarget
hasTestdata bool
importPath string
}

// goTarget contains information used to generate an individual Go rule
Expand Down Expand Up @@ -107,9 +108,16 @@ func (pkg *goPackage) addFile(c *config.Config, er *embedResolver, info fileInfo
if info.isCgo {
return fmt.Errorf("%s: use of cgo in test not supported", info.path)
}
pkg.test.addFile(c, er, info)
if getGoConfig(c).testMode == fileTestMode || len(pkg.tests) == 0 {
pkg.tests = append(pkg.tests, goTarget{})
}
// Add the the file to the most recently added test target (in fileTestMode)
// or the only test target (in defaultMode).
// In both cases, this will be the last element in the slice.
test := &pkg.tests[len(pkg.tests)-1]
test.addFile(c, er, info)
if !info.isExternalTest {
pkg.test.hasInternalTest = true
test.hasInternalTest = true
}
default:
pkg.library.addFile(c, er, info)
Expand All @@ -136,8 +144,11 @@ func (pkg *goPackage) firstGoFile() string {
goSrcs := []platformStringsBuilder{
pkg.library.sources,
pkg.binary.sources,
pkg.test.sources,
}
for _, test := range pkg.tests {
goSrcs = append(goSrcs, test.sources)
}

for _, sb := range goSrcs {
if sb.strs != nil {
for s := range sb.strs {
Expand All @@ -151,7 +162,15 @@ func (pkg *goPackage) firstGoFile() string {
}

func (pkg *goPackage) haveCgo() bool {
return pkg.library.cgo || pkg.binary.cgo || pkg.test.cgo
if pkg.library.cgo || pkg.binary.cgo {
return true
}
for _, t := range pkg.tests {
if t.cgo {
return true
}
}
return false
}

func (pkg *goPackage) inferImportPath(c *config.Config) error {
Expand Down Expand Up @@ -217,6 +236,22 @@ func testNameByConvention(nc namingConvention, imp string) string {
return libName + "_test"
}

// testNameFromSingleSource returns a suitable name for a go_test using the
// single Go source file name.
func testNameFromSingleSource(src string) string {
if i := strings.LastIndexByte(src, '.'); i >= 0 {
src = src[0:i]
}
libName := libNameFromImportPath(src)
if libName == "" {
return ""
}
if strings.HasSuffix(libName, "_test") {
return libName
}
return libName + "_test"
}

// binName returns a suitable name for a go_binary.
func binName(rel, prefix, repoRoot string) string {
return pathtools.RelBaseName(rel, prefix, repoRoot)
Expand Down
1 change: 1 addition & 0 deletions language/go/testdata/tests_per_file/BUILD.old
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# gazelle:go_test file
35 changes: 35 additions & 0 deletions language/go/testdata/tests_per_file/BUILD.want
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "tests_per_file",
srcs = ["lib.go"],
_gazelle_imports = [],
importpath = "example.com/repo/tests_per_file",
visibility = ["//visibility:public"],
)

go_test(
name = "bar_test",
srcs = ["bar_test.go"],
_gazelle_imports = ["testing"],
embed = [":tests_per_file"],
)

go_test(
name = "external_test",
srcs = ["external_test.go"],
_gazelle_imports = [
"example.com/repo/tests_per_file",
"testing",
],
)

go_test(
name = "foo_test",
srcs = ["foo_test.go"],
_gazelle_imports = [
"github.com/bazelbuild/bazel-gazelle/testtools",
"testing",
],
embed = [":tests_per_file"],
)
5 changes: 5 additions & 0 deletions language/go/testdata/tests_per_file/bar_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package tests_per_file

import "testing"

func TestStuff(t *testing.T) {}
11 changes: 11 additions & 0 deletions language/go/testdata/tests_per_file/external_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package tests_per_file_test

import (
"testing"

"example.com/repo/tests_per_file"
)

func TestStuff(t *testing.T) {
var _ tests_per_file.Type
}
11 changes: 11 additions & 0 deletions language/go/testdata/tests_per_file/foo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package tests_per_file

import (
"testing"

"github.com/bazelbuild/bazel-gazelle/testtools"
)

type fileSpec testtools.FileSpec

func TestStuff(t *testing.T) {}
3 changes: 3 additions & 0 deletions language/go/testdata/tests_per_file/lib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package tests_per_file

type Type int
29 changes: 29 additions & 0 deletions language/go/testdata/tests_per_file_from_default/BUILD.old
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# gazelle:go_test file
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "tests_per_file_from_default",
srcs = ["lib.go"],
_gazelle_imports = [],
importpath = "example.com/repo/tests_per_file_from_default",
visibility = ["//visibility:public"],
)

go_test(
name = "test_per_file_from_default_test",
srcs = [
"bar_test.go",
"foo_test.go",
],
_gazelle_imports = ["testing"],
embed = [":tests_per_file_from_default"],
)

go_test(
name = "external_test",
srcs = ["external_test.go"],
_gazelle_imports = [
"example.com/repo/tests_per_file_from_default",
"testing",
],
)
Loading