github.com/phpstudyer/protoreflect@v1.7.2/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/phpstudyer/protoreflect/desc" 15 _ "github.com/phpstudyer/protoreflect/internal/testprotos" 16 "github.com/phpstudyer/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 58 checkFiles(t, fds[0], exp, map[string]struct{}{}) 59 } 60 61 func checkFiles(t *testing.T, act, exp *desc.FileDescriptor, checked map[string]struct{}) { 62 if _, ok := checked[act.GetName()]; ok { 63 // already checked 64 return 65 } 66 checked[act.GetName()] = struct{}{} 67 68 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())) 69 70 for i, dep := range act.GetDependencies() { 71 checkFiles(t, dep, exp.GetDependencies()[i], checked) 72 } 73 } 74 75 func toString(m proto.Message) string { 76 msh := jsonpb.Marshaler{Indent: " "} 77 s, err := msh.MarshalToString(m) 78 if err != nil { 79 panic(err) 80 } 81 return s 82 } 83 84 func TestLinkerValidation(t *testing.T) { 85 testCases := []struct { 86 input map[string]string 87 errMsg string 88 }{ 89 { 90 map[string]string{ 91 "foo.proto": "import \"foo2.proto\"; message fubar{}", 92 }, 93 `foo.proto:1:8: file not found: foo2.proto`, 94 }, 95 { 96 map[string]string{ 97 "foo.proto": "import \"foo2.proto\"; message fubar{}", 98 "foo2.proto": "import \"foo.proto\"; message baz{}", 99 }, 100 `foo.proto:1:8: cycle found in imports: "foo.proto" -> "foo2.proto" -> "foo.proto"`, 101 }, 102 { 103 map[string]string{ 104 "foo.proto": "message foo {} enum foo { V = 0; }", 105 }, 106 "foo.proto:1:16: duplicate symbol foo: already defined as message", 107 }, 108 { 109 map[string]string{ 110 "foo.proto": "message foo { optional string a = 1; optional string a = 2; }", 111 }, 112 "foo.proto:1:38: duplicate symbol foo.a: already defined as field", 113 }, 114 { 115 map[string]string{ 116 "foo.proto": "message foo {}", 117 "foo2.proto": "enum foo { V = 0; }", 118 }, 119 "foo2.proto:1:1: duplicate symbol foo: already defined as message in \"foo.proto\"", 120 }, 121 { 122 map[string]string{ 123 "foo.proto": "message foo { optional blah a = 1; }", 124 }, 125 "foo.proto:1:24: field foo.a: unknown type blah", 126 }, 127 { 128 map[string]string{ 129 "foo.proto": "message foo { optional bar.baz a = 1; } service bar { rpc baz (foo) returns (foo); }", 130 }, 131 "foo.proto:1:24: field foo.a: invalid type: bar.baz is a method, not a message or enum", 132 }, 133 { 134 map[string]string{ 135 "foo.proto": "message foo { extensions 1 to 2; } extend foo { optional string a = 1; } extend foo { optional int32 b = 1; }", 136 }, 137 "foo.proto:1:106: field b: duplicate extension: a and b are both using tag 1", 138 }, 139 { 140 map[string]string{ 141 "foo.proto": "package fu.baz; extend foobar { optional string a = 1; }", 142 }, 143 "foo.proto:1:24: unknown extendee type foobar", 144 }, 145 { 146 map[string]string{ 147 "foo.proto": "package fu.baz; service foobar{} extend foobar { optional string a = 1; }", 148 }, 149 "foo.proto:1:41: extendee is invalid: fu.baz.foobar is a service, not a message", 150 }, 151 { 152 map[string]string{ 153 "foo.proto": "package fu.baz; message foobar{ extensions 1; } extend foobar { optional string a = 2; }", 154 }, 155 "foo.proto:1:85: field fu.baz.a: tag 2 is not in valid range for extended type fu.baz.foobar", 156 }, 157 { 158 map[string]string{ 159 "foo.proto": "package fu.baz; import public \"foo2.proto\"; message foobar{ optional baz a = 1; }", 160 "foo2.proto": "package fu.baz; import \"foo3.proto\"; message fizzle{ }", 161 "foo3.proto": "package fu.baz; message baz{ }", 162 }, 163 "foo.proto:1:70: field fu.baz.foobar.a: unknown type baz", 164 }, 165 { 166 map[string]string{ 167 "foo.proto": "package fu.baz; message foobar{ repeated string a = 1 [default = \"abc\"]; }", 168 }, 169 "foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is repeated", 170 }, 171 { 172 map[string]string{ 173 "foo.proto": "package fu.baz; message foobar{ optional foobar a = 1 [default = { a: {} }]; }", 174 }, 175 "foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is a message", 176 }, 177 { 178 map[string]string{ 179 "foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = { a: \"abc\" }]; }", 180 }, 181 "foo.proto:1:66: field fu.baz.foobar.a: default value cannot be a message", 182 }, 183 { 184 map[string]string{ 185 "foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = 1.234]; }", 186 }, 187 "foo.proto:1:66: field fu.baz.foobar.a: option default: expecting string, got double", 188 }, 189 { 190 map[string]string{ 191 "foo.proto": "package fu.baz; enum abc { OK=0; NOK=1; } message foobar{ optional abc a = 1 [default = NACK]; }", 192 }, 193 "foo.proto:1:89: field fu.baz.foobar.a: option default: enum fu.baz.abc has no value named NACK", 194 }, 195 { 196 map[string]string{ 197 "foo.proto": "option b = 123;", 198 }, 199 "foo.proto:1:8: option b: field b of google.protobuf.FileOptions does not exist", 200 }, 201 { 202 map[string]string{ 203 "foo.proto": "option (foo.bar) = 123;", 204 }, 205 "foo.proto:1:8: unknown extension foo.bar", 206 }, 207 { 208 map[string]string{ 209 "foo.proto": "option uninterpreted_option = { };", 210 }, 211 "foo.proto:1:8: invalid option 'uninterpreted_option'", 212 }, 213 { 214 map[string]string{ 215 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 216 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 217 "extend foo { optional int32 b = 10; }\n" + 218 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 219 "option (f).b = 123;", 220 }, 221 "foo.proto:5:12: option (f).b: field b of foo does not exist", 222 }, 223 { 224 map[string]string{ 225 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 226 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 227 "extend foo { optional int32 b = 10; }\n" + 228 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 229 "option (f).a = 123;", 230 }, 231 "foo.proto:5:16: option (f).a: expecting string, got integer", 232 }, 233 { 234 map[string]string{ 235 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 236 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 237 "extend foo { optional int32 b = 10; }\n" + 238 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 239 "option (b) = 123;", 240 }, 241 "foo.proto:5:8: option (b): extension b should extend google.protobuf.FileOptions but instead extends foo", 242 }, 243 { 244 map[string]string{ 245 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 246 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 247 "extend foo { optional int32 b = 10; }\n" + 248 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 249 "option (foo) = 123;", 250 }, 251 "foo.proto:5:8: invalid extension: foo is a message, not an extension", 252 }, 253 { 254 map[string]string{ 255 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 256 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 257 "extend foo { optional int32 b = 10; }\n" + 258 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 259 "option (foo.a) = 123;", 260 }, 261 "foo.proto:5:8: invalid extension: foo.a is a field but not an extension", 262 }, 263 { 264 map[string]string{ 265 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 266 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 267 "extend foo { optional int32 b = 10; }\n" + 268 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 269 "option (f) = { a: [ 123 ] };", 270 }, 271 "foo.proto:5:19: option (f): value is an array but field is not repeated", 272 }, 273 { 274 map[string]string{ 275 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 276 "message foo { repeated string a = 1; extensions 10 to 20; }\n" + 277 "extend foo { optional int32 b = 10; }\n" + 278 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 279 "option (f) = { a: [ \"a\", \"b\", 123 ] };", 280 }, 281 "foo.proto:5:31: option (f): expecting string, got integer", 282 }, 283 { 284 map[string]string{ 285 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 286 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 287 "extend foo { optional int32 b = 10; }\n" + 288 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 289 "option (f) = { a: \"a\" };\n" + 290 "option (f) = { a: \"b\" };", 291 }, 292 "foo.proto:6:8: option (f): non-repeated option field f already set", 293 }, 294 { 295 map[string]string{ 296 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 297 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 298 "extend foo { optional int32 b = 10; }\n" + 299 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 300 "option (f) = { a: \"a\" };\n" + 301 "option (f).a = \"b\";", 302 }, 303 "foo.proto:6:12: option (f).a: non-repeated option field a already set", 304 }, 305 { 306 map[string]string{ 307 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 308 "message foo { optional string a = 1; extensions 10 to 20; }\n" + 309 "extend foo { optional int32 b = 10; }\n" + 310 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 311 "option (f) = { a: \"a\" };\n" + 312 "option (f).(b) = \"b\";", 313 }, 314 "foo.proto:6:18: option (f).(b): expecting int32, got string", 315 }, 316 { 317 map[string]string{ 318 "foo.proto": "import \"google/protobuf/descriptor.proto\";\n" + 319 "message foo { required string a = 1; required string b = 2; }\n" + 320 "extend google.protobuf.FileOptions { optional foo f = 20000; }\n" + 321 "option (f) = { a: \"a\" };\n", 322 }, 323 "foo.proto:1:1: error in file options: some required fields missing: (f).b", 324 }, 325 } 326 for i, tc := range testCases { 327 acc := func(filename string) (io.ReadCloser, error) { 328 f, ok := tc.input[filename] 329 if !ok { 330 return nil, fmt.Errorf("file not found: %s", filename) 331 } 332 return ioutil.NopCloser(strings.NewReader(f)), nil 333 } 334 names := make([]string, 0, len(tc.input)) 335 for k := range tc.input { 336 names = append(names, k) 337 } 338 _, err := Parser{Accessor: acc}.ParseFiles(names...) 339 if err == nil { 340 t.Errorf("case %d: expecting validation error %q; instead got no error", i, tc.errMsg) 341 } else if err.Error() != tc.errMsg { 342 t.Errorf("case %d: expecting validation error %q; instead got: %q", i, tc.errMsg, err) 343 } 344 } 345 } 346 347 func TestProto3Enums(t *testing.T) { 348 file1 := `syntax = "<SYNTAX>"; enum bar { A = 0; B = 1; }` 349 file2 := `syntax = "<SYNTAX>"; import "f1.proto"; message foo { <LABEL> bar bar = 1; }` 350 getFileContents := func(file, syntax string) string { 351 contents := strings.Replace(file, "<SYNTAX>", syntax, 1) 352 label := "" 353 if syntax == "proto2" { 354 label = "optional" 355 } 356 return strings.Replace(contents, "<LABEL>", label, 1) 357 } 358 359 syntaxOptions := []string{"proto2", "proto3"} 360 for _, o1 := range syntaxOptions { 361 fc1 := getFileContents(file1, o1) 362 363 for _, o2 := range syntaxOptions { 364 fc2 := getFileContents(file2, o2) 365 366 // now parse the protos 367 acc := func(filename string) (io.ReadCloser, error) { 368 var data string 369 switch filename { 370 case "f1.proto": 371 data = fc1 372 case "f2.proto": 373 data = fc2 374 default: 375 return nil, fmt.Errorf("file not found: %s", filename) 376 } 377 return ioutil.NopCloser(strings.NewReader(data)), nil 378 } 379 _, err := Parser{Accessor: acc}.ParseFiles("f1.proto", "f2.proto") 380 381 if o1 != o2 && o2 == "proto3" { 382 expected := "f2.proto:1:54: field foo.bar: cannot use proto2 enum bar in a proto3 message" 383 if err == nil { 384 t.Errorf("expecting validation error; instead got no error") 385 } else if err.Error() != expected { 386 t.Errorf("expecting validation error %q; instead got: %q", expected, err) 387 } 388 } else { 389 // other cases succeed (okay to for proto2 to use enum from proto3 file and 390 // obviously okay for proto2 importing proto2 and proto3 importing proto3) 391 testutil.Ok(t, err) 392 } 393 } 394 } 395 } 396 397 func TestCustomErrorReporterWithLinker(t *testing.T) { 398 input := map[string]string{ 399 "a/b/b.proto": `package a.b; 400 401 import "google/protobuf/descriptor.proto"; 402 403 extend google.protobuf.FieldOptions { 404 optional Foo foo = 50001; 405 } 406 407 message Foo { 408 optional string bar = 1; 409 }`, 410 "a/c/c.proto": `import "a/b/b.proto"; 411 412 message ReferencesFooOption { 413 optional string baz = 1 [(a.b.foo).bat = "hello"]; 414 }`, 415 } 416 errMsg := "a/c/c.proto:4:38: field ReferencesFooOption.baz: option (a.b.foo).bat: field bat of a.b.Foo does not exist" 417 418 acc := func(filename string) (io.ReadCloser, error) { 419 f, ok := input[filename] 420 if !ok { 421 return nil, fmt.Errorf("file not found: %s", filename) 422 } 423 return ioutil.NopCloser(strings.NewReader(f)), nil 424 } 425 names := make([]string, 0, len(input)) 426 for k := range input { 427 names = append(names, k) 428 } 429 var errs []error 430 _, err := Parser{ 431 Accessor: acc, 432 ErrorReporter: func(errorWithPos ErrorWithPos) error { 433 errs = append(errs, errorWithPos) 434 // need to return nil to make sure this test case works 435 // this will result in us only getting an error from errorHandler.getError() 436 // we need to make sure this is called correctly in the linker so that all 437 // errors are properly propagated from the return value of linkFiles(), and 438 // therefor Parse returns ErrInvalidSource 439 return nil 440 }, 441 }.ParseFiles(names...) 442 if err != ErrInvalidSource { 443 t.Errorf("expecting validation error %v; instead got: %v", ErrInvalidSource, err) 444 } else if len(errs) != 1 || errs[0].Error() != errMsg { 445 t.Errorf("expecting validation error %q; instead got: %q", errs[0].Error(), errMsg) 446 } 447 }