github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/language/proto/generate_test.go (about)

     1  /* Copyright 2018 The Bazel Authors. All rights reserved.
     2  
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7     http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package proto
    17  
    18  import (
    19  	"os"
    20  	"path/filepath"
    21  	"reflect"
    22  	"runtime"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/bazelbuild/bazel-gazelle/config"
    27  	"github.com/bazelbuild/bazel-gazelle/language"
    28  	"github.com/bazelbuild/bazel-gazelle/merger"
    29  	"github.com/bazelbuild/bazel-gazelle/resolve"
    30  	"github.com/bazelbuild/bazel-gazelle/rule"
    31  	"github.com/bazelbuild/bazel-gazelle/testtools"
    32  	"github.com/bazelbuild/bazel-gazelle/walk"
    33  
    34  	bzl "github.com/bazelbuild/buildtools/build"
    35  )
    36  
    37  func TestGenerateRules(t *testing.T) {
    38  	if runtime.GOOS == "windows" {
    39  		// TODO(jayconrod): set up testdata directory on windows before running test
    40  		if _, err := os.Stat("testdata"); os.IsNotExist(err) {
    41  			t.Skip("testdata missing on windows due to lack of symbolic links")
    42  		} else if err != nil {
    43  			t.Fatal(err)
    44  		}
    45  	}
    46  
    47  	c, lang, _ := testConfig(t, "testdata")
    48  
    49  	walk.Walk(c, []config.Configurer{lang}, []string{"testdata"}, walk.VisitAllUpdateSubdirsMode, func(dir, rel string, c *config.Config, update bool, oldFile *rule.File, subdirs, regularFiles, genFiles []string) {
    50  		isTest := false
    51  		for _, name := range regularFiles {
    52  			if name == "BUILD.want" {
    53  				isTest = true
    54  				break
    55  			}
    56  		}
    57  		if !isTest {
    58  			return
    59  		}
    60  		t.Run(rel, func(t *testing.T) {
    61  			res := lang.GenerateRules(language.GenerateArgs{
    62  				Config:       c,
    63  				Dir:          dir,
    64  				Rel:          rel,
    65  				File:         oldFile,
    66  				Subdirs:      subdirs,
    67  				RegularFiles: regularFiles,
    68  				GenFiles:     genFiles,
    69  			})
    70  			if len(res.Empty) > 0 {
    71  				t.Errorf("got %d empty rules; want 0", len(res.Empty))
    72  			}
    73  			f := rule.EmptyFile("test", "")
    74  			for _, r := range res.Gen {
    75  				r.Insert(f)
    76  			}
    77  			convertImportsAttrs(f)
    78  			merger.FixLoads(f, lang.(language.ModuleAwareLanguage).ApparentLoads(func(string) string { return "" }))
    79  			f.Sync()
    80  			got := string(bzl.Format(f.File))
    81  			wantPath := filepath.Join(dir, "BUILD.want")
    82  			wantBytes, err := os.ReadFile(wantPath)
    83  			if err != nil {
    84  				t.Fatalf("error reading %s: %v", wantPath, err)
    85  			}
    86  			want := string(wantBytes)
    87  
    88  			if got != want {
    89  				t.Errorf("GenerateRules %q: got:\n%s\nwant:\n%s", rel, got, want)
    90  			}
    91  		})
    92  	})
    93  }
    94  
    95  func TestGenerateRulesEmpty(t *testing.T) {
    96  	lang := NewLanguage()
    97  	c := config.New()
    98  	c.Exts[protoName] = &ProtoConfig{}
    99  
   100  	oldContent := []byte(`
   101  proto_library(
   102      name = "dead_proto",
   103      srcs = ["foo.proto"],
   104  )
   105  
   106  proto_library(
   107      name = "live_proto",
   108      srcs = ["bar.proto"],
   109  )
   110  
   111  COMPLICATED_SRCS = ["baz.proto"]
   112  
   113  proto_library(
   114      name = "complicated_proto",
   115      srcs = COMPLICATED_SRCS,
   116  )
   117  `)
   118  	old, err := rule.LoadData("BUILD.bazel", "", oldContent)
   119  	if err != nil {
   120  		t.Fatal(err)
   121  	}
   122  	genFiles := []string{"bar.proto"}
   123  	res := lang.GenerateRules(language.GenerateArgs{
   124  		Config:   c,
   125  		Rel:      "foo",
   126  		File:     old,
   127  		GenFiles: genFiles,
   128  	})
   129  	if len(res.Gen) > 0 {
   130  		t.Errorf("got %d generated rules; want 0", len(res.Gen))
   131  	}
   132  	f := rule.EmptyFile("test", "")
   133  	for _, r := range res.Empty {
   134  		r.Insert(f)
   135  	}
   136  	f.Sync()
   137  	got := strings.TrimSpace(string(bzl.Format(f.File)))
   138  	want := `proto_library(name = "dead_proto")`
   139  	if got != want {
   140  		t.Errorf("got:\n%s\nwant:\n%s", got, want)
   141  	}
   142  }
   143  
   144  func TestGeneratePackage(t *testing.T) {
   145  	if runtime.GOOS == "windows" {
   146  		// TODO(jayconrod): set up testdata directory on windows before running test
   147  		if _, err := os.Stat("testdata"); os.IsNotExist(err) {
   148  			t.Skip("testdata missing on windows due to lack of symbolic links")
   149  		} else if err != nil {
   150  			t.Fatal(err)
   151  		}
   152  	}
   153  
   154  	lang := NewLanguage()
   155  	c, _, _ := testConfig(t, "testdata")
   156  	dir := filepath.FromSlash("testdata/protos")
   157  	res := lang.GenerateRules(language.GenerateArgs{
   158  		Config:       c,
   159  		Dir:          dir,
   160  		Rel:          "protos",
   161  		RegularFiles: []string{"foo.proto"},
   162  	})
   163  	r := res.Gen[0]
   164  	got := r.PrivateAttr(PackageKey).(Package)
   165  	want := Package{
   166  		Name: "bar.foo",
   167  		Files: map[string]FileInfo{
   168  			"foo.proto": {
   169  				Path:        filepath.Join(dir, "foo.proto"),
   170  				Name:        "foo.proto",
   171  				PackageName: "bar.foo",
   172  				Options:     []Option{{Key: "go_package", Value: "example.com/repo/protos"}},
   173  				Imports: []string{
   174  					"google/protobuf/any.proto",
   175  					"protos/sub/sub.proto",
   176  				},
   177  				HasServices: true,
   178  			},
   179  		},
   180  		Imports: map[string]bool{
   181  			"google/protobuf/any.proto": true,
   182  			"protos/sub/sub.proto":      true,
   183  		},
   184  		Options: map[string]string{
   185  			"go_package": "example.com/repo/protos",
   186  		},
   187  		HasServices: true,
   188  	}
   189  	if !reflect.DeepEqual(got, want) {
   190  		t.Errorf("got %#v; want %#v", got, want)
   191  	}
   192  }
   193  
   194  func TestFileModeImports(t *testing.T) {
   195  	if runtime.GOOS == "windows" {
   196  		// TODO(jayconrod): set up testdata directory on windows before running test
   197  		if _, err := os.Stat("testdata"); os.IsNotExist(err) {
   198  			t.Skip("testdata missing on windows due to lack of symbolic links")
   199  		} else if err != nil {
   200  			t.Fatal(err)
   201  		}
   202  	}
   203  
   204  	lang := NewLanguage()
   205  	c, _, _ := testConfig(t, "testdata")
   206  	c.Exts[protoName] = &ProtoConfig{
   207  		Mode: FileMode,
   208  	}
   209  
   210  	dir := filepath.FromSlash("testdata/file_mode")
   211  	res := lang.GenerateRules(language.GenerateArgs{
   212  		Config:       c,
   213  		Dir:          dir,
   214  		Rel:          "file_mode",
   215  		RegularFiles: []string{"foo.proto", "bar.proto"},
   216  	})
   217  
   218  	if len(res.Gen) != 2 {
   219  		t.Error("expected 2 generated packages")
   220  	}
   221  
   222  	bar := res.Gen[0].PrivateAttr(PackageKey).(Package)
   223  	foo := res.Gen[1].PrivateAttr(PackageKey).(Package)
   224  
   225  	// I believe the packages are sorted by name, but just in case..
   226  	if bar.RuleName == "foo" {
   227  		bar, foo = foo, bar
   228  	}
   229  
   230  	expectedFoo := Package{
   231  		Name:     "file_mode",
   232  		RuleName: "foo",
   233  		Files: map[string]FileInfo{
   234  			"foo.proto": {
   235  				Path:        filepath.Join(dir, "foo.proto"),
   236  				Name:        "foo.proto",
   237  				PackageName: "file_mode",
   238  			},
   239  		},
   240  		Imports: map[string]bool{},
   241  		Options: map[string]string{},
   242  	}
   243  
   244  	expectedBar := Package{
   245  		Name:     "file_mode",
   246  		RuleName: "bar",
   247  		Files: map[string]FileInfo{
   248  			"bar.proto": {
   249  				Path:        filepath.Join(dir, "bar.proto"),
   250  				Name:        "bar.proto",
   251  				PackageName: "file_mode",
   252  				Imports: []string{
   253  					"file_mode/foo.proto",
   254  				},
   255  			},
   256  		},
   257  		// Imports should contain foo.proto. This is specific to file mode.
   258  		// In package mode, this import would be omitted as both foo.proto
   259  		// and bar.proto exist within the same package.
   260  		Imports: map[string]bool{
   261  			"file_mode/foo.proto": true,
   262  		},
   263  		Options: map[string]string{},
   264  	}
   265  
   266  	if !reflect.DeepEqual(foo, expectedFoo) {
   267  		t.Errorf("got %#v; want %#v", foo, expectedFoo)
   268  	}
   269  	if !reflect.DeepEqual(bar, expectedBar) {
   270  		t.Errorf("got %#v; want %#v", bar, expectedBar)
   271  	}
   272  }
   273  
   274  // TestConsumedGenFiles checks that generated files that have been consumed by
   275  // other rules should not be added to the rule
   276  func TestConsumedGenFiles(t *testing.T) {
   277  	if runtime.GOOS == "windows" {
   278  		// TODO(jayconrod): set up testdata directory on windows before running test
   279  		if _, err := os.Stat("testdata"); os.IsNotExist(err) {
   280  			t.Skip("testdata missing on windows due to lack of symbolic links")
   281  		} else if err != nil {
   282  			t.Fatal(err)
   283  		}
   284  	}
   285  
   286  	oldContent := []byte(`
   287  proto_library(
   288      name = "existing_gen_proto",
   289      srcs = ["gen.proto"],
   290  )
   291  proto_library(
   292      name = "dead_proto",
   293      srcs = ["dead.proto"],
   294  )
   295  `)
   296  	old, err := rule.LoadData("BUILD.bazel", "", oldContent)
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  
   301  	genRule1 := rule.NewRule("proto_library", "gen_proto")
   302  	genRule1.SetAttr("srcs", []string{"gen.proto"})
   303  	genRule2 := rule.NewRule("filegroup", "filegroup_protos")
   304  	genRule2.SetAttr("srcs", []string{"gen.proto", "gen_not_consumed.proto"})
   305  
   306  	c, lang, _ := testConfig(t, "testdata")
   307  
   308  	res := lang.GenerateRules(language.GenerateArgs{
   309  		Config:       c,
   310  		Dir:          filepath.FromSlash("testdata/protos"),
   311  		File:         old,
   312  		Rel:          "protos",
   313  		RegularFiles: []string{"foo.proto"},
   314  		GenFiles:     []string{"gen.proto", "gen_not_consumed.proto"},
   315  		OtherGen:     []*rule.Rule{genRule1, genRule2},
   316  	})
   317  
   318  	// Make sure that "gen.proto" is not added to existing foo_proto rule
   319  	// because it is consumed by existing_gen_proto proto_library.
   320  	// "gen_not_consumed.proto" is added to existing foo_proto rule because
   321  	// it is not consumed by "proto_library". "filegroup" consumption is
   322  	// ignored.
   323  	fg := rule.EmptyFile("test_gen", "")
   324  	for _, r := range res.Gen {
   325  		r.Insert(fg)
   326  	}
   327  	gotGen := strings.TrimSpace(string(fg.Format()))
   328  	wantGen := `proto_library(
   329      name = "protos_proto",
   330      srcs = [
   331          "foo.proto",
   332          "gen_not_consumed.proto",
   333      ],
   334      visibility = ["//visibility:public"],
   335  )`
   336  
   337  	if gotGen != wantGen {
   338  		t.Errorf("got:\n%s\nwant:\n%s", gotGen, wantGen)
   339  	}
   340  
   341  	// Make sure that gen.proto is not among empty because it is in GenFiles
   342  	fe := rule.EmptyFile("test_empty", "")
   343  	for _, r := range res.Empty {
   344  		r.Insert(fe)
   345  	}
   346  	got := strings.TrimSpace(string(fe.Format()))
   347  	want := `proto_library(name = "dead_proto")`
   348  	if got != want {
   349  		t.Errorf("got:\n%s\nwant:\n%s", got, want)
   350  	}
   351  }
   352  
   353  func testConfig(t *testing.T, repoRoot string) (*config.Config, language.Language, []config.Configurer) {
   354  	cexts := []config.Configurer{
   355  		&config.CommonConfigurer{},
   356  		&walk.Configurer{},
   357  		&resolve.Configurer{},
   358  	}
   359  	lang := NewLanguage()
   360  	c := testtools.NewTestConfig(t, cexts, []language.Language{lang}, []string{
   361  		"-build_file_name=BUILD.old",
   362  		"-repo_root=" + repoRoot,
   363  	})
   364  	cexts = append(cexts, lang)
   365  	return c, lang, cexts
   366  }
   367  
   368  // convertImportsAttrs copies private attributes to regular attributes, which
   369  // will later be written out to build files. This allows tests to check the
   370  // values of private attributes with simple string comparison.
   371  func convertImportsAttrs(f *rule.File) {
   372  	for _, r := range f.Rules {
   373  		v := r.PrivateAttr(config.GazelleImportsKey)
   374  		if v != nil {
   375  			r.SetAttr(config.GazelleImportsKey, v)
   376  		}
   377  	}
   378  }