github.com/jhump/protocompile@v0.0.0-20221021153901-4f6f732835e8/linker/linker_test.go (about) 1 package linker_test 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "math" 10 "strconv" 11 "strings" 12 "testing" 13 14 "github.com/stretchr/testify/assert" 15 "google.golang.org/protobuf/encoding/protojson" 16 "google.golang.org/protobuf/encoding/protowire" 17 "google.golang.org/protobuf/proto" 18 "google.golang.org/protobuf/reflect/protodesc" 19 "google.golang.org/protobuf/reflect/protoreflect" 20 "google.golang.org/protobuf/reflect/protoregistry" 21 "google.golang.org/protobuf/types/descriptorpb" 22 23 "github.com/jhump/protocompile" 24 _ "github.com/jhump/protocompile/internal/testprotos" 25 "github.com/jhump/protocompile/linker" 26 ) 27 28 func TestSimpleLink(t *testing.T) { 29 compiler := protocompile.Compiler{ 30 Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{ 31 ImportPaths: []string{"../internal/testprotos"}, 32 }), 33 } 34 fds, err := compiler.Compile(context.Background(), "desc_test_complex.proto") 35 if !assert.Nil(t, err) { 36 return 37 } 38 39 res := fds[0].(linker.Result) 40 fdset := loadDescriptorSet(t, "../internal/testprotos/desc_test_complex.protoset", linker.ResolverFromFile(fds[0])) 41 checkFiles(t, res, (*fdsProtoSet)(fdset), map[string]struct{}{}) 42 } 43 44 func loadDescriptorSet(t *testing.T, path string, res linker.Resolver) *descriptorpb.FileDescriptorSet { 45 data, err := ioutil.ReadFile(path) 46 if !assert.Nil(t, err) { 47 t.Fail() 48 } 49 var fdset descriptorpb.FileDescriptorSet 50 err = proto.UnmarshalOptions{Resolver: res}.Unmarshal(data, &fdset) 51 if !assert.Nil(t, err) { 52 t.Fail() 53 } 54 return &fdset 55 } 56 57 func TestMultiFileLink(t *testing.T) { 58 for _, name := range []string{"desc_test_defaults.proto", "desc_test_field_types.proto", "desc_test_options.proto", "desc_test_wellknowntypes.proto"} { 59 compiler := protocompile.Compiler{ 60 Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{ 61 ImportPaths: []string{"../internal/testprotos"}, 62 }), 63 } 64 fds, err := compiler.Compile(context.Background(), name) 65 if !assert.Nil(t, err) { 66 continue 67 } 68 69 res := fds[0].(linker.Result) 70 checkFiles(t, res, (*regProtoSet)(protoregistry.GlobalFiles), map[string]struct{}{}) 71 } 72 } 73 74 func TestProto3Optional(t *testing.T) { 75 compiler := protocompile.Compiler{ 76 Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{ 77 ImportPaths: []string{"../internal/testprotos"}, 78 }), 79 } 80 fds, err := compiler.Compile(context.Background(), "desc_test_proto3_optional.proto") 81 if !assert.Nil(t, err) { 82 return 83 } 84 85 fdset := loadDescriptorSet(t, "../internal/testprotos/desc_test_proto3_optional.protoset", fds.AsResolver()) 86 87 res := fds[0].(linker.Result) 88 checkFiles(t, res, (*fdsProtoSet)(fdset), map[string]struct{}{}) 89 } 90 91 func checkFiles(t *testing.T, act protoreflect.FileDescriptor, expSet fileProtoSet, checked map[string]struct{}) { 92 if _, ok := checked[act.Path()]; ok { 93 // already checked 94 return 95 } 96 checked[act.Path()] = struct{}{} 97 98 expProto := expSet.findFile(act.Path()) 99 actProto := toProto(act) 100 checkFileDescriptor(t, actProto, expProto) 101 102 for i := 0; i < act.Imports().Len(); i++ { 103 checkFiles(t, act.Imports().Get(i), expSet, checked) 104 } 105 } 106 107 func checkFileDescriptor(t *testing.T, act, exp *descriptorpb.FileDescriptorProto) { 108 compareFiles(t, fmt.Sprintf("%q", act.GetName()), exp, act) 109 } 110 111 func toProto(f protoreflect.FileDescriptor) *descriptorpb.FileDescriptorProto { 112 type canProto interface { 113 Proto() *descriptorpb.FileDescriptorProto 114 } 115 if can, ok := f.(canProto); ok { 116 return can.Proto() 117 } 118 return protodesc.ToFileDescriptorProto(f) 119 } 120 121 func toString(m proto.Message) string { 122 mo := protojson.MarshalOptions{Indent: " ", Multiline: true} 123 js, err := mo.Marshal(m) 124 if err != nil { 125 panic(err) 126 } 127 return string(js) 128 } 129 130 type fileProtoSet interface { 131 findFile(name string) *descriptorpb.FileDescriptorProto 132 } 133 134 type fdsProtoSet descriptorpb.FileDescriptorSet 135 136 var _ fileProtoSet = &fdsProtoSet{} 137 138 func (fps *fdsProtoSet) findFile(name string) *descriptorpb.FileDescriptorProto { 139 files := (*descriptorpb.FileDescriptorSet)(fps).File 140 for _, fd := range files { 141 if fd.GetName() == name { 142 return fd 143 } 144 } 145 return nil 146 } 147 148 type regProtoSet protoregistry.Files 149 150 var _ fileProtoSet = ®ProtoSet{} 151 152 func (fps *regProtoSet) findFile(name string) *descriptorpb.FileDescriptorProto { 153 f, err := (*protoregistry.Files)(fps).FindFileByPath(name) 154 if err != nil { 155 return nil 156 } 157 return toProto(f) 158 } 159 160 func TestLinkerValidation(t *testing.T) { 161 testCases := []struct { 162 input map[string]string 163 errMsg string 164 }{ 165 { 166 map[string]string{ 167 "foo.proto": `syntax = "proto3"; package namespace.a; import "foo2.proto"; import "foo3.proto"; import "foo4.proto"; message Foo{ b.Bar a = 1; b.Baz b = 2; b.Buzz c = 3; }`, 168 "foo2.proto": `syntax = "proto3"; package namespace.b; message Bar{}`, 169 "foo3.proto": `syntax = "proto3"; package namespace.b; message Baz{}`, 170 "foo4.proto": `syntax = "proto3"; package namespace.b; message Buzz{}`, 171 }, 172 "", // should succeed 173 }, 174 { 175 map[string]string{ 176 "foo.proto": "import \"foo2.proto\"; message fubar{}", 177 }, 178 `foo.proto:1:8: file not found: foo2.proto`, 179 }, 180 { 181 map[string]string{ 182 "foo.proto": "import \"foo2.proto\"; message fubar{}", 183 "foo2.proto": "import \"foo.proto\"; message baz{}", 184 }, 185 // since files are compiled concurrently, there are two possible outcomes 186 `foo.proto:1:8: cycle found in imports: "foo.proto" -> "foo2.proto" -> "foo.proto"` + 187 ` || foo2.proto:1:8: cycle found in imports: "foo2.proto" -> "foo.proto" -> "foo2.proto"`, 188 }, 189 { 190 map[string]string{ 191 "foo.proto": "enum foo { bar = 1; baz = 2; } enum fu { bar = 1; baz = 2; }", 192 }, 193 `foo.proto:1:42: symbol "bar" already defined at foo.proto:1:12; protobuf uses C++ scoping rules for enum values, so they exist in the scope enclosing the enum`, 194 }, 195 { 196 map[string]string{ 197 "foo.proto": "message foo {} enum foo { V = 0; }", 198 }, 199 `foo.proto:1:21: symbol "foo" already defined at foo.proto:1:9`, 200 }, 201 { 202 map[string]string{ 203 "foo.proto": "message foo { optional string a = 1; optional string a = 2; }", 204 }, 205 `foo.proto:1:54: symbol "foo.a" already defined at foo.proto:1:31`, 206 }, 207 { 208 map[string]string{ 209 "foo.proto": "message foo {}", 210 "foo2.proto": "enum foo { V = 0; }", 211 }, 212 // since files are compiled concurrently, there are two possible outcomes 213 "foo.proto:1:9: symbol \"foo\" already defined at foo2.proto:1:6" + 214 " || foo2.proto:1:6: symbol \"foo\" already defined at foo.proto:1:9", 215 }, 216 { 217 map[string]string{ 218 "foo.proto": "message foo { optional blah a = 1; }", 219 }, 220 "foo.proto:1:24: field foo.a: unknown type blah", 221 }, 222 { 223 map[string]string{ 224 "foo.proto": "message foo { optional bar.baz a = 1; } service bar { rpc baz (foo) returns (foo); }", 225 }, 226 "foo.proto:1:24: field foo.a: invalid type: bar.baz is a method, not a message or enum", 227 }, 228 { 229 map[string]string{ 230 "foo.proto": "message foo { extensions 1 to 2; } extend foo { optional string a = 1; } extend foo { optional int32 b = 1; }", 231 }, 232 "foo.proto:1:106: extension with tag 1 for message foo already defined at foo.proto:1:69", 233 }, 234 { 235 map[string]string{ 236 "foo.proto": "package fu.baz; extend foobar { optional string a = 1; }", 237 }, 238 "foo.proto:1:24: unknown extendee type foobar", 239 }, 240 { 241 map[string]string{ 242 "foo.proto": "package fu.baz; service foobar{} extend foobar { optional string a = 1; }", 243 }, 244 "foo.proto:1:41: extendee is invalid: fu.baz.foobar is a service, not a message", 245 }, 246 { 247 map[string]string{ 248 "foo.proto": "message foo{} message bar{} service foobar{ rpc foo(foo) returns (bar); }", 249 }, 250 "foo.proto:1:53: method foobar.foo: invalid request type: foobar.foo is a method, not a message", 251 }, 252 { 253 map[string]string{ 254 "foo.proto": "message foo{} message bar{} service foobar{ rpc foo(bar) returns (foo); }", 255 }, 256 "foo.proto:1:67: method foobar.foo: invalid response type: foobar.foo is a method, not a message", 257 }, 258 { 259 map[string]string{ 260 "foo.proto": "package fu.baz; message foobar{ extensions 1; } extend foobar { optional string a = 2; }", 261 }, 262 "foo.proto:1:85: field fu.baz.a: tag 2 is not in valid range for extended type fu.baz.foobar", 263 }, 264 { 265 map[string]string{ 266 "foo.proto": "package fu.baz; import public \"foo2.proto\"; message foobar{ optional baz a = 1; }", 267 "foo2.proto": "package fu.baz; import \"foo3.proto\"; message fizzle{ }", 268 "foo3.proto": "package fu.baz; message baz{ }", 269 }, 270 "foo.proto:1:70: field fu.baz.foobar.a: unknown type baz; resolved to fu.baz which is not defined; consider using a leading dot", 271 }, 272 { 273 map[string]string{ 274 "foo.proto": ` 275 syntax = "proto2"; 276 package foo; 277 import "google/protobuf/descriptor.proto"; 278 extend google.protobuf.FileOptions { optional string fil_foo = 12000; } 279 extend google.protobuf.MessageOptions { optional string msg_foo = 12000; } 280 extend google.protobuf.FieldOptions { optional string fld_foo = 12000 [(fld_foo) = "extension"]; } 281 extend google.protobuf.OneofOptions { optional string oof_foo = 12000; } 282 extend google.protobuf.EnumOptions { optional string enm_foo = 12000; } 283 extend google.protobuf.EnumValueOptions { optional string env_foo = 12000; } 284 extend google.protobuf.ExtensionRangeOptions { optional string ext_foo = 12000; } 285 extend google.protobuf.ServiceOptions { optional string svc_foo = 12000; } 286 extend google.protobuf.MethodOptions { optional string mtd_foo = 12000; } 287 option (fil_foo) = "file"; 288 message Foo { 289 option (msg_foo) = "message"; 290 oneof foo { 291 option (oof_foo) = "oneof"; 292 string bar = 1 [(fld_foo) = "field"]; 293 } 294 extensions 100 to 200 [(ext_foo) = "extensionrange"]; 295 } 296 enum Baz { 297 option (enm_foo) = "enum"; 298 ZERO = 0 [(env_foo) = "enumvalue"]; 299 } 300 service FooService { 301 option (svc_foo) = "service"; 302 rpc Bar(Foo) returns (Foo) { 303 option (mtd_foo) = "method"; 304 } 305 } 306 `, 307 }, 308 "", // should succeed 309 }, 310 { 311 map[string]string{ 312 "foo.proto": "package fu.baz; message foobar{ repeated string a = 1 [default = \"abc\"]; }", 313 }, 314 "foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is repeated", 315 }, 316 { 317 map[string]string{ 318 "foo.proto": "package fu.baz; message foobar{ optional foobar a = 1 [default = { a: {} }]; }", 319 }, 320 "foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is a message", 321 }, 322 { 323 map[string]string{ 324 "foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = { a: \"abc\" }]; }", 325 }, 326 "foo.proto:1:66: field fu.baz.foobar.a: default value cannot be a message", 327 }, 328 { 329 map[string]string{ 330 "foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = 1.234]; }", 331 }, 332 "foo.proto:1:66: field fu.baz.foobar.a: option default: expecting string, got double", 333 }, 334 { 335 map[string]string{ 336 "foo.proto": "package fu.baz; enum abc { OK=0; NOK=1; } message foobar{ optional abc a = 1 [default = NACK]; }", 337 }, 338 "foo.proto:1:89: field fu.baz.foobar.a: option default: enum fu.baz.abc has no value named NACK", 339 }, 340 { 341 map[string]string{ 342 "foo.proto": "option b = 123;", 343 }, 344 "foo.proto:1:8: option b: field b of google.protobuf.FileOptions does not exist", 345 }, 346 { 347 map[string]string{ 348 "foo.proto": "option (foo.bar) = 123;", 349 }, 350 "foo.proto:1:8: unknown extension foo.bar", 351 }, 352 { 353 map[string]string{ 354 "foo.proto": "option uninterpreted_option = { };", 355 }, 356 "foo.proto:1:8: invalid option 'uninterpreted_option'", 357 }, 358 { 359 map[string]string{ 360 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 361 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 362 "extend foo { optional int32 b = 10; }\n" + 363 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 364 "option (f).b = 123;", 365 }, 366 "foo.proto:5:12: option (f).b: field b of foo does not exist", 367 }, 368 { 369 map[string]string{ 370 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 371 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 372 "extend foo { optional int32 b = 10; }\n" + 373 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 374 "option (f).a = 123;", 375 }, 376 "foo.proto:5:16: option (f).a: expecting string, got integer", 377 }, 378 { 379 map[string]string{ 380 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 381 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 382 "extend foo { optional int32 b = 10; }\n" + 383 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 384 "option (b) = 123;", 385 }, 386 "foo.proto:5:8: option (b): extension b should extend google.protobuf.FileOptions but instead extends foo", 387 }, 388 { 389 map[string]string{ 390 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 391 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 392 "extend foo { optional int32 b = 10; }\n" + 393 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 394 "option (foo) = 123;", 395 }, 396 "foo.proto:5:8: invalid extension: foo is a message, not an extension", 397 }, 398 { 399 map[string]string{ 400 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 401 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 402 "extend foo { optional int32 b = 10; }\n" + 403 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 404 "option (foo.a) = 123;", 405 }, 406 "foo.proto:5:8: invalid extension: foo.a is a field but not an extension", 407 }, 408 { 409 map[string]string{ 410 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 411 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 412 "extend foo { optional int32 b = 10; }\n" + 413 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 414 "option (f) = { a: [ 123 ] };", 415 }, 416 "foo.proto:5:19: option (f): value is an array but field is not repeated", 417 }, 418 { 419 map[string]string{ 420 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 421 "message foo { repeated string a = 1; extensions 10 to 20; }\n" + 422 "extend foo { optional int32 b = 10; }\n" + 423 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 424 "option (f) = { a: [ \"a\", \"b\", 123 ] };", 425 }, 426 "foo.proto:5:31: option (f): expecting string, got integer", 427 }, 428 { 429 map[string]string{ 430 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 431 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 432 "extend foo { optional int32 b = 10; }\n" + 433 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 434 "option (f) = { a: \"a\" };\n" + 435 "option (f) = { a: \"b\" };", 436 }, 437 "foo.proto:6:8: option (f): non-repeated option field (f) already set", 438 }, 439 { 440 map[string]string{ 441 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 442 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 443 "extend foo { optional int32 b = 10; }\n" + 444 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 445 "option (f) = { a: \"a\" };\n" + 446 "option (f).a = \"b\";", 447 }, 448 "foo.proto:6:12: option (f).a: non-repeated option field a already set", 449 }, 450 { 451 map[string]string{ 452 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 453 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 454 "extend foo { optional int32 b = 10; }\n" + 455 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 456 "option (f) = { a: \"a\" };\n" + 457 "option (f).(b) = \"b\";", 458 }, 459 "foo.proto:6:18: option (f).(b): expecting int32, got string", 460 }, 461 { 462 map[string]string{ 463 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 464 "message foo { required string a = 1; required string b = 2; }\n" + 465 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 466 "option (f) = { a: \"a\" };\n", 467 }, 468 "foo.proto:1:1: error in file options: some required fields missing: (f).b", 469 }, 470 { 471 map[string]string{ 472 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional int32 bar = 1; }", 473 }, 474 "foo.proto:1:99: messages with message-set wire format cannot contain scalar extensions, only messages", 475 }, 476 { 477 map[string]string{ 478 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional Foo bar = 1; }", 479 }, 480 "", // should succeed 481 }, 482 { 483 map[string]string{ 484 "foo.proto": "message Foo { extensions 1 to max; } extend Foo { optional int32 bar = 536870912; }", 485 }, 486 "foo.proto:1:72: field bar: tag 536870912 is not in valid range for extended type Foo", 487 }, 488 { 489 map[string]string{ 490 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to max; } extend Foo { optional Foo bar = 536870912; }", 491 }, 492 "", // should succeed 493 }, 494 { 495 map[string]string{ 496 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional int32 bar = 1; }", 497 }, 498 "foo.proto:1:99: messages with message-set wire format cannot contain scalar extensions, only messages", 499 }, 500 { 501 map[string]string{ 502 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional Foo bar = 1; }", 503 }, 504 "", // should succeed 505 }, 506 { 507 map[string]string{ 508 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { repeated Foo bar = 1; }", 509 }, 510 "foo.proto:1:90: messages with message-set wire format cannot contain repeated extensions, only optional", 511 }, 512 { 513 map[string]string{ 514 "foo.proto": "message Foo { extensions 1 to max; } extend Foo { optional int32 bar = 536870912; }", 515 }, 516 "foo.proto:1:72: field bar: tag 536870912 is not in valid range for extended type Foo", 517 }, 518 { 519 map[string]string{ 520 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to max; } extend Foo { optional Foo bar = 536870912; }", 521 }, 522 "", // should succeed 523 }, 524 { 525 map[string]string{ 526 "foo.proto": `syntax = "proto3"; package com.google; import "google/protobuf/wrappers.proto"; message Foo { google.protobuf.StringValue str = 1; }`, 527 }, 528 "foo.proto:1:95: field com.google.Foo.str: unknown type google.protobuf.StringValue; resolved to com.google.protobuf.StringValue which is not defined; consider using a leading dot", 529 }, 530 { 531 map[string]string{ 532 "foo.proto": "syntax = \"proto2\";\n" + 533 "import \"google/protobuf/descriptor.proto\";\n" + 534 "message Foo {\n" + 535 " optional group Bar = 1 { optional string name = 1; }\n" + 536 "}\n" + 537 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 538 "message Baz { option (foo).bar.name = \"abc\"; }\n", 539 }, 540 "", // should succeed 541 }, 542 { 543 map[string]string{ 544 "foo.proto": "syntax = \"proto2\";\n" + 545 "import \"google/protobuf/descriptor.proto\";\n" + 546 "message Foo {\n" + 547 " optional group Bar = 1 { optional string name = 1; }\n" + 548 "}\n" + 549 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 550 "message Baz { option (foo).Bar.name = \"abc\"; }\n", 551 }, 552 "foo.proto:7:28: message Baz: option (foo).Bar.name: field Bar of Foo does not exist", 553 }, 554 { 555 map[string]string{ 556 "foo.proto": "syntax = \"proto2\";\n" + 557 "import \"google/protobuf/descriptor.proto\";\n" + 558 "extend google.protobuf.MessageOptions {\n" + 559 " optional group Foo = 10001 { optional string name = 1; }\n" + 560 "}\n" + 561 "message Bar { option (foo).name = \"abc\"; }\n", 562 }, 563 "", // should succeed 564 }, 565 { 566 map[string]string{ 567 "foo.proto": "syntax = \"proto2\";\n" + 568 "import \"google/protobuf/descriptor.proto\";\n" + 569 "extend google.protobuf.MessageOptions {\n" + 570 " optional group Foo = 10001 { optional string name = 1; }\n" + 571 "}\n" + 572 "message Bar { option (Foo).name = \"abc\"; }\n", 573 }, 574 "foo.proto:6:22: message Bar: invalid extension: Foo is a message, not an extension", 575 }, 576 { 577 map[string]string{ 578 "foo.proto": "syntax = \"proto2\";\n" + 579 "import \"google/protobuf/descriptor.proto\";\n" + 580 "message Foo {\n" + 581 " optional group Bar = 1 { optional string name = 1; }\n" + 582 "}\n" + 583 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 584 "message Baz { option (foo) = { Bar< name: \"abc\" > }; }\n", 585 }, 586 "", // should succeed 587 }, 588 { 589 map[string]string{ 590 "foo.proto": "syntax = \"proto2\";\n" + 591 "import \"google/protobuf/descriptor.proto\";\n" + 592 "message Foo {\n" + 593 " optional group Bar = 1 { optional string name = 1; }\n" + 594 "}\n" + 595 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 596 "message Baz { option (foo) = { bar< name: \"abc\" > }; }\n", 597 }, 598 "foo.proto:7:30: message Baz: option (foo): field bar not found (did you mean the group named Bar?)", 599 }, 600 { 601 map[string]string{ 602 "foo.proto": "syntax = \"proto2\";\n" + 603 "import \"google/protobuf/descriptor.proto\";\n" + 604 "message Foo { extensions 1 to 10; }\n" + 605 "extend Foo { optional group Bar = 10 { optional string name = 1; } }\n" + 606 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 607 "message Baz { option (foo) = { [bar]< name: \"abc\" > }; }\n", 608 }, 609 "", // should succeed 610 }, 611 { 612 map[string]string{ 613 "foo.proto": "syntax = \"proto2\";\n" + 614 "import \"google/protobuf/descriptor.proto\";\n" + 615 "message Foo { extensions 1 to 10; }\n" + 616 "extend Foo { optional group Bar = 10 { optional string name = 1; } }\n" + 617 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 618 "message Baz { option (foo) = { [Bar]< name: \"abc\" > }; }\n", 619 }, 620 "foo.proto:6:30: message Baz: option (foo): field Bar not found", 621 }, 622 { 623 map[string]string{ 624 "foo.proto": "syntax = \"proto3\";\n" + 625 "import \"google/protobuf/descriptor.proto\";\n" + 626 "message Foo { oneof bar { string baz = 1; string buzz = 2; } }\n" + 627 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 628 "message Baz { option (foo) = { baz: \"abc\" buzz: \"xyz\" }; }\n", 629 }, 630 `foo.proto:5:43: message Baz: option (foo): oneof "bar" already has field "baz" set`, 631 }, 632 { 633 map[string]string{ 634 "foo.proto": "syntax = \"proto3\";\n" + 635 "import \"google/protobuf/descriptor.proto\";\n" + 636 "message Foo { oneof bar { string baz = 1; string buzz = 2; } }\n" + 637 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 638 "message Baz {\n" + 639 " option (foo).baz = \"abc\";\n" + 640 " option (foo).buzz = \"xyz\";\n" + 641 "}", 642 }, 643 `foo.proto:7:16: message Baz: option (foo).buzz: oneof "bar" already has field "baz" set`, 644 }, 645 { 646 map[string]string{ 647 "foo.proto": "syntax = \"proto3\";\n" + 648 "import \"google/protobuf/descriptor.proto\";\n" + 649 "message Foo { oneof bar { google.protobuf.DescriptorProto baz = 1; google.protobuf.DescriptorProto buzz = 2; } }\n" + 650 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 651 "message Baz {\n" + 652 " option (foo).baz.name = \"abc\";\n" + 653 " option (foo).buzz.name = \"xyz\";\n" + 654 "}", 655 }, 656 `foo.proto:7:16: message Baz: option (foo).buzz.name: oneof "bar" already has field "baz" set`, 657 }, 658 { 659 map[string]string{ 660 "foo.proto": "syntax = \"proto3\";\n" + 661 "import \"google/protobuf/descriptor.proto\";\n" + 662 "message Foo { oneof bar { google.protobuf.DescriptorProto baz = 1; google.protobuf.DescriptorProto buzz = 2; } }\n" + 663 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 664 "message Baz {\n" + 665 " option (foo).baz.options.(foo).baz.name = \"abc\";\n" + 666 " option (foo).baz.options.(foo).buzz.name = \"xyz\";\n" + 667 "}", 668 }, 669 `foo.proto:7:34: message Baz: option (foo).baz.options.(foo).buzz.name: oneof "bar" already has field "baz" set`, 670 }, 671 } 672 673 for i, tc := range testCases { 674 t.Log("test case", i+1) 675 acc := func(filename string) (io.ReadCloser, error) { 676 f, ok := tc.input[filename] 677 if !ok { 678 return nil, fmt.Errorf("file not found: %s", filename) 679 } 680 return ioutil.NopCloser(strings.NewReader(f)), nil 681 } 682 names := make([]string, 0, len(tc.input)) 683 for k := range tc.input { 684 names = append(names, k) 685 } 686 687 compiler := protocompile.Compiler{ 688 Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{ 689 Accessor: acc, 690 }), 691 } 692 _, err := compiler.Compile(context.Background(), names...) 693 if tc.errMsg == "" { 694 if err != nil { 695 t.Errorf("case %d: expecting no error; instead got error %q", i, err) 696 } 697 } else if err == nil { 698 t.Errorf("case %d: expecting validation error %q; instead got no error", i, tc.errMsg) 699 } else { 700 msgs := strings.Split(tc.errMsg, " || ") 701 found := false 702 for _, errMsg := range msgs { 703 if err.Error() == errMsg { 704 found = true 705 break 706 } 707 } 708 if !found { 709 t.Errorf("case %d: expecting validation error %q; instead got: %q", i, tc.errMsg, err) 710 } 711 } 712 } 713 } 714 715 func TestProto3Enums(t *testing.T) { 716 file1 := `syntax = "<SYNTAX>"; enum bar { A = 0; B = 1; }` 717 file2 := `syntax = "<SYNTAX>"; import "f1.proto"; message foo { <LABEL> bar bar = 1; }` 718 getFileContents := func(file, syntax string) string { 719 contents := strings.Replace(file, "<SYNTAX>", syntax, 1) 720 label := "" 721 if syntax == "proto2" { 722 label = "optional" 723 } 724 return strings.Replace(contents, "<LABEL>", label, 1) 725 } 726 727 syntaxOptions := []string{"proto2", "proto3"} 728 for _, o1 := range syntaxOptions { 729 fc1 := getFileContents(file1, o1) 730 731 for _, o2 := range syntaxOptions { 732 fc2 := getFileContents(file2, o2) 733 734 // now parse the protos 735 acc := func(filename string) (io.ReadCloser, error) { 736 var data string 737 switch filename { 738 case "f1.proto": 739 data = fc1 740 case "f2.proto": 741 data = fc2 742 default: 743 return nil, fmt.Errorf("file not found: %s", filename) 744 } 745 return ioutil.NopCloser(strings.NewReader(data)), nil 746 } 747 compiler := protocompile.Compiler{ 748 Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{ 749 Accessor: acc, 750 }), 751 } 752 _, err := compiler.Compile(context.Background(), "f1.proto", "f2.proto") 753 754 if o1 != o2 && o2 == "proto3" { 755 expected := "f2.proto:1:54: field foo.bar: cannot use proto2 enum bar in a proto3 message" 756 if err == nil { 757 t.Errorf("expecting validation error; instead got no error") 758 } else if err.Error() != expected { 759 t.Errorf("expecting validation error %q; instead got: %q", expected, err) 760 } 761 } else { 762 // other cases succeed (okay to for proto2 to use enum from proto3 file and 763 // obviously okay for proto2 importing proto2 and proto3 importing proto3) 764 assert.Nil(t, err) 765 } 766 } 767 } 768 } 769 770 // adapted from implementation of proto.Equal, but records an error for each discrepancy 771 // found (does NOT exit early when a discrepancy is found) 772 func compareFiles(t *testing.T, path string, exp, act *descriptorpb.FileDescriptorProto) { 773 if (exp == nil) != (act == nil) { 774 if exp == nil { 775 t.Errorf("%s: expected is nil; actual is not", path) 776 } else { 777 t.Errorf("%s: expected is not nil, but actual is", path) 778 } 779 return 780 } 781 mexp := exp.ProtoReflect() 782 mact := act.ProtoReflect() 783 if mexp.IsValid() != mact.IsValid() { 784 if mexp.IsValid() { 785 t.Errorf("%s: expected is valid; actual is not", path) 786 } else { 787 t.Errorf("%s: expected is not valid, but actual is", path) 788 } 789 return 790 } 791 compareMessages(t, path, mexp, mact) 792 } 793 794 func compareMessages(t *testing.T, path string, exp, act protoreflect.Message) { 795 if exp.Descriptor() != act.Descriptor() { 796 t.Errorf("%s: descriptors do not match: exp %#v, actual %#v", path, exp.Descriptor(), act.Descriptor()) 797 return 798 } 799 exp.Range(func(fd protoreflect.FieldDescriptor, expVal protoreflect.Value) bool { 800 name := fieldDisplayName(fd) 801 actVal := act.Get(fd) 802 if !act.Has(fd) { 803 t.Errorf("%s: expected has field %s but actual does not", path, name) 804 } else { 805 compareFields(t, path+"."+name, fd, expVal, actVal) 806 } 807 return true 808 }) 809 act.Range(func(fd protoreflect.FieldDescriptor, actVal protoreflect.Value) bool { 810 name := fieldDisplayName(fd) 811 if !exp.Has(fd) { 812 t.Errorf("%s: actual has field %s but expected does not", path, name) 813 } 814 return true 815 }) 816 817 compareUnknown(t, path, exp.GetUnknown(), act.GetUnknown()) 818 } 819 820 func fieldDisplayName(fd protoreflect.FieldDescriptor) string { 821 if fd.IsExtension() { 822 return "(" + string(fd.FullName()) + ")" 823 } 824 return string(fd.Name()) 825 } 826 827 func compareFields(t *testing.T, path string, fd protoreflect.FieldDescriptor, exp, act protoreflect.Value) { 828 switch { 829 case fd.IsList(): 830 compareLists(t, path, fd, exp.List(), act.List()) 831 case fd.IsMap(): 832 compareMaps(t, path, fd, exp.Map(), act.Map()) 833 default: 834 compareValues(t, path, fd, exp, act) 835 } 836 } 837 838 func compareMaps(t *testing.T, path string, fd protoreflect.FieldDescriptor, exp, act protoreflect.Map) { 839 exp.Range(func(k protoreflect.MapKey, expVal protoreflect.Value) bool { 840 actVal := act.Get(k) 841 if !act.Has(k) { 842 t.Errorf("%s: expected map has key %s but actual does not", path, k.String()) 843 } else { 844 compareValues(t, path+"["+k.String()+"]", fd.MapValue(), expVal, actVal) 845 } 846 return true 847 }) 848 act.Range(func(k protoreflect.MapKey, actVal protoreflect.Value) bool { 849 if !exp.Has(k) { 850 t.Errorf("%s: actual map has key %s but expected does not", path, k.String()) 851 } 852 return true 853 }) 854 } 855 856 func compareLists(t *testing.T, path string, fd protoreflect.FieldDescriptor, exp, act protoreflect.List) { 857 if exp.Len() != act.Len() { 858 t.Errorf("%s: expected is list with %d items but actual has %d", path, exp.Len(), act.Len()) 859 } 860 lim := exp.Len() 861 if act.Len() < lim { 862 lim = act.Len() 863 } 864 for i := 0; i < lim; i++ { 865 compareValues(t, path+"["+strconv.Itoa(i)+"]", fd, exp.Get(i), act.Get(i)) 866 } 867 } 868 869 func compareValues(t *testing.T, path string, fd protoreflect.FieldDescriptor, exp, act protoreflect.Value) { 870 if fd.Kind() == protoreflect.MessageKind || fd.Kind() == protoreflect.GroupKind { 871 compareMessages(t, path, exp.Message(), act.Message()) 872 return 873 } 874 875 var eq bool 876 switch fd.Kind() { 877 case protoreflect.BoolKind: 878 eq = exp.Bool() == act.Bool() 879 case protoreflect.EnumKind: 880 eq = exp.Enum() == act.Enum() 881 case protoreflect.Int32Kind, protoreflect.Sint32Kind, 882 protoreflect.Int64Kind, protoreflect.Sint64Kind, 883 protoreflect.Sfixed32Kind, protoreflect.Sfixed64Kind: 884 eq = exp.Int() == act.Int() 885 case protoreflect.Uint32Kind, protoreflect.Uint64Kind, 886 protoreflect.Fixed32Kind, protoreflect.Fixed64Kind: 887 eq = exp.Uint() == act.Uint() 888 case protoreflect.FloatKind, protoreflect.DoubleKind: 889 fx := exp.Float() 890 fy := act.Float() 891 if math.IsNaN(fx) || math.IsNaN(fy) { 892 eq = math.IsNaN(fx) && math.IsNaN(fy) 893 } else { 894 eq = fx == fy 895 } 896 case protoreflect.StringKind: 897 eq = exp.String() == act.String() 898 case protoreflect.BytesKind: 899 eq = bytes.Equal(exp.Bytes(), act.Bytes()) 900 default: 901 eq = exp.Interface() == act.Interface() 902 } 903 if !eq { 904 t.Errorf("%s: expected is %v but actual is %v", path, exp, act) 905 } 906 } 907 908 func compareUnknown(t *testing.T, path string, exp, act protoreflect.RawFields) { 909 if bytes.Equal(exp, act) { 910 return 911 } 912 913 mexp := make(map[protoreflect.FieldNumber]protoreflect.RawFields) 914 mact := make(map[protoreflect.FieldNumber]protoreflect.RawFields) 915 for len(exp) > 0 { 916 fnum, _, n := protowire.ConsumeField(exp) 917 mexp[fnum] = append(mexp[fnum], exp[:n]...) 918 exp = exp[n:] 919 } 920 for len(act) > 0 { 921 fnum, _, n := protowire.ConsumeField(act) 922 bact := act[:n] 923 mact[fnum] = append(mact[fnum], bact...) 924 if bexp, ok := mexp[fnum]; !ok { 925 t.Errorf("%s: expected has data for unknown field with tag %d but actual does not", path, fnum) 926 } else if !bytes.Equal(bexp, bact) { 927 t.Errorf("%s: expected has %v for unknown field with tag %d but actual has %v", path, bexp, fnum, bact) 928 } 929 act = act[n:] 930 } 931 for fnum := range mexp { 932 _, ok := mact[fnum] 933 if !ok { 934 t.Errorf("%s: actual has data for unknown field with tag %d but expected does not", path, fnum) 935 } 936 } 937 }