github.com/Big-big-orange/protoreflect@v0.0.0-20240408141420-285cedfdf6a4/desc/protoparse/parser_test.go (about)

     1  package protoparse
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/bufbuild/protocompile/parser"
    16  	"github.com/bufbuild/protocompile/reporter"
    17  	"github.com/golang/protobuf/proto"
    18  	"google.golang.org/protobuf/types/descriptorpb"
    19  
    20  	"github.com/Big-big-orange/protoreflect/desc"
    21  	"github.com/Big-big-orange/protoreflect/internal/testprotos"
    22  	"github.com/Big-big-orange/protoreflect/internal/testutil"
    23  )
    24  
    25  func TestEmptyParse(t *testing.T) {
    26  	p := Parser{
    27  		Accessor: func(filename string) (io.ReadCloser, error) {
    28  			return ioutil.NopCloser(bytes.NewReader(nil)), nil
    29  		},
    30  	}
    31  	fd, err := p.ParseFiles("foo.proto")
    32  	testutil.Ok(t, err)
    33  	testutil.Eq(t, 1, len(fd))
    34  	testutil.Eq(t, "foo.proto", fd[0].GetName())
    35  	testutil.Eq(t, 0, len(fd[0].GetDependencies()))
    36  	testutil.Eq(t, 0, len(fd[0].GetMessageTypes()))
    37  	testutil.Eq(t, 0, len(fd[0].GetEnumTypes()))
    38  	testutil.Eq(t, 0, len(fd[0].GetExtensions()))
    39  	testutil.Eq(t, 0, len(fd[0].GetServices()))
    40  }
    41  
    42  func TestJunkParse(t *testing.T) {
    43  	// inputs that have been found in the past to cause panics by oss-fuzz
    44  	inputs := map[string]string{
    45  		"case-34232": `'';`,
    46  		"case-34238": `.`,
    47  	}
    48  	for name, input := range inputs {
    49  		protoName := fmt.Sprintf("%s.proto", name)
    50  		p := Parser{
    51  			Accessor: FileContentsFromMap(map[string]string{protoName: input}),
    52  		}
    53  		_, err := p.ParseFiles(protoName)
    54  		// we expect this to error... but we don't want it to panic
    55  		testutil.Nok(t, err, "junk input should have returned error")
    56  		t.Logf("error from parse: %v", err)
    57  	}
    58  }
    59  
    60  func TestSimpleParse(t *testing.T) {
    61  	protos := map[string]parser.Result{}
    62  
    63  	// Just verify that we can successfully parse the same files we use for
    64  	// testing. We do a *very* shallow check of what was parsed because we know
    65  	// it won't be fully correct until after linking. (So that will be tested
    66  	// below, where we parse *and* link.)
    67  	res, err := parseFileForTest("../../internal/testprotos/desc_test1.proto")
    68  	testutil.Ok(t, err)
    69  	fd := res.FileDescriptorProto()
    70  	testutil.Eq(t, "../../internal/testprotos/desc_test1.proto", fd.GetName())
    71  	testutil.Eq(t, "testprotos", fd.GetPackage())
    72  	testutil.Require(t, hasExtension(fd, "xtm"))
    73  	testutil.Require(t, hasMessage(fd, "TestMessage"))
    74  	protos[fd.GetName()] = res
    75  
    76  	res, err = parseFileForTest("../../internal/testprotos/desc_test2.proto")
    77  	testutil.Ok(t, err)
    78  	fd = res.FileDescriptorProto()
    79  	testutil.Eq(t, "../../internal/testprotos/desc_test2.proto", fd.GetName())
    80  	testutil.Eq(t, "testprotos", fd.GetPackage())
    81  	testutil.Require(t, hasExtension(fd, "groupx"))
    82  	testutil.Require(t, hasMessage(fd, "GroupX"))
    83  	testutil.Require(t, hasMessage(fd, "Frobnitz"))
    84  	protos[fd.GetName()] = res
    85  
    86  	res, err = parseFileForTest("../../internal/testprotos/desc_test_defaults.proto")
    87  	testutil.Ok(t, err)
    88  	fd = res.FileDescriptorProto()
    89  	testutil.Eq(t, "../../internal/testprotos/desc_test_defaults.proto", fd.GetName())
    90  	testutil.Eq(t, "testprotos", fd.GetPackage())
    91  	testutil.Require(t, hasMessage(fd, "PrimitiveDefaults"))
    92  	protos[fd.GetName()] = res
    93  
    94  	res, err = parseFileForTest("../../internal/testprotos/desc_test_field_types.proto")
    95  	testutil.Ok(t, err)
    96  	fd = res.FileDescriptorProto()
    97  	testutil.Eq(t, "../../internal/testprotos/desc_test_field_types.proto", fd.GetName())
    98  	testutil.Eq(t, "testprotos", fd.GetPackage())
    99  	testutil.Require(t, hasEnum(fd, "TestEnum"))
   100  	testutil.Require(t, hasMessage(fd, "UnaryFields"))
   101  	protos[fd.GetName()] = res
   102  
   103  	res, err = parseFileForTest("../../internal/testprotos/desc_test_options.proto")
   104  	testutil.Ok(t, err)
   105  	fd = res.FileDescriptorProto()
   106  	testutil.Eq(t, "../../internal/testprotos/desc_test_options.proto", fd.GetName())
   107  	testutil.Eq(t, "testprotos", fd.GetPackage())
   108  	testutil.Require(t, hasExtension(fd, "mfubar"))
   109  	testutil.Require(t, hasEnum(fd, "ReallySimpleEnum"))
   110  	testutil.Require(t, hasMessage(fd, "ReallySimpleMessage"))
   111  	protos[fd.GetName()] = res
   112  
   113  	res, err = parseFileForTest("../../internal/testprotos/desc_test_proto3.proto")
   114  	testutil.Ok(t, err)
   115  	fd = res.FileDescriptorProto()
   116  	testutil.Eq(t, "../../internal/testprotos/desc_test_proto3.proto", fd.GetName())
   117  	testutil.Eq(t, "testprotos", fd.GetPackage())
   118  	testutil.Require(t, hasEnum(fd, "Proto3Enum"))
   119  	testutil.Require(t, hasService(fd, "TestService"))
   120  	protos[fd.GetName()] = res
   121  
   122  	res, err = parseFileForTest("../../internal/testprotos/desc_test_wellknowntypes.proto")
   123  	testutil.Ok(t, err)
   124  	fd = res.FileDescriptorProto()
   125  	testutil.Eq(t, "../../internal/testprotos/desc_test_wellknowntypes.proto", fd.GetName())
   126  	testutil.Eq(t, "testprotos", fd.GetPackage())
   127  	testutil.Require(t, hasMessage(fd, "TestWellKnownTypes"))
   128  	protos[fd.GetName()] = res
   129  
   130  	res, err = parseFileForTest("../../internal/testprotos/nopkg/desc_test_nopkg.proto")
   131  	testutil.Ok(t, err)
   132  	fd = res.FileDescriptorProto()
   133  	testutil.Eq(t, "../../internal/testprotos/nopkg/desc_test_nopkg.proto", fd.GetName())
   134  	testutil.Eq(t, "", fd.GetPackage())
   135  	protos[fd.GetName()] = res
   136  
   137  	res, err = parseFileForTest("../../internal/testprotos/nopkg/desc_test_nopkg_new.proto")
   138  	testutil.Ok(t, err)
   139  	fd = res.FileDescriptorProto()
   140  	testutil.Eq(t, "../../internal/testprotos/nopkg/desc_test_nopkg_new.proto", fd.GetName())
   141  	testutil.Eq(t, "", fd.GetPackage())
   142  	testutil.Require(t, hasMessage(fd, "TopLevel"))
   143  	protos[fd.GetName()] = res
   144  
   145  	res, err = parseFileForTest("../../internal/testprotos/pkg/desc_test_pkg.proto")
   146  	testutil.Ok(t, err)
   147  	fd = res.FileDescriptorProto()
   148  	testutil.Eq(t, "../../internal/testprotos/pkg/desc_test_pkg.proto", fd.GetName())
   149  	testutil.Eq(t, "jhump.protoreflect.desc", fd.GetPackage())
   150  	testutil.Require(t, hasEnum(fd, "Foo"))
   151  	testutil.Require(t, hasMessage(fd, "Bar"))
   152  	protos[fd.GetName()] = res
   153  
   154  	// We'll also check our fixup logic to make sure it correctly rewrites the
   155  	// names of the files to match corresponding import statementes. This should
   156  	// strip the "../../internal/testprotos/" prefix from each file.
   157  	protos, _ = fixupFilenames(protos)
   158  	var actual []string
   159  	for n := range protos {
   160  		actual = append(actual, n)
   161  	}
   162  	sort.Strings(actual)
   163  	expected := []string{
   164  		"desc_test1.proto",
   165  		"desc_test2.proto",
   166  		"desc_test_defaults.proto",
   167  		"desc_test_field_types.proto",
   168  		"desc_test_options.proto",
   169  		"desc_test_proto3.proto",
   170  		"desc_test_wellknowntypes.proto",
   171  		"nopkg/desc_test_nopkg.proto",
   172  		"nopkg/desc_test_nopkg_new.proto",
   173  		"pkg/desc_test_pkg.proto",
   174  	}
   175  	testutil.Eq(t, expected, actual)
   176  }
   177  
   178  func parseFileForTest(filename string) (parser.Result, error) {
   179  	filenames := []string{filename}
   180  	res, _ := Parser{}.getResolver(filenames)
   181  	rep := reporter.NewHandler(nil)
   182  	results, err := parseToProtos(res, filenames, rep, true)
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	return results[0], nil
   187  }
   188  
   189  func hasExtension(fd *descriptorpb.FileDescriptorProto, name string) bool {
   190  	for _, ext := range fd.Extension {
   191  		if ext.GetName() == name {
   192  			return true
   193  		}
   194  	}
   195  	return false
   196  }
   197  
   198  func hasMessage(fd *descriptorpb.FileDescriptorProto, name string) bool {
   199  	for _, md := range fd.MessageType {
   200  		if md.GetName() == name {
   201  			return true
   202  		}
   203  	}
   204  	return false
   205  }
   206  
   207  func hasEnum(fd *descriptorpb.FileDescriptorProto, name string) bool {
   208  	for _, ed := range fd.EnumType {
   209  		if ed.GetName() == name {
   210  			return true
   211  		}
   212  	}
   213  	return false
   214  }
   215  
   216  func hasService(fd *descriptorpb.FileDescriptorProto, name string) bool {
   217  	for _, sd := range fd.Service {
   218  		if sd.GetName() == name {
   219  			return true
   220  		}
   221  	}
   222  	return false
   223  }
   224  
   225  func TestAggregateValueInUninterpretedOptions(t *testing.T) {
   226  	res, err := parseFileForTest("../../internal/testprotos/desc_test_complex.proto")
   227  	testutil.Ok(t, err)
   228  	fd := res.FileDescriptorProto()
   229  
   230  	// service TestTestService, method UserAuth; first option
   231  	aggregateValue1 := *fd.Service[0].Method[0].Options.UninterpretedOption[0].AggregateValue
   232  	testutil.Eq(t, `authenticated : true permission : { action : LOGIN entity : "client" }`, aggregateValue1)
   233  
   234  	// service TestTestService, method Get; first option
   235  	aggregateValue2 := *fd.Service[0].Method[1].Options.UninterpretedOption[0].AggregateValue
   236  	testutil.Eq(t, `authenticated : true permission : { action : READ entity : "user" }`, aggregateValue2)
   237  
   238  	// message Another; first option
   239  	aggregateValue3 := *fd.MessageType[4].Options.UninterpretedOption[0].AggregateValue
   240  	testutil.Eq(t, `foo : "abc" s < name : "foo" , id : 123 > , array : [ 1 , 2 , 3 ] , r : [ < name : "f" > , { name : "s" } , { id : 456 } ] ,`, aggregateValue3)
   241  
   242  	// message Test.Nested._NestedNested; second option (rept)
   243  	//  (Test.Nested is at index 1 instead of 0 because of implicit nested message from map field m)
   244  	aggregateValue4 := *fd.MessageType[1].NestedType[1].NestedType[0].Options.UninterpretedOption[1].AggregateValue
   245  	testutil.Eq(t, `foo : "goo" [ foo . bar . Test . Nested . _NestedNested . _garblez ] : "boo"`, aggregateValue4)
   246  }
   247  
   248  func TestParseFilesMessageComments(t *testing.T) {
   249  	p := Parser{
   250  		IncludeSourceCodeInfo: true,
   251  	}
   252  	protos, err := p.ParseFiles("../../internal/testprotos/desc_test1.proto")
   253  	testutil.Ok(t, err)
   254  	comments := ""
   255  	expected := " Comment for TestMessage\n"
   256  	for _, p := range protos {
   257  		msg := p.FindMessage("testprotos.TestMessage")
   258  		if msg != nil {
   259  			si := msg.GetSourceInfo()
   260  			if si != nil {
   261  				comments = si.GetLeadingComments()
   262  			}
   263  			break
   264  		}
   265  	}
   266  	testutil.Eq(t, expected, comments)
   267  }
   268  
   269  func TestParseFilesWithImportsNoImportPath(t *testing.T) {
   270  	relFilePaths := []string{
   271  		"a/b/b1.proto",
   272  		"a/b/b2.proto",
   273  		"c/c.proto",
   274  	}
   275  
   276  	pwd, err := os.Getwd()
   277  	testutil.Require(t, err == nil, "%v", err)
   278  
   279  	err = os.Chdir("../../internal/testprotos/protoparse")
   280  	testutil.Require(t, err == nil, "%v", err)
   281  	p := Parser{}
   282  	protos, parseErr := p.ParseFiles(relFilePaths...)
   283  	err = os.Chdir(pwd)
   284  	testutil.Require(t, err == nil, "%v", err)
   285  	testutil.Require(t, parseErr == nil, "%v", parseErr)
   286  
   287  	testutil.Ok(t, err)
   288  	testutil.Eq(t, len(relFilePaths), len(protos))
   289  }
   290  
   291  func TestParseFilesWithDependencies(t *testing.T) {
   292  	// Create some file contents that import a non-well-known proto.
   293  	// (One of the protos in internal/testprotos is fine.)
   294  	contents := map[string]string{
   295  		"test.proto": `
   296  			syntax = "proto3";
   297  			import "desc_test_wellknowntypes.proto";
   298  
   299  			message TestImportedType {
   300  				testprotos.TestWellKnownTypes imported_field = 1;
   301  			}
   302  		`,
   303  	}
   304  
   305  	// Establish that we *can* parse the source file with a parser that
   306  	// registers the dependency.
   307  	t.Run("DependencyIncluded", func(t *testing.T) {
   308  		// Create a dependency-aware parser.
   309  		parser := Parser{
   310  			Accessor: FileContentsFromMap(contents),
   311  			LookupImport: func(imp string) (*desc.FileDescriptor, error) {
   312  				if imp == "desc_test_wellknowntypes.proto" {
   313  					return desc.LoadFileDescriptor(imp)
   314  				}
   315  				return nil, errors.New("unexpected filename")
   316  			},
   317  		}
   318  		if _, err := parser.ParseFiles("test.proto"); err != nil {
   319  			t.Errorf("Could not parse with a non-well-known import: %v", err)
   320  		}
   321  	})
   322  	t.Run("DependencyIncludedProto", func(t *testing.T) {
   323  		// Create a dependency-aware parser.
   324  		parser := Parser{
   325  			Accessor: FileContentsFromMap(contents),
   326  			LookupImportProto: func(imp string) (*descriptorpb.FileDescriptorProto, error) {
   327  				if imp == "desc_test_wellknowntypes.proto" {
   328  					fileDescriptor, err := desc.LoadFileDescriptor(imp)
   329  					if err != nil {
   330  						return nil, err
   331  					}
   332  					return fileDescriptor.AsFileDescriptorProto(), nil
   333  				}
   334  				return nil, errors.New("unexpected filename")
   335  			},
   336  		}
   337  		if _, err := parser.ParseFiles("test.proto"); err != nil {
   338  			t.Errorf("Could not parse with a non-well-known import: %v", err)
   339  		}
   340  	})
   341  
   342  	// Establish that we *can not* parse the source file with a parser that
   343  	// did not register the dependency.
   344  	t.Run("DependencyExcluded", func(t *testing.T) {
   345  		// Create a dependency-aware parser.
   346  		parser := Parser{
   347  			Accessor: FileContentsFromMap(contents),
   348  		}
   349  		if _, err := parser.ParseFiles("test.proto"); err == nil {
   350  			t.Errorf("Expected parse to fail due to lack of an import.")
   351  		}
   352  	})
   353  
   354  	// Establish that the accessor has precedence over LookupImport.
   355  	t.Run("AccessorWins", func(t *testing.T) {
   356  		// Create a dependency-aware parser that should never be called.
   357  		parser := Parser{
   358  			Accessor: FileContentsFromMap(map[string]string{
   359  				"test.proto": `syntax = "proto3";`,
   360  			}),
   361  			LookupImport: func(imp string) (*desc.FileDescriptor, error) {
   362  				// It's okay for descriptor.proto to be requested implicitly, but
   363  				// nothing else should make it here since it should instead be
   364  				// retrieved via Accessor.
   365  				if imp != "google/protobuf/descriptor.proto" {
   366  					t.Errorf("LookupImport was called on a filename available to the Accessor: %q", imp)
   367  				}
   368  				return nil, errors.New("unimportant")
   369  			},
   370  		}
   371  		if _, err := parser.ParseFiles("test.proto"); err != nil {
   372  			t.Error(err)
   373  		}
   374  	})
   375  }
   376  
   377  func TestParseFilesReturnsKnownExtensions(t *testing.T) {
   378  	accessor := func(filename string) (io.ReadCloser, error) {
   379  		if filename == "desc_test3.proto" {
   380  			return io.NopCloser(strings.NewReader(`
   381  				syntax = "proto3";
   382  				import "desc_test_complex.proto";
   383  				message Foo {
   384  					foo.bar.Simple field = 1;
   385  				}
   386  			`)), nil
   387  		}
   388  		return os.Open(filepath.Join("../../internal/testprotos", filename))
   389  	}
   390  	p := Parser{
   391  		Accessor: accessor,
   392  	}
   393  	fds, err := p.ParseFiles("desc_test3.proto")
   394  	testutil.Ok(t, err)
   395  	fd := fds[0].GetDependencies()[0]
   396  	md := fd.FindMessage("foo.bar.Test.Nested._NestedNested")
   397  	testutil.Require(t, md != nil)
   398  	val, err := proto.GetExtension(md.GetOptions(), testprotos.E_Rept)
   399  	testutil.Ok(t, err)
   400  	_, ok := val.([]*testprotos.Test)
   401  	testutil.Require(t, ok)
   402  }
   403  
   404  func TestParseCommentsBeforeDot(t *testing.T) {
   405  	accessor := FileContentsFromMap(map[string]string{
   406  		"test.proto": `
   407  syntax = "proto3";
   408  message Foo {
   409    // leading comments
   410    .Foo foo = 1;
   411  }
   412  `,
   413  	})
   414  
   415  	p := Parser{
   416  		Accessor:              accessor,
   417  		IncludeSourceCodeInfo: true,
   418  	}
   419  	fds, err := p.ParseFiles("test.proto")
   420  	testutil.Ok(t, err)
   421  
   422  	comment := fds[0].GetMessageTypes()[0].GetFields()[0].GetSourceInfo().GetLeadingComments()
   423  	testutil.Eq(t, " leading comments\n", comment)
   424  }
   425  
   426  func TestParseInferImportPaths_SimpleNoOp(t *testing.T) {
   427  	sources := map[string]string{
   428  		"test.proto": `
   429  		syntax = "proto3";
   430  		import "google/protobuf/struct.proto";
   431  		message Foo {
   432  			string name = 1;
   433  			repeated uint64 refs = 2;
   434  			google.protobuf.Struct meta = 3;
   435  		}`,
   436  	}
   437  	p := Parser{
   438  		Accessor:         FileContentsFromMap(sources),
   439  		InferImportPaths: true,
   440  	}
   441  	fds, err := p.ParseFiles("test.proto")
   442  	testutil.Ok(t, err)
   443  	testutil.Eq(t, 1, len(fds))
   444  }
   445  
   446  func TestParseInferImportPaths_FixesNestedPaths(t *testing.T) {
   447  	sources := FileContentsFromMap(map[string]string{
   448  		"/foo/bar/a.proto": `
   449  			syntax = "proto3";
   450  			import "baz/b.proto";
   451  			message A {
   452  				B b = 1;
   453  			}`,
   454  		"/foo/bar/baz/b.proto": `
   455  			syntax = "proto3";
   456  			import "baz/c.proto";
   457  			message B {
   458  				C c = 1;
   459  			}`,
   460  		"/foo/bar/baz/c.proto": `
   461  			syntax = "proto3";
   462  			message C {}`,
   463  		"/foo/bar/baz/d.proto": `
   464  			syntax = "proto3";
   465  			import "a.proto";
   466  			message D {
   467  				A a = 1;
   468  			}`,
   469  	})
   470  
   471  	testCases := []struct {
   472  		name      string
   473  		cwd       string
   474  		filenames []string
   475  		expect    []string
   476  	}{
   477  		{
   478  			name:      "outside hierarchy",
   479  			cwd:       "/buzz",
   480  			filenames: []string{"../foo/bar/a.proto", "../foo/bar/baz/b.proto", "../foo/bar/baz/c.proto", "../foo/bar/baz/d.proto"},
   481  		},
   482  		{
   483  			name:      "inside hierarchy",
   484  			cwd:       "/foo",
   485  			filenames: []string{"bar/a.proto", "bar/baz/b.proto", "bar/baz/c.proto", "bar/baz/d.proto"},
   486  		},
   487  		{
   488  			name:      "import path root (no-op)",
   489  			cwd:       "/foo/bar",
   490  			filenames: []string{"a.proto", "baz/b.proto", "baz/c.proto", "baz/d.proto"},
   491  		},
   492  		{
   493  			name:      "inside leaf directory",
   494  			cwd:       "/foo/bar/baz",
   495  			filenames: []string{"../a.proto", "b.proto", "c.proto", "d.proto"},
   496  			// NB: Expected names differ from above cases because nothing imports d.proto.
   497  			//     So when inferring the root paths, the fact that d.proto is defined in
   498  			//     the baz sub-directory will not be discovered. That's okay since the parse
   499  			//     operation still succeeds.
   500  			expect: []string{"a.proto", "baz/b.proto", "baz/c.proto", "d.proto"},
   501  		},
   502  	}
   503  	for _, testCase := range testCases {
   504  		t.Run(testCase.name, func(t *testing.T) {
   505  			p := Parser{
   506  				Accessor:         sources,
   507  				ImportPaths:      []string{testCase.cwd, "/foo/bar"},
   508  				InferImportPaths: true,
   509  			}
   510  			fds, err := p.ParseFiles(testCase.filenames...)
   511  			testutil.Ok(t, err)
   512  			testutil.Eq(t, 4, len(fds))
   513  			var expectedNames []string
   514  			if len(testCase.expect) == 0 {
   515  				expectedNames = []string{"a.proto", "baz/b.proto", "baz/c.proto", "baz/d.proto"}
   516  			} else {
   517  				testutil.Eq(t, 4, len(testCase.expect))
   518  				expectedNames = testCase.expect
   519  			}
   520  			// check that they have the expected name
   521  			testutil.Eq(t, expectedNames[0], fds[0].GetName())
   522  			testutil.Eq(t, expectedNames[1], fds[1].GetName())
   523  			testutil.Eq(t, expectedNames[2], fds[2].GetName())
   524  			testutil.Eq(t, expectedNames[3], fds[3].GetName())
   525  		})
   526  	}
   527  }
   528  
   529  func TestParseFilesButDoNotLink_DoesNotUseImportPaths(t *testing.T) {
   530  	tempdir, err := os.MkdirTemp("", "protoparse")
   531  	testutil.Ok(t, err)
   532  	defer func() {
   533  		_ = os.RemoveAll(tempdir)
   534  	}()
   535  	err = os.WriteFile(filepath.Join(tempdir, "extra.proto"), []byte("package extra;"), 0644)
   536  	testutil.Ok(t, err)
   537  	mainPath := filepath.Join(tempdir, "main.proto")
   538  	err = os.WriteFile(mainPath, []byte("package main; import \"extra.proto\";"), 0644)
   539  	testutil.Ok(t, err)
   540  	p := Parser{
   541  		ImportPaths: []string{tempdir},
   542  	}
   543  	fds, err := p.ParseFilesButDoNotLink(mainPath)
   544  	testutil.Ok(t, err)
   545  	testutil.Eq(t, 1, len(fds))
   546  }