package fs import ( "bytes" "io" "io/ioutil" "os" "gotest.tools/v3/assert" ) // resourcePath is an adaptor for resources so they can be used as a Path // with PathOps. type resourcePath struct{} func (p *resourcePath) Path() string { return "manifest: not a filesystem path" } func (p *resourcePath) Remove() {} type filePath struct { resourcePath file *file } func (p *filePath) SetContent(content io.ReadCloser) { p.file.content = content } func (p *filePath) SetUID(uid uint32) { p.file.uid = uid } func (p *filePath) SetGID(gid uint32) { p.file.gid = gid } type directoryPath struct { resourcePath directory *directory } func (p *directoryPath) SetUID(uid uint32) { p.directory.uid = uid } func (p *directoryPath) SetGID(gid uint32) { p.directory.gid = gid } func (p *directoryPath) AddSymlink(path, target string) error { p.directory.items[path] = &symlink{ resource: newResource(defaultSymlinkMode), target: target, } return nil } func (p *directoryPath) AddFile(path string, ops ...PathOp) error { newFile := &file{resource: newResource(0)} p.directory.items[path] = newFile exp := &filePath{file: newFile} return applyPathOps(exp, ops) } func (p *directoryPath) AddGlobFiles(glob string, ops ...PathOp) error { newFile := &file{resource: newResource(0)} newFilePath := &filePath{file: newFile} p.directory.filepathGlobs[glob] = newFilePath return applyPathOps(newFilePath, ops) } func (p *directoryPath) AddDirectory(path string, ops ...PathOp) error { newDir := newDirectoryWithDefaults() p.directory.items[path] = newDir exp := &directoryPath{directory: newDir} return applyPathOps(exp, ops) } // Expected returns a Manifest with a directory structured created by ops. The // PathOp operations are applied to the manifest as expectations of the // filesystem structure and properties. func Expected(t assert.TestingT, ops ...PathOp) Manifest { if ht, ok := t.(helperT); ok { ht.Helper() } newDir := newDirectoryWithDefaults() e := &directoryPath{directory: newDir} assert.NilError(t, applyPathOps(e, ops)) return Manifest{root: newDir} } func newDirectoryWithDefaults() *directory { return &directory{ resource: newResource(defaultRootDirMode), items: make(map[string]dirEntry), filepathGlobs: make(map[string]*filePath), } } func newResource(mode os.FileMode) resource { return resource{ mode: mode, uid: currentUID(), gid: currentGID(), } } func currentUID() uint32 { return normalizeID(os.Getuid()) } func currentGID() uint32 { return normalizeID(os.Getgid()) } func normalizeID(id int) uint32 { // ids will be -1 on windows if id < 0 { return 0 } return uint32(id) } var anyFileContent = ioutil.NopCloser(bytes.NewReader(nil)) // MatchAnyFileContent is a PathOp that updates a Manifest so that the file // at path may contain any content. func MatchAnyFileContent(path Path) error { if m, ok := path.(*filePath); ok { m.SetContent(anyFileContent) } return nil } // MatchContentIgnoreCarriageReturn is a PathOp that ignores cariage return // discrepancies. func MatchContentIgnoreCarriageReturn(path Path) error { if m, ok := path.(*filePath); ok { m.file.ignoreCariageReturn = true } return nil } const anyFile = "*" // MatchExtraFiles is a PathOp that updates a Manifest to allow a directory // to contain unspecified files. func MatchExtraFiles(path Path) error { if m, ok := path.(*directoryPath); ok { return m.AddFile(anyFile) } return nil } // CompareResult is the result of comparison. // // See gotest.tools/assert/cmp.StringResult for a convenient implementation of // this interface. type CompareResult interface { Success() bool FailureMessage() string } // MatchFileContent is a PathOp that updates a Manifest to use the provided // function to determine if a file's content matches the expectation. func MatchFileContent(f func([]byte) CompareResult) PathOp { return func(path Path) error { if m, ok := path.(*filePath); ok { m.file.compareContentFunc = f } return nil } } // MatchFilesWithGlob is a PathOp that updates a Manifest to match files using // glob pattern, and check them using the ops. func MatchFilesWithGlob(glob string, ops ...PathOp) PathOp { return func(path Path) error { if m, ok := path.(*directoryPath); ok { return m.AddGlobFiles(glob, ops...) } return nil } } // anyFileMode is represented by uint32_max const anyFileMode os.FileMode = 4294967295 // MatchAnyFileMode is a PathOp that updates a Manifest so that the resource at path // will match any file mode. func MatchAnyFileMode(path Path) error { if m, ok := path.(manifestResource); ok { m.SetMode(anyFileMode) } return nil }