github.com/googleapis/api-linter@v1.65.2/lint/rule_test.go (about) 1 // Copyright 2019 Google LLC 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 // https://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 package lint 16 17 import ( 18 "reflect" 19 "testing" 20 21 "github.com/jhump/protoreflect/desc" 22 "github.com/jhump/protoreflect/desc/builder" 23 ) 24 25 func TestFileRule(t *testing.T) { 26 // Create a file descriptor with nothing in it. 27 fd, err := builder.NewFile("test.proto").Build() 28 if err != nil { 29 t.Fatalf("Could not build file descriptor: %q", err) 30 } 31 32 // Iterate over the tests and run them. 33 for _, test := range makeLintRuleTests(fd) { 34 t.Run(test.testName, func(t *testing.T) { 35 rule := &FileRule{ 36 Name: RuleName("test"), 37 OnlyIf: func(fd *desc.FileDescriptor) bool { 38 return fd.GetName() == "test.proto" 39 }, 40 LintFile: func(fd *desc.FileDescriptor) []Problem { 41 return test.problems 42 }, 43 } 44 45 // Run the rule and assert that we got what we expect. 46 test.runRule(rule, fd, t) 47 }) 48 } 49 } 50 51 func TestMessageRule(t *testing.T) { 52 // Create a file descriptor with two messages in it. 53 fd, err := builder.NewFile("test.proto").AddMessage( 54 builder.NewMessage("Book"), 55 ).AddMessage( 56 builder.NewMessage("Author"), 57 ).Build() 58 if err != nil { 59 t.Fatalf("Failed to build file descriptor.") 60 } 61 62 // Iterate over the tests and run them. 63 for _, test := range makeLintRuleTests(fd.GetMessageTypes()[1]) { 64 t.Run(test.testName, func(t *testing.T) { 65 // Create the message rule. 66 rule := &MessageRule{ 67 Name: RuleName("test"), 68 OnlyIf: func(m *desc.MessageDescriptor) bool { 69 return m.GetName() == "Author" 70 }, 71 LintMessage: func(m *desc.MessageDescriptor) []Problem { 72 return test.problems 73 }, 74 } 75 76 // Run the rule and assert that we got what we expect. 77 test.runRule(rule, fd, t) 78 }) 79 } 80 } 81 82 // Establish that nested messages are tested. 83 func TestMessageRuleNested(t *testing.T) { 84 // Create a file descriptor with a message and nested message in it. 85 fd, err := builder.NewFile("test.proto").AddMessage( 86 builder.NewMessage("Book").AddNestedMessage(builder.NewMessage("Author")), 87 ).Build() 88 if err != nil { 89 t.Fatalf("Failed to build file descriptor.") 90 } 91 92 // Iterate over the tests and run them. 93 for _, test := range makeLintRuleTests(fd.GetMessageTypes()[0].GetNestedMessageTypes()[0]) { 94 t.Run(test.testName, func(t *testing.T) { 95 // Create the message rule. 96 rule := &MessageRule{ 97 Name: RuleName("test"), 98 OnlyIf: func(m *desc.MessageDescriptor) bool { 99 return m.GetName() == "Author" 100 }, 101 LintMessage: func(m *desc.MessageDescriptor) []Problem { 102 return test.problems 103 }, 104 } 105 106 // Run the rule and assert that we got what we expect. 107 test.runRule(rule, fd, t) 108 }) 109 } 110 } 111 112 func TestFieldRule(t *testing.T) { 113 // Create a file descriptor with one message and two fields in that message. 114 fd, err := builder.NewFile("test.proto").AddMessage( 115 builder.NewMessage("Book").AddField( 116 builder.NewField("title", builder.FieldTypeString()), 117 ).AddField( 118 builder.NewField("edition_count", builder.FieldTypeInt32()), 119 ), 120 ).Build() 121 if err != nil { 122 t.Fatalf("Failed to build file descriptor.") 123 } 124 125 // Iterate over the tests and run them. 126 for _, test := range makeLintRuleTests(fd.GetMessageTypes()[0].GetFields()[1]) { 127 t.Run(test.testName, func(t *testing.T) { 128 // Create the field rule. 129 rule := &FieldRule{ 130 Name: RuleName("test"), 131 OnlyIf: func(f *desc.FieldDescriptor) bool { 132 return f.GetName() == "edition_count" 133 }, 134 LintField: func(f *desc.FieldDescriptor) []Problem { 135 return test.problems 136 }, 137 } 138 139 // Run the rule and assert that we got what we expect. 140 test.runRule(rule, fd, t) 141 }) 142 } 143 } 144 145 func TestServiceRule(t *testing.T) { 146 // Create a file descriptor with a service. 147 fd, err := builder.NewFile("test.proto").AddService( 148 builder.NewService("Library"), 149 ).Build() 150 if err != nil { 151 t.Fatalf("Failed to build a file descriptor: %q", err) 152 } 153 154 // Iterate over the tests and run them. 155 for _, test := range makeLintRuleTests(fd.GetServices()[0]) { 156 t.Run(test.testName, func(t *testing.T) { 157 // Create the service rule. 158 rule := &ServiceRule{ 159 Name: RuleName("test"), 160 LintService: func(s *desc.ServiceDescriptor) []Problem { 161 return test.problems 162 }, 163 } 164 165 // Run the rule and assert that we got what we expect. 166 test.runRule(rule, fd, t) 167 }) 168 } 169 } 170 171 func TestMethodRule(t *testing.T) { 172 // Create a file descriptor with a service. 173 book := builder.RpcTypeMessage(builder.NewMessage("Book"), false) 174 fd, err := builder.NewFile("test.proto").AddService( 175 builder.NewService("Library").AddMethod( 176 builder.NewMethod( 177 "GetBook", 178 builder.RpcTypeMessage(builder.NewMessage("GetBookRequest"), false), 179 book, 180 ), 181 ).AddMethod( 182 builder.NewMethod( 183 "CreateBook", 184 builder.RpcTypeMessage(builder.NewMessage("CreateBookRequest"), false), 185 book, 186 ), 187 ), 188 ).Build() 189 if err != nil { 190 t.Fatalf("Failed to build a file descriptor: %q", err) 191 } 192 193 // Iterate over the tests and run them. 194 for _, test := range makeLintRuleTests(fd.GetServices()[0].GetMethods()[1]) { 195 t.Run(test.testName, func(t *testing.T) { 196 // Create the method rule. 197 rule := &MethodRule{ 198 Name: RuleName("test"), 199 OnlyIf: func(m *desc.MethodDescriptor) bool { 200 return m.GetName() == "CreateBook" 201 }, 202 LintMethod: func(m *desc.MethodDescriptor) []Problem { 203 return test.problems 204 }, 205 } 206 207 // Run the rule and assert that we got what we expect. 208 test.runRule(rule, fd, t) 209 }) 210 } 211 } 212 213 func TestEnumRule(t *testing.T) { 214 // Create a file descriptor with top-level enums. 215 fd, err := builder.NewFile("test.proto").AddEnum( 216 builder.NewEnum("Format").AddValue(builder.NewEnumValue("PDF")), 217 ).AddEnum( 218 builder.NewEnum("Edition").AddValue(builder.NewEnumValue("PUBLISHER_ONLY")), 219 ).Build() 220 if err != nil { 221 t.Fatalf("Error building test proto:%s ", err) 222 } 223 224 for _, test := range makeLintRuleTests(fd.GetEnumTypes()[1]) { 225 t.Run(test.testName, func(t *testing.T) { 226 // Create the enum rule. 227 rule := &EnumRule{ 228 Name: RuleName("test"), 229 OnlyIf: func(e *desc.EnumDescriptor) bool { 230 return e.GetName() == "Edition" 231 }, 232 LintEnum: func(e *desc.EnumDescriptor) []Problem { 233 return test.problems 234 }, 235 } 236 237 // Run the rule and assert that we got what we expect. 238 test.runRule(rule, fd, t) 239 }) 240 } 241 } 242 243 func TestEnumValueRule(t *testing.T) { 244 // Create a file descriptor with a top-level enum with values. 245 fd, err := builder.NewFile("test.proto").AddEnum( 246 builder.NewEnum("Format").AddValue(builder.NewEnumValue("YAML")).AddValue(builder.NewEnumValue("JSON")), 247 ).Build() 248 if err != nil { 249 t.Fatalf("Error building test proto:%s ", err) 250 } 251 252 for _, test := range makeLintRuleTests(fd.GetEnumTypes()[0].GetValues()[1]) { 253 t.Run(test.testName, func(t *testing.T) { 254 // Create the enum value rule. 255 rule := &EnumValueRule{ 256 Name: RuleName("test"), 257 OnlyIf: func(e *desc.EnumValueDescriptor) bool { 258 return e.GetName() == "JSON" 259 }, 260 LintEnumValue: func(e *desc.EnumValueDescriptor) []Problem { 261 return test.problems 262 }, 263 } 264 265 // Run the rule and assert that we got what we expect. 266 test.runRule(rule, fd, t) 267 }) 268 } 269 } 270 271 func TestEnumRuleNested(t *testing.T) { 272 // Create a file descriptor with top-level enums. 273 fd, err := builder.NewFile("test.proto").AddMessage( 274 builder.NewMessage("Book").AddNestedEnum( 275 builder.NewEnum("Format").AddValue(builder.NewEnumValue("PDF")), 276 ).AddNestedEnum( 277 builder.NewEnum("Edition").AddValue(builder.NewEnumValue("PUBLISHER_ONLY")), 278 ), 279 ).Build() 280 if err != nil { 281 t.Fatalf("Error building test proto:%s ", err) 282 } 283 284 for _, test := range makeLintRuleTests(fd.GetMessageTypes()[0].GetNestedEnumTypes()[1]) { 285 t.Run(test.testName, func(t *testing.T) { 286 // Create the enum rule. 287 rule := &EnumRule{ 288 Name: RuleName("test"), 289 OnlyIf: func(e *desc.EnumDescriptor) bool { 290 return e.GetName() == "Edition" 291 }, 292 LintEnum: func(e *desc.EnumDescriptor) []Problem { 293 return test.problems 294 }, 295 } 296 297 // Run the rule and assert that we got what we expect. 298 test.runRule(rule, fd, t) 299 }) 300 } 301 } 302 303 func TestDescriptorRule(t *testing.T) { 304 // Create a file with one of everything in it. 305 book := builder.NewMessage("Book").AddNestedEnum( 306 builder.NewEnum("Format").AddValue( 307 builder.NewEnumValue("FORMAT_UNSPECIFIED"), 308 ).AddValue(builder.NewEnumValue("PAPERBACK")), 309 ).AddField(builder.NewField("name", builder.FieldTypeString())).AddNestedMessage( 310 builder.NewMessage("Author"), 311 ) 312 fd, err := builder.NewFile("library.proto").AddMessage(book).AddService( 313 builder.NewService("Library").AddMethod( 314 builder.NewMethod( 315 "ConjureBook", 316 builder.RpcTypeMessage(book, false), 317 builder.RpcTypeMessage(book, false), 318 ), 319 ), 320 ).AddEnum(builder.NewEnum("State").AddValue(builder.NewEnumValue("AVAILABLE"))).Build() 321 if err != nil { 322 t.Fatalf("%v", err) 323 } 324 325 // Create a rule that lets us verify that each descriptor was visited. 326 visited := make(map[string]desc.Descriptor) 327 rule := &DescriptorRule{ 328 Name: RuleName("test"), 329 OnlyIf: func(d desc.Descriptor) bool { 330 return d.GetName() != "FORMAT_UNSPECIFIED" 331 }, 332 LintDescriptor: func(d desc.Descriptor) []Problem { 333 visited[d.GetName()] = d 334 return nil 335 }, 336 } 337 338 // Run the rule. 339 rule.Lint(fd) 340 341 // Verify that each descriptor was visited. 342 // We do not care what order they were visited in. 343 wantDescriptors := []string{ 344 "Author", "Book", "ConjureBook", "Format", "PAPERBACK", 345 "name", "Library", "State", "AVAILABLE", 346 } 347 if got, want := rule.GetName(), "test"; string(got) != want { 348 t.Errorf("Got name %q, wanted %q", got, want) 349 } 350 if got, want := len(visited), len(wantDescriptors); got != want { 351 t.Errorf("Got %d descriptors, wanted %d.", got, want) 352 } 353 for _, name := range wantDescriptors { 354 if _, ok := visited[name]; !ok { 355 t.Errorf("Missing descriptor %q.", name) 356 } 357 } 358 } 359 360 type lintRuleTest struct { 361 testName string 362 problems []Problem 363 } 364 365 // runRule runs a rule within a test environment. 366 func (test *lintRuleTest) runRule(rule ProtoRule, fd *desc.FileDescriptor, t *testing.T) { 367 // Establish that the metadata methods work. 368 if got, want := string(rule.GetName()), string(RuleName("test")); got != want { 369 t.Errorf("Got %q for GetName(), expected %q", got, want) 370 } 371 372 // Run the rule's lint function on the file descriptor 373 // and assert that we got what we expect. 374 if got, want := rule.Lint(fd), test.problems; !reflect.DeepEqual(got, want) { 375 t.Errorf("Got %v problems; expected %v.", got, want) 376 } 377 } 378 379 // makeLintRuleTests generates boilerplate tests that are consistent for 380 // each type of rule. 381 func makeLintRuleTests(d desc.Descriptor) []lintRuleTest { 382 return []lintRuleTest{ 383 {"NoProblems", []Problem{}}, 384 {"OneProblem", []Problem{{ 385 Message: "There was a problem.", 386 Descriptor: d, 387 }}}, 388 {"TwoProblems", []Problem{ 389 { 390 Message: "This was the first problem.", 391 Descriptor: d, 392 }, 393 { 394 Message: "This was the second problem.", 395 Descriptor: d, 396 }, 397 }}, 398 } 399 }