github.com/jhump/protoreflect@v1.16.0/desc/protoprint/print_test.go (about) 1 package protoprint 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "testing" 11 12 "google.golang.org/protobuf/proto" 13 "google.golang.org/protobuf/reflect/protoregistry" 14 "google.golang.org/protobuf/types/descriptorpb" 15 16 "github.com/jhump/protoreflect/desc" 17 "github.com/jhump/protoreflect/desc/protoparse" 18 _ "github.com/jhump/protoreflect/internal/testprotos" 19 "github.com/jhump/protoreflect/internal/testutil" 20 ) 21 22 const ( 23 // When false, test behaves normally, checking output against golden test files. 24 // But when changed to true, running test will actually re-generate golden test 25 // files (which assumes output is correct). 26 regenerateMode = false 27 28 testFilesDirectory = "testfiles" 29 ) 30 31 func reverseByName(a, b Element) bool { 32 // custom sort that is practically the *reverse* of default sort 33 // order, though things like fields/extensions/enum values are 34 // sorted by name (descending) instead of by number 35 36 if a.Kind() != b.Kind() { 37 return a.Kind() > b.Kind() 38 } 39 switch a.Kind() { 40 case KindExtension: 41 if a.Extendee() != b.Extendee() { 42 return a.Extendee() > b.Extendee() 43 } 44 case KindOption: 45 if a.IsCustomOption() != b.IsCustomOption() { 46 return a.IsCustomOption() 47 } 48 } 49 if a.Name() != b.Name() { 50 return a.Name() > b.Name() 51 } 52 if a.Number() != b.Number() { 53 return a.Number() > b.Number() 54 } 55 aStart, aEnd := a.NumberRange() 56 bStart, bEnd := b.NumberRange() 57 if aStart != bStart { 58 return aStart > bStart 59 } 60 return aEnd > bEnd 61 } 62 63 func TestPrinter(t *testing.T) { 64 prs := map[string]*Printer{ 65 "default": {}, 66 "compact": {Compact: true, ShortOptionsExpansionThresholdCount: 5, ShortOptionsExpansionThresholdLength: 100, MessageLiteralExpansionThresholdLength: 80}, 67 "no-trailing-comments": {OmitComments: CommentsTrailing}, 68 "trailing-on-next-line": {TrailingCommentsOnSeparateLine: true}, 69 "only-doc-comments": {OmitComments: CommentsNonDoc}, 70 "multiline-style-comments": {Indent: "\t", PreferMultiLineStyleComments: true}, 71 "sorted": {Indent: " ", SortElements: true, OmitDetachedComments: true}, 72 "sorted-AND-multiline-style-comments": {PreferMultiLineStyleComments: true, SortElements: true}, 73 "custom-sort": {CustomSortFunction: reverseByName}, 74 } 75 76 // create descriptors to print 77 files := []string{ 78 "../../internal/testprotos/desc_test_comments.protoset", 79 "../../internal/testprotos/desc_test_complex_source_info.protoset", 80 "../../internal/testprotos/desc_test_editions.protoset", 81 "../../internal/testprotos/descriptor.protoset", 82 "../../internal/testprotos/desc_test1.protoset", 83 "../../internal/testprotos/proto3_optional/desc_test_proto3_optional.protoset", 84 } 85 fds := make([]*desc.FileDescriptor, len(files)+1) 86 for i, file := range files { 87 fd, err := loadProtoset(file) 88 testutil.Ok(t, err) 89 fds[i] = fd 90 } 91 // extra descriptor that has no source info 92 // NB: We can't use desc.LoadFileDescriptor here because that, under the hood, will get 93 // source code info from the desc/sourceinfo package! So explicitly load the version 94 // from the underlying registry, which will NOT have source code info. 95 underlyingFd, err := protoregistry.GlobalFiles.FindFileByPath("desc_test2.proto") 96 testutil.Ok(t, err) 97 fd, err := desc.WrapFile(underlyingFd) 98 testutil.Ok(t, err) 99 testutil.Require(t, fd.AsFileDescriptorProto().SourceCodeInfo == nil) 100 fds[len(files)] = fd 101 102 for _, fd := range fds { 103 for name, pr := range prs { 104 baseName := filepath.Base(fd.GetName()) 105 ext := filepath.Ext(baseName) 106 baseName = baseName[:len(baseName)-len(ext)] 107 goldenFile := fmt.Sprintf("%s-%s.proto", baseName, name) 108 109 checkFile(t, pr, fd, goldenFile) 110 } 111 } 112 } 113 114 func loadProtoset(path string) (*desc.FileDescriptor, error) { 115 var fds descriptorpb.FileDescriptorSet 116 f, err := os.Open(path) 117 if err != nil { 118 return nil, err 119 } 120 defer f.Close() 121 bb, err := ioutil.ReadAll(f) 122 if err != nil { 123 return nil, err 124 } 125 if err = proto.Unmarshal(bb, &fds); err != nil { 126 return nil, err 127 } 128 return desc.CreateFileDescriptorFromSet(&fds) 129 } 130 131 func checkFile(t *testing.T, pr *Printer, fd *desc.FileDescriptor, goldenFile string) { 132 var buf bytes.Buffer 133 err := pr.PrintProtoFile(fd, &buf) 134 testutil.Ok(t, err) 135 136 checkContents(t, buf.String(), goldenFile) 137 } 138 139 func TestParseAndPrintPreservesAsMuchAsPossible(t *testing.T) { 140 pa := protoparse.Parser{ImportPaths: []string{"../../internal/testprotos"}, IncludeSourceCodeInfo: true} 141 fds, err := pa.ParseFiles("desc_test_comments.proto") 142 testutil.Ok(t, err) 143 fd := fds[0] 144 checkFile(t, &Printer{}, fd, "test-preserve-comments.proto") 145 checkFile(t, &Printer{OmitComments: CommentsNonDoc}, fd, "test-preserve-doc-comments.proto") 146 } 147 148 func TestParseAndPrintWithUnrecognizedOptions(t *testing.T) { 149 files := map[string]string{"test.proto": ` 150 syntax = "proto3"; 151 152 import "google/protobuf/descriptor.proto"; 153 154 message Test {} 155 156 message Foo { 157 repeated Bar bar = 1; 158 159 message Bar { 160 Baz baz = 1; 161 string name = 2; 162 } 163 164 enum Baz { 165 ZERO = 0; 166 FROB = 1; 167 NITZ = 2; 168 } 169 } 170 171 extend google.protobuf.MethodOptions { 172 Foo foo = 54321; 173 } 174 175 service TestService { 176 rpc Get (Test) returns (Test) { 177 option (foo).bar = { baz:FROB name:"abc" }; 178 option (foo).bar = { baz:NITZ name:"xyz" }; 179 } 180 } 181 `} 182 183 pa := &protoparse.Parser{ 184 Accessor: protoparse.FileContentsFromMap(files), 185 } 186 fds, err := pa.ParseFiles("test.proto") 187 testutil.Ok(t, err) 188 189 // Sanity check that this resulted in unrecognized options 190 unk := fds[0].FindSymbol("TestService.Get").(*desc.MethodDescriptor).GetMethodOptions().ProtoReflect().GetUnknown() 191 testutil.Require(t, len(unk) > 0) 192 193 checkFile(t, &Printer{}, fds[0], "test-unrecognized-options.proto") 194 } 195 196 func TestPrintUninterpretedOptions(t *testing.T) { 197 files := map[string]string{"test.proto": ` 198 syntax = "proto2"; 199 package pkg; 200 option go_package = "some.pkg"; 201 import "google/protobuf/descriptor.proto"; 202 message Options { 203 optional bool some_option_value = 1; 204 } 205 extend google.protobuf.MessageOptions { 206 optional Options my_some_option = 11964; 207 } 208 message SomeMessage { 209 option (.pkg.my_some_option) = {some_option_value : true}; 210 } 211 `} 212 213 pa := &protoparse.Parser{ 214 Accessor: protoparse.FileContentsFromMap(files), 215 } 216 fds, err := pa.ParseFilesButDoNotLink("test.proto") 217 testutil.Ok(t, err) 218 219 // Sanity check that this resulted in uninterpreted options 220 unint := fds[0].MessageType[1].Options.UninterpretedOption 221 testutil.Require(t, len(unint) > 0) 222 223 descFd, err := desc.WrapFile((*descriptorpb.FileDescriptorProto)(nil).ProtoReflect().Descriptor().ParentFile()) 224 testutil.Ok(t, err) 225 fd, err := desc.CreateFileDescriptor(fds[0], descFd) 226 testutil.Ok(t, err) 227 228 checkFile(t, &Printer{}, fd, "test-uninterpreted-options.proto") 229 } 230 231 func TestPrintNonFileDescriptors(t *testing.T) { 232 pa := protoparse.Parser{ImportPaths: []string{"../../internal/testprotos"}, IncludeSourceCodeInfo: true} 233 fds, err := pa.ParseFiles("desc_test_comments.proto") 234 testutil.Ok(t, err) 235 fd := fds[0] 236 237 var buf bytes.Buffer 238 crawl(t, fd, &Printer{}, &buf) 239 checkContents(t, buf.String(), "test-non-files-full.txt") 240 241 buf.Reset() 242 crawl(t, fd, &Printer{OmitComments: CommentsNonDoc, Compact: true, SortElements: true, ForceFullyQualifiedNames: true}, &buf) 243 checkContents(t, buf.String(), "test-non-files-compact.txt") 244 } 245 246 func crawl(t *testing.T, d desc.Descriptor, p *Printer, out io.Writer) { 247 str, err := p.PrintProtoToString(d) 248 testutil.Ok(t, err) 249 fmt.Fprintf(out, "-------- %s (%T) --------\n", d.GetFullyQualifiedName(), d) 250 fmt.Fprint(out, str) 251 252 switch d := d.(type) { 253 case *desc.FileDescriptor: 254 for _, md := range d.GetMessageTypes() { 255 crawl(t, md, p, out) 256 } 257 for _, ed := range d.GetEnumTypes() { 258 crawl(t, ed, p, out) 259 } 260 for _, extd := range d.GetExtensions() { 261 crawl(t, extd, p, out) 262 } 263 for _, sd := range d.GetServices() { 264 crawl(t, sd, p, out) 265 } 266 case *desc.MessageDescriptor: 267 for _, fd := range d.GetFields() { 268 crawl(t, fd, p, out) 269 } 270 for _, ood := range d.GetOneOfs() { 271 crawl(t, ood, p, out) 272 } 273 for _, md := range d.GetNestedMessageTypes() { 274 crawl(t, md, p, out) 275 } 276 for _, ed := range d.GetNestedEnumTypes() { 277 crawl(t, ed, p, out) 278 } 279 for _, extd := range d.GetNestedExtensions() { 280 crawl(t, extd, p, out) 281 } 282 case *desc.EnumDescriptor: 283 for _, evd := range d.GetValues() { 284 crawl(t, evd, p, out) 285 } 286 case *desc.ServiceDescriptor: 287 for _, mtd := range d.GetMethods() { 288 crawl(t, mtd, p, out) 289 } 290 } 291 } 292 293 func checkContents(t *testing.T, actualContents string, goldenFileName string) { 294 goldenFileName = filepath.Join(testFilesDirectory, goldenFileName) 295 296 if regenerateMode { 297 err := ioutil.WriteFile(goldenFileName, []byte(actualContents), 0666) 298 testutil.Ok(t, err) 299 } 300 301 // verify that output matches golden test files 302 b, err := ioutil.ReadFile(goldenFileName) 303 testutil.Ok(t, err) 304 305 testutil.Eq(t, string(b), actualContents, "wrong file contents for %s", goldenFileName) 306 } 307 308 func TestQuoteString(t *testing.T) { 309 // other tests have examples of encountering invalid UTF8 and printable unicode 310 // so this is just for testing how unprintable valid unicode characters are rendered 311 s := quotedString("\x04") 312 testutil.Eq(t, "\"\\004\"", s) 313 s = quotedString("\x7F") 314 testutil.Eq(t, "\"\\177\"", s) 315 s = quotedString("\u2028") 316 testutil.Eq(t, "\"\\u2028\"", s) 317 s = quotedString("\U0010FFFF") 318 testutil.Eq(t, "\"\\U0010FFFF\"", s) 319 }