github.com/xiaoshude/protoreflect@v1.16.1-0.20220310024924-8c94d7247598/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/xiaoshude/protoreflect/desc"
    15  	_ "github.com/xiaoshude/protoreflect/internal/testprotos"
    16  	"github.com/xiaoshude/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  	for _, dep := range exp.GetDependencies() {
    58  		dep.AsFileDescriptorProto().SourceCodeInfo = nil
    59  	}
    60  
    61  	checkFiles(t, fds[0], exp, map[string]struct{}{})
    62  }
    63  
    64  func checkFiles(t *testing.T, act, exp *desc.FileDescriptor, checked map[string]struct{}) {
    65  	if _, ok := checked[act.GetName()]; ok {
    66  		// already checked
    67  		return
    68  	}
    69  	checked[act.GetName()] = struct{}{}
    70  
    71  	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()))
    72  
    73  	for i, dep := range act.GetDependencies() {
    74  		checkFiles(t, dep, exp.GetDependencies()[i], checked)
    75  	}
    76  }
    77  
    78  func toString(m proto.Message) string {
    79  	msh := jsonpb.Marshaler{Indent: "  "}
    80  	s, err := msh.MarshalToString(m)
    81  	if err != nil {
    82  		panic(err)
    83  	}
    84  	return s
    85  }
    86  
    87  func TestLinkerValidation(t *testing.T) {
    88  	testCases := []struct {
    89  		input  map[string]string
    90  		errMsg string
    91  	}{
    92  		{
    93  			map[string]string{
    94  				"foo.proto":  `syntax = "proto3"; package namespace.a; import "foo2.proto"; import "foo3.proto"; import "foo4.proto"; message Foo{ b.Bar a = 1; b.Baz b = 2; b.Buzz c = 3; }`,
    95  				"foo2.proto": `syntax = "proto3"; package namespace.b; message Bar{}`,
    96  				"foo3.proto": `syntax = "proto3"; package namespace.b; message Baz{}`,
    97  				"foo4.proto": `syntax = "proto3"; package namespace.b; message Buzz{}`,
    98  			},
    99  			"", // should succeed
   100  		},
   101  		{
   102  			map[string]string{
   103  				"foo.proto": "import \"foo2.proto\"; message fubar{}",
   104  			},
   105  			`foo.proto:1:8: file not found: foo2.proto`,
   106  		},
   107  		{
   108  			map[string]string{
   109  				"foo.proto":  "import \"foo2.proto\"; message fubar{}",
   110  				"foo2.proto": "import \"foo.proto\"; message baz{}",
   111  			},
   112  			`foo.proto:1:8: cycle found in imports: "foo.proto" -> "foo2.proto" -> "foo.proto"`,
   113  		},
   114  		{
   115  			map[string]string{
   116  				"foo.proto": "enum foo { bar = 1; baz = 2; } enum fu { bar = 1; baz = 2; }",
   117  			},
   118  			`foo.proto:1:42: duplicate symbol bar: already defined as enum value; protobuf uses C++ scoping rules for enum values, so they exist in the scope enclosing the enum`,
   119  		},
   120  		{
   121  			map[string]string{
   122  				"foo.proto": "message foo {} enum foo { V = 0; }",
   123  			},
   124  			"foo.proto:1:16: duplicate symbol foo: already defined as message",
   125  		},
   126  		{
   127  			map[string]string{
   128  				"foo.proto": "message foo { optional string a = 1; optional string a = 2; }",
   129  			},
   130  			"foo.proto:1:38: duplicate symbol foo.a: already defined as field",
   131  		},
   132  		{
   133  			map[string]string{
   134  				"foo.proto":  "message foo {}",
   135  				"foo2.proto": "enum foo { V = 0; }",
   136  			},
   137  			"foo2.proto:1:1: duplicate symbol foo: already defined as message in \"foo.proto\"",
   138  		},
   139  		{
   140  			map[string]string{
   141  				"foo.proto": "message foo { optional blah a = 1; }",
   142  			},
   143  			"foo.proto:1:24: field foo.a: unknown type blah",
   144  		},
   145  		{
   146  			map[string]string{
   147  				"foo.proto": "message foo { optional bar.baz a = 1; } service bar { rpc baz (foo) returns (foo); }",
   148  			},
   149  			"foo.proto:1:24: field foo.a: invalid type: bar.baz is a method, not a message or enum",
   150  		},
   151  		{
   152  			map[string]string{
   153  				"foo.proto": "message foo { extensions 1 to 2; } extend foo { optional string a = 1; } extend foo { optional int32 b = 1; }",
   154  			},
   155  			"foo.proto:1:106: field b: duplicate extension: a and b are both using tag 1",
   156  		},
   157  		{
   158  			map[string]string{
   159  				"foo.proto": "package fu.baz; extend foobar { optional string a = 1; }",
   160  			},
   161  			"foo.proto:1:24: unknown extendee type foobar",
   162  		},
   163  		{
   164  			map[string]string{
   165  				"foo.proto": "package fu.baz; service foobar{} extend foobar { optional string a = 1; }",
   166  			},
   167  			"foo.proto:1:41: extendee is invalid: fu.baz.foobar is a service, not a message",
   168  		},
   169  		{
   170  			map[string]string{
   171  				"foo.proto": "package fu.baz; message foobar{ extensions 1; } extend foobar { optional string a = 2; }",
   172  			},
   173  			"foo.proto:1:85: field fu.baz.a: tag 2 is not in valid range for extended type fu.baz.foobar",
   174  		},
   175  		{
   176  			map[string]string{
   177  				"foo.proto":  "package fu.baz; import public \"foo2.proto\"; message foobar{ optional baz a = 1; }",
   178  				"foo2.proto": "package fu.baz; import \"foo3.proto\"; message fizzle{ }",
   179  				"foo3.proto": "package fu.baz; message baz{ }",
   180  			},
   181  			"foo.proto:1:70: field fu.baz.foobar.a: unknown type baz; resolved to fu.baz which is not defined; consider using a leading dot",
   182  		},
   183  		{
   184  			map[string]string{
   185  				"foo.proto": `
   186  					syntax = "proto2";
   187  					package foo;
   188  					import "google/protobuf/descriptor.proto";
   189  					extend google.protobuf.FileOptions           { optional string fil_foo = 12000; }
   190  					extend google.protobuf.MessageOptions        { optional string msg_foo = 12000; }
   191  					extend google.protobuf.FieldOptions          { optional string fld_foo = 12000 [(fld_foo) = "extension"]; }
   192  					extend google.protobuf.OneofOptions          { optional string oof_foo = 12000; }
   193  					extend google.protobuf.EnumOptions           { optional string enm_foo = 12000; }
   194  					extend google.protobuf.EnumValueOptions      { optional string env_foo = 12000; }
   195  					extend google.protobuf.ExtensionRangeOptions { optional string ext_foo = 12000; }
   196  					extend google.protobuf.ServiceOptions        { optional string svc_foo = 12000; }
   197  					extend google.protobuf.MethodOptions         { optional string mtd_foo = 12000; }
   198  					option (fil_foo) = "file";
   199  					message Bar {
   200  						option (msg_foo) = "message";
   201  						oneof foo {
   202  							option (oof_foo) = "oneof";
   203  							string bar = 1 [(fld_foo) = "field"];
   204  						}
   205  						extensions 100 to 200 [(ext_foo) = "extensionrange"];
   206  					}
   207  					enum Baz {
   208  						option (enm_foo) = "enum";
   209  						ZERO = 0 [(env_foo) = "enumvalue"];
   210  					}
   211  					service FooService {
   212  						option (svc_foo) = "service";
   213  						rpc Bar(Bar) returns (Bar) {
   214  							option (mtd_foo) = "method";
   215  						}
   216  					}
   217  					`,
   218  			},
   219  			"", // should success
   220  		},
   221  		{
   222  			map[string]string{
   223  				"foo.proto": "package fu.baz; message foobar{ repeated string a = 1 [default = \"abc\"]; }",
   224  			},
   225  			"foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is repeated",
   226  		},
   227  		{
   228  			map[string]string{
   229  				"foo.proto": "package fu.baz; message foobar{ optional foobar a = 1 [default = { a: {} }]; }",
   230  			},
   231  			"foo.proto:1:56: field fu.baz.foobar.a: default value cannot be set because field is a message",
   232  		},
   233  		{
   234  			map[string]string{
   235  				"foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = { a: \"abc\" }]; }",
   236  			},
   237  			"foo.proto:1:66: field fu.baz.foobar.a: default value cannot be a message",
   238  		},
   239  		{
   240  			map[string]string{
   241  				"foo.proto": "package fu.baz; message foobar{ optional string a = 1 [default = 1.234]; }",
   242  			},
   243  			"foo.proto:1:66: field fu.baz.foobar.a: option default: expecting string, got double",
   244  		},
   245  		{
   246  			map[string]string{
   247  				"foo.proto": "package fu.baz; enum abc { OK=0; NOK=1; } message foobar{ optional abc a = 1 [default = NACK]; }",
   248  			},
   249  			"foo.proto:1:89: field fu.baz.foobar.a: option default: enum fu.baz.abc has no value named NACK",
   250  		},
   251  		{
   252  			map[string]string{
   253  				"foo.proto": "option b = 123;",
   254  			},
   255  			"foo.proto:1:8: option b: field b of google.protobuf.FileOptions does not exist",
   256  		},
   257  		{
   258  			map[string]string{
   259  				"foo.proto": "option (foo.bar) = 123;",
   260  			},
   261  			"foo.proto:1:8: unknown extension foo.bar",
   262  		},
   263  		{
   264  			map[string]string{
   265  				"foo.proto": "option uninterpreted_option = { };",
   266  			},
   267  			"foo.proto:1:8: invalid option 'uninterpreted_option'",
   268  		},
   269  		{
   270  			map[string]string{
   271  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   272  					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
   273  					"extend foo { optional int32 b = 10; }\n" +
   274  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   275  					"option (f).b = 123;",
   276  			},
   277  			"foo.proto:5:12: option (f).b: field b of foo does not exist",
   278  		},
   279  		{
   280  			map[string]string{
   281  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   282  					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
   283  					"extend foo { optional int32 b = 10; }\n" +
   284  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   285  					"option (f).a = 123;",
   286  			},
   287  			"foo.proto:5:16: option (f).a: expecting string, got integer",
   288  		},
   289  		{
   290  			map[string]string{
   291  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   292  					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
   293  					"extend foo { optional int32 b = 10; }\n" +
   294  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   295  					"option (b) = 123;",
   296  			},
   297  			"foo.proto:5:8: option (b): extension b should extend google.protobuf.FileOptions but instead extends foo",
   298  		},
   299  		{
   300  			map[string]string{
   301  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   302  					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
   303  					"extend foo { optional int32 b = 10; }\n" +
   304  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   305  					"option (foo) = 123;",
   306  			},
   307  			"foo.proto:5:8: invalid extension: foo is a message, not an extension",
   308  		},
   309  		{
   310  			map[string]string{
   311  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   312  					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
   313  					"extend foo { optional int32 b = 10; }\n" +
   314  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   315  					"option (foo.a) = 123;",
   316  			},
   317  			"foo.proto:5:8: invalid extension: foo.a is a field but not an extension",
   318  		},
   319  		{
   320  			map[string]string{
   321  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   322  					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
   323  					"extend foo { optional int32 b = 10; }\n" +
   324  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   325  					"option (f) = { a: [ 123 ] };",
   326  			},
   327  			"foo.proto:5:19: option (f): value is an array but field is not repeated",
   328  		},
   329  		{
   330  			map[string]string{
   331  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   332  					"message foo { repeated string a = 1; extensions 10 to 20; }\n" +
   333  					"extend foo { optional int32 b = 10; }\n" +
   334  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   335  					"option (f) = { a: [ \"a\", \"b\", 123 ] };",
   336  			},
   337  			"foo.proto:5:31: option (f): expecting string, got integer",
   338  		},
   339  		{
   340  			map[string]string{
   341  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   342  					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
   343  					"extend foo { optional int32 b = 10; }\n" +
   344  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   345  					"option (f) = { a: \"a\" };\n" +
   346  					"option (f) = { a: \"b\" };",
   347  			},
   348  			"foo.proto:6:8: option (f): non-repeated option field f already set",
   349  		},
   350  		{
   351  			map[string]string{
   352  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   353  					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
   354  					"extend foo { optional int32 b = 10; }\n" +
   355  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   356  					"option (f) = { a: \"a\" };\n" +
   357  					"option (f).a = \"b\";",
   358  			},
   359  			"foo.proto:6:12: option (f).a: non-repeated option field a already set",
   360  		},
   361  		{
   362  			map[string]string{
   363  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   364  					"message foo { optional string a = 1; extensions 10 to 20; }\n" +
   365  					"extend foo { optional int32 b = 10; }\n" +
   366  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   367  					"option (f) = { a: \"a\" };\n" +
   368  					"option (f).(b) = \"b\";",
   369  			},
   370  			"foo.proto:6:18: option (f).(b): expecting int32, got string",
   371  		},
   372  		{
   373  			map[string]string{
   374  				"foo.proto": "import \"google/protobuf/descriptor.proto\";\n" +
   375  					"message foo { required string a = 1; required string b = 2; }\n" +
   376  					"extend google.protobuf.FileOptions { optional foo f = 20000; }\n" +
   377  					"option (f) = { a: \"a\" };\n",
   378  			},
   379  			"foo.proto:1:1: error in file options: some required fields missing: (f).b",
   380  		},
   381  		{
   382  			map[string]string{
   383  				"foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional int32 bar = 1; }",
   384  			},
   385  			"foo.proto:1:99: messages with message-set wire format cannot contain scalar extensions, only messages",
   386  		},
   387  		{
   388  			map[string]string{
   389  				"foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to 100; } extend Foo { optional Foo bar = 1; }",
   390  			},
   391  			"", // should succeed
   392  		},
   393  		{
   394  			map[string]string{
   395  				"foo.proto": "message Foo { extensions 1 to max; } extend Foo { optional int32 bar = 536870912; }",
   396  			},
   397  			"foo.proto:1:72: field bar: tag 536870912 is not in valid range for extended type Foo",
   398  		},
   399  		{
   400  			map[string]string{
   401  				"foo.proto": "message Foo { option message_set_wire_format = true; extensions 1 to max; } extend Foo { optional Foo bar = 536870912; }",
   402  			},
   403  			"", // should succeed
   404  		},
   405  		{
   406  			map[string]string{
   407  				"foo.proto": `syntax = "proto3"; package com.google; import "google/protobuf/wrappers.proto"; message Foo { google.protobuf.StringValue str = 1; }`,
   408  			},
   409  			"foo.proto:1:95: field com.google.Foo.str: unknown type google.protobuf.StringValue; resolved to com.google.protobuf.StringValue which is not defined; consider using a leading dot",
   410  		},
   411  	}
   412  	for i, tc := range testCases {
   413  		acc := func(filename string) (io.ReadCloser, error) {
   414  			f, ok := tc.input[filename]
   415  			if !ok {
   416  				return nil, fmt.Errorf("file not found: %s", filename)
   417  			}
   418  			return ioutil.NopCloser(strings.NewReader(f)), nil
   419  		}
   420  		names := make([]string, 0, len(tc.input))
   421  		for k := range tc.input {
   422  			names = append(names, k)
   423  		}
   424  		_, err := Parser{Accessor: acc}.ParseFiles(names...)
   425  		if tc.errMsg == "" {
   426  			if err != nil {
   427  				t.Errorf("case %d: expecting no error; instead got error %q", i, err)
   428  			}
   429  		} else if err == nil {
   430  			t.Errorf("case %d: expecting validation error %q; instead got no error", i, tc.errMsg)
   431  		} else if err.Error() != tc.errMsg {
   432  			t.Errorf("case %d: expecting validation error %q; instead got: %q", i, tc.errMsg, err)
   433  		}
   434  	}
   435  }
   436  
   437  func TestProto3Enums(t *testing.T) {
   438  	file1 := `syntax = "<SYNTAX>"; enum bar { A = 0; B = 1; }`
   439  	file2 := `syntax = "<SYNTAX>"; import "f1.proto"; message foo { <LABEL> bar bar = 1; }`
   440  	getFileContents := func(file, syntax string) string {
   441  		contents := strings.Replace(file, "<SYNTAX>", syntax, 1)
   442  		label := ""
   443  		if syntax == "proto2" {
   444  			label = "optional"
   445  		}
   446  		return strings.Replace(contents, "<LABEL>", label, 1)
   447  	}
   448  
   449  	syntaxOptions := []string{"proto2", "proto3"}
   450  	for _, o1 := range syntaxOptions {
   451  		fc1 := getFileContents(file1, o1)
   452  
   453  		for _, o2 := range syntaxOptions {
   454  			fc2 := getFileContents(file2, o2)
   455  
   456  			// now parse the protos
   457  			acc := func(filename string) (io.ReadCloser, error) {
   458  				var data string
   459  				switch filename {
   460  				case "f1.proto":
   461  					data = fc1
   462  				case "f2.proto":
   463  					data = fc2
   464  				default:
   465  					return nil, fmt.Errorf("file not found: %s", filename)
   466  				}
   467  				return ioutil.NopCloser(strings.NewReader(data)), nil
   468  			}
   469  			_, err := Parser{Accessor: acc}.ParseFiles("f1.proto", "f2.proto")
   470  
   471  			if o1 != o2 && o2 == "proto3" {
   472  				expected := "f2.proto:1:54: field foo.bar: cannot use proto2 enum bar in a proto3 message"
   473  				if err == nil {
   474  					t.Errorf("expecting validation error; instead got no error")
   475  				} else if err.Error() != expected {
   476  					t.Errorf("expecting validation error %q; instead got: %q", expected, err)
   477  				}
   478  			} else {
   479  				// other cases succeed (okay to for proto2 to use enum from proto3 file and
   480  				// obviously okay for proto2 importing proto2 and proto3 importing proto3)
   481  				testutil.Ok(t, err)
   482  			}
   483  		}
   484  	}
   485  }
   486  
   487  func TestCustomErrorReporterWithLinker(t *testing.T) {
   488  	input := map[string]string{
   489  		"a/b/b.proto": `package a.b;
   490  
   491  import "google/protobuf/descriptor.proto";
   492  
   493  extend google.protobuf.FieldOptions {
   494    optional Foo foo = 50001;
   495  }
   496  
   497  message Foo {
   498    optional string bar = 1;
   499  }`,
   500  		"a/c/c.proto": `import "a/b/b.proto";
   501  
   502  message ReferencesFooOption {
   503    optional string baz = 1 [(a.b.foo).bat = "hello"];
   504  }`,
   505  	}
   506  	errMsg := "a/c/c.proto:4:38: field ReferencesFooOption.baz: option (a.b.foo).bat: field bat of a.b.Foo does not exist"
   507  
   508  	acc := func(filename string) (io.ReadCloser, error) {
   509  		f, ok := input[filename]
   510  		if !ok {
   511  			return nil, fmt.Errorf("file not found: %s", filename)
   512  		}
   513  		return ioutil.NopCloser(strings.NewReader(f)), nil
   514  	}
   515  	names := make([]string, 0, len(input))
   516  	for k := range input {
   517  		names = append(names, k)
   518  	}
   519  	var errs []error
   520  	_, err := Parser{
   521  		Accessor: acc,
   522  		ErrorReporter: func(errorWithPos ErrorWithPos) error {
   523  			errs = append(errs, errorWithPos)
   524  			// need to return nil to make sure this test case works
   525  			// this will result in us only getting an error from errorHandler.getError()
   526  			// we need to make sure this is called correctly in the linker so that all
   527  			// errors are properly propagated from the return value of linkFiles(), and
   528  			// therefor Parse returns ErrInvalidSource
   529  			return nil
   530  		},
   531  	}.ParseFiles(names...)
   532  	if err != ErrInvalidSource {
   533  		t.Errorf("expecting validation error %v; instead got: %v", ErrInvalidSource, err)
   534  	} else if len(errs) != 1 || errs[0].Error() != errMsg {
   535  		t.Errorf("expecting validation error %q; instead got: %q", errs[0].Error(), errMsg)
   536  	}
   537  }