@@ -14,13 +14,17 @@ import (
1414 "strings"
1515 "testing"
1616
17+ remoteexecution "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
1718 "github.com/bazelbuild/rules_go/go/runfiles"
19+ "github.com/buildbarn/bb-remote-execution/pkg/cas"
1820 "github.com/buildbarn/bb-remote-execution/pkg/filesystem/pool"
1921 "github.com/buildbarn/bb-remote-execution/pkg/filesystem/virtual"
2022 virtual_configuration "github.com/buildbarn/bb-remote-execution/pkg/filesystem/virtual/configuration"
2123 virtual_pb "github.com/buildbarn/bb-remote-execution/pkg/proto/configuration/filesystem/virtual"
2224 "github.com/buildbarn/bb-storage/pkg/blockdevice"
2325 "github.com/buildbarn/bb-storage/pkg/clock"
26+ "github.com/buildbarn/bb-storage/pkg/digest"
27+ bb_path "github.com/buildbarn/bb-storage/pkg/filesystem/path"
2428 "github.com/buildbarn/bb-storage/pkg/program"
2529 "github.com/buildbarn/bb-storage/pkg/util"
2630 "github.com/stretchr/testify/require"
@@ -39,7 +43,7 @@ func findFreeDriveLetter() (string, error) {
3943 return "" , fmt .Errorf ("no free drive letters available" )
4044}
4145
42- func createWinFSPMountForTest (t * testing.T , terminationGroup program.Group , caseSensitive bool ) (string , blockdevice.BlockDevice ) {
46+ func createWinFSPForTest (t * testing.T , terminationGroup program.Group , caseSensitive bool ) (string , blockdevice.BlockDevice , virtual_configuration. Mount , virtual. PrepopulatedDirectory , virtual. SymlinkFactory ) {
4347 // We can't run winfsp-tests at a directory path due to
4448 // https://github.com/winfsp/winfsp/issues/279. Instead find a free drive
4549 // letter and run it there instead.
@@ -75,38 +79,42 @@ func createWinFSPMountForTest(t *testing.T, terminationGroup program.Group, case
7579
7680 // Create a virtual directory to hold new files.
7781 defaultAttributesSetter := func (requested virtual.AttributesMask , attributes * virtual.Attributes ) {}
78- err = mount .Expose (
79- terminationGroup ,
80- virtual .NewInMemoryPrepopulatedDirectory (
81- virtual .NewHandleAllocatingFileAllocator (
82- virtual .NewPoolBackedFileAllocator (
83- pool .NewBlockDeviceBackedFilePool (
84- bd ,
85- pool .NewBitmapSectorAllocator (uint32 (sectorCount )),
86- sectorSizeBytes ,
87- ),
88- util .DefaultErrorLogger ,
89- defaultAttributesSetter ,
90- virtual .NoNamedAttributesFactory ,
82+ symlinkFactory := virtual .NewHandleAllocatingSymlinkFactory (
83+ virtual .BaseSymlinkFactory ,
84+ handleAllocator .New (),
85+ )
86+ rootDir := virtual .NewInMemoryPrepopulatedDirectory (
87+ virtual .NewHandleAllocatingFileAllocator (
88+ virtual .NewPoolBackedFileAllocator (
89+ pool .NewBlockDeviceBackedFilePool (
90+ bd ,
91+ pool .NewBitmapSectorAllocator (uint32 (sectorCount )),
92+ sectorSizeBytes ,
9193 ),
92- handleAllocator ,
94+ util .DefaultErrorLogger ,
95+ defaultAttributesSetter ,
96+ virtual .NoNamedAttributesFactory ,
9397 ),
94- virtual .NewHandleAllocatingSymlinkFactory (
95- virtual .BaseSymlinkFactory ,
96- handleAllocator .New (),
97- ),
98- util .DefaultErrorLogger ,
9998 handleAllocator ,
100- sort .Sort ,
101- func (s string ) bool { return false },
102- clock .SystemClock ,
103- normalizer ,
104- defaultAttributesSetter ,
105- virtual .NoNamedAttributesFactory ,
10699 ),
100+ symlinkFactory ,
101+ util .DefaultErrorLogger ,
102+ handleAllocator ,
103+ sort .Sort ,
104+ func (s string ) bool { return false },
105+ clock .SystemClock ,
106+ normalizer ,
107+ defaultAttributesSetter ,
108+ virtual .NoNamedAttributesFactory ,
107109 )
108- require .NoError (t , err , "Failed to expose mount point" )
109110
111+ return vfsPath , bd , mount , rootDir , symlinkFactory
112+ }
113+
114+ func createWinFSPMountForTest (t * testing.T , terminationGroup program.Group , caseSensitive bool ) (string , blockdevice.BlockDevice ) {
115+ vfsPath , bd , mount , rootDir , _ := createWinFSPForTest (t , terminationGroup , caseSensitive )
116+ err := mount .Expose (terminationGroup , rootDir )
117+ require .NoError (t , err , "Failed to expose mount point" )
110118 return vfsPath , bd
111119}
112120
@@ -417,6 +425,140 @@ func TestWinFSPFileSystemGetSecurityByNameIntegration(t *testing.T) {
417425 })
418426}
419427
428+ // staticDirectoryWalker is a simple DirectoryWalker that returns a
429+ // fixed Directory proto. Used to test the CAS ingestion path with
430+ // symlinks whose targets use UNIX path separators (per the REv2 spec).
431+ type staticDirectoryWalker struct {
432+ directory * remoteexecution.Directory
433+ }
434+
435+ func (w * staticDirectoryWalker ) GetDirectory (ctx context.Context ) (* remoteexecution.Directory , error ) {
436+ return w .directory , nil
437+ }
438+
439+ func (w * staticDirectoryWalker ) GetChild (d digest.Digest ) cas.DirectoryWalker {
440+ return & staticDirectoryWalker {}
441+ }
442+
443+ func (w * staticDirectoryWalker ) GetDescription () string {
444+ return "static test directory"
445+ }
446+
447+ func (w * staticDirectoryWalker ) GetContainingDigest () digest.Digest {
448+ return digest .MustNewDigest ("test" , remoteexecution .DigestFunction_SHA256 , "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" , 0 )
449+ }
450+
451+ func TestWinFSPFileSystemStatFollowsSymlink (t * testing.T ) {
452+ program .RunLocal (context .Background (), func (ctx context.Context , siblingsGroup , dependenciesGroup program.Group ) error {
453+ vfsPath , bd , mount , rootDir , symlinkFactory := createWinFSPForTest (t , dependenciesGroup , false )
454+ defer bd .Close ()
455+
456+ // Build a pnpm-style node_modules layout with chained
457+ // symlinks. Symlinks are created through the CAS
458+ // ingestion path (NewCASInitialContentsFetcher) so that
459+ // their UNIX-formatted targets from the REv2 proto are
460+ // normalized to native Windows format, exercising the
461+ // real production code path.
462+ digestFunction := digest .MustNewFunction ("test" , remoteexecution .DigestFunction_SHA256 )
463+ noopFileReadMonitorFactory := virtual .FileReadMonitorFactory (
464+ func (name bb_path.Component ) virtual.FileReadMonitor {
465+ return func () {}
466+ })
467+
468+ // Helper to create a chain of nested directories.
469+ mkdirs := func (parent virtual.PrepopulatedDirectory , names ... string ) virtual.PrepopulatedDirectory {
470+ for _ , name := range names {
471+ child , err := parent .CreateAndEnterPrepopulatedDirectory (bb_path .MustNewComponent (name ))
472+ require .NoError (t , err )
473+ parent = child
474+ }
475+ return parent
476+ }
477+
478+ // Helper to create symlink children via the CAS
479+ // ingestion path, so that UNIX-formatted targets from
480+ // the REv2 proto are normalized to native Windows format.
481+ addCASSymlinks := func (parent virtual.PrepopulatedDirectory , symlinks []* remoteexecution.SymlinkNode ) {
482+ children , err := virtual .NewCASInitialContentsFetcher (
483+ ctx ,
484+ & staticDirectoryWalker {directory : & remoteexecution.Directory {
485+ Symlinks : symlinks ,
486+ }},
487+ nil , symlinkFactory , digestFunction ,
488+ ).FetchContents (noopFileReadMonitorFactory )
489+ require .NoError (t , err )
490+ require .NoError (t , parent .CreateChildren (children , false ))
491+ }
492+
493+ // store/pkg — the real directory.
494+ mkdirs (rootDir , "store" , "pkg" )
495+
496+ // store-link -> store/pkg (single symlink).
497+ addCASSymlinks (rootDir , []* remoteexecution.SymlinkNode {
498+ {Name : "store-link" , Target : "store/pkg" },
499+ })
500+
501+ // node_modules/.pnpm/pkg@1.0.0/node_modules/pkg -> ../../../../store/pkg
502+ nmDir := mkdirs (rootDir , "node_modules" )
503+ innerNmDir := mkdirs (nmDir , ".pnpm" , "pkg@1.0.0" , "node_modules" )
504+ addCASSymlinks (innerNmDir , []* remoteexecution.SymlinkNode {
505+ {Name : "pkg" , Target : "../../../../store/pkg" },
506+ })
507+
508+ // node_modules/pkg -> .pnpm/pkg@1.0.0/node_modules/pkg (chained symlink).
509+ addCASSymlinks (nmDir , []* remoteexecution.SymlinkNode {
510+ {Name : "pkg" , Target : ".pnpm/pkg@1.0.0/node_modules/pkg" },
511+ })
512+
513+ require .NoError (t , mount .Expose (dependenciesGroup , rootDir ))
514+
515+ // Write a file into the real directory after mounting.
516+ testContent := []byte (`{"name":"pkg"}` )
517+ require .NoError (t , os .WriteFile (
518+ filepath .Join (vfsPath , "store" , "pkg" , "package.json" ),
519+ testContent , 0o644 ,
520+ ))
521+
522+ t .Run ("SingleSymlink" , func (t * testing.T ) {
523+ singleSymlinkPath := filepath .Join (vfsPath , "store-link" )
524+ info , err := os .Stat (singleSymlinkPath )
525+ require .NoError (t , err )
526+ require .True (t , info .IsDir ())
527+
528+ content , err := os .ReadFile (filepath .Join (singleSymlinkPath , "package.json" ))
529+ require .NoError (t , err )
530+ require .Equal (t , testContent , content )
531+ })
532+
533+ t .Run ("ChainedSymlinks" , func (t * testing.T ) {
534+ symlinkPath := filepath .Join (vfsPath , "node_modules" , "pkg" )
535+
536+ info , err := os .Lstat (symlinkPath )
537+ require .NoError (t , err )
538+ require .NotZero (t , info .Mode ()& os .ModeSymlink )
539+
540+ target , err := os .Readlink (symlinkPath )
541+ require .NoError (t , err )
542+ require .Equal (t , `.pnpm\pkg@1.0.0\node_modules\pkg` , target )
543+
544+ info , err = os .Stat (symlinkPath )
545+ require .NoError (t , err )
546+ require .True (t , info .IsDir ())
547+
548+ content , err := os .ReadFile (filepath .Join (symlinkPath , "package.json" ))
549+ require .NoError (t , err )
550+ require .Equal (t , testContent , content )
551+
552+ entries , err := os .ReadDir (symlinkPath )
553+ require .NoError (t , err )
554+ require .Len (t , entries , 1 )
555+ require .Equal (t , "package.json" , entries [0 ].Name ())
556+ })
557+
558+ return nil
559+ })
560+ }
561+
420562func TestWinFSPFileSystemCasePreserving (t * testing.T ) {
421563 program .RunLocal (context .Background (), func (ctx context.Context , siblingsGroup , dependenciesGroup program.Group ) error {
422564 vfsPath , bd := createWinFSPMountForTest (t , dependenciesGroup , false )
0 commit comments