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