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 }