Skip to content

Commit

Permalink
add a go_test directive to enable generating go_test targets per _tes…
Browse files Browse the repository at this point in the history
…t.go file (bazel-contrib#1597)

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

* address comments

* add new tests for updating to per-file mode

---------

Co-authored-by: Fabian Meumertzheim <[email protected]>
  • Loading branch information
2 people authored and jeromep-stripe committed Mar 22, 2024
1 parent f09c9de commit fc149a2
Show file tree
Hide file tree
Showing 22 changed files with 389 additions and 22 deletions.
9 changes: 9 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,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"

"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)
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
59 changes: 42 additions & 17 deletions language/go/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,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 @@ -640,24 +639,50 @@ 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 embeds []string
if pkg.test.hasInternalTest {
if library != "" {
embeds = append(embeds, 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, embeds)
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 embeds []string
if test.hasInternalTest {
if library != "" {
embeds = append(embeds, library)
}
}
g.setCommonAttrs(goTest, pkg.rel, nil, test, embeds)
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
43 changes: 39 additions & 4 deletions language/go/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
type goPackage struct {
name, dir, rel string
library, binary, test goTarget
tests []goTarget
proto protoTarget
hasTestdata bool
hasMainFunction bool
Expand Down Expand Up @@ -108,9 +109,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.hasMainFunction = pkg.hasMainFunction || info.hasMainFunction
Expand Down Expand Up @@ -138,8 +146,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 @@ -153,7 +164,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 @@ -219,6 +238,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

0 comments on commit fc149a2

Please sign in to comment.