1package protoparse 2 3import ( 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/jhump/protoreflect/desc" 15 _ "github.com/jhump/protoreflect/internal/testprotos" 16 "github.com/jhump/protoreflect/internal/testutil" 17) 18 19func 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 31func 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 43func 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 64func 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 78func 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 87func 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": "message foo{} message bar{} service foobar{ rpc foo(foo) returns (bar); }", 172 }, 173 "foo.proto:1:53: method foobar.foo: invalid request type: foobar.foo is a method, not a message", 174 }, 175 { 176 map[string]string{ 177 "foo.proto": "message foo{} message bar{} service foobar{ rpc foo(bar) returns (foo); }", 178 }, 179 "foo.proto:1:67: method foobar.foo: invalid response type: foobar.foo is a method, not a message", 180 }, 181 { 182 map[string]string{ 183 "foo.proto": "package fu.baz; message foobar{ extensions 1; } extend foobar { optional string a = 2; }", 184 }, 185 "foo.proto:1:85: field fu.baz.a: tag 2 is not in valid range for extended type fu.baz.foobar", 186 }, 187 { 188 map[string]string{ 189 "foo.proto": "package fu.baz; import public \"foo2.proto\"; message foobar{ optional baz a = 1; }", 190 "foo2.proto": "package fu.baz; import \"foo3.proto\"; message fizzle{ }", 191 "foo3.proto": "package fu.baz; message baz{ }", 192 }, 193 "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", 194 }, 195 { 196 map[string]string{ 197 "foo.proto": ` 198 syntax = "proto2"; 199 package foo; 200 import "google/protobuf/descriptor.proto"; 201 extend google.protobuf.FileOptions { optional string fil_foo = 12000; } 202 extend google.protobuf.MessageOptions { optional string msg_foo = 12000; } 203 extend google.protobuf.FieldOptions { optional string fld_foo = 12000 [(fld_foo) = "extension"]; } 204 extend google.protobuf.OneofOptions { optional string oof_foo = 12000; } 205 extend google.protobuf.EnumOptions { optional string enm_foo = 12000; } 206 extend google.protobuf.EnumValueOptions { optional string env_foo = 12000; } 207 extend google.protobuf.ExtensionRangeOptions { optional string ext_foo = 12000; } 208 extend google.protobuf.ServiceOptions { optional string svc_foo = 12000; } 209 extend google.protobuf.MethodOptions { optional string mtd_foo = 12000; } 210 option (fil_foo) = "file"; 211 message Foo { 212 option (msg_foo) = "message"; 213 oneof foo { 214 option (oof_foo) = "oneof"; 215 string bar = 1 [(fld_foo) = "field"]; 216 } 217 extensions 100 to 200 [(ext_foo) = "extensionrange"]; 218 } 219 enum Baz { 220 option (enm_foo) = "enum"; 221 ZERO = 0 [(env_foo) = "enumvalue"]; 222 } 223 service FooService { 224 option (svc_foo) = "service"; 225 rpc Bar(Foo) returns (Foo) { 226 option (mtd_foo) = "method"; 227 } 228 } 229 `, 230 }, 231 "", // should success 232 }, 233 { 234 map[string]string{ 235 "foo.proto": "package fu.baz; message foobar{ repeated string a = 1 [default = \"abc\"]; }", 236 }, 237 "foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is repeated", 238 }, 239 { 240 map[string]string{ 241 "foo.proto": "package fu.baz; message foobar{ optional foobar a = 1 [default = { a: {} }]; }", 242 }, 243 "foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is a message", 244 }, 245 { 246 map[string]string{ 247 "foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = { a: \"abc\" }]; }", 248 }, 249 "foo.proto:1:66: field fu.baz.foobar.a: default value cannot be a message", 250 }, 251 { 252 map[string]string{ 253 "foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = 1.234]; }", 254 }, 255 "foo.proto:1:66: field fu.baz.foobar.a: option default: expecting string, got double", 256 }, 257 { 258 map[string]string{ 259 "foo.proto": "package fu.baz; enum abc { OK=0; NOK=1; } message foobar{ optional abc a = 1 [default = NACK]; }", 260 }, 261 "foo.proto:1:89: field fu.baz.foobar.a: option default: enum fu.baz.abc has no value named NACK", 262 }, 263 { 264 map[string]string{ 265 "foo.proto": "option b = 123;", 266 }, 267 "foo.proto:1:8: option b: field b of google.protobuf.FileOptions does not exist", 268 }, 269 { 270 map[string]string{ 271 "foo.proto": "option (foo.bar) = 123;", 272 }, 273 "foo.proto:1:8: unknown extension foo.bar", 274 }, 275 { 276 map[string]string{ 277 "foo.proto": "option uninterpreted_option = { };", 278 }, 279 "foo.proto:1:8: invalid option 'uninterpreted_option'", 280 }, 281 { 282 map[string]string{ 283 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 284 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 285 "extend foo { optional int32 b = 10; }\n" + 286 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 287 "option (f).b = 123;", 288 }, 289 "foo.proto:5:12: option (f).b: field b of foo does not exist", 290 }, 291 { 292 map[string]string{ 293 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 294 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 295 "extend foo { optional int32 b = 10; }\n" + 296 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 297 "option (f).a = 123;", 298 }, 299 "foo.proto:5:16: option (f).a: expecting string, got integer", 300 }, 301 { 302 map[string]string{ 303 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 304 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 305 "extend foo { optional int32 b = 10; }\n" + 306 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 307 "option (b) = 123;", 308 }, 309 "foo.proto:5:8: option (b): extension b should extend google.protobuf.FileOptions but instead extends foo", 310 }, 311 { 312 map[string]string{ 313 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 314 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 315 "extend foo { optional int32 b = 10; }\n" + 316 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 317 "option (foo) = 123;", 318 }, 319 "foo.proto:5:8: invalid extension: foo is a message, not an extension", 320 }, 321 { 322 map[string]string{ 323 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 324 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 325 "extend foo { optional int32 b = 10; }\n" + 326 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 327 "option (foo.a) = 123;", 328 }, 329 "foo.proto:5:8: invalid extension: foo.a is a field but not an extension", 330 }, 331 { 332 map[string]string{ 333 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 334 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 335 "extend foo { optional int32 b = 10; }\n" + 336 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 337 "option (f) = { a: [ 123 ] };", 338 }, 339 "foo.proto:5:19: option (f): value is an array but field is not repeated", 340 }, 341 { 342 map[string]string{ 343 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 344 "message foo { repeated string a = 1; extensions 10 to 20; }\n" + 345 "extend foo { optional int32 b = 10; }\n" + 346 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 347 "option (f) = { a: [ \"a\", \"b\", 123 ] };", 348 }, 349 "foo.proto:5:31: option (f): expecting string, got integer", 350 }, 351 { 352 map[string]string{ 353 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 354 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 355 "extend foo { optional int32 b = 10; }\n" + 356 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 357 "option (f) = { a: \"a\" };\n" + 358 "option (f) = { a: \"b\" };", 359 }, 360 "foo.proto:6:8: option (f): non-repeated option field f already set", 361 }, 362 { 363 map[string]string{ 364 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 365 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 366 "extend foo { optional int32 b = 10; }\n" + 367 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 368 "option (f) = { a: \"a\" };\n" + 369 "option (f).a = \"b\";", 370 }, 371 "foo.proto:6:12: option (f).a: non-repeated option field a already set", 372 }, 373 { 374 map[string]string{ 375 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 376 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 377 "extend foo { optional int32 b = 10; }\n" + 378 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 379 "option (f) = { a: \"a\" };\n" + 380 "option (f).(b) = \"b\";", 381 }, 382 "foo.proto:6:18: option (f).(b): expecting int32, got string", 383 }, 384 { 385 map[string]string{ 386 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 387 "message foo { required string a = 1; required string b = 2; }\n" + 388 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 389 "option (f) = { a: \"a\" };\n", 390 }, 391 "foo.proto:1:1: error in file options: some required fields missing: (f).b", 392 }, 393 { 394 map[string]string{ 395 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional int32 bar = 1; }", 396 }, 397 "foo.proto:1:99: messages with message-set wire format cannot contain scalar extensions, only messages", 398 }, 399 { 400 map[string]string{ 401 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional Foo bar = 1; }", 402 }, 403 "", // should succeed 404 }, 405 { 406 map[string]string{ 407 "foo.proto": "message Foo { extensions 1 to max; } extend Foo { optional int32 bar = 536870912; }", 408 }, 409 "foo.proto:1:72: field bar: tag 536870912 is not in valid range for extended type Foo", 410 }, 411 { 412 map[string]string{ 413 "foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to max; } extend Foo { optional Foo bar = 536870912; }", 414 }, 415 "", // should succeed 416 }, 417 { 418 map[string]string{ 419 "foo.proto": `syntax = "proto3"; package com.google; import "google/protobuf/wrappers.proto"; message Foo { google.protobuf.StringValue str = 1; }`, 420 }, 421 "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", 422 }, 423 { 424 map[string]string{ 425 "foo.proto": "syntax = \"proto2\";\n" + 426 "import \"google/protobuf/descriptor.proto\";\n" + 427 "message Foo {\n" + 428 " optional group Bar = 1 { optional string name = 1; }\n" + 429 "}\n" + 430 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 431 "message Baz { option (foo).bar.name = \"abc\"; }\n", 432 }, 433 "", // should succeed 434 }, 435 { 436 map[string]string{ 437 "foo.proto": "syntax = \"proto2\";\n" + 438 "import \"google/protobuf/descriptor.proto\";\n" + 439 "message Foo {\n" + 440 " optional group Bar = 1 { optional string name = 1; }\n" + 441 "}\n" + 442 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 443 "message Baz { option (foo).Bar.name = \"abc\"; }\n", 444 }, 445 "foo.proto:7:28: message Baz: option (foo).Bar.name: field Bar of Foo does not exist", 446 }, 447 { 448 map[string]string{ 449 "foo.proto": "syntax = \"proto2\";\n" + 450 "import \"google/protobuf/descriptor.proto\";\n" + 451 "extend google.protobuf.MessageOptions {\n" + 452 " optional group Foo = 10001 { optional string name = 1; }\n" + 453 "}\n" + 454 "message Bar { option (foo).name = \"abc\"; }\n", 455 }, 456 "", // should succeed 457 }, 458 { 459 map[string]string{ 460 "foo.proto": "syntax = \"proto2\";\n" + 461 "import \"google/protobuf/descriptor.proto\";\n" + 462 "extend google.protobuf.MessageOptions {\n" + 463 " optional group Foo = 10001 { optional string name = 1; }\n" + 464 "}\n" + 465 "message Bar { option (Foo).name = \"abc\"; }\n", 466 }, 467 "foo.proto:6:22: message Bar: invalid extension: Foo is a message, not an extension", 468 }, 469 { 470 map[string]string{ 471 "foo.proto": "syntax = \"proto2\";\n" + 472 "import \"google/protobuf/descriptor.proto\";\n" + 473 "message Foo {\n" + 474 " optional group Bar = 1 { optional string name = 1; }\n" + 475 "}\n" + 476 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 477 "message Baz { option (foo) = { Bar< name: \"abc\" > }; }\n", 478 }, 479 "", // should succeed 480 }, 481 { 482 map[string]string{ 483 "foo.proto": "syntax = \"proto2\";\n" + 484 "import \"google/protobuf/descriptor.proto\";\n" + 485 "message Foo {\n" + 486 " optional group Bar = 1 { optional string name = 1; }\n" + 487 "}\n" + 488 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 489 "message Baz { option (foo) = { bar< name: \"abc\" > }; }\n", 490 }, 491 "foo.proto:7:30: message Baz: option (foo): field bar not found (did you mean the group named Bar?)", 492 }, 493 { 494 map[string]string{ 495 "foo.proto": "syntax = \"proto2\";\n" + 496 "import \"google/protobuf/descriptor.proto\";\n" + 497 "message Foo { extensions 1 to 10; }\n" + 498 "extend Foo { optional group Bar = 10 { optional string name = 1; } }\n" + 499 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 500 "message Baz { option (foo) = { [bar]< name: \"abc\" > }; }\n", 501 }, 502 "", // should succeed 503 }, 504 { 505 map[string]string{ 506 "foo.proto": "syntax = \"proto2\";\n" + 507 "import \"google/protobuf/descriptor.proto\";\n" + 508 "message Foo { extensions 1 to 10; }\n" + 509 "extend Foo { optional group Bar = 10 { optional string name = 1; } }\n" + 510 "extend google.protobuf.MessageOptions { optional Foo foo = 10001; }\n" + 511 "message Baz { option (foo) = { [Bar]< name: \"abc\" > }; }\n", 512 }, 513 "foo.proto:6:30: message Baz: option (foo): field Bar not found", 514 }, 515 } 516 for i, tc := range testCases { 517 acc := func(filename string) (io.ReadCloser, error) { 518 f, ok := tc.input[filename] 519 if !ok { 520 return nil, fmt.Errorf("file not found: %s", filename) 521 } 522 return ioutil.NopCloser(strings.NewReader(f)), nil 523 } 524 names := make([]string, 0, len(tc.input)) 525 for k := range tc.input { 526 names = append(names, k) 527 } 528 _, err := Parser{Accessor: acc}.ParseFiles(names...) 529 if tc.errMsg == "" { 530 if err != nil { 531 t.Errorf("case %d: expecting no error; instead got error %q", i, err) 532 } 533 } else if err == nil { 534 t.Errorf("case %d: expecting validation error %q; instead got no error", i, tc.errMsg) 535 } else if err.Error() != tc.errMsg { 536 t.Errorf("case %d: expecting validation error %q; instead got: %q", i, tc.errMsg, err) 537 } 538 } 539} 540 541func TestProto3Enums(t *testing.T) { 542 file1 := `syntax = "<SYNTAX>"; enum bar { A = 0; B = 1; }` 543 file2 := `syntax = "<SYNTAX>"; import "f1.proto"; message foo { <LABEL> bar bar = 1; }` 544 getFileContents := func(file, syntax string) string { 545 contents := strings.Replace(file, "<SYNTAX>", syntax, 1) 546 label := "" 547 if syntax == "proto2" { 548 label = "optional" 549 } 550 return strings.Replace(contents, "<LABEL>", label, 1) 551 } 552 553 syntaxOptions := []string{"proto2", "proto3"} 554 for _, o1 := range syntaxOptions { 555 fc1 := getFileContents(file1, o1) 556 557 for _, o2 := range syntaxOptions { 558 fc2 := getFileContents(file2, o2) 559 560 // now parse the protos 561 acc := func(filename string) (io.ReadCloser, error) { 562 var data string 563 switch filename { 564 case "f1.proto": 565 data = fc1 566 case "f2.proto": 567 data = fc2 568 default: 569 return nil, fmt.Errorf("file not found: %s", filename) 570 } 571 return ioutil.NopCloser(strings.NewReader(data)), nil 572 } 573 _, err := Parser{Accessor: acc}.ParseFiles("f1.proto", "f2.proto") 574 575 if o1 != o2 && o2 == "proto3" { 576 expected := "f2.proto:1:54: field foo.bar: cannot use proto2 enum bar in a proto3 message" 577 if err == nil { 578 t.Errorf("expecting validation error; instead got no error") 579 } else if err.Error() != expected { 580 t.Errorf("expecting validation error %q; instead got: %q", expected, err) 581 } 582 } else { 583 // other cases succeed (okay to for proto2 to use enum from proto3 file and 584 // obviously okay for proto2 importing proto2 and proto3 importing proto3) 585 testutil.Ok(t, err) 586 } 587 } 588 } 589} 590 591func TestCustomErrorReporterWithLinker(t *testing.T) { 592 input := map[string]string{ 593 "a/b/b.proto": `package a.b; 594 595import "google/protobuf/descriptor.proto"; 596 597extend google.protobuf.FieldOptions { 598 optional Foo foo = 50001; 599} 600 601message Foo { 602 optional string bar = 1; 603}`, 604 "a/c/c.proto": `import "a/b/b.proto"; 605 606message ReferencesFooOption { 607 optional string baz = 1 [(a.b.foo).bat = "hello"]; 608}`, 609 } 610 errMsg := "a/c/c.proto:4:38: field ReferencesFooOption.baz: option (a.b.foo).bat: field bat of a.b.Foo does not exist" 611 612 acc := func(filename string) (io.ReadCloser, error) { 613 f, ok := input[filename] 614 if !ok { 615 return nil, fmt.Errorf("file not found: %s", filename) 616 } 617 return ioutil.NopCloser(strings.NewReader(f)), nil 618 } 619 names := make([]string, 0, len(input)) 620 for k := range input { 621 names = append(names, k) 622 } 623 var errs []error 624 _, err := Parser{ 625 Accessor: acc, 626 ErrorReporter: func(errorWithPos ErrorWithPos) error { 627 errs = append(errs, errorWithPos) 628 // need to return nil to make sure this test case works 629 // this will result in us only getting an error from errorHandler.getError() 630 // we need to make sure this is called correctly in the linker so that all 631 // errors are properly propagated from the return value of linkFiles(), and 632 // therefor Parse returns ErrInvalidSource 633 return nil 634 }, 635 }.ParseFiles(names...) 636 if err != ErrInvalidSource { 637 t.Errorf("expecting validation error %v; instead got: %v", ErrInvalidSource, err) 638 } else if len(errs) != 1 || errs[0].Error() != errMsg { 639 t.Errorf("expecting validation error %q; instead got: %q", errs[0].Error(), errMsg) 640 } 641} 642