github.com/jhump/protocompile@v0.0.0-20221021153901-4f6f732835e8/compiler_test.go (about)

     1  package protocompile
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"os"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  	"google.golang.org/protobuf/reflect/protodesc"
    12  	"google.golang.org/protobuf/reflect/protoregistry"
    13  
    14  	_ "github.com/jhump/protocompile/internal/testprotos"
    15  )
    16  
    17  func TestParseFilesMessageComments(t *testing.T) {
    18  	comp := Compiler{
    19  		Resolver:          &SourceResolver{},
    20  		IncludeSourceInfo: true,
    21  	}
    22  	ctx := context.Background()
    23  	files, err := comp.Compile(ctx, "internal/testprotos/desc_test1.proto")
    24  	if !assert.Nil(t, err, "%v", err) {
    25  		t.FailNow()
    26  	}
    27  	comments := ""
    28  	expected := " Comment for TestMessage\n"
    29  	for _, fd := range files {
    30  		msg := fd.Messages().ByName("TestMessage")
    31  		if msg != nil {
    32  			si := fd.SourceLocations().ByDescriptor(msg)
    33  			if si.Path != nil {
    34  				comments = si.LeadingComments
    35  			}
    36  			break
    37  		}
    38  	}
    39  	assert.Equal(t, expected, comments)
    40  }
    41  
    42  func TestParseFilesWithImportsNoImportPath(t *testing.T) {
    43  	relFilePaths := []string{
    44  		"a/b/b1.proto",
    45  		"a/b/b2.proto",
    46  		"c/c.proto",
    47  	}
    48  
    49  	pwd, err := os.Getwd()
    50  	assert.Nil(t, err, "%v", err)
    51  
    52  	err = os.Chdir("internal/testprotos/more")
    53  	assert.Nil(t, err, "%v", err)
    54  	defer func() {
    55  		// restore working directory
    56  		_ = os.Chdir(pwd)
    57  	}()
    58  
    59  	comp := Compiler{
    60  		Resolver: WithStandardImports(&SourceResolver{}),
    61  	}
    62  	ctx := context.Background()
    63  	protos, err := comp.Compile(ctx, relFilePaths...)
    64  	if !assert.Nil(t, err, "%v", err) {
    65  		t.FailNow()
    66  	}
    67  	assert.Equal(t, len(relFilePaths), len(protos))
    68  }
    69  
    70  func TestParseFilesWithDependencies(t *testing.T) {
    71  	// Create some file contents that import a non-well-known proto.
    72  	// (One of the protos in internal/testprotos is fine.)
    73  	contents := map[string]string{
    74  		"test.proto": `
    75  			syntax = "proto3";
    76  			import "desc_test_wellknowntypes.proto";
    77  
    78  			message TestImportedType {
    79  				testprotos.TestWellKnownTypes imported_field = 1;
    80  			}
    81  		`,
    82  	}
    83  	baseResolver := ResolverFunc(func(f string) (SearchResult, error) {
    84  		s, ok := contents[f]
    85  		if !ok {
    86  			return SearchResult{}, os.ErrNotExist
    87  		}
    88  		return SearchResult{Source: strings.NewReader(s)}, nil
    89  	})
    90  
    91  	wktDesc, err := protoregistry.GlobalFiles.FindFileByPath("desc_test_wellknowntypes.proto")
    92  	assert.Nil(t, err)
    93  	wktDescProto := protodesc.ToFileDescriptorProto(wktDesc)
    94  	ctx := context.Background()
    95  
    96  	// Establish that we *can* parse the source file with a parser that
    97  	// registers the dependency.
    98  	t.Run("DependencyIncluded", func(t *testing.T) {
    99  		// Create a dependency-aware compiler.
   100  		compiler := Compiler{
   101  			Resolver: ResolverFunc(func(f string) (SearchResult, error) {
   102  				if f == "desc_test_wellknowntypes.proto" {
   103  					return SearchResult{Desc: wktDesc}, nil
   104  				}
   105  				return baseResolver.FindFileByPath(f)
   106  			}),
   107  		}
   108  		_, err := compiler.Compile(ctx, "test.proto")
   109  		assert.Nil(t, err, "%v", err)
   110  	})
   111  	t.Run("DependencyIncludedProto", func(t *testing.T) {
   112  		// Create a dependency-aware compiler.
   113  		compiler := Compiler{
   114  			Resolver: WithStandardImports(ResolverFunc(func(f string) (SearchResult, error) {
   115  				if f == "desc_test_wellknowntypes.proto" {
   116  					return SearchResult{Proto: wktDescProto}, nil
   117  				}
   118  				return baseResolver.FindFileByPath(f)
   119  			})),
   120  		}
   121  		_, err := compiler.Compile(ctx, "test.proto")
   122  		assert.Nil(t, err, "%v", err)
   123  	})
   124  
   125  	// Establish that we *can not* parse the source file with a parser that
   126  	// did not register the dependency.
   127  	t.Run("DependencyExcluded", func(t *testing.T) {
   128  		// Create a dependency-UNaware parser.
   129  		compiler := Compiler{Resolver: baseResolver}
   130  		_, err := compiler.Compile(ctx, "test.proto")
   131  		assert.NotNil(t, err, "expected parse to fail")
   132  	})
   133  
   134  	// Establish that the accessor has precedence over LookupImport.
   135  	t.Run("AccessorWins", func(t *testing.T) {
   136  		// Create a dependency-aware parser that should never be called.
   137  		compiler := Compiler{
   138  			Resolver: ResolverFunc(func(f string) (SearchResult, error) {
   139  				if f == "test.proto" {
   140  					return SearchResult{Source: strings.NewReader(`syntax = "proto3";`)}, nil
   141  				}
   142  				t.Errorf("resolved was called for unexpected filename %q", f)
   143  				return SearchResult{}, os.ErrNotExist
   144  			}),
   145  		}
   146  		_, err := compiler.Compile(ctx, "test.proto")
   147  		assert.Nil(t, err)
   148  	})
   149  }
   150  
   151  func TestParseCommentsBeforeDot(t *testing.T) {
   152  	accessor := SourceAccessorFromMap(map[string]string{
   153  		"test.proto": `
   154  syntax = "proto3";
   155  message Foo {
   156    // leading comments
   157    .Foo foo = 1;
   158  }
   159  `,
   160  	})
   161  
   162  	compiler := Compiler{
   163  		Resolver:          &SourceResolver{Accessor: accessor},
   164  		IncludeSourceInfo: true,
   165  	}
   166  	ctx := context.Background()
   167  	fds, err := compiler.Compile(ctx, "test.proto")
   168  	assert.Nil(t, err)
   169  
   170  	field := fds[0].Messages().Get(0).Fields().Get(0)
   171  	comment := fds[0].SourceLocations().ByDescriptor(field).LeadingComments
   172  	assert.Equal(t, " leading comments\n", comment)
   173  }
   174  
   175  func TestParseCustomOptions(t *testing.T) {
   176  	accessor := SourceAccessorFromMap(map[string]string{
   177  		"test.proto": `
   178  syntax = "proto3";
   179  import "google/protobuf/descriptor.proto";
   180  extend google.protobuf.MessageOptions {
   181      string foo = 30303;
   182      int64 bar = 30304;
   183  }
   184  message Foo {
   185    option (.foo) = "foo";
   186    option (bar) = 123;
   187  }
   188  `,
   189  	})
   190  
   191  	compiler := Compiler{
   192  		Resolver:          WithStandardImports(&SourceResolver{Accessor: accessor}),
   193  		IncludeSourceInfo: true,
   194  	}
   195  	ctx := context.Background()
   196  	fds, err := compiler.Compile(ctx, "test.proto")
   197  	if !assert.Nil(t, err, "%v", err) {
   198  		t.FailNow()
   199  	}
   200  
   201  	ext := fds[0].Extensions().ByName("foo")
   202  	md := fds[0].Messages().Get(0)
   203  	fooVal := md.Options().ProtoReflect().Get(ext)
   204  	assert.Equal(t, "foo", fooVal.String())
   205  
   206  	ext = fds[0].Extensions().ByName("bar")
   207  	barVal := md.Options().ProtoReflect().Get(ext)
   208  	assert.Equal(t, int64(123), barVal.Int())
   209  }
   210  
   211  func TestPanicHandling(t *testing.T) {
   212  	c := Compiler{
   213  		Resolver: ResolverFunc(func(string) (SearchResult, error) {
   214  			panic(errors.New("mui mui bad"))
   215  		}),
   216  	}
   217  	_, err := c.Compile(context.Background(), "test.proto")
   218  	panicErr := err.(PanicError)
   219  	t.Logf("%v\n\n%v", panicErr, panicErr.Stack)
   220  }