/* Copyright 2018 The Bazel Authors. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package golang import ( "os" "path" "path/filepath" "runtime" "strings" "testing" "github.com/bazelbuild/bazel-gazelle/config" "github.com/bazelbuild/bazel-gazelle/language" "github.com/bazelbuild/bazel-gazelle/language/proto" "github.com/bazelbuild/bazel-gazelle/merger" "github.com/bazelbuild/bazel-gazelle/rule" "github.com/bazelbuild/bazel-gazelle/walk" bzl "github.com/bazelbuild/buildtools/build" "github.com/bazelbuild/rules_go/go/tools/bazel" "github.com/google/go-cmp/cmp" ) func TestGenerateRules(t *testing.T) { testdataDir := "testdata" if runtime.GOOS == "windows" { var err error testdataDir, err = bazel.NewTmpDir("testdata") if err != nil { t.Fatal(err) } files, _ := bazel.ListRunfiles() parent := "language/go/testdata" for _, rf := range files { rel, err := filepath.Rel(parent, rf.ShortPath) if err != nil { continue } if strings.HasPrefix(rel, "..") { // make sure we're not moving around file that we're not inrerested in continue } newPath := filepath.FromSlash(path.Join(testdataDir, rel)) if err := os.MkdirAll(filepath.FromSlash(filepath.Dir(newPath)), os.ModePerm); err != nil { t.Fatal(err) } if err := os.Link(filepath.FromSlash(rf.Path), newPath); err != nil { t.Fatal(err) } } } c, langs, cexts := testConfig( t, "-build_file_name=BUILD.old", "-go_prefix=example.com/repo", "-repo_root="+testdataDir) // runfiles are symbolic links, which we need Walk to follow. content := []byte(` # gazelle:follow ** `) f, err := rule.LoadData(filepath.FromSlash("BUILD.config"), "config", content) if err != nil { t.Fatal(err) } for _, cext := range cexts { cext.Configure(c, "", f) } var loads []rule.LoadInfo for _, lang := range langs { loads = append(loads, lang.(language.ModuleAwareLanguage).ApparentLoads(func(string) string { return "" })...) } var testsFound int walk.Walk(c, cexts, []string{testdataDir}, walk.VisitAllUpdateSubdirsMode, func(dir, rel string, c *config.Config, update bool, oldFile *rule.File, subdirs, regularFiles, genFiles []string) { t.Run(rel, func(t *testing.T) { var empty, gen []*rule.Rule for _, lang := range langs { res := lang.GenerateRules(language.GenerateArgs{ Config: c, Dir: dir, Rel: rel, File: oldFile, Subdirs: subdirs, RegularFiles: regularFiles, GenFiles: genFiles, OtherEmpty: empty, OtherGen: gen, }) empty = append(empty, res.Empty...) gen = append(gen, res.Gen...) } isTest := false for _, name := range regularFiles { if name == "BUILD.want" { isTest = true break } } if !isTest { // GenerateRules may have side effects, so we need to run it, even if // there's no test. return } testsFound += 1 f := rule.EmptyFile("test", "") for _, r := range gen { r.Insert(f) } convertImportsAttrs(f) merger.FixLoads(f, loads) f.Sync() got := string(bzl.Format(f.File)) wantPath := filepath.Join(dir, "BUILD.want") wantBytes, err := os.ReadFile(wantPath) if err != nil { t.Fatalf("error reading %s: %v", wantPath, err) } want := string(wantBytes) want = strings.ReplaceAll(want, "\r\n", "\n") if diff := cmp.Diff(want, got); diff != "" { t.Errorf("(-want, +got): %s", diff) } }) }) // Avoid spurious success if we fail to find any tests. if testsFound == 0 { t.Error("No rule generation tests were found") } } func TestGenerateRulesEmpty(t *testing.T) { c, langs, _ := testConfig(t, "-go_prefix=example.com/repo") goLang := langs[1].(*goLang) res := goLang.GenerateRules(language.GenerateArgs{ Config: c, Dir: "./foo", Rel: "foo", }) if len(res.Gen) > 0 { t.Errorf("got %d generated rules; want 0", len(res.Gen)) } f := rule.EmptyFile("test", "") for _, r := range res.Empty { r.Insert(f) } f.Sync() got := strings.TrimSpace(string(bzl.Format(f.File))) want := strings.TrimSpace(` filegroup(name = "go_default_library_protos") go_proto_library(name = "foo_go_proto") go_library(name = "foo") go_binary(name = "foo") go_test(name = "foo_test") `) if got != want { t.Errorf("got:\n%s\nwant:\n%s", got, want) } } func TestGenerateRulesEmptyLegacyProto(t *testing.T) { c, langs, _ := testConfig(t, "-proto=legacy") goLang := langs[len(langs)-1].(*goLang) res := goLang.GenerateRules(language.GenerateArgs{ Config: c, Dir: "./foo", Rel: "foo", }) for _, e := range res.Empty { if kind := e.Kind(); kind == "proto_library" || kind == "go_proto_library" || kind == "go_grpc_library" { t.Errorf("deleted rule %s ; should not delete in legacy proto mode", kind) } } } func TestGenerateRulesEmptyPackageProto(t *testing.T) { c, langs, _ := testConfig(t, "-proto=package", "-go_prefix=example.com/repo") oldContent := []byte(` proto_library( name = "dead_proto", srcs = ["dead.proto"], ) `) old, err := rule.LoadData("BUILD.bazel", "", oldContent) if err != nil { t.Fatal(err) } var empty []*rule.Rule for _, lang := range langs { res := lang.GenerateRules(language.GenerateArgs{ Config: c, Dir: "./foo", Rel: "foo", File: old, OtherEmpty: empty, }) empty = append(empty, res.Empty...) } f := rule.EmptyFile("test", "") for _, r := range empty { r.Insert(f) } f.Sync() got := strings.TrimSpace(string(bzl.Format(f.File))) want := strings.TrimSpace(` proto_library(name = "dead_proto") go_proto_library(name = "dead_go_proto") filegroup(name = "go_default_library_protos") go_proto_library(name = "foo_go_proto") go_library(name = "foo") go_binary(name = "foo") go_test(name = "foo_test") `) if got != want { t.Errorf("got:\n%s\nwant:\n%s", got, want) } } func TestGenerateRulesPrebuiltGoProtoRules(t *testing.T) { for _, protoFlag := range []string{ "-proto=default", "-proto=package", } { t.Run("with flag: "+protoFlag, func(t *testing.T) { c, langs, _ := testConfig(t, protoFlag) goLang := langs[len(langs)-1].(*goLang) res := goLang.GenerateRules(language.GenerateArgs{ Config: c, Dir: "./foo", Rel: "foo", OtherGen: prebuiltProtoRules(), }) if len(res.Gen) != 0 { t.Errorf("got %d generated rules; want 0", len(res.Gen)) } f := rule.EmptyFile("test", "") for _, r := range res.Gen { r.Insert(f) } f.Sync() got := strings.TrimSpace(string(bzl.Format(f.File))) want := strings.TrimSpace(` `) if got != want { t.Errorf("got:\n%s\nwant:\n%s", got, want) } }) } } // Test generated files that have been consumed by other rules should not be // added to the go_default_library rule func TestConsumedGenFiles(t *testing.T) { args := language.GenerateArgs{ RegularFiles: []string{"regular.go"}, GenFiles: []string{"mocks.go"}, Config: &config.Config{ Exts: make(map[string]interface{}), }, } otherRule := rule.NewRule("go_library", "go_mock_library") otherRule.SetAttr("srcs", []string{"mocks.go"}) args.OtherGen = append(args.OtherGen, otherRule) gl := goLang{ goPkgRels: make(map[string]bool), } gl.Configure(args.Config, "", nil) res := gl.GenerateRules(args) got := res.Gen[0].AttrStrings("srcs") want := []string{"regular.go"} if len(got) != len(want) || got[0] != want[0] { t.Errorf("got:\n%s\nwant:\n%s", got, want) } } // Test visibility attribute is only set if no default visibility is provided // by the file or other rules. func TestShouldSetVisibility(t *testing.T) { if !shouldSetVisibility(language.GenerateArgs{}) { t.Error("got 'False' for shouldSetVisibility with default args; expected 'True'") } if !shouldSetVisibility(language.GenerateArgs{ File: rule.EmptyFile("path", "pkg"), }) { t.Error("got 'False' for shouldSetVisibility with empty file; expected 'True'") } fileWithDefaultVisibile, _ := rule.LoadData("path", "pkg", []byte(`package(default_visibility = "//src:__subpackages__")`)) if shouldSetVisibility(language.GenerateArgs{ File: fileWithDefaultVisibile, }) { t.Error("got 'True' for shouldSetVisibility with file with default visibility; expected 'False'") } defaultVisibilityRule := rule.NewRule("package", "") defaultVisibilityRule.SetAttr("default_visibility", []string{"//src:__subpackages__"}) if shouldSetVisibility(language.GenerateArgs{ OtherGen: []*rule.Rule{defaultVisibilityRule}, }) { t.Error("got 'True' for shouldSetVisibility with rule defining a default visibility; expected 'False'") } } func prebuiltProtoRules() []*rule.Rule { protoRule := rule.NewRule("proto_library", "foo_proto") protoRule.SetAttr("srcs", []string{"foo.proto"}) protoRule.SetAttr("visibility", []string{"//visibility:public"}) protoRule.SetPrivateAttr(proto.PackageKey, proto.Package{ Name: "foo", Files: map[string]proto.FileInfo{ "foo.proto": {}, }, Imports: map[string]bool{}, Options: map[string]string{}, }, ) goProtoRule := rule.NewRule("go_proto_library", "foo_go_proto") goProtoRule.SetAttr("compilers", []string{"@io_bazel_rules_go//proto:go_proto"}) goProtoRule.SetAttr("importpath", "hello/world/foo") goProtoRule.SetAttr("proto", ":foo_proto") protoRule.SetAttr("visibility", []string{"//visibility:public"}) return []*rule.Rule{protoRule, goProtoRule} } // convertImportsAttrs copies private attributes to regular attributes, which // will later be written out to build files. This allows tests to check the // values of private attributes with simple string comparison. func convertImportsAttrs(f *rule.File) { for _, r := range f.Rules { v := r.PrivateAttr(config.GazelleImportsKey) if v != nil { r.SetAttr(config.GazelleImportsKey, v) } } }