File: testdir.go

package info (click to toggle)
elvish 0.21.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 6,372 kB
  • sloc: javascript: 236; sh: 130; python: 104; makefile: 88; xml: 9
file content (229 lines) | stat: -rw-r--r-- 5,929 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
package testutil

import (
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"src.elv.sh/pkg/env"
	"src.elv.sh/pkg/must"
)

// TempDir creates a temporary directory for testing that will be removed
// after the test finishes. It is different from testing.TB.TempDir in that it
// resolves symlinks in the path of the directory.
//
// It panics if the test directory cannot be created or symlinks cannot be
// resolved. It is only suitable for use in tests.
func TempDir(c Cleanuper) string {
	dir, err := os.MkdirTemp("", "elvishtest.")
	if err != nil {
		panic(err)
	}
	dir, err = filepath.EvalSymlinks(dir)
	if err != nil {
		panic(err)
	}
	c.Cleanup(func() {
		err := os.RemoveAll(dir)
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed to remove temp dir %s: %v\n", dir, err)
		}
	})
	return dir
}

// TempHome is equivalent to Setenv(c, env.HOME, TempDir(c))
func TempHome(c Cleanuper) string {
	return Setenv(c, env.HOME, TempDir(c))
}

// Chdir changes into a directory, and restores the original working directory
// when a test finishes. It returns the directory for easier chaining.
func Chdir(c Cleanuper, dir string) string {
	oldWd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	must.Chdir(dir)
	c.Cleanup(func() {
		must.Chdir(oldWd)
	})
	return dir
}

// InTempDir is equivalent to Chdir(c, TempDir(c)).
func InTempDir(c Cleanuper) string {
	return Chdir(c, TempDir(c))
}

// InTempHome is equivalent to Setenv(c, env.HOME, InTempDir(c))
func InTempHome(c Cleanuper) string {
	return Setenv(c, env.HOME, InTempDir(c))
}

// Dir describes the layout of a directory. The keys of the map represent
// filenames. Each value is either a string (for the content of a regular file
// with permission 0644), a File, or a Dir.
type Dir map[string]any

// File describes a file to create.
type File struct {
	Perm    os.FileMode
	Content string
}

// ApplyDir creates the given filesystem layout in the current directory.
func ApplyDir(dir Dir) {
	ApplyDirIn(dir, "")
}

// ApplyDirIn creates the given filesystem layout in a given directory.
func ApplyDirIn(dir Dir, root string) {
	for name, file := range dir {
		path := filepath.Join(root, name)
		switch file := file.(type) {
		case string:
			must.OK(os.WriteFile(path, []byte(file), 0644))
		case File:
			must.OK(os.WriteFile(path, []byte(file.Content), file.Perm))
		case Dir:
			must.OK(os.MkdirAll(path, 0755))
			ApplyDirIn(file, path)
		default:
			panic(fmt.Sprintf("file is neither string, Dir, or Symlink: %v", file))
		}
	}
}

// fs.FS implementation for Dir.

func (dir Dir) Open(name string) (fs.File, error) {
	if !fs.ValidPath(name) {
		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
	}
	if name == "." {
		return newFsDir(".", dir), nil
	}
	currentDir := dir
	currentName := name
	for {
		first, rest, moreLevels := strings.Cut(currentName, "/")
		file, ok := currentDir[first]
		if !ok {
			return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
		}
		if !moreLevels {
			return newFsFileOrDir(name, file), nil
		}
		if nextDir, ok := file.(Dir); ok {
			currentDir = nextDir
			currentName = rest
		} else {
			return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
		}
	}
}

func newFsFileOrDir(name string, x any) fs.File {
	switch x := x.(type) {
	case Dir:
		return newFsDir(name, x)
	case File:
		return fsFile{newFsFileInfo(path.Base(name), x).(fileInfo), strings.NewReader(x.Content)}
	case string:
		return fsFile{newFsFileInfo(path.Base(name), x).(fileInfo), strings.NewReader(x)}
	default:
		panic(fmt.Sprintf("file is neither string, File or Dir: %v", x))
	}
}

func newFsFileInfo(basename string, x any) fs.FileInfo {
	switch x := x.(type) {
	case Dir:
		return dirInfo{basename}
	case File:
		return fileInfo{basename, x.Perm, len(x.Content)}
	case string:
		return fileInfo{basename, 0o644, len(x)}
	default:
		panic(fmt.Sprintf("file is neither string, File or Dir: %v", x))
	}
}

type fsDir struct {
	info    dirInfo
	readErr error
	entries []fs.DirEntry
}

var errIsDir = errors.New("is a directory")

func newFsDir(name string, dir Dir) *fsDir {
	info := dirInfo{path.Base(name)}
	readErr := &fs.PathError{Op: "read", Path: name, Err: errIsDir}
	entries := make([]fs.DirEntry, 0, len(dir))
	for name, file := range dir {
		entries = append(entries, fs.FileInfoToDirEntry(newFsFileInfo(name, file)))
	}
	return &fsDir{info, readErr, entries}
}

func (fd *fsDir) Stat() (fs.FileInfo, error) { return fd.info, nil }
func (fd *fsDir) Read([]byte) (int, error)   { return 0, fd.readErr }
func (fd *fsDir) Close() error               { return nil }

func (fd *fsDir) ReadDir(n int) ([]fs.DirEntry, error) {
	if n <= 0 || (n >= len(fd.entries) && len(fd.entries) != 0) {
		ret := fd.entries
		fd.entries = nil
		return ret, nil
	}
	if len(fd.entries) == 0 {
		return nil, io.EOF
	}
	ret := fd.entries[:n]
	fd.entries = fd.entries[n:]
	return ret, nil
}

type dirInfo struct{ basename string }

var t0 = time.Unix(0, 0).UTC()

func (di dirInfo) Name() string    { return di.basename }
func (dirInfo) Size() int64        { return 0 }
func (dirInfo) Mode() fs.FileMode  { return fs.ModeDir | 0o755 }
func (dirInfo) ModTime() time.Time { return t0 }
func (dirInfo) IsDir() bool        { return true }
func (dirInfo) Sys() any           { return nil }

type fsFile struct {
	info fileInfo
	*strings.Reader
}

func (ff fsFile) Stat() (fs.FileInfo, error) {
	return ff.info, nil
}

func (ff fsFile) Close() error { return nil }

type fileInfo struct {
	basename string
	perm     fs.FileMode
	size     int
}

func (fi fileInfo) Name() string      { return fi.basename }
func (fi fileInfo) Size() int64       { return int64(fi.size) }
func (fi fileInfo) Mode() fs.FileMode { return fi.perm }
func (fileInfo) ModTime() time.Time   { return t0 }
func (fileInfo) IsDir() bool          { return false }
func (fileInfo) Sys() any             { return nil }