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