diff --git a/README.rst b/README.rst index ee80d0bf0..821af6e00 100644 --- a/README.rst +++ b/README.rst @@ -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. | diff --git a/language/go/config.go b/language/go/config.go index 3d47efac3..ae9462612 100644 --- a/language/go/config.go +++ b/language/go/config.go @@ -19,7 +19,6 @@ import ( "errors" "flag" "fmt" - "github.com/bazelbuild/bazel-gazelle/internal/module" "go/build" "io/ioutil" "log" @@ -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" @@ -126,6 +127,9 @@ 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 @@ -133,11 +137,45 @@ type goConfig struct { 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, @@ -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", @@ -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)) diff --git a/language/go/generate.go b/language/go/generate.go index 1e18f8c3a..53e397a12 100644 --- a/language/go/generate.go +++ b/language/go/generate.go @@ -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 { @@ -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. diff --git a/language/go/package.go b/language/go/package.go index 0b7871a5d..f04cfb96e 100644 --- a/language/go/package.go +++ b/language/go/package.go @@ -34,6 +34,7 @@ import ( type goPackage struct { name, dir, rel string library, binary, test goTarget + tests []goTarget proto protoTarget hasTestdata bool hasMainFunction bool @@ -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 @@ -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 { @@ -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 { @@ -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) diff --git a/language/go/testdata/tests_per_file/BUILD.old b/language/go/testdata/tests_per_file/BUILD.old new file mode 100644 index 000000000..6b4a37eb8 --- /dev/null +++ b/language/go/testdata/tests_per_file/BUILD.old @@ -0,0 +1 @@ +# gazelle:go_test file diff --git a/language/go/testdata/tests_per_file/BUILD.want b/language/go/testdata/tests_per_file/BUILD.want new file mode 100644 index 000000000..a85f6654f --- /dev/null +++ b/language/go/testdata/tests_per_file/BUILD.want @@ -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"], +) diff --git a/language/go/testdata/tests_per_file/bar_test.go b/language/go/testdata/tests_per_file/bar_test.go new file mode 100644 index 000000000..0a4417afe --- /dev/null +++ b/language/go/testdata/tests_per_file/bar_test.go @@ -0,0 +1,5 @@ +package tests_per_file + +import "testing" + +func TestStuff(t *testing.T) {} diff --git a/language/go/testdata/tests_per_file/external_test.go b/language/go/testdata/tests_per_file/external_test.go new file mode 100644 index 000000000..bd46bea97 --- /dev/null +++ b/language/go/testdata/tests_per_file/external_test.go @@ -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 +} diff --git a/language/go/testdata/tests_per_file/foo_test.go b/language/go/testdata/tests_per_file/foo_test.go new file mode 100644 index 000000000..5ba5ff86e --- /dev/null +++ b/language/go/testdata/tests_per_file/foo_test.go @@ -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) {} diff --git a/language/go/testdata/tests_per_file/lib.go b/language/go/testdata/tests_per_file/lib.go new file mode 100644 index 000000000..3eb35ae5c --- /dev/null +++ b/language/go/testdata/tests_per_file/lib.go @@ -0,0 +1,3 @@ +package tests_per_file + +type Type int diff --git a/language/go/testdata/tests_per_file_from_default/BUILD.old b/language/go/testdata/tests_per_file_from_default/BUILD.old new file mode 100644 index 000000000..767a95bba --- /dev/null +++ b/language/go/testdata/tests_per_file_from_default/BUILD.old @@ -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", + ], +) diff --git a/language/go/testdata/tests_per_file_from_default/BUILD.want b/language/go/testdata/tests_per_file_from_default/BUILD.want new file mode 100644 index 000000000..3da9bdf8d --- /dev/null +++ b/language/go/testdata/tests_per_file_from_default/BUILD.want @@ -0,0 +1,35 @@ +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 = "bar_test", + srcs = ["bar_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", + ], +) + +go_test( + name = "foo_test", + srcs = ["foo_test.go"], + _gazelle_imports = [ + "github.com/bazelbuild/bazel-gazelle/testtools", + "testing", + ], + embed = [":tests_per_file_from_default"], +) diff --git a/language/go/testdata/tests_per_file_from_default/bar_test.go b/language/go/testdata/tests_per_file_from_default/bar_test.go new file mode 100644 index 000000000..6b68bbd88 --- /dev/null +++ b/language/go/testdata/tests_per_file_from_default/bar_test.go @@ -0,0 +1,5 @@ +package tests_per_file_from_default + +import "testing" + +func TestOtherStuff(t *testing.T) {} diff --git a/language/go/testdata/tests_per_file_from_default/external_test.go b/language/go/testdata/tests_per_file_from_default/external_test.go new file mode 100644 index 000000000..c0a40228e --- /dev/null +++ b/language/go/testdata/tests_per_file_from_default/external_test.go @@ -0,0 +1,11 @@ +package tests_per_file_from_default_test + +import ( + "testing" + + "example.com/repo/tests_per_file_from_default" +) + +func TestStuff(t *testing.T) { + var _ tests_per_file_from_default.Type +} diff --git a/language/go/testdata/tests_per_file_from_default/foo_test.go b/language/go/testdata/tests_per_file_from_default/foo_test.go new file mode 100644 index 000000000..c73d79e11 --- /dev/null +++ b/language/go/testdata/tests_per_file_from_default/foo_test.go @@ -0,0 +1,11 @@ +package tests_per_file_from_default + +import ( + "testing" + + "github.com/bazelbuild/bazel-gazelle/testtools" +) + +type fileSpec testtools.FileSpec + +func TestStuff(t *testing.T) {} diff --git a/language/go/testdata/tests_per_file_from_default/lib.go b/language/go/testdata/tests_per_file_from_default/lib.go new file mode 100644 index 000000000..2ff71a381 --- /dev/null +++ b/language/go/testdata/tests_per_file_from_default/lib.go @@ -0,0 +1,3 @@ +package tests_per_file_from_default + +type Type int diff --git a/language/go/testdata/tests_per_file_new_file/BUILD.old b/language/go/testdata/tests_per_file_new_file/BUILD.old new file mode 100644 index 000000000..dbf1b15dd --- /dev/null +++ b/language/go/testdata/tests_per_file_new_file/BUILD.old @@ -0,0 +1,26 @@ +# gazelle:go_test file +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "tests_per_file_new_file", + srcs = ["lib.go"], + _gazelle_imports = [], + importpath = "example.com/repo/tests_per_file_new_file", + visibility = ["//visibility:public"], +) + +go_test( + name = "bar_test", + srcs = ["bar_test.go"], + _gazelle_imports = ["testing"], + embed = [":tests_per_file_new_file"], +) + +go_test( + name = "external_test", + srcs = ["external_test.go"], + _gazelle_imports = [ + "example.com/repo/tests_per_file_new_file", + "testing", + ], +) diff --git a/language/go/testdata/tests_per_file_new_file/BUILD.want b/language/go/testdata/tests_per_file_new_file/BUILD.want new file mode 100644 index 000000000..5b3978863 --- /dev/null +++ b/language/go/testdata/tests_per_file_new_file/BUILD.want @@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "tests_per_file_new_file", + srcs = ["lib.go"], + _gazelle_imports = [], + importpath = "example.com/repo/tests_per_file_new_file", + visibility = ["//visibility:public"], +) + +go_test( + name = "bar_test", + srcs = ["bar_test.go"], + _gazelle_imports = ["testing"], + embed = [":tests_per_file_new_file"], +) + +go_test( + name = "external_test", + srcs = ["external_test.go"], + _gazelle_imports = [ + "example.com/repo/tests_per_file_new_file", + "testing", + ], +) + +go_test( + name = "foo_test", + srcs = ["foo_test.go"], + _gazelle_imports = [ + "github.com/bazelbuild/bazel-gazelle/testtools", + "testing", + ], + embed = [":tests_per_file_new_file"], +) diff --git a/language/go/testdata/tests_per_file_new_file/bar_test.go b/language/go/testdata/tests_per_file_new_file/bar_test.go new file mode 100644 index 000000000..d649a28f5 --- /dev/null +++ b/language/go/testdata/tests_per_file_new_file/bar_test.go @@ -0,0 +1,5 @@ +package tests_per_file_new_file + +import "testing" + +func TestStuff(t *testing.T) {} diff --git a/language/go/testdata/tests_per_file_new_file/external_test.go b/language/go/testdata/tests_per_file_new_file/external_test.go new file mode 100644 index 000000000..a4ac08ae4 --- /dev/null +++ b/language/go/testdata/tests_per_file_new_file/external_test.go @@ -0,0 +1,11 @@ +package tests_per_file_new_file_test + +import ( + "testing" + + "example.com/repo/tests_per_file_new_file" +) + +func TestStuff(t *testing.T) { + var _ tests_per_file_new_file.Type +} diff --git a/language/go/testdata/tests_per_file_new_file/foo_test.go b/language/go/testdata/tests_per_file_new_file/foo_test.go new file mode 100644 index 000000000..b53ff3cda --- /dev/null +++ b/language/go/testdata/tests_per_file_new_file/foo_test.go @@ -0,0 +1,11 @@ +package tests_per_file_new_file + +import ( + "testing" + + "github.com/bazelbuild/bazel-gazelle/testtools" +) + +type fileSpec testtools.FileSpec + +func TestStuff(t *testing.T) {} diff --git a/language/go/testdata/tests_per_file_new_file/lib.go b/language/go/testdata/tests_per_file_new_file/lib.go new file mode 100644 index 000000000..032f6d34e --- /dev/null +++ b/language/go/testdata/tests_per_file_new_file/lib.go @@ -0,0 +1,3 @@ +package tests_per_file_new_file + +type Type int