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