github.com/bakjos/protoreflect@v1.9.2/desc/protoparse/parser_test.go (about) 1 package protoparse 2 3 import ( 4 "bytes" 5 "errors" 6 "io" 7 "io/ioutil" 8 "os" 9 "sort" 10 "testing" 11 12 "github.com/golang/protobuf/proto" 13 dpb "github.com/golang/protobuf/protoc-gen-go/descriptor" 14 15 "github.com/bakjos/protoreflect/codec" 16 "github.com/bakjos/protoreflect/desc" 17 "github.com/bakjos/protoreflect/internal" 18 "github.com/bakjos/protoreflect/internal/testutil" 19 ) 20 21 func TestEmptyParse(t *testing.T) { 22 p := Parser{ 23 Accessor: func(filename string) (io.ReadCloser, error) { 24 return ioutil.NopCloser(bytes.NewReader(nil)), nil 25 }, 26 } 27 fd, err := p.ParseFiles("foo.proto") 28 testutil.Ok(t, err) 29 testutil.Eq(t, 1, len(fd)) 30 testutil.Eq(t, "foo.proto", fd[0].GetName()) 31 testutil.Eq(t, 0, len(fd[0].GetDependencies())) 32 testutil.Eq(t, 0, len(fd[0].GetMessageTypes())) 33 testutil.Eq(t, 0, len(fd[0].GetEnumTypes())) 34 testutil.Eq(t, 0, len(fd[0].GetExtensions())) 35 testutil.Eq(t, 0, len(fd[0].GetServices())) 36 } 37 38 func TestSimpleParse(t *testing.T) { 39 protos := map[string]*parseResult{} 40 41 // Just verify that we can successfully parse the same files we use for 42 // testing. We do a *very* shallow check of what was parsed because we know 43 // it won't be fully correct until after linking. (So that will be tested 44 // below, where we parse *and* link.) 45 res, err := parseFileForTest("../../internal/testprotos/desc_test1.proto") 46 testutil.Ok(t, err) 47 fd := res.fd 48 testutil.Eq(t, "../../internal/testprotos/desc_test1.proto", fd.GetName()) 49 testutil.Eq(t, "testprotos", fd.GetPackage()) 50 testutil.Require(t, hasExtension(fd, "xtm")) 51 testutil.Require(t, hasMessage(fd, "TestMessage")) 52 protos[fd.GetName()] = res 53 54 res, err = parseFileForTest("../../internal/testprotos/desc_test2.proto") 55 testutil.Ok(t, err) 56 fd = res.fd 57 testutil.Eq(t, "../../internal/testprotos/desc_test2.proto", fd.GetName()) 58 testutil.Eq(t, "testprotos", fd.GetPackage()) 59 testutil.Require(t, hasExtension(fd, "groupx")) 60 testutil.Require(t, hasMessage(fd, "GroupX")) 61 testutil.Require(t, hasMessage(fd, "Frobnitz")) 62 protos[fd.GetName()] = res 63 64 res, err = parseFileForTest("../../internal/testprotos/desc_test_defaults.proto") 65 testutil.Ok(t, err) 66 fd = res.fd 67 testutil.Eq(t, "../../internal/testprotos/desc_test_defaults.proto", fd.GetName()) 68 testutil.Eq(t, "testprotos", fd.GetPackage()) 69 testutil.Require(t, hasMessage(fd, "PrimitiveDefaults")) 70 protos[fd.GetName()] = res 71 72 res, err = parseFileForTest("../../internal/testprotos/desc_test_field_types.proto") 73 testutil.Ok(t, err) 74 fd = res.fd 75 testutil.Eq(t, "../../internal/testprotos/desc_test_field_types.proto", fd.GetName()) 76 testutil.Eq(t, "testprotos", fd.GetPackage()) 77 testutil.Require(t, hasEnum(fd, "TestEnum")) 78 testutil.Require(t, hasMessage(fd, "UnaryFields")) 79 protos[fd.GetName()] = res 80 81 res, err = parseFileForTest("../../internal/testprotos/desc_test_options.proto") 82 testutil.Ok(t, err) 83 fd = res.fd 84 testutil.Eq(t, "../../internal/testprotos/desc_test_options.proto", fd.GetName()) 85 testutil.Eq(t, "testprotos", fd.GetPackage()) 86 testutil.Require(t, hasExtension(fd, "mfubar")) 87 testutil.Require(t, hasEnum(fd, "ReallySimpleEnum")) 88 testutil.Require(t, hasMessage(fd, "ReallySimpleMessage")) 89 protos[fd.GetName()] = res 90 91 res, err = parseFileForTest("../../internal/testprotos/desc_test_proto3.proto") 92 testutil.Ok(t, err) 93 fd = res.fd 94 testutil.Eq(t, "../../internal/testprotos/desc_test_proto3.proto", fd.GetName()) 95 testutil.Eq(t, "testprotos", fd.GetPackage()) 96 testutil.Require(t, hasEnum(fd, "Proto3Enum")) 97 testutil.Require(t, hasService(fd, "TestService")) 98 protos[fd.GetName()] = res 99 100 res, err = parseFileForTest("../../internal/testprotos/desc_test_wellknowntypes.proto") 101 testutil.Ok(t, err) 102 fd = res.fd 103 testutil.Eq(t, "../../internal/testprotos/desc_test_wellknowntypes.proto", fd.GetName()) 104 testutil.Eq(t, "testprotos", fd.GetPackage()) 105 testutil.Require(t, hasMessage(fd, "TestWellKnownTypes")) 106 protos[fd.GetName()] = res 107 108 res, err = parseFileForTest("../../internal/testprotos/nopkg/desc_test_nopkg.proto") 109 testutil.Ok(t, err) 110 fd = res.fd 111 testutil.Eq(t, "../../internal/testprotos/nopkg/desc_test_nopkg.proto", fd.GetName()) 112 testutil.Eq(t, "", fd.GetPackage()) 113 protos[fd.GetName()] = res 114 115 res, err = parseFileForTest("../../internal/testprotos/nopkg/desc_test_nopkg_new.proto") 116 testutil.Ok(t, err) 117 fd = res.fd 118 testutil.Eq(t, "../../internal/testprotos/nopkg/desc_test_nopkg_new.proto", fd.GetName()) 119 testutil.Eq(t, "", fd.GetPackage()) 120 testutil.Require(t, hasMessage(fd, "TopLevel")) 121 protos[fd.GetName()] = res 122 123 res, err = parseFileForTest("../../internal/testprotos/pkg/desc_test_pkg.proto") 124 testutil.Ok(t, err) 125 fd = res.fd 126 testutil.Eq(t, "../../internal/testprotos/pkg/desc_test_pkg.proto", fd.GetName()) 127 testutil.Eq(t, "jhump.protoreflect.desc", fd.GetPackage()) 128 testutil.Require(t, hasEnum(fd, "Foo")) 129 testutil.Require(t, hasMessage(fd, "Bar")) 130 protos[fd.GetName()] = res 131 132 // We'll also check our fixup logic to make sure it correctly rewrites the 133 // names of the files to match corresponding import statementes. This should 134 // strip the "../../internal/testprotos/" prefix from each file. 135 protos = fixupFilenames(protos) 136 var actual []string 137 for n := range protos { 138 actual = append(actual, n) 139 } 140 sort.Strings(actual) 141 expected := []string{ 142 "desc_test1.proto", 143 "desc_test2.proto", 144 "desc_test_defaults.proto", 145 "desc_test_field_types.proto", 146 "desc_test_options.proto", 147 "desc_test_proto3.proto", 148 "desc_test_wellknowntypes.proto", 149 "nopkg/desc_test_nopkg.proto", 150 "nopkg/desc_test_nopkg_new.proto", 151 "pkg/desc_test_pkg.proto", 152 } 153 testutil.Eq(t, expected, actual) 154 } 155 156 func parseFileForTest(filename string) (*parseResult, error) { 157 f, err := os.Open(filename) 158 if err != nil { 159 return nil, err 160 } 161 defer func() { 162 _ = f.Close() 163 }() 164 errs := newErrorHandler(nil, nil) 165 res := parseProto(filename, f, errs, true, true) 166 return res, errs.getError() 167 } 168 169 func hasExtension(fd *dpb.FileDescriptorProto, name string) bool { 170 for _, ext := range fd.Extension { 171 if ext.GetName() == name { 172 return true 173 } 174 } 175 return false 176 } 177 178 func hasMessage(fd *dpb.FileDescriptorProto, name string) bool { 179 for _, md := range fd.MessageType { 180 if md.GetName() == name { 181 return true 182 } 183 } 184 return false 185 } 186 187 func hasEnum(fd *dpb.FileDescriptorProto, name string) bool { 188 for _, ed := range fd.EnumType { 189 if ed.GetName() == name { 190 return true 191 } 192 } 193 return false 194 } 195 196 func hasService(fd *dpb.FileDescriptorProto, name string) bool { 197 for _, sd := range fd.Service { 198 if sd.GetName() == name { 199 return true 200 } 201 } 202 return false 203 } 204 205 func TestAggregateValueInUninterpretedOptions(t *testing.T) { 206 res, err := parseFileForTest("../../internal/testprotos/desc_test_complex.proto") 207 testutil.Ok(t, err) 208 fd := res.fd 209 210 aggregateValue1 := *fd.Service[0].Method[0].Options.UninterpretedOption[0].AggregateValue 211 testutil.Eq(t, "{ authenticated: true permission{ action: LOGIN entity: \"client\" } }", aggregateValue1) 212 213 aggregateValue2 := *fd.Service[0].Method[1].Options.UninterpretedOption[0].AggregateValue 214 testutil.Eq(t, "{ authenticated: true permission{ action: READ entity: \"user\" } }", aggregateValue2) 215 } 216 217 func TestParseFilesMessageComments(t *testing.T) { 218 p := Parser{ 219 IncludeSourceCodeInfo: true, 220 } 221 protos, err := p.ParseFiles("../../internal/testprotos/desc_test1.proto") 222 testutil.Ok(t, err) 223 comments := "" 224 expected := " Comment for TestMessage\n" 225 for _, p := range protos { 226 msg := p.FindMessage("testprotos.TestMessage") 227 if msg != nil { 228 si := msg.GetSourceInfo() 229 if si != nil { 230 comments = si.GetLeadingComments() 231 } 232 break 233 } 234 } 235 testutil.Eq(t, expected, comments) 236 } 237 238 func TestParseFilesWithImportsNoImportPath(t *testing.T) { 239 relFilePaths := []string{ 240 "a/b/b1.proto", 241 "a/b/b2.proto", 242 "c/c.proto", 243 } 244 245 pwd, err := os.Getwd() 246 testutil.Require(t, err == nil, "%v", err) 247 248 err = os.Chdir("../../internal/testprotos/protoparse") 249 testutil.Require(t, err == nil, "%v", err) 250 p := Parser{} 251 protos, parseErr := p.ParseFiles(relFilePaths...) 252 err = os.Chdir(pwd) 253 testutil.Require(t, err == nil, "%v", err) 254 testutil.Require(t, parseErr == nil, "%v", parseErr) 255 256 testutil.Ok(t, err) 257 testutil.Eq(t, len(relFilePaths), len(protos)) 258 } 259 260 func TestParseFilesWithDependencies(t *testing.T) { 261 // Create some file contents that import a non-well-known proto. 262 // (One of the protos in internal/testprotos is fine.) 263 contents := map[string]string{ 264 "test.proto": ` 265 syntax = "proto3"; 266 import "desc_test_wellknowntypes.proto"; 267 268 message TestImportedType { 269 testprotos.TestWellKnownTypes imported_field = 1; 270 } 271 `, 272 } 273 274 // Establish that we *can* parse the source file with a parser that 275 // registers the dependency. 276 t.Run("DependencyIncluded", func(t *testing.T) { 277 // Create a dependency-aware parser. 278 parser := Parser{ 279 Accessor: FileContentsFromMap(contents), 280 LookupImport: func(imp string) (*desc.FileDescriptor, error) { 281 if imp == "desc_test_wellknowntypes.proto" { 282 return desc.LoadFileDescriptor(imp) 283 } 284 return nil, errors.New("unexpected filename") 285 }, 286 } 287 if _, err := parser.ParseFiles("test.proto"); err != nil { 288 t.Errorf("Could not parse with a non-well-known import: %v", err) 289 } 290 }) 291 t.Run("DependencyIncludedProto", func(t *testing.T) { 292 // Create a dependency-aware parser. 293 parser := Parser{ 294 Accessor: FileContentsFromMap(contents), 295 LookupImportProto: func(imp string) (*dpb.FileDescriptorProto, error) { 296 if imp == "desc_test_wellknowntypes.proto" { 297 fileDescriptor, err := desc.LoadFileDescriptor(imp) 298 if err != nil { 299 return nil, err 300 } 301 return fileDescriptor.AsFileDescriptorProto(), nil 302 } 303 return nil, errors.New("unexpected filename") 304 }, 305 } 306 if _, err := parser.ParseFiles("test.proto"); err != nil { 307 t.Errorf("Could not parse with a non-well-known import: %v", err) 308 } 309 }) 310 311 // Establish that we *can not* parse the source file with a parser that 312 // did not register the dependency. 313 t.Run("DependencyExcluded", func(t *testing.T) { 314 // Create a dependency-aware parser. 315 parser := Parser{ 316 Accessor: FileContentsFromMap(contents), 317 } 318 if _, err := parser.ParseFiles("test.proto"); err == nil { 319 t.Errorf("Expected parse to fail due to lack of an import.") 320 } 321 }) 322 323 // Establish that the accessor has precedence over LookupImport. 324 t.Run("AccessorWins", func(t *testing.T) { 325 // Create a dependency-aware parser that should never be called. 326 parser := Parser{ 327 Accessor: FileContentsFromMap(map[string]string{ 328 "test.proto": `syntax = "proto3";`, 329 }), 330 LookupImport: func(imp string) (*desc.FileDescriptor, error) { 331 t.Errorf("LookupImport was called on a filename available to the Accessor.") 332 return nil, errors.New("unimportant") 333 }, 334 } 335 if _, err := parser.ParseFiles("test.proto"); err != nil { 336 t.Error(err) 337 } 338 }) 339 } 340 341 func TestParseCommentsBeforeDot(t *testing.T) { 342 accessor := FileContentsFromMap(map[string]string{ 343 "test.proto": ` 344 syntax = "proto3"; 345 message Foo { 346 // leading comments 347 .Foo foo = 1; 348 } 349 `, 350 }) 351 352 p := Parser{ 353 Accessor: accessor, 354 IncludeSourceCodeInfo: true, 355 } 356 fds, err := p.ParseFiles("test.proto") 357 testutil.Ok(t, err) 358 359 comment := fds[0].GetMessageTypes()[0].GetFields()[0].GetSourceInfo().GetLeadingComments() 360 testutil.Eq(t, " leading comments\n", comment) 361 } 362 363 func TestParseCustomOptions(t *testing.T) { 364 accessor := FileContentsFromMap(map[string]string{ 365 "test.proto": ` 366 syntax = "proto3"; 367 import "google/protobuf/descriptor.proto"; 368 extend google.protobuf.MessageOptions { 369 string foo = 30303; 370 int64 bar = 30304; 371 } 372 message Foo { 373 option (.foo) = "foo"; 374 option (bar) = 123; 375 } 376 `, 377 }) 378 379 p := Parser{ 380 Accessor: accessor, 381 IncludeSourceCodeInfo: true, 382 } 383 fds, err := p.ParseFiles("test.proto") 384 testutil.Ok(t, err) 385 386 md := fds[0].GetMessageTypes()[0] 387 opts := md.GetMessageOptions() 388 data := internal.GetUnrecognized(opts) 389 buf := codec.NewBuffer(data) 390 391 tag, wt, err := buf.DecodeTagAndWireType() 392 testutil.Ok(t, err) 393 testutil.Eq(t, int32(30303), tag) 394 testutil.Eq(t, int8(proto.WireBytes), wt) 395 fieldData, err := buf.DecodeRawBytes(false) 396 testutil.Ok(t, err) 397 testutil.Eq(t, "foo", string(fieldData)) 398 399 tag, wt, err = buf.DecodeTagAndWireType() 400 testutil.Ok(t, err) 401 testutil.Eq(t, int32(30304), tag) 402 testutil.Eq(t, int8(proto.WireVarint), wt) 403 fieldVal, err := buf.DecodeVarint() 404 testutil.Ok(t, err) 405 testutil.Eq(t, uint64(123), fieldVal) 406 }