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 }