src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/testutil/testdir.go (about)

     1  package testutil
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"io/fs"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"src.elv.sh/pkg/env"
    15  	"src.elv.sh/pkg/must"
    16  )
    17  
    18  // TempDir creates a temporary directory for testing that will be removed
    19  // after the test finishes. It is different from testing.TB.TempDir in that it
    20  // resolves symlinks in the path of the directory.
    21  //
    22  // It panics if the test directory cannot be created or symlinks cannot be
    23  // resolved. It is only suitable for use in tests.
    24  func TempDir(c Cleanuper) string {
    25  	dir, err := os.MkdirTemp("", "elvishtest.")
    26  	if err != nil {
    27  		panic(err)
    28  	}
    29  	dir, err = filepath.EvalSymlinks(dir)
    30  	if err != nil {
    31  		panic(err)
    32  	}
    33  	c.Cleanup(func() {
    34  		err := os.RemoveAll(dir)
    35  		if err != nil {
    36  			fmt.Fprintf(os.Stderr, "failed to remove temp dir %s: %v\n", dir, err)
    37  		}
    38  	})
    39  	return dir
    40  }
    41  
    42  // TempHome is equivalent to Setenv(c, env.HOME, TempDir(c))
    43  func TempHome(c Cleanuper) string {
    44  	return Setenv(c, env.HOME, TempDir(c))
    45  }
    46  
    47  // Chdir changes into a directory, and restores the original working directory
    48  // when a test finishes. It returns the directory for easier chaining.
    49  func Chdir(c Cleanuper, dir string) string {
    50  	oldWd, err := os.Getwd()
    51  	if err != nil {
    52  		panic(err)
    53  	}
    54  	must.Chdir(dir)
    55  	c.Cleanup(func() {
    56  		must.Chdir(oldWd)
    57  	})
    58  	return dir
    59  }
    60  
    61  // InTempDir is equivalent to Chdir(c, TempDir(c)).
    62  func InTempDir(c Cleanuper) string {
    63  	return Chdir(c, TempDir(c))
    64  }
    65  
    66  // InTempHome is equivalent to Setenv(c, env.HOME, InTempDir(c))
    67  func InTempHome(c Cleanuper) string {
    68  	return Setenv(c, env.HOME, InTempDir(c))
    69  }
    70  
    71  // Dir describes the layout of a directory. The keys of the map represent
    72  // filenames. Each value is either a string (for the content of a regular file
    73  // with permission 0644), a File, or a Dir.
    74  type Dir map[string]any
    75  
    76  // File describes a file to create.
    77  type File struct {
    78  	Perm    os.FileMode
    79  	Content string
    80  }
    81  
    82  // ApplyDir creates the given filesystem layout in the current directory.
    83  func ApplyDir(dir Dir) {
    84  	ApplyDirIn(dir, "")
    85  }
    86  
    87  // ApplyDirIn creates the given filesystem layout in a given directory.
    88  func ApplyDirIn(dir Dir, root string) {
    89  	for name, file := range dir {
    90  		path := filepath.Join(root, name)
    91  		switch file := file.(type) {
    92  		case string:
    93  			must.OK(os.WriteFile(path, []byte(file), 0644))
    94  		case File:
    95  			must.OK(os.WriteFile(path, []byte(file.Content), file.Perm))
    96  		case Dir:
    97  			must.OK(os.MkdirAll(path, 0755))
    98  			ApplyDirIn(file, path)
    99  		default:
   100  			panic(fmt.Sprintf("file is neither string, Dir, or Symlink: %v", file))
   101  		}
   102  	}
   103  }
   104  
   105  // fs.FS implementation for Dir.
   106  
   107  func (dir Dir) Open(name string) (fs.File, error) {
   108  	if !fs.ValidPath(name) {
   109  		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
   110  	}
   111  	if name == "." {
   112  		return newFsDir(".", dir), nil
   113  	}
   114  	currentDir := dir
   115  	currentName := name
   116  	for {
   117  		first, rest, moreLevels := strings.Cut(currentName, "/")
   118  		file, ok := currentDir[first]
   119  		if !ok {
   120  			return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
   121  		}
   122  		if !moreLevels {
   123  			return newFsFileOrDir(name, file), nil
   124  		}
   125  		if nextDir, ok := file.(Dir); ok {
   126  			currentDir = nextDir
   127  			currentName = rest
   128  		} else {
   129  			return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
   130  		}
   131  	}
   132  }
   133  
   134  func newFsFileOrDir(name string, x any) fs.File {
   135  	switch x := x.(type) {
   136  	case Dir:
   137  		return newFsDir(name, x)
   138  	case File:
   139  		return fsFile{newFsFileInfo(path.Base(name), x).(fileInfo), strings.NewReader(x.Content)}
   140  	case string:
   141  		return fsFile{newFsFileInfo(path.Base(name), x).(fileInfo), strings.NewReader(x)}
   142  	default:
   143  		panic(fmt.Sprintf("file is neither string, File or Dir: %v", x))
   144  	}
   145  }
   146  
   147  func newFsFileInfo(basename string, x any) fs.FileInfo {
   148  	switch x := x.(type) {
   149  	case Dir:
   150  		return dirInfo{basename}
   151  	case File:
   152  		return fileInfo{basename, x.Perm, len(x.Content)}
   153  	case string:
   154  		return fileInfo{basename, 0o644, len(x)}
   155  	default:
   156  		panic(fmt.Sprintf("file is neither string, File or Dir: %v", x))
   157  	}
   158  }
   159  
   160  type fsDir struct {
   161  	info    dirInfo
   162  	readErr error
   163  	entries []fs.DirEntry
   164  }
   165  
   166  var errIsDir = errors.New("is a directory")
   167  
   168  func newFsDir(name string, dir Dir) *fsDir {
   169  	info := dirInfo{path.Base(name)}
   170  	readErr := &fs.PathError{Op: "read", Path: name, Err: errIsDir}
   171  	entries := make([]fs.DirEntry, 0, len(dir))
   172  	for name, file := range dir {
   173  		entries = append(entries, fs.FileInfoToDirEntry(newFsFileInfo(name, file)))
   174  	}
   175  	return &fsDir{info, readErr, entries}
   176  }
   177  
   178  func (fd *fsDir) Stat() (fs.FileInfo, error) { return fd.info, nil }
   179  func (fd *fsDir) Read([]byte) (int, error)   { return 0, fd.readErr }
   180  func (fd *fsDir) Close() error               { return nil }
   181  
   182  func (fd *fsDir) ReadDir(n int) ([]fs.DirEntry, error) {
   183  	if n <= 0 || (n >= len(fd.entries) && len(fd.entries) != 0) {
   184  		ret := fd.entries
   185  		fd.entries = nil
   186  		return ret, nil
   187  	}
   188  	if len(fd.entries) == 0 {
   189  		return nil, io.EOF
   190  	}
   191  	ret := fd.entries[:n]
   192  	fd.entries = fd.entries[n:]
   193  	return ret, nil
   194  }
   195  
   196  type dirInfo struct{ basename string }
   197  
   198  var t0 = time.Unix(0, 0).UTC()
   199  
   200  func (di dirInfo) Name() string    { return di.basename }
   201  func (dirInfo) Size() int64        { return 0 }
   202  func (dirInfo) Mode() fs.FileMode  { return fs.ModeDir | 0o755 }
   203  func (dirInfo) ModTime() time.Time { return t0 }
   204  func (dirInfo) IsDir() bool        { return true }
   205  func (dirInfo) Sys() any           { return nil }
   206  
   207  type fsFile struct {
   208  	info fileInfo
   209  	*strings.Reader
   210  }
   211  
   212  func (ff fsFile) Stat() (fs.FileInfo, error) {
   213  	return ff.info, nil
   214  }
   215  
   216  func (ff fsFile) Close() error { return nil }
   217  
   218  type fileInfo struct {
   219  	basename string
   220  	perm     fs.FileMode
   221  	size     int
   222  }
   223  
   224  func (fi fileInfo) Name() string      { return fi.basename }
   225  func (fi fileInfo) Size() int64       { return int64(fi.size) }
   226  func (fi fileInfo) Mode() fs.FileMode { return fi.perm }
   227  func (fileInfo) ModTime() time.Time   { return t0 }
   228  func (fileInfo) IsDir() bool          { return false }
   229  func (fileInfo) Sys() any             { return nil }