forked from toolshed/abra
chore: vendor
This commit is contained in:
129
vendor/gotest.tools/v3/fs/file.go
vendored
Normal file
129
vendor/gotest.tools/v3/fs/file.go
vendored
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
Package fs provides tools for creating temporary files, and testing the
|
||||
contents and structure of a directory.
|
||||
*/
|
||||
package fs // import "gotest.tools/v3/fs"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/internal/cleanup"
|
||||
)
|
||||
|
||||
// Path objects return their filesystem path. Path may be implemented by a
|
||||
// real filesystem object (such as File and Dir) or by a type which updates
|
||||
// entries in a Manifest.
|
||||
type Path interface {
|
||||
Path() string
|
||||
Remove()
|
||||
}
|
||||
|
||||
var (
|
||||
_ Path = &Dir{}
|
||||
_ Path = &File{}
|
||||
)
|
||||
|
||||
// File is a temporary file on the filesystem
|
||||
type File struct {
|
||||
path string
|
||||
}
|
||||
|
||||
type helperT interface {
|
||||
Helper()
|
||||
}
|
||||
|
||||
// NewFile creates a new file in a temporary directory using prefix as part of
|
||||
// the filename. The PathOps are applied to the before returning the File.
|
||||
//
|
||||
// When used with Go 1.14+ the file will be automatically removed when the test
|
||||
// ends, unless the TEST_NOCLEANUP env var is set to true.
|
||||
func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
tempfile, err := os.CreateTemp("", cleanPrefix(prefix)+"-")
|
||||
assert.NilError(t, err)
|
||||
|
||||
file := &File{path: tempfile.Name()}
|
||||
cleanup.Cleanup(t, file.Remove)
|
||||
|
||||
assert.NilError(t, tempfile.Close())
|
||||
assert.NilError(t, applyPathOps(file, ops))
|
||||
return file
|
||||
}
|
||||
|
||||
func cleanPrefix(prefix string) string {
|
||||
// windows requires both / and \ are replaced
|
||||
if runtime.GOOS == "windows" {
|
||||
prefix = strings.Replace(prefix, string(os.PathSeparator), "-", -1)
|
||||
}
|
||||
return strings.Replace(prefix, "/", "-", -1)
|
||||
}
|
||||
|
||||
// Path returns the full path to the file
|
||||
func (f *File) Path() string {
|
||||
return f.path
|
||||
}
|
||||
|
||||
// Remove the file
|
||||
func (f *File) Remove() {
|
||||
_ = os.Remove(f.path)
|
||||
}
|
||||
|
||||
// Dir is a temporary directory
|
||||
type Dir struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// NewDir returns a new temporary directory using prefix as part of the directory
|
||||
// name. The PathOps are applied before returning the Dir.
|
||||
//
|
||||
// When used with Go 1.14+ the directory will be automatically removed when the test
|
||||
// ends, unless the TEST_NOCLEANUP env var is set to true.
|
||||
func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
path, err := os.MkdirTemp("", cleanPrefix(prefix)+"-")
|
||||
assert.NilError(t, err)
|
||||
dir := &Dir{path: path}
|
||||
cleanup.Cleanup(t, dir.Remove)
|
||||
|
||||
assert.NilError(t, applyPathOps(dir, ops))
|
||||
return dir
|
||||
}
|
||||
|
||||
// Path returns the full path to the directory
|
||||
func (d *Dir) Path() string {
|
||||
return d.path
|
||||
}
|
||||
|
||||
// Remove the directory
|
||||
func (d *Dir) Remove() {
|
||||
_ = os.RemoveAll(d.path)
|
||||
}
|
||||
|
||||
// Join returns a new path with this directory as the base of the path
|
||||
func (d *Dir) Join(parts ...string) string {
|
||||
return filepath.Join(append([]string{d.Path()}, parts...)...)
|
||||
}
|
||||
|
||||
// DirFromPath returns a Dir for a path that already exists. No directory is created.
|
||||
// Unlike NewDir the directory will not be removed automatically when the test exits,
|
||||
// it is the callers responsibly to remove the directory.
|
||||
// DirFromPath can be used with Apply to modify an existing directory.
|
||||
//
|
||||
// If the path does not already exist, use NewDir instead.
|
||||
func DirFromPath(t assert.TestingT, path string, ops ...PathOp) *Dir {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
|
||||
dir := &Dir{path: path}
|
||||
assert.NilError(t, applyPathOps(dir, ops))
|
||||
return dir
|
||||
}
|
142
vendor/gotest.tools/v3/fs/manifest.go
vendored
Normal file
142
vendor/gotest.tools/v3/fs/manifest.go
vendored
Normal file
@ -0,0 +1,142 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
// Manifest stores the expected structure and properties of files and directories
|
||||
// in a filesystem.
|
||||
type Manifest struct {
|
||||
root *directory
|
||||
}
|
||||
|
||||
type resource struct {
|
||||
mode os.FileMode
|
||||
uid uint32
|
||||
gid uint32
|
||||
}
|
||||
|
||||
type file struct {
|
||||
resource
|
||||
content io.ReadCloser
|
||||
ignoreCariageReturn bool
|
||||
compareContentFunc func(b []byte) CompareResult
|
||||
}
|
||||
|
||||
func (f *file) Type() string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
type symlink struct {
|
||||
resource
|
||||
target string
|
||||
}
|
||||
|
||||
func (f *symlink) Type() string {
|
||||
return "symlink"
|
||||
}
|
||||
|
||||
type directory struct {
|
||||
resource
|
||||
items map[string]dirEntry
|
||||
filepathGlobs map[string]*filePath
|
||||
}
|
||||
|
||||
func (f *directory) Type() string {
|
||||
return "directory"
|
||||
}
|
||||
|
||||
type dirEntry interface {
|
||||
Type() string
|
||||
}
|
||||
|
||||
// ManifestFromDir creates a [Manifest] by reading the directory at path. The
|
||||
// manifest stores the structure and properties of files in the directory.
|
||||
// ManifestFromDir can be used with [Equal] to compare two directories.
|
||||
func ManifestFromDir(t assert.TestingT, path string) Manifest {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
|
||||
manifest, err := manifestFromDir(path)
|
||||
assert.NilError(t, err)
|
||||
return manifest
|
||||
}
|
||||
|
||||
func manifestFromDir(path string) (Manifest, error) {
|
||||
info, err := os.Stat(path)
|
||||
switch {
|
||||
case err != nil:
|
||||
return Manifest{}, err
|
||||
case !info.IsDir():
|
||||
return Manifest{}, fmt.Errorf("path %s must be a directory", path)
|
||||
}
|
||||
|
||||
directory, err := newDirectory(path, info)
|
||||
return Manifest{root: directory}, err
|
||||
}
|
||||
|
||||
func newDirectory(path string, info os.FileInfo) (*directory, error) {
|
||||
items := make(map[string]dirEntry)
|
||||
children, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, child := range children {
|
||||
fullPath := filepath.Join(path, child.Name())
|
||||
items[child.Name()], err = getTypedResource(fullPath, child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &directory{
|
||||
resource: newResourceFromInfo(info),
|
||||
items: items,
|
||||
filepathGlobs: make(map[string]*filePath),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getTypedResource(path string, entry os.DirEntry) (dirEntry, error) {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case info.IsDir():
|
||||
return newDirectory(path, info)
|
||||
case info.Mode()&os.ModeSymlink != 0:
|
||||
return newSymlink(path, info)
|
||||
// TODO: devices, pipes?
|
||||
default:
|
||||
return newFile(path, info)
|
||||
}
|
||||
}
|
||||
|
||||
func newSymlink(path string, info os.FileInfo) (*symlink, error) {
|
||||
target, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &symlink{
|
||||
resource: newResourceFromInfo(info),
|
||||
target: target,
|
||||
}, err
|
||||
}
|
||||
|
||||
func newFile(path string, info os.FileInfo) (*file, error) {
|
||||
// TODO: defer file opening to reduce number of open FDs?
|
||||
readCloser, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &file{
|
||||
resource: newResourceFromInfo(info),
|
||||
content: readCloser,
|
||||
}, err
|
||||
}
|
38
vendor/gotest.tools/v3/fs/manifest_unix.go
vendored
Normal file
38
vendor/gotest.tools/v3/fs/manifest_unix.go
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const defaultRootDirMode = os.ModeDir | 0700
|
||||
|
||||
var defaultSymlinkMode = os.ModeSymlink | 0777
|
||||
|
||||
func init() {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
defaultSymlinkMode = os.ModeSymlink | 0755
|
||||
}
|
||||
}
|
||||
|
||||
func newResourceFromInfo(info os.FileInfo) resource {
|
||||
statT := info.Sys().(*syscall.Stat_t)
|
||||
return resource{
|
||||
mode: info.Mode(),
|
||||
uid: statT.Uid,
|
||||
gid: statT.Gid,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *filePath) SetMode(mode os.FileMode) {
|
||||
p.file.mode = mode
|
||||
}
|
||||
|
||||
func (p *directoryPath) SetMode(mode os.FileMode) {
|
||||
p.directory.mode = mode | os.ModeDir
|
||||
}
|
22
vendor/gotest.tools/v3/fs/manifest_windows.go
vendored
Normal file
22
vendor/gotest.tools/v3/fs/manifest_windows.go
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
package fs
|
||||
|
||||
import "os"
|
||||
|
||||
const (
|
||||
defaultRootDirMode = os.ModeDir | 0777
|
||||
defaultSymlinkMode = os.ModeSymlink | 0666
|
||||
)
|
||||
|
||||
func newResourceFromInfo(info os.FileInfo) resource {
|
||||
return resource{mode: info.Mode()}
|
||||
}
|
||||
|
||||
func (p *filePath) SetMode(mode os.FileMode) {
|
||||
bits := mode & 0600
|
||||
p.file.mode = bits + bits/010 + bits/0100
|
||||
}
|
||||
|
||||
// TODO: is mode ignored on windows?
|
||||
func (p *directoryPath) SetMode(mode os.FileMode) {
|
||||
p.directory.mode = defaultRootDirMode
|
||||
}
|
278
vendor/gotest.tools/v3/fs/ops.go
vendored
Normal file
278
vendor/gotest.tools/v3/fs/ops.go
vendored
Normal file
@ -0,0 +1,278 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
const defaultFileMode = 0644
|
||||
|
||||
// PathOp is a function which accepts a [Path] and performs an operation on that
|
||||
// path. When called with real filesystem objects ([File] or [Dir]) a PathOp modifies
|
||||
// the filesystem at the path. When used with a [Manifest] object a PathOp updates
|
||||
// the manifest to expect a value.
|
||||
type PathOp func(path Path) error
|
||||
|
||||
type manifestResource interface {
|
||||
SetMode(mode os.FileMode)
|
||||
SetUID(uid uint32)
|
||||
SetGID(gid uint32)
|
||||
}
|
||||
|
||||
type manifestFile interface {
|
||||
manifestResource
|
||||
SetContent(content io.ReadCloser)
|
||||
}
|
||||
|
||||
type manifestDirectory interface {
|
||||
manifestResource
|
||||
AddSymlink(path, target string) error
|
||||
AddFile(path string, ops ...PathOp) error
|
||||
AddDirectory(path string, ops ...PathOp) error
|
||||
}
|
||||
|
||||
// WithContent writes content to a file at [Path]
|
||||
func WithContent(content string) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestFile); ok {
|
||||
m.SetContent(io.NopCloser(strings.NewReader(content)))
|
||||
return nil
|
||||
}
|
||||
return os.WriteFile(path.Path(), []byte(content), defaultFileMode)
|
||||
}
|
||||
}
|
||||
|
||||
// WithBytes write bytes to a file at [Path]
|
||||
func WithBytes(raw []byte) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestFile); ok {
|
||||
m.SetContent(io.NopCloser(bytes.NewReader(raw)))
|
||||
return nil
|
||||
}
|
||||
return os.WriteFile(path.Path(), raw, defaultFileMode)
|
||||
}
|
||||
}
|
||||
|
||||
// WithReaderContent copies the reader contents to the file at [Path]
|
||||
func WithReaderContent(r io.Reader) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestFile); ok {
|
||||
m.SetContent(io.NopCloser(r))
|
||||
return nil
|
||||
}
|
||||
f, err := os.OpenFile(path.Path(), os.O_WRONLY, defaultFileMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, r)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// AsUser changes ownership of the file system object at [Path]
|
||||
func AsUser(uid, gid int) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestResource); ok {
|
||||
m.SetUID(uint32(uid))
|
||||
m.SetGID(uint32(gid))
|
||||
return nil
|
||||
}
|
||||
return os.Chown(path.Path(), uid, gid)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFile creates a file in the directory at path with content
|
||||
func WithFile(filename, content string, ops ...PathOp) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestDirectory); ok {
|
||||
ops = append([]PathOp{WithContent(content), WithMode(defaultFileMode)}, ops...)
|
||||
return m.AddFile(filename, ops...)
|
||||
}
|
||||
|
||||
fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename))
|
||||
if err := createFile(fullpath, content); err != nil {
|
||||
return err
|
||||
}
|
||||
return applyPathOps(&File{path: fullpath}, ops)
|
||||
}
|
||||
}
|
||||
|
||||
func createFile(fullpath string, content string) error {
|
||||
return os.WriteFile(fullpath, []byte(content), defaultFileMode)
|
||||
}
|
||||
|
||||
// WithFiles creates all the files in the directory at path with their content
|
||||
func WithFiles(files map[string]string) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestDirectory); ok {
|
||||
for filename, content := range files {
|
||||
// TODO: remove duplication with WithFile
|
||||
if err := m.AddFile(filename, WithContent(content), WithMode(defaultFileMode)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for filename, content := range files {
|
||||
fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename))
|
||||
if err := createFile(fullpath, content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// FromDir copies the directory tree from the source path into the new [Dir]
|
||||
func FromDir(source string) PathOp {
|
||||
return func(path Path) error {
|
||||
if _, ok := path.(manifestDirectory); ok {
|
||||
return fmt.Errorf("use manifest.FromDir")
|
||||
}
|
||||
return copyDirectory(source, path.Path())
|
||||
}
|
||||
}
|
||||
|
||||
// WithDir creates a subdirectory in the directory at path. Additional [PathOp]
|
||||
// can be used to modify the subdirectory
|
||||
func WithDir(name string, ops ...PathOp) PathOp {
|
||||
const defaultMode = 0755
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestDirectory); ok {
|
||||
ops = append([]PathOp{WithMode(defaultMode)}, ops...)
|
||||
return m.AddDirectory(name, ops...)
|
||||
}
|
||||
|
||||
fullpath := filepath.Join(path.Path(), filepath.FromSlash(name))
|
||||
err := os.MkdirAll(fullpath, defaultMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return applyPathOps(&Dir{path: fullpath}, ops)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the PathOps to the [File]
|
||||
func Apply(t assert.TestingT, path Path, ops ...PathOp) {
|
||||
if ht, ok := t.(helperT); ok {
|
||||
ht.Helper()
|
||||
}
|
||||
assert.NilError(t, applyPathOps(path, ops))
|
||||
}
|
||||
|
||||
func applyPathOps(path Path, ops []PathOp) error {
|
||||
for _, op := range ops {
|
||||
if err := op(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithMode sets the file mode on the directory or file at [Path]
|
||||
func WithMode(mode os.FileMode) PathOp {
|
||||
return func(path Path) error {
|
||||
if m, ok := path.(manifestResource); ok {
|
||||
m.SetMode(mode)
|
||||
return nil
|
||||
}
|
||||
return os.Chmod(path.Path(), mode)
|
||||
}
|
||||
}
|
||||
|
||||
func copyDirectory(source, dest string) error {
|
||||
entries, err := os.ReadDir(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
sourcePath := filepath.Join(source, entry.Name())
|
||||
destPath := filepath.Join(dest, entry.Name())
|
||||
err = copyEntry(entry, destPath, sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyEntry(entry os.DirEntry, destPath string, sourcePath string) error {
|
||||
if entry.IsDir() {
|
||||
if err := os.Mkdir(destPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return copyDirectory(sourcePath, destPath)
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return copySymLink(sourcePath, destPath)
|
||||
}
|
||||
return copyFile(sourcePath, destPath)
|
||||
}
|
||||
|
||||
func copySymLink(source, dest string) error {
|
||||
link, err := os.Readlink(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Symlink(link, dest)
|
||||
}
|
||||
|
||||
func copyFile(source, dest string) error {
|
||||
content, err := os.ReadFile(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dest, content, 0644)
|
||||
}
|
||||
|
||||
// WithSymlink creates a symlink in the directory which links to target.
|
||||
// Target must be a path relative to the directory.
|
||||
//
|
||||
// Note: the argument order is the inverse of [os.Symlink] to be consistent with
|
||||
// the other functions in this package.
|
||||
func WithSymlink(path, target string) PathOp {
|
||||
return func(root Path) error {
|
||||
if v, ok := root.(manifestDirectory); ok {
|
||||
return v.AddSymlink(path, target)
|
||||
}
|
||||
return os.Symlink(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path))
|
||||
}
|
||||
}
|
||||
|
||||
// WithHardlink creates a link in the directory which links to target.
|
||||
// Target must be a path relative to the directory.
|
||||
//
|
||||
// Note: the argument order is the inverse of [os.Link] to be consistent with
|
||||
// the other functions in this package.
|
||||
func WithHardlink(path, target string) PathOp {
|
||||
return func(root Path) error {
|
||||
if _, ok := root.(manifestDirectory); ok {
|
||||
return fmt.Errorf("WithHardlink not implemented for manifests")
|
||||
}
|
||||
return os.Link(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path))
|
||||
}
|
||||
}
|
||||
|
||||
// WithTimestamps sets the access and modification times of the file system object
|
||||
// at path.
|
||||
func WithTimestamps(atime, mtime time.Time) PathOp {
|
||||
return func(root Path) error {
|
||||
if _, ok := root.(manifestDirectory); ok {
|
||||
return fmt.Errorf("WithTimestamp not implemented for manifests")
|
||||
}
|
||||
return os.Chtimes(root.Path(), atime, mtime)
|
||||
}
|
||||
}
|
198
vendor/gotest.tools/v3/fs/path.go
vendored
Normal file
198
vendor/gotest.tools/v3/fs/path.go
vendored
Normal file
@ -0,0 +1,198 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"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 = io.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/v3/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
|
||||
}
|
276
vendor/gotest.tools/v3/fs/report.go
vendored
Normal file
276
vendor/gotest.tools/v3/fs/report.go
vendored
Normal file
@ -0,0 +1,276 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/internal/format"
|
||||
)
|
||||
|
||||
// Equal compares a directory to the expected structured described by a manifest
|
||||
// and returns success if they match. If they do not match the failure message
|
||||
// will contain all the differences between the directory structure and the
|
||||
// expected structure defined by the [Manifest].
|
||||
//
|
||||
// Equal is a [cmp.Comparison] which can be used with [gotest.tools/v3/assert.Assert].
|
||||
func Equal(path string, expected Manifest) cmp.Comparison {
|
||||
return func() cmp.Result {
|
||||
actual, err := manifestFromDir(path)
|
||||
if err != nil {
|
||||
return cmp.ResultFromError(err)
|
||||
}
|
||||
failures := eqDirectory(string(os.PathSeparator), expected.root, actual.root)
|
||||
if len(failures) == 0 {
|
||||
return cmp.ResultSuccess
|
||||
}
|
||||
msg := fmt.Sprintf("directory %s does not match expected:\n", path)
|
||||
return cmp.ResultFailure(msg + formatFailures(failures))
|
||||
}
|
||||
}
|
||||
|
||||
type failure struct {
|
||||
path string
|
||||
problems []problem
|
||||
}
|
||||
|
||||
type problem string
|
||||
|
||||
func notEqual(property string, x, y interface{}) problem {
|
||||
return problem(fmt.Sprintf("%s: expected %s got %s", property, x, y))
|
||||
}
|
||||
|
||||
func errProblem(reason string, err error) problem {
|
||||
return problem(fmt.Sprintf("%s: %s", reason, err))
|
||||
}
|
||||
|
||||
func existenceProblem(filename, reason string, args ...interface{}) problem {
|
||||
return problem(filename + ": " + fmt.Sprintf(reason, args...))
|
||||
}
|
||||
|
||||
func eqResource(x, y resource) []problem {
|
||||
var p []problem
|
||||
if x.uid != y.uid {
|
||||
p = append(p, notEqual("uid", x.uid, y.uid))
|
||||
}
|
||||
if x.gid != y.gid {
|
||||
p = append(p, notEqual("gid", x.gid, y.gid))
|
||||
}
|
||||
if x.mode != anyFileMode && x.mode != y.mode {
|
||||
p = append(p, notEqual("mode", x.mode, y.mode))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func removeCarriageReturn(in []byte) []byte {
|
||||
return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1)
|
||||
}
|
||||
|
||||
func eqFile(x, y *file) []problem {
|
||||
p := eqResource(x.resource, y.resource)
|
||||
|
||||
switch {
|
||||
case x.content == nil:
|
||||
p = append(p, existenceProblem("content", "expected content is nil"))
|
||||
return p
|
||||
case x.content == anyFileContent:
|
||||
return p
|
||||
case y.content == nil:
|
||||
p = append(p, existenceProblem("content", "actual content is nil"))
|
||||
return p
|
||||
}
|
||||
|
||||
xContent, xErr := io.ReadAll(x.content)
|
||||
defer x.content.Close()
|
||||
yContent, yErr := io.ReadAll(y.content)
|
||||
defer y.content.Close()
|
||||
|
||||
if xErr != nil {
|
||||
p = append(p, errProblem("failed to read expected content", xErr))
|
||||
}
|
||||
if yErr != nil {
|
||||
p = append(p, errProblem("failed to read actual content", xErr))
|
||||
}
|
||||
if xErr != nil || yErr != nil {
|
||||
return p
|
||||
}
|
||||
|
||||
if x.compareContentFunc != nil {
|
||||
r := x.compareContentFunc(yContent)
|
||||
if !r.Success() {
|
||||
p = append(p, existenceProblem("content", r.FailureMessage()))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
if x.ignoreCariageReturn || y.ignoreCariageReturn {
|
||||
xContent = removeCarriageReturn(xContent)
|
||||
yContent = removeCarriageReturn(yContent)
|
||||
}
|
||||
|
||||
if !bytes.Equal(xContent, yContent) {
|
||||
p = append(p, diffContent(xContent, yContent))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func diffContent(x, y []byte) problem {
|
||||
diff := format.UnifiedDiff(format.DiffConfig{
|
||||
A: string(x),
|
||||
B: string(y),
|
||||
From: "expected",
|
||||
To: "actual",
|
||||
})
|
||||
// Remove the trailing newline in the diff. A trailing newline is always
|
||||
// added to a problem by formatFailures.
|
||||
diff = strings.TrimSuffix(diff, "\n")
|
||||
return problem("content:\n" + indent(diff, " "))
|
||||
}
|
||||
|
||||
func indent(s, prefix string) string {
|
||||
buf := new(bytes.Buffer)
|
||||
lines := strings.SplitAfter(s, "\n")
|
||||
for _, line := range lines {
|
||||
buf.WriteString(prefix + line)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func eqSymlink(x, y *symlink) []problem {
|
||||
p := eqResource(x.resource, y.resource)
|
||||
xTarget := x.target
|
||||
yTarget := y.target
|
||||
if runtime.GOOS == "windows" {
|
||||
xTarget = strings.ToLower(xTarget)
|
||||
yTarget = strings.ToLower(yTarget)
|
||||
}
|
||||
if xTarget != yTarget {
|
||||
p = append(p, notEqual("target", x.target, y.target))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func eqDirectory(path string, x, y *directory) []failure {
|
||||
p := eqResource(x.resource, y.resource)
|
||||
var f []failure
|
||||
matchedFiles := make(map[string]bool)
|
||||
|
||||
for _, name := range sortedKeys(x.items) {
|
||||
if name == anyFile {
|
||||
continue
|
||||
}
|
||||
matchedFiles[name] = true
|
||||
xEntry := x.items[name]
|
||||
yEntry, ok := y.items[name]
|
||||
if !ok {
|
||||
p = append(p, existenceProblem(name, "expected %s to exist", xEntry.Type()))
|
||||
continue
|
||||
}
|
||||
|
||||
if xEntry.Type() != yEntry.Type() {
|
||||
p = append(p, notEqual(name, xEntry.Type(), yEntry.Type()))
|
||||
continue
|
||||
}
|
||||
|
||||
f = append(f, eqEntry(filepath.Join(path, name), xEntry, yEntry)...)
|
||||
}
|
||||
|
||||
if len(x.filepathGlobs) != 0 {
|
||||
for _, name := range sortedKeys(y.items) {
|
||||
m := matchGlob(name, y.items[name], x.filepathGlobs)
|
||||
matchedFiles[name] = m.match
|
||||
f = append(f, m.failures...)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := x.items[anyFile]; ok {
|
||||
return maybeAppendFailure(f, path, p)
|
||||
}
|
||||
for _, name := range sortedKeys(y.items) {
|
||||
if !matchedFiles[name] {
|
||||
p = append(p, existenceProblem(name, "unexpected %s", y.items[name].Type()))
|
||||
}
|
||||
}
|
||||
return maybeAppendFailure(f, path, p)
|
||||
}
|
||||
|
||||
func maybeAppendFailure(failures []failure, path string, problems []problem) []failure {
|
||||
if len(problems) > 0 {
|
||||
return append(failures, failure{path: path, problems: problems})
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func sortedKeys(items map[string]dirEntry) []string {
|
||||
keys := make([]string, 0, len(items))
|
||||
for key := range items {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// eqEntry assumes x and y to be the same type
|
||||
func eqEntry(path string, x, y dirEntry) []failure {
|
||||
resp := func(problems []problem) []failure {
|
||||
if len(problems) == 0 {
|
||||
return nil
|
||||
}
|
||||
return []failure{{path: path, problems: problems}}
|
||||
}
|
||||
|
||||
switch typed := x.(type) {
|
||||
case *file:
|
||||
return resp(eqFile(typed, y.(*file)))
|
||||
case *symlink:
|
||||
return resp(eqSymlink(typed, y.(*symlink)))
|
||||
case *directory:
|
||||
return eqDirectory(path, typed, y.(*directory))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type globMatch struct {
|
||||
match bool
|
||||
failures []failure
|
||||
}
|
||||
|
||||
func matchGlob(name string, yEntry dirEntry, globs map[string]*filePath) globMatch {
|
||||
m := globMatch{}
|
||||
|
||||
for glob, expectedFile := range globs {
|
||||
ok, err := filepath.Match(glob, name)
|
||||
if err != nil {
|
||||
p := errProblem("failed to match glob pattern", err)
|
||||
f := failure{path: name, problems: []problem{p}}
|
||||
m.failures = append(m.failures, f)
|
||||
}
|
||||
if ok {
|
||||
m.match = true
|
||||
m.failures = eqEntry(name, expectedFile.file, yEntry)
|
||||
return m
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func formatFailures(failures []failure) string {
|
||||
sort.Slice(failures, func(i, j int) bool {
|
||||
return failures[i].path < failures[j].path
|
||||
})
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
for _, failure := range failures {
|
||||
buf.WriteString(failure.path + "\n")
|
||||
for _, problem := range failure.problems {
|
||||
buf.WriteString(" " + string(problem) + "\n")
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
Reference in New Issue
Block a user