github.com/xiaoshude/protoreflect@v1.16.1-0.20220310024924-8c94d7247598/desc/protoparse/linker_test.go (about) 1 package protoparse 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "strings" 8 "testing" 9 10 "github.com/golang/protobuf/jsonpb" 11 "github.com/golang/protobuf/proto" 12 dpb "github.com/golang/protobuf/protoc-gen-go/descriptor" 13 14 "github.com/xiaoshude/protoreflect/desc" 15 _ "github.com/xiaoshude/protoreflect/internal/testprotos" 16 "github.com/xiaoshude/protoreflect/internal/testutil" 17 ) 18 19 func TestSimpleLink(t *testing.T) { 20 fds, err := Parser{ImportPaths: []string{"../../internal/testprotos"}}.ParseFiles("desc_test_complex.proto") 21 testutil.Ok(t, err) 22 23 b, err := ioutil.ReadFile("../../internal/testprotos/desc_test_complex.protoset") 24 testutil.Ok(t, err) 25 var files dpb.FileDescriptorSet 26 err = proto.Unmarshal(b, &files) 27 testutil.Ok(t, err) 28 testutil.Require(t, proto.Equal(files.File[0], fds[0].AsProto()), "linked descriptor did not match output from protoc:\nwanted: %s\ngot: %s", toString(files.File[0]), toString(fds[0].AsProto())) 29 } 30 31 func TestMultiFileLink(t *testing.T) { 32 for _, name := range []string{"desc_test2.proto", "desc_test_defaults.proto", "desc_test_field_types.proto", "desc_test_options.proto", "desc_test_proto3.proto", "desc_test_wellknowntypes.proto"} { 33 fds, err := Parser{ImportPaths: []string{"../../internal/testprotos"}}.ParseFiles(name) 34 testutil.Ok(t, err) 35 36 exp, err := desc.LoadFileDescriptor(name) 37 testutil.Ok(t, err) 38 39 checkFiles(t, fds[0], exp, map[string]struct{}{}) 40 } 41 } 42 43 func TestProto3Optional(t *testing.T) { 44 fds, err := Parser{ImportPaths: []string{"../../internal/testprotos"}}.ParseFiles("proto3_optional/desc_test_proto3_optional.proto") 45 testutil.Ok(t, err) 46 47 data, err := ioutil.ReadFile("../../internal/testprotos/proto3_optional/desc_test_proto3_optional.protoset") 48 testutil.Ok(t, err) 49 var fdset dpb.FileDescriptorSet 50 err = proto.Unmarshal(data, &fdset) 51 testutil.Ok(t, err) 52 53 exp, err := desc.CreateFileDescriptorFromSet(&fdset) 54 testutil.Ok(t, err) 55 // not comparing source code info 56 exp.AsFileDescriptorProto().SourceCodeInfo = nil 57 for _, dep := range exp.GetDependencies() { 58 dep.AsFileDescriptorProto().SourceCodeInfo = nil 59 } 60 61 checkFiles(t, fds[0], exp, map[string]struct{}{}) 62 } 63 64 func checkFiles(t *testing.T, act, exp *desc.FileDescriptor, checked map[string]struct{}) { 65 if _, ok := checked[act.GetName()]; ok { 66 // already checked 67 return 68 } 69 checked[act.GetName()] = struct{}{} 70 71 testutil.Require(t, proto.Equal(exp.AsFileDescriptorProto(), act.AsProto()), "linked descriptor did not match output from protoc:\nwanted: %s\ngot: %s", toString(exp.AsProto()), toString(act.AsProto())) 72 73 for i, dep := range act.GetDependencies() { 74 checkFiles(t, dep, exp.GetDependencies()[i], checked) 75 } 76 } 77 78 func toString(m proto.Message) string { 79 msh := jsonpb.Marshaler{Indent: " "} 80 s, err := msh.MarshalToString(m) 81 if err != nil { 82 panic(err) 83 } 84 return s 85 } 86 87 func TestLinkerValidation(t *testing.T) { 88 testCases := []struct { 89 input map[string]string 90 errMsg string 91 }{ 92 { 93 map[string]string{ 94 "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; }`, 95 "foo2.proto": `syntax = "proto3"; package namespace.b; message Bar{}`, 96 "foo3.proto": `syntax = "proto3"; package namespace.b; message Baz{}`, 97 "foo4.proto": `syntax = "proto3"; package namespace.b; message Buzz{}`, 98 }, 99 "", // should succeed 100 }, 101 { 102 map[string]string{ 103 "foo.proto": "import \"foo2.proto\"; message fubar{}", 104 }, 105 `foo.proto:1:8: file not found: foo2.proto`, 106 }, 107 { 108 map[string]string{ 109 "foo.proto": "import \"foo2.proto\"; message fubar{}", 110 "foo2.proto": "import \"foo.proto\"; message baz{}", 111 }, 112 `foo.proto:1:8: cycle found in imports: "foo.proto" -> "foo2.proto" -> "foo.proto"`, 113 }, 114 { 115 map[string]string{ 116 "foo.proto": "enum foo { bar = 1; baz = 2; } enum fu { bar = 1; baz = 2; }", 117 }, 118 `foo.proto:1:42: duplicate symbol bar: already defined as enum value; protobuf uses C++ scoping rules for enum values, so they exist in the scope enclosing the enum`, 119 }, 120 { 121 map[string]string{ 122 "foo.proto": "message foo {} enum foo { V = 0; }", 123 }, 124 "foo.proto:1:16: duplicate symbol foo: already defined as message", 125 }, 126 { 127 map[string]string{ 128 "foo.proto": "message foo { optional string a = 1; optional string a = 2; }", 129 }, 130 "foo.proto:1:38: duplicate symbol foo.a: already defined as field", 131 }, 132 { 133 map[string]string{ 134 "foo.proto": "message foo {}", 135 "foo2.proto": "enum foo { V = 0; }", 136 }, 137 "foo2.proto:1:1: duplicate symbol foo: already defined as message in \"foo.proto\"", 138 }, 139 { 140 map[string]string{ 141 "foo.proto": "message foo { optional blah a = 1; }", 142 }, 143 "foo.proto:1:24: field foo.a: unknown type blah", 144 }, 145 { 146 map[string]string{ 147 "foo.proto": "message foo { optional bar.baz a = 1; } service bar { rpc baz (foo) returns (foo); }", 148 }, 149 "foo.proto:1:24: field foo.a: invalid type: bar.baz is a method, not a message or enum", 150 }, 151 { 152 map[string]string{ 153 "foo.proto": "message foo { extensions 1 to 2; } extend foo { optional string a = 1; } extend foo { optional int32 b = 1; }", 154 }, 155 "foo.proto:1:106: field b: duplicate extension: a and b are both using tag 1", 156 }, 157 { 158 map[string]string{ 159 "foo.proto": "package fu.baz; extend foobar { optional string a = 1; }", 160 }, 161 "foo.proto:1:24: unknown extendee type foobar", 162 }, 163 { 164 map[string]string{ 165 "foo.proto": "package fu.baz; service foobar{} extend foobar { optional string a = 1; }", 166 }, 167 "foo.proto:1:41: extendee is invalid: fu.baz.foobar is a service, not a message", 168 }, 169 { 170 map[string]string{ 171 "foo.proto": "package fu.baz; message foobar{ extensions 1; } extend foobar { optional string a = 2; }", 172 }, 173 "foo.proto:1:85: field fu.baz.a: tag 2 is not in valid range for extended type fu.baz.foobar", 174 }, 175 { 176 map[string]string{ 177 "foo.proto": "package fu.baz; import public \"foo2.proto\"; message foobar{ optional baz a = 1; }", 178 "foo2.proto": "package fu.baz; import \"foo3.proto\"; message fizzle{ }", 179 "foo3.proto": "package fu.baz; message baz{ }", 180 }, 181 "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", 182 }, 183 { 184 map[string]string{ 185 "foo.proto": ` 186 syntax = "proto2"; 187 package foo; 188 import "google/protobuf/descriptor.proto"; 189 extend google.protobuf.FileOptions { optional string fil_foo = 12000; } 190 extend google.protobuf.MessageOptions { optional string msg_foo = 12000; } 191 extend google.protobuf.FieldOptions { optional string fld_foo = 12000 [(fld_foo) = "extension"]; } 192 extend google.protobuf.OneofOptions { optional string oof_foo = 12000; } 193 extend google.protobuf.EnumOptions { optional string enm_foo = 12000; } 194 extend google.protobuf.EnumValueOptions { optional string env_foo = 12000; } 195 extend google.protobuf.ExtensionRangeOptions { optional string ext_foo = 12000; } 196 extend google.protobuf.ServiceOptions { optional string svc_foo = 12000; } 197 extend google.protobuf.MethodOptions { optional string mtd_foo = 12000; } 198 option (fil_foo) = "file"; 199 message Bar { 200 option (msg_foo) = "message"; 201 oneof foo { 202 option (oof_foo) = "oneof"; 203 string bar = 1 [(fld_foo) = "field"]; 204 } 205 extensions 100 to 200 [(ext_foo) = "extensionrange"]; 206 } 207 enum Baz { 208 option (enm_foo) = "enum"; 209 ZERO = 0 [(env_foo) = "enumvalue"]; 210 } 211 service FooService { 212 option (svc_foo) = "service"; 213 rpc Bar(Bar) returns (Bar) { 214 option (mtd_foo) = "method"; 215 } 216 } 217 `, 218 }, 219 "", // should success 220 }, 221 { 222 map[string]string{ 223 "foo.proto": "package fu.baz; message foobar{ repeated string a = 1 [default = \"abc\"]; }", 224 }, 225 "foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is repeated", 226 }, 227 { 228 map[string]string{ 229 "foo.proto": "package fu.baz; message foobar{ optional foobar a = 1 [default = { a: {} }]; }", 230 }, 231 "foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is a message", 232 }, 233 { 234 map[string]string{ 235 "foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = { a: \"abc\" }]; }", 236 }, 237 "foo.proto:1:66: field fu.baz.foobar.a: default value cannot be a message", 238 }, 239 { 240 map[string]string{ 241 "foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = 1.234]; }", 242 }, 243 "foo.proto:1:66: field fu.baz.foobar.a: option default: expecting string, got double", 244 }, 245 { 246 map[string]string{ 247 "foo.proto": "package fu.baz; enum abc { OK=0; NOK=1; } message foobar{ optional abc a = 1 [default = NACK]; }", 248 }, 249 "foo.proto:1:89: field fu.baz.foobar.a: option default: enum fu.baz.abc has no value named NACK", 250 }, 251 { 252 map[string]string{ 253 "foo.proto": "option b = 123;", 254 }, 255 "foo.proto:1:8: option b: field b of google.protobuf.FileOptions does not exist", 256 }, 257 { 258 map[string]string{ 259 "foo.proto": "option (foo.bar) = 123;", 260 }, 261 "foo.proto:1:8: unknown extension foo.bar", 262 }, 263 { 264 map[string]string{ 265 "foo.proto": "option uninterpreted_option = { };", 266 }, 267 "foo.proto:1:8: invalid option 'uninterpreted_option'", 268 }, 269 { 270 map[string]string{ 271 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 272 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 273 "extend foo { optional int32 b = 10; }\n" + 274 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 275 "option (f).b = 123;", 276 }, 277 "foo.proto:5:12: option (f).b: field b of foo does not exist", 278 }, 279 { 280 map[string]string{ 281 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 282 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 283 "extend foo { optional int32 b = 10; }\n" + 284 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 285 "option (f).a = 123;", 286 }, 287 "foo.proto:5:16: option (f).a: expecting string, got integer", 288 }, 289 { 290 map[string]string{ 291 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 292 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 293 "extend foo { optional int32 b = 10; }\n" + 294 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 295 "option (b) = 123;", 296 }, 297 "foo.proto:5:8: option (b): extension b should extend google.protobuf.FileOptions but instead extends foo", 298 }, 299 { 300 map[string]string{ 301 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 302 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 303 "extend foo { optional int32 b = 10; }\n" + 304 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 305 "option (foo) = 123;", 306 }, 307 "foo.proto:5:8: invalid extension: foo is a message, not an extension", 308 }, 309 { 310 map[string]string{ 311 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 312 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 313 "extend foo { optional int32 b = 10; }\n" + 314 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 315 "option (foo.a) = 123;", 316 }, 317 "foo.proto:5:8: invalid extension: foo.a is a field but not an extension", 318 }, 319 { 320 map[string]string{ 321 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 322 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 323 "extend foo { optional int32 b = 10; }\n" + 324 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 325 "option (f) = { a: [ 123 ] };", 326 }, 327 "foo.proto:5:19: option (f): value is an array but field is not repeated", 328 }, 329 { 330 map[string]string{ 331 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 332 "message foo { repeated string a = 1; extensions 10 to 20; }\n" + 333 "extend foo { optional int32 b = 10; }\n" + 334 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 335 "option (f) = { a: [ \"a\", \"b\", 123 ] };", 336 }, 337 "foo.proto:5:31: option (f): expecting string, got integer", 338 }, 339 { 340 map[string]string{ 341 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 342 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 343 "extend foo { optional int32 b = 10; }\n" + 344 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 345 "option (f) = { a: \"a\" };\n" + 346 "option (f) = { a: \"b\" };", 347 }, 348 "foo.proto:6:8: option (f): non-repeated option field f already set", 349 }, 350 { 351 map[string]string{ 352 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 353 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 354 "extend foo { optional int32 b = 10; }\n" + 355 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 356 "option (f) = { a: \"a\" };\n" + 357 "option (f).a = \"b\";", 358 }, 359 "foo.proto:6:12: option (f).a: non-repeated option field a already set", 360 }, 361 { 362 map[string]string{ 363 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 364 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 365 "extend foo { optional int32 b = 10; }\n" + 366 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 367 "option (f) = { a: \"a\" };\n" + 368 "option (f).(b) = \"b\";", 369 }, 370 "foo.proto:6:18: option (f).(b): expecting int32, got string", 371 }, 372 { 373 map[string]string{ 374 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 375 "message foo { required string a = 1; required string b = 2; }\n" + 376 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 377 "option (f) = { a: \"a\" };\n", 378 }, 379 "foo.proto:1:1: error in file options: some required fields missing: (f).b", 380 }, 381 { 382 map[string]string{ 383 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional int32 bar = 1; }", 384 }, 385 "foo.proto:1:99: messages with message-set wire format cannot contain scalar extensions, only messages", 386 }, 387 { 388 map[string]string{ 389 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional Foo bar = 1; }", 390 }, 391 "", // should succeed 392 }, 393 { 394 map[string]string{ 395 "foo.proto": "message Foo { extensions 1 to max; } extend Foo { optional int32 bar = 536870912; }", 396 }, 397 "foo.proto:1:72: field bar: tag 536870912 is not in valid range for extended type Foo", 398 }, 399 { 400 map[string]string{ 401 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to max; } extend Foo { optional Foo bar = 536870912; }", 402 }, 403 "", // should succeed 404 }, 405 { 406 map[string]string{ 407 "foo.proto": `syntax = "proto3"; package com.google; import "google/protobuf/wrappers.proto"; message Foo { google.protobuf.StringValue str = 1; }`, 408 }, 409 "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", 410 }, 411 } 412 for i, tc := range testCases { 413 acc := func(filename string) (io.ReadCloser, error) { 414 f, ok := tc.input[filename] 415 if !ok { 416 return nil, fmt.Errorf("file not found: %s", filename) 417 } 418 return ioutil.NopCloser(strings.NewReader(f)), nil 419 } 420 names := make([]string, 0, len(tc.input)) 421 for k := range tc.input { 422 names = append(names, k) 423 } 424 _, err := Parser{Accessor: acc}.ParseFiles(names...) 425 if tc.errMsg == "" { 426 if err != nil { 427 t.Errorf("case %d: expecting no error; instead got error %q", i, err) 428 } 429 } else if err == nil { 430 t.Errorf("case %d: expecting validation error %q; instead got no error", i, tc.errMsg) 431 } else if err.Error() != tc.errMsg { 432 t.Errorf("case %d: expecting validation error %q; instead got: %q", i, tc.errMsg, err) 433 } 434 } 435 } 436 437 func TestProto3Enums(t *testing.T) { 438 file1 := `syntax = "<SYNTAX>"; enum bar { A = 0; B = 1; }` 439 file2 := `syntax = "<SYNTAX>"; import "f1.proto"; message foo { <LABEL> bar bar = 1; }` 440 getFileContents := func(file, syntax string) string { 441 contents := strings.Replace(file, "<SYNTAX>", syntax, 1) 442 label := "" 443 if syntax == "proto2" { 444 label = "optional" 445 } 446 return strings.Replace(contents, "<LABEL>", label, 1) 447 } 448 449 syntaxOptions := []string{"proto2", "proto3"} 450 for _, o1 := range syntaxOptions { 451 fc1 := getFileContents(file1, o1) 452 453 for _, o2 := range syntaxOptions { 454 fc2 := getFileContents(file2, o2) 455 456 // now parse the protos 457 acc := func(filename string) (io.ReadCloser, error) { 458 var data string 459 switch filename { 460 case "f1.proto": 461 data = fc1 462 case "f2.proto": 463 data = fc2 464 default: 465 return nil, fmt.Errorf("file not found: %s", filename) 466 } 467 return ioutil.NopCloser(strings.NewReader(data)), nil 468 } 469 _, err := Parser{Accessor: acc}.ParseFiles("f1.proto", "f2.proto") 470 471 if o1 != o2 && o2 == "proto3" { 472 expected := "f2.proto:1:54: field foo.bar: cannot use proto2 enum bar in a proto3 message" 473 if err == nil { 474 t.Errorf("expecting validation error; instead got no error") 475 } else if err.Error() != expected { 476 t.Errorf("expecting validation error %q; instead got: %q", expected, err) 477 } 478 } else { 479 // other cases succeed (okay to for proto2 to use enum from proto3 file and 480 // obviously okay for proto2 importing proto2 and proto3 importing proto3) 481 testutil.Ok(t, err) 482 } 483 } 484 } 485 } 486 487 func TestCustomErrorReporterWithLinker(t *testing.T) { 488 input := map[string]string{ 489 "a/b/b.proto": `package a.b; 490 491 import "google/protobuf/descriptor.proto"; 492 493 extend google.protobuf.FieldOptions { 494 optional Foo foo = 50001; 495 } 496 497 message Foo { 498 optional string bar = 1; 499 }`, 500 "a/c/c.proto": `import "a/b/b.proto"; 501 502 message ReferencesFooOption { 503 optional string baz = 1 [(a.b.foo).bat = "hello"]; 504 }`, 505 } 506 errMsg := "a/c/c.proto:4:38: field ReferencesFooOption.baz: option (a.b.foo).bat: field bat of a.b.Foo does not exist" 507 508 acc := func(filename string) (io.ReadCloser, error) { 509 f, ok := input[filename] 510 if !ok { 511 return nil, fmt.Errorf("file not found: %s", filename) 512 } 513 return ioutil.NopCloser(strings.NewReader(f)), nil 514 } 515 names := make([]string, 0, len(input)) 516 for k := range input { 517 names = append(names, k) 518 } 519 var errs []error 520 _, err := Parser{ 521 Accessor: acc, 522 ErrorReporter: func(errorWithPos ErrorWithPos) error { 523 errs = append(errs, errorWithPos) 524 // need to return nil to make sure this test case works 525 // this will result in us only getting an error from errorHandler.getError() 526 // we need to make sure this is called correctly in the linker so that all 527 // errors are properly propagated from the return value of linkFiles(), and 528 // therefor Parse returns ErrInvalidSource 529 return nil 530 }, 531 }.ParseFiles(names...) 532 if err != ErrInvalidSource { 533 t.Errorf("expecting validation error %v; instead got: %v", ErrInvalidSource, err) 534 } else if len(errs) != 1 || errs[0].Error() != errMsg { 535 t.Errorf("expecting validation error %q; instead got: %q", errs[0].Error(), errMsg) 536 } 537 }