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  }