vendor tsgo
This commit is contained in:
679
tools/tsgo/internal/vfs/vfstest/vfstest.go
Normal file
679
tools/tsgo/internal/vfs/vfstest/vfstest.go
Normal file
@@ -0,0 +1,679 @@
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"iter"
|
||||
"maps"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/iovfs"
|
||||
)
|
||||
|
||||
type MapFS struct {
|
||||
// mu protects m.
|
||||
// A single mutex is sufficient as we only use fstest.Map's Open method.
|
||||
mu sync.RWMutex
|
||||
|
||||
// keys in m are canonicalPaths
|
||||
m fstest.MapFS
|
||||
|
||||
useCaseSensitiveFileNames bool
|
||||
|
||||
symlinks map[canonicalPath]canonicalPath
|
||||
|
||||
clock Clock
|
||||
}
|
||||
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
SinceStart() time.Duration
|
||||
}
|
||||
|
||||
type clockImpl struct {
|
||||
start time.Time
|
||||
}
|
||||
|
||||
func (c *clockImpl) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (c *clockImpl) SinceStart() time.Duration {
|
||||
return time.Since(c.start)
|
||||
}
|
||||
|
||||
var (
|
||||
_ iovfs.RealpathFS = (*MapFS)(nil)
|
||||
_ iovfs.WritableFS = (*MapFS)(nil)
|
||||
)
|
||||
|
||||
type sys struct {
|
||||
original any
|
||||
realpath string
|
||||
}
|
||||
|
||||
// FromMap creates a new [vfs.FS] from a map of paths to file contents.
|
||||
// Those file contents may be strings, byte slices, or [fstest.MapFile]s.
|
||||
//
|
||||
// The paths must be normalized absolute paths according to the tspath package,
|
||||
// without trailing directory separators.
|
||||
// The paths must be all POSIX-style or all Windows-style, but not both.
|
||||
func FromMap[File any](m map[string]File, useCaseSensitiveFileNames bool) vfs.FS {
|
||||
return FromMapWithClock(m, useCaseSensitiveFileNames, &clockImpl{start: time.Now()})
|
||||
}
|
||||
|
||||
// FromMapWithClock creates a new [vfs.FS] from a map of paths to file contents.
|
||||
// Those file contents may be strings, byte slices, or [fstest.MapFile]s.
|
||||
//
|
||||
// The paths must be normalized absolute paths according to the tspath package,
|
||||
// without trailing directory separators.
|
||||
// The paths must be all POSIX-style or all Windows-style, but not both.
|
||||
func FromMapWithClock[File any](m map[string]File, useCaseSensitiveFileNames bool, clock Clock) vfs.FS {
|
||||
posix := false
|
||||
windows := false
|
||||
|
||||
checkPath := func(p string) {
|
||||
if !tspath.IsRootedDiskPath(p) {
|
||||
panic(fmt.Sprintf("non-rooted path %q", p))
|
||||
}
|
||||
|
||||
if normal := tspath.RemoveTrailingDirectorySeparator(tspath.NormalizePath(p)); normal != p {
|
||||
panic(fmt.Sprintf("non-normalized path %q", p))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(p, "/") {
|
||||
posix = true
|
||||
} else {
|
||||
windows = true
|
||||
}
|
||||
}
|
||||
|
||||
mfs := make(fstest.MapFS, len(m))
|
||||
// Sorted creation to ensure times are always guaranteed to be in order.
|
||||
keys := slices.Collect(maps.Keys(m))
|
||||
slices.SortFunc(keys, comparePathsByParts)
|
||||
for _, p := range keys {
|
||||
f := m[p]
|
||||
checkPath(p)
|
||||
|
||||
var file *fstest.MapFile
|
||||
switch f := any(f).(type) {
|
||||
case string:
|
||||
file = &fstest.MapFile{Data: []byte(f), ModTime: clock.Now()}
|
||||
case []byte:
|
||||
file = &fstest.MapFile{Data: f, ModTime: clock.Now()}
|
||||
case *fstest.MapFile:
|
||||
fCopy := *f
|
||||
fCopy.ModTime = clock.Now()
|
||||
file = &fCopy
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid file type %T", f))
|
||||
}
|
||||
|
||||
if file.Mode&fs.ModeSymlink != 0 {
|
||||
target := string(file.Data)
|
||||
checkPath(target)
|
||||
|
||||
target, _ = strings.CutPrefix(target, "/")
|
||||
fileCopy := *file
|
||||
fileCopy.Data = []byte(target)
|
||||
file = &fileCopy
|
||||
}
|
||||
|
||||
p, _ = strings.CutPrefix(p, "/")
|
||||
mfs[p] = file
|
||||
}
|
||||
|
||||
if posix && windows {
|
||||
panic("mixed posix and windows paths")
|
||||
}
|
||||
|
||||
return iovfs.From(convertMapFS(mfs, useCaseSensitiveFileNames, clock), useCaseSensitiveFileNames)
|
||||
}
|
||||
|
||||
func convertMapFS(input fstest.MapFS, useCaseSensitiveFileNames bool, clock Clock) *MapFS {
|
||||
if clock == nil {
|
||||
clock = &clockImpl{start: time.Now()}
|
||||
}
|
||||
m := &MapFS{
|
||||
m: make(fstest.MapFS, len(input)),
|
||||
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
|
||||
clock: clock,
|
||||
}
|
||||
|
||||
// Verify that the input is well-formed.
|
||||
canonicalPaths := make(map[canonicalPath]string, len(input))
|
||||
for path := range input {
|
||||
canonical := m.getCanonicalPath(path)
|
||||
if other, ok := canonicalPaths[canonical]; ok {
|
||||
// Ensure consistent panic messages
|
||||
path, other = min(path, other), max(path, other)
|
||||
panic(fmt.Sprintf("duplicate path: %q and %q have the same canonical path", path, other))
|
||||
}
|
||||
canonicalPaths[canonical] = path
|
||||
}
|
||||
|
||||
// Sort the input by depth and path so we ensure parent dirs are created
|
||||
// before their children, if explicitly specified by the input.
|
||||
inputKeys := slices.Collect(maps.Keys(input))
|
||||
slices.SortFunc(inputKeys, comparePathsByParts)
|
||||
|
||||
for _, p := range inputKeys {
|
||||
file := input[p]
|
||||
|
||||
// Create all missing intermediate directories so we can attach the realpath to each of them.
|
||||
// fstest.MapFS doesn't require this as it synthesizes directories on the fly, but it's a lot
|
||||
// harder to reapply a realpath onto those when we're deep in some FileInfo method.
|
||||
if dir := dirName(p); dir != "" {
|
||||
if err := m.mkdirAll(dir, 0o777); err != nil {
|
||||
panic(fmt.Sprintf("failed to create intermediate directories for %q: %v", p, err))
|
||||
}
|
||||
}
|
||||
m.setEntry(p, m.getCanonicalPath(p), *file)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func comparePathsByParts(a, b string) int {
|
||||
for {
|
||||
aStart, aEnd, aOk := strings.Cut(a, "/")
|
||||
bStart, bEnd, bOk := strings.Cut(b, "/")
|
||||
|
||||
if !aOk || !bOk {
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
if r := strings.Compare(aStart, bStart); r != 0 {
|
||||
return r
|
||||
}
|
||||
|
||||
a, b = aEnd, bEnd
|
||||
}
|
||||
}
|
||||
|
||||
type canonicalPath string
|
||||
|
||||
func (m *MapFS) getCanonicalPath(p string) canonicalPath {
|
||||
return canonicalPath(tspath.GetCanonicalFileName(p, m.useCaseSensitiveFileNames))
|
||||
}
|
||||
|
||||
func (m *MapFS) open(p canonicalPath) (fs.File, error) {
|
||||
return m.m.Open(string(p))
|
||||
}
|
||||
|
||||
func (m *MapFS) remove(path string) error {
|
||||
canonical := m.getCanonicalPath(path)
|
||||
canonicalString := string(canonical)
|
||||
fileInfo := m.m[canonicalString]
|
||||
if fileInfo == nil {
|
||||
// file does not exist
|
||||
return nil
|
||||
}
|
||||
delete(m.m, canonicalString)
|
||||
delete(m.symlinks, canonical)
|
||||
|
||||
if fileInfo.Mode.IsDir() {
|
||||
canonicalString += "/"
|
||||
for path := range m.m {
|
||||
if strings.HasPrefix(path, canonicalString) {
|
||||
delete(m.m, path)
|
||||
delete(m.symlinks, canonicalPath(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Symlink(target string) *fstest.MapFile {
|
||||
return &fstest.MapFile{
|
||||
Data: []byte(target),
|
||||
Mode: fs.ModeSymlink,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MapFS) getFollowingSymlinks(p canonicalPath) (*fstest.MapFile, canonicalPath, error) {
|
||||
return m.getFollowingSymlinksWorker(p, "", "")
|
||||
}
|
||||
|
||||
type brokenSymlinkError struct {
|
||||
from, to canonicalPath
|
||||
}
|
||||
|
||||
func (e *brokenSymlinkError) Error() string {
|
||||
return fmt.Sprintf("broken symlink %q -> %q", e.from, e.to)
|
||||
}
|
||||
|
||||
func isBrokenSymlinkError(err error) bool {
|
||||
_, ok := errors.AsType[*brokenSymlinkError](err)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *MapFS) getFollowingSymlinksWorker(p canonicalPath, symlinkFrom, symlinkTo canonicalPath) (*fstest.MapFile, canonicalPath, error) {
|
||||
if file, ok := m.m[string(p)]; ok && file.Mode&fs.ModeSymlink == 0 {
|
||||
return file, p, nil
|
||||
}
|
||||
|
||||
if target, ok := m.symlinks[p]; ok {
|
||||
return m.getFollowingSymlinksWorker(target, p, target)
|
||||
}
|
||||
|
||||
// This could be a path underneath a symlinked directory.
|
||||
for other, target := range m.symlinks {
|
||||
if len(other) < len(p) && other == p[:len(other)] && p[len(other)] == '/' {
|
||||
return m.getFollowingSymlinksWorker(target+p[len(other):], other, target)
|
||||
}
|
||||
}
|
||||
|
||||
err := fs.ErrNotExist
|
||||
if symlinkFrom != "" {
|
||||
err = &brokenSymlinkError{symlinkFrom, symlinkTo}
|
||||
}
|
||||
return nil, p, err
|
||||
}
|
||||
|
||||
func (m *MapFS) set(p canonicalPath, file *fstest.MapFile) {
|
||||
m.m[string(p)] = file
|
||||
}
|
||||
|
||||
func (m *MapFS) setEntry(realpath string, canonical canonicalPath, file fstest.MapFile) {
|
||||
if realpath == "" || canonical == "" {
|
||||
panic("empty path")
|
||||
}
|
||||
|
||||
file.Sys = &sys{
|
||||
original: file.Sys,
|
||||
realpath: realpath,
|
||||
}
|
||||
m.set(canonical, &file)
|
||||
|
||||
if file.Mode&fs.ModeSymlink != 0 {
|
||||
if m.symlinks == nil {
|
||||
m.symlinks = make(map[canonicalPath]canonicalPath)
|
||||
}
|
||||
m.symlinks[canonical] = m.getCanonicalPath(string(file.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func splitPath(s string, offset int) (before, after string) {
|
||||
idx := strings.IndexByte(s[offset:], '/')
|
||||
if idx < 0 {
|
||||
return s, ""
|
||||
}
|
||||
return s[:idx+offset], s[idx+1+offset:]
|
||||
}
|
||||
|
||||
func dirName(p string) string {
|
||||
dir, _ := path.Split(p)
|
||||
return strings.TrimSuffix(dir, "/")
|
||||
}
|
||||
|
||||
func baseName(p string) string {
|
||||
_, file := path.Split(p)
|
||||
return file
|
||||
}
|
||||
|
||||
func (m *MapFS) mkdirAll(p string, perm fs.FileMode) error {
|
||||
if p == "" {
|
||||
panic("empty path")
|
||||
}
|
||||
|
||||
// Fast path; already exists.
|
||||
if other, _, err := m.getFollowingSymlinks(m.getCanonicalPath(p)); err == nil {
|
||||
if !other.Mode.IsDir() {
|
||||
return fmt.Errorf("mkdir %q: path exists but is not a directory", p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var toCreate []string
|
||||
offset := 0
|
||||
for {
|
||||
dir, rest := splitPath(p, offset)
|
||||
canonical := m.getCanonicalPath(dir)
|
||||
other, otherPath, err := m.getFollowingSymlinks(canonical)
|
||||
if err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
toCreate = append(toCreate, dir)
|
||||
} else {
|
||||
if !other.Mode.IsDir() {
|
||||
return fmt.Errorf("mkdir %q: path exists but is not a directory", otherPath)
|
||||
}
|
||||
if canonical != otherPath {
|
||||
// We have a symlinked parent, reset and start again.
|
||||
p = other.Sys.(*sys).realpath + "/" + rest
|
||||
toCreate = toCreate[:0]
|
||||
offset = 0
|
||||
continue
|
||||
}
|
||||
}
|
||||
if rest == "" {
|
||||
break
|
||||
}
|
||||
offset = len(dir) + 1
|
||||
}
|
||||
|
||||
for _, dir := range toCreate {
|
||||
m.setEntry(dir, m.getCanonicalPath(dir), fstest.MapFile{
|
||||
Mode: fs.ModeDir | perm&^umask,
|
||||
ModTime: m.clock.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
fs.FileInfo
|
||||
sys any
|
||||
realpath string
|
||||
}
|
||||
|
||||
func (fi *fileInfo) Name() string {
|
||||
return baseName(fi.realpath)
|
||||
}
|
||||
|
||||
func (fi *fileInfo) Sys() any {
|
||||
return fi.sys
|
||||
}
|
||||
|
||||
type file struct {
|
||||
fs.File
|
||||
fileInfo *fileInfo
|
||||
}
|
||||
|
||||
func (f *file) Stat() (fs.FileInfo, error) {
|
||||
return f.fileInfo, nil
|
||||
}
|
||||
|
||||
type readDirFile struct {
|
||||
fs.ReadDirFile
|
||||
fileInfo *fileInfo
|
||||
}
|
||||
|
||||
func (f *readDirFile) Stat() (fs.FileInfo, error) {
|
||||
return f.fileInfo, nil
|
||||
}
|
||||
|
||||
func (f *readDirFile) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
list, err := f.ReadDirFile.ReadDir(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries := make([]fs.DirEntry, len(list))
|
||||
for i, entry := range list {
|
||||
info := must(entry.Info())
|
||||
newInfo, ok := convertInfo(info)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unexpected synthesized dir: %q", info.Name()))
|
||||
}
|
||||
entries[i] = fs.FileInfoToDirEntry(newInfo)
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (m *MapFS) Open(name string) (fs.File, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
_, cp, _ := m.getFollowingSymlinks(m.getCanonicalPath(name))
|
||||
f, err := m.open(cp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := must(f.Stat())
|
||||
|
||||
newInfo, ok := convertInfo(info)
|
||||
if !ok {
|
||||
// This is a synthesized dir.
|
||||
if name != "." {
|
||||
panic(fmt.Sprintf("unexpected synthesized dir: %q", name))
|
||||
}
|
||||
|
||||
return &readDirFile{
|
||||
ReadDirFile: f.(fs.ReadDirFile),
|
||||
fileInfo: &fileInfo{
|
||||
FileInfo: info,
|
||||
sys: info.Sys(),
|
||||
realpath: ".",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if f, ok := f.(fs.ReadDirFile); ok {
|
||||
return &readDirFile{
|
||||
ReadDirFile: f,
|
||||
fileInfo: newInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &file{
|
||||
File: f,
|
||||
fileInfo: newInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MapFS) Realpath(name string) (string, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
file, _, err := m.getFollowingSymlinks(m.getCanonicalPath(name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return file.Sys.(*sys).realpath, nil
|
||||
}
|
||||
|
||||
func convertInfo(info fs.FileInfo) (*fileInfo, bool) {
|
||||
sys, ok := info.Sys().(*sys)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &fileInfo{
|
||||
FileInfo: info,
|
||||
sys: sys.original,
|
||||
realpath: sys.realpath,
|
||||
}, true
|
||||
}
|
||||
|
||||
const umask = 0o022
|
||||
|
||||
func (m *MapFS) MkdirAll(path string, perm fs.FileMode) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
return m.mkdirAll(path, perm)
|
||||
}
|
||||
|
||||
func (m *MapFS) AddSymlink(path string, target string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
canonical := m.getCanonicalPath(path)
|
||||
m.setEntry(path, canonical, fstest.MapFile{
|
||||
Data: []byte(target),
|
||||
Mode: fs.ModeSymlink,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *MapFS) WriteFile(path string, data string, perm fs.FileMode) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if parent := dirName(path); parent != "" {
|
||||
canonical := m.getCanonicalPath(parent)
|
||||
parentFile, _, err := m.getFollowingSymlinks(canonical)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write %q: %w", path, err)
|
||||
}
|
||||
if !parentFile.Mode.IsDir() {
|
||||
return fmt.Errorf("write %q: parent path exists but is not a directory", path)
|
||||
}
|
||||
}
|
||||
|
||||
file, cp, err := m.getFollowingSymlinks(m.getCanonicalPath(path))
|
||||
if err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) && !isBrokenSymlinkError(err) {
|
||||
// No other errors are possible.
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
if !file.Mode.IsRegular() {
|
||||
return fmt.Errorf("write %q: path exists but is not a regular file", path)
|
||||
}
|
||||
}
|
||||
|
||||
m.setEntry(path, cp, fstest.MapFile{
|
||||
Data: unsafe.Slice(unsafe.StringData(data), len(data)),
|
||||
ModTime: m.clock.Now(),
|
||||
Mode: perm &^ umask,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MapFS) AppendFile(path string, data string, perm fs.FileMode) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if parent := dirName(path); parent != "" {
|
||||
canonical := m.getCanonicalPath(parent)
|
||||
parentFile, _, err := m.getFollowingSymlinks(canonical)
|
||||
if err != nil {
|
||||
return fmt.Errorf("append %q: %w", path, err)
|
||||
}
|
||||
if !parentFile.Mode.IsDir() {
|
||||
return fmt.Errorf("append %q: parent path exists but is not a directory", path)
|
||||
}
|
||||
}
|
||||
|
||||
var existing []byte
|
||||
var existingMode fs.FileMode
|
||||
file, cp, err := m.getFollowingSymlinks(m.getCanonicalPath(path))
|
||||
if err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) && !isBrokenSymlinkError(err) {
|
||||
// No other errors are possible.
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
if !file.Mode.IsRegular() {
|
||||
return fmt.Errorf("append %q: path exists but is not a regular file", path)
|
||||
}
|
||||
existing = file.Data
|
||||
existingMode = file.Mode
|
||||
}
|
||||
|
||||
combined := make([]byte, 0, len(existing)+len(data))
|
||||
combined = append(combined, existing...)
|
||||
combined = append(combined, data...)
|
||||
|
||||
mode := existingMode
|
||||
if mode == 0 {
|
||||
mode = perm &^ umask
|
||||
}
|
||||
|
||||
m.setEntry(path, cp, fstest.MapFile{
|
||||
Data: combined,
|
||||
ModTime: m.clock.Now(),
|
||||
Mode: mode,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MapFS) Remove(path string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
return m.remove(path)
|
||||
}
|
||||
|
||||
func (m *MapFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
canonical := m.getCanonicalPath(path)
|
||||
canonicalString := string(canonical)
|
||||
fileInfo := m.m[canonicalString]
|
||||
if fileInfo == nil {
|
||||
// file does not exist
|
||||
return fs.ErrNotExist
|
||||
}
|
||||
fileInfo.ModTime = mTime
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MapFS) GetTargetOfSymlink(path string) (string, bool) {
|
||||
path, _ = strings.CutPrefix(path, "/")
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
canonical := m.getCanonicalPath(path)
|
||||
canonicalString := string(canonical)
|
||||
if fileInfo, ok := m.m[canonicalString]; ok {
|
||||
if fileInfo.Mode&fs.ModeSymlink != 0 {
|
||||
return "/" + string(fileInfo.Data), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (m *MapFS) GetModTime(path string) time.Time {
|
||||
path, _ = strings.CutPrefix(path, "/")
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
canonical := m.getCanonicalPath(path)
|
||||
canonicalString := string(canonical)
|
||||
if fileInfo, ok := m.m[canonicalString]; ok {
|
||||
return fileInfo.ModTime
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (m *MapFS) Entries() iter.Seq2[string, *fstest.MapFile] {
|
||||
return func(yield func(string, *fstest.MapFile) bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
inputKeys := slices.Collect(maps.Keys(m.m))
|
||||
slices.SortFunc(inputKeys, comparePathsByParts)
|
||||
|
||||
for _, p := range inputKeys {
|
||||
file := m.m[p]
|
||||
path := file.Sys.(*sys).realpath
|
||||
if !tspath.PathIsAbsolute(path) {
|
||||
path = "/" + path
|
||||
}
|
||||
if !yield(path, file) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MapFS) GetFileInfo(path string) *fstest.MapFile {
|
||||
path, _ = strings.CutPrefix(path, "/")
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
canonical := m.getCanonicalPath(path)
|
||||
canonicalString := string(canonical)
|
||||
return m.m[canonicalString]
|
||||
}
|
||||
|
||||
func must[T any](v T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
751
tools/tsgo/internal/vfs/vfstest/vfstest_test.go
Normal file
751
tools/tsgo/internal/vfs/vfstest/vfstest_test.go
Normal file
@@ -0,0 +1,751 @@
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io/fs"
|
||||
"math/rand/v2"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/testutil"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestInsensitive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
contents := []byte("bar")
|
||||
|
||||
vfs := convertMapFS(fstest.MapFS{
|
||||
"foo/bar/baz": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"foo/bar2/baz2": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"foo/bar3/baz3": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
}, false /*useCaseSensitiveFileNames*/, nil)
|
||||
|
||||
sensitive, err := fs.ReadFile(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, sensitive, contents)
|
||||
sensitiveInfo, err := fs.Stat(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, sensitiveInfo.Sys(), 1234)
|
||||
sensitiveRealPath, err := vfs.Realpath("foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, sensitiveRealPath, "foo/bar/baz")
|
||||
entries, err := fs.ReadDir(vfs, "foo")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, dirEntriesToNames(entries), []string{"bar", "bar2", "bar3"})
|
||||
|
||||
_, err = vfs.Realpath("does/not/exist")
|
||||
assert.ErrorContains(t, err, "file does not exist")
|
||||
_, err = fs.Stat(vfs, "does/not/exist")
|
||||
assert.ErrorContains(t, err, "file does not exist")
|
||||
|
||||
assert.NilError(t, fstest.TestFS(vfs, "foo/bar/baz"))
|
||||
|
||||
insensitive, err := fs.ReadFile(vfs, "Foo/Bar/Baz")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, insensitive, contents)
|
||||
insensitiveInfo, err := fs.Stat(vfs, "Foo/Bar/Baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, insensitiveInfo.Sys(), 1234)
|
||||
insensitiveRealPath, err := vfs.Realpath("Foo/Bar/Baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, insensitiveRealPath, "foo/bar/baz")
|
||||
entries, err = fs.ReadDir(vfs, "Foo")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, dirEntriesToNames(entries), []string{"bar", "bar2", "bar3"})
|
||||
|
||||
_, err = vfs.Realpath("Does/Not/Exist")
|
||||
assert.ErrorContains(t, err, "file does not exist")
|
||||
_, err = fs.Stat(vfs, "Does/Not/Exist")
|
||||
assert.ErrorContains(t, err, "file does not exist")
|
||||
|
||||
// TODO: TestFS doesn't understand case-insensitive file systems.
|
||||
// This same thing would happen with an os.Dir on Windows.
|
||||
// assert.NilError(t, fstest.TestFS(vfs, "Foo/Bar/Baz"))
|
||||
}
|
||||
|
||||
func TestInsensitiveUpper(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
contents := []byte("bar")
|
||||
|
||||
vfs := convertMapFS(fstest.MapFS{
|
||||
"Foo/Bar/Baz": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"Foo/Bar2/Baz2": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"Foo/Bar3/Baz3": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
}, false /*useCaseSensitiveFileNames*/, nil)
|
||||
|
||||
sensitive, err := fs.ReadFile(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, sensitive, contents)
|
||||
sensitiveInfo, err := fs.Stat(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, sensitiveInfo.Sys(), 1234)
|
||||
entries, err := fs.ReadDir(vfs, "foo")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, dirEntriesToNames(entries), []string{"Bar", "Bar2", "Bar3"})
|
||||
|
||||
// assert.NilError(t, fstest.TestFS(vfs, "foo/bar/baz"))
|
||||
|
||||
insensitive, err := fs.ReadFile(vfs, "Foo/Bar/Baz")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, insensitive, contents)
|
||||
insensitiveInfo, err := fs.Stat(vfs, "Foo/Bar/Baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, insensitiveInfo.Sys(), 1234)
|
||||
entries, err = fs.ReadDir(vfs, "Foo")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, dirEntriesToNames(entries), []string{"Bar", "Bar2", "Bar3"})
|
||||
|
||||
assert.NilError(t, fstest.TestFS(vfs, "Foo/Bar/Baz"))
|
||||
}
|
||||
|
||||
func TestSensitive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
contents := []byte("bar")
|
||||
|
||||
vfs := convertMapFS(fstest.MapFS{
|
||||
"foo/bar/baz": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"foo/bar2/baz2": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"foo/bar3/baz3": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
}, true /*useCaseSensitiveFileNames*/, nil)
|
||||
|
||||
sensitive, err := fs.ReadFile(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, sensitive, contents)
|
||||
sensitiveInfo, err := fs.Stat(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, sensitiveInfo.Sys(), 1234)
|
||||
|
||||
assert.NilError(t, fstest.TestFS(vfs, "foo/bar/baz"))
|
||||
|
||||
_, err = fs.ReadFile(vfs, "Foo/Bar/Baz")
|
||||
assert.ErrorContains(t, err, "file does not exist")
|
||||
}
|
||||
|
||||
func TestSensitiveDuplicatePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testfs := fstest.MapFS{
|
||||
"foo": &fstest.MapFile{
|
||||
Data: []byte("bar"),
|
||||
},
|
||||
"Foo": &fstest.MapFile{
|
||||
Data: []byte("baz"),
|
||||
},
|
||||
}
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
convertMapFS(testfs, false /*useCaseSensitiveFileNames*/, nil)
|
||||
}, `duplicate path: "Foo" and "foo" have the same canonical path`)
|
||||
}
|
||||
|
||||
func TestInsensitiveDuplicatePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testfs := fstest.MapFS{
|
||||
"foo": &fstest.MapFile{
|
||||
Data: []byte("bar"),
|
||||
},
|
||||
"Foo": &fstest.MapFile{
|
||||
Data: []byte("baz"),
|
||||
},
|
||||
}
|
||||
|
||||
convertMapFS(testfs, true /*useCaseSensitiveFileNames*/, nil)
|
||||
}
|
||||
|
||||
func dirEntriesToNames(entries []fs.DirEntry) []string {
|
||||
names := make([]string, len(entries))
|
||||
for i, entry := range entries {
|
||||
names[i] = entry.Name()
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func TestWritableFS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap[any](nil, false)
|
||||
|
||||
err := fs.WriteFile("/foo/bar/baz", "hello, world")
|
||||
assert.NilError(t, err)
|
||||
|
||||
content, ok := fs.ReadFile("/foo/bar/baz")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
err = fs.WriteFile("/foo/bar/baz", "goodbye, world")
|
||||
assert.NilError(t, err)
|
||||
|
||||
content, ok = fs.ReadFile("/foo/bar/baz")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "goodbye, world")
|
||||
|
||||
err = fs.WriteFile("/foo/bar/baz/oops", "goodbye, world")
|
||||
assert.ErrorContains(t, err, `mkdir "foo/bar/baz": path exists but is not a directory`)
|
||||
}
|
||||
|
||||
func TestWritableFSDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := FromMap[any](nil, false)
|
||||
|
||||
_ = fs.WriteFile("/foo/bar/file.ts", "remove")
|
||||
assert.Assert(t, fs.FileExists("/foo/bar/file.ts"))
|
||||
err := fs.Remove("/foo/bar/file.ts")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !fs.FileExists("/foo/bar/file.ts"))
|
||||
|
||||
_ = fs.WriteFile("/foo/bar/test/remove2.ts", "remove2")
|
||||
assert.Assert(t, fs.DirectoryExists("/foo/bar/test"))
|
||||
err = fs.Remove("/foo/bar/test")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !fs.FileExists("/foo/bar/test/remove2.ts"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/foo/bar/test"))
|
||||
|
||||
// no errors when removing file/dir that does not exist
|
||||
err = fs.Remove("/foo/bar/test")
|
||||
assert.NilError(t, err)
|
||||
err = fs.Remove("/foo/bar/file.ts")
|
||||
assert.NilError(t, err)
|
||||
|
||||
_ = fs.WriteFile("/foo/barbar", "remove2")
|
||||
_ = fs.Remove("/foo/bar")
|
||||
assert.Assert(t, fs.FileExists("/foo/barbar"))
|
||||
}
|
||||
|
||||
func TestStress(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap[any](nil, false)
|
||||
|
||||
ops := []func(){
|
||||
func() { _ = fs.WriteFile("/foo/bar/baz.txt", "hello, world") },
|
||||
func() { fs.ReadFile("/foo/bar/baz.txt") },
|
||||
func() { fs.DirectoryExists("/foo/bar") },
|
||||
func() { fs.FileExists("/foo/bar") },
|
||||
func() { fs.FileExists("/foo/bar/baz.txt") },
|
||||
func() { fs.GetAccessibleEntries("/foo/bar") },
|
||||
func() { fs.Realpath("/foo/bar/baz.txt") },
|
||||
func() {
|
||||
_ = fs.WalkDir("/", func(path string, d vfs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.Info()
|
||||
return err
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range runtime.GOMAXPROCS(0) {
|
||||
wg.Go(func() {
|
||||
randomOps := slices.Clone(ops)
|
||||
rand.Shuffle(len(randomOps), func(i, j int) {
|
||||
randomOps[i], randomOps[j] = randomOps[j], randomOps[i]
|
||||
})
|
||||
|
||||
for i := range 10000 {
|
||||
randomOps[i%len(randomOps)]()
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestParentDirFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testfs := fstest.MapFS{
|
||||
"foo": &fstest.MapFile{
|
||||
Data: []byte("bar"),
|
||||
},
|
||||
"foo/oops": &fstest.MapFile{
|
||||
Data: []byte("baz"),
|
||||
},
|
||||
}
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
convertMapFS(testfs, false /*useCaseSensitiveFileNames*/, nil)
|
||||
}, `failed to create intermediate directories for "foo/oops": mkdir "foo": path exists but is not a directory`)
|
||||
}
|
||||
|
||||
func TestFromMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("POSIX", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/string": "hello, world",
|
||||
"/bytes": []byte("hello, world"),
|
||||
"/mapfile": &fstest.MapFile{
|
||||
Data: []byte("hello, world"),
|
||||
},
|
||||
}, false)
|
||||
|
||||
content, ok := fs.ReadFile("/string")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/bytes")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/mapfile")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
})
|
||||
|
||||
t.Run("Windows", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"c:/string": "hello, world",
|
||||
"d:/bytes": []byte("hello, world"),
|
||||
"e:/mapfile": &fstest.MapFile{
|
||||
Data: []byte("hello, world"),
|
||||
},
|
||||
}, false)
|
||||
|
||||
content, ok := fs.ReadFile("c:/string")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("d:/bytes")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("e:/mapfile")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
})
|
||||
|
||||
t.Run("Mixed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
FromMap(map[string]any{
|
||||
"/string": "hello, world",
|
||||
"c:/bytes": []byte("hello, world"),
|
||||
}, false)
|
||||
}, `mixed posix and windows paths`)
|
||||
})
|
||||
|
||||
t.Run("NonRooted", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
FromMap(map[string]any{
|
||||
"string": "hello, world",
|
||||
}, false)
|
||||
}, `non-rooted path "string"`)
|
||||
})
|
||||
|
||||
t.Run("NonNormalized", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
FromMap(map[string]any{
|
||||
"/string/": "hello, world",
|
||||
}, false)
|
||||
}, `non-normalized path "/string/"`)
|
||||
})
|
||||
|
||||
t.Run("NonNormalized2", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
FromMap(map[string]any{
|
||||
"/string/../foo": "hello, world",
|
||||
}, false)
|
||||
}, `non-normalized path "/string/../foo"`)
|
||||
})
|
||||
|
||||
t.Run("InvalidFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
FromMap(map[string]any{
|
||||
"/string": 1234,
|
||||
}, false)
|
||||
}, `invalid file type int`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVFSTestMapFS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]string{
|
||||
"/foo.ts": "hello, world",
|
||||
"/dir1/file1.ts": "export const foo = 42;",
|
||||
"/dir1/file2.ts": "export const foo = 42;",
|
||||
"/dir2/file1.ts": "export const foo = 42;",
|
||||
}, false /*useCaseSensitiveFileNames*/)
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content, ok := fs.ReadFile("/foo.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/does/not/exist.ts")
|
||||
assert.Assert(t, !ok)
|
||||
assert.Equal(t, content, "")
|
||||
})
|
||||
|
||||
t.Run("Realpath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
realpath := fs.Realpath("/foo.ts")
|
||||
assert.Equal(t, realpath, "/foo.ts")
|
||||
|
||||
realpath = fs.Realpath("/Foo.ts")
|
||||
assert.Equal(t, realpath, "/foo.ts")
|
||||
|
||||
realpath = fs.Realpath("/does/not/exist.ts")
|
||||
assert.Equal(t, realpath, "/does/not/exist.ts")
|
||||
})
|
||||
|
||||
t.Run("UseCaseSensitiveFileNames", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Assert(t, !fs.UseCaseSensitiveFileNames())
|
||||
})
|
||||
}
|
||||
|
||||
func TestVFSTestMapFSWindows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]string{
|
||||
"c:/foo.ts": "hello, world",
|
||||
"c:/dir1/file1.ts": "export const foo = 42;",
|
||||
"c:/dir1/file2.ts": "export const foo = 42;",
|
||||
"c:/dir2/file1.ts": "export const foo = 42;",
|
||||
}, false)
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content, ok := fs.ReadFile("c:/foo.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("c:/does/not/exist.ts")
|
||||
assert.Assert(t, !ok)
|
||||
assert.Equal(t, content, "")
|
||||
})
|
||||
|
||||
t.Run("Realpath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
realpath := fs.Realpath("c:/foo.ts")
|
||||
assert.Equal(t, realpath, "c:/foo.ts")
|
||||
|
||||
realpath = fs.Realpath("c:/Foo.ts")
|
||||
assert.Equal(t, realpath, "c:/foo.ts")
|
||||
|
||||
realpath = fs.Realpath("c:/does/not/exist.ts")
|
||||
assert.Equal(t, realpath, "c:/does/not/exist.ts")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBOM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const expected = "hello, world"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
order binary.ByteOrder
|
||||
bom [2]byte
|
||||
}{
|
||||
{"BigEndian", binary.BigEndian, [2]byte{0xFE, 0xFF}},
|
||||
{"LittleEndian", binary.LittleEndian, [2]byte{0xFF, 0xFE}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var codePoints []uint16
|
||||
|
||||
for _, r := range expected {
|
||||
codePoints = utf16.AppendRune(codePoints, r)
|
||||
}
|
||||
|
||||
buf := tt.bom[:]
|
||||
|
||||
for _, r := range codePoints {
|
||||
var err error
|
||||
buf, err = binary.Append(buf, tt.order, r)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
fs := FromMap(map[string][]byte{
|
||||
"/foo.ts": buf,
|
||||
}, true)
|
||||
|
||||
content, ok := fs.ReadFile("/foo.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, expected)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("UTF8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string][]byte{
|
||||
"/foo.ts": []byte("\xEF\xBB\xBF" + expected),
|
||||
}, true)
|
||||
|
||||
content, ok := fs.ReadFile("/foo.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, expected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSymlink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/foo.ts": "hello, world",
|
||||
"/symlink.ts": Symlink("/foo.ts"),
|
||||
"/some/dir/file.ts": "hello, world",
|
||||
"/some/dirlink": Symlink("/some/dir"),
|
||||
"/a": Symlink("/b"),
|
||||
"/b": Symlink("/c"),
|
||||
"/c": Symlink("/d"),
|
||||
"/d/existing.ts": "this is existing.ts",
|
||||
}, false)
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content, ok := fs.ReadFile("/symlink.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/some/dirlink/file.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/a/existing.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "this is existing.ts")
|
||||
})
|
||||
|
||||
t.Run("Realpath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
realpath := fs.Realpath("/symlink.ts")
|
||||
assert.Equal(t, realpath, "/foo.ts")
|
||||
|
||||
realpath = fs.Realpath("/some/dirlink")
|
||||
assert.Equal(t, realpath, "/some/dir")
|
||||
|
||||
realpath = fs.Realpath("/some/dirlink/file.ts")
|
||||
assert.Equal(t, realpath, "/some/dir/file.ts")
|
||||
})
|
||||
|
||||
t.Run("FileExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Assert(t, fs.FileExists("/symlink.ts"))
|
||||
assert.Assert(t, fs.FileExists("/some/dirlink/file.ts"))
|
||||
assert.Assert(t, fs.FileExists("/a/existing.ts"))
|
||||
})
|
||||
|
||||
t.Run("DirectoryExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Assert(t, fs.DirectoryExists("/some/dirlink"))
|
||||
assert.Assert(t, fs.DirectoryExists("/d"))
|
||||
assert.Assert(t, fs.DirectoryExists("/c"))
|
||||
assert.Assert(t, fs.DirectoryExists("/b"))
|
||||
assert.Assert(t, fs.DirectoryExists("/a"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWritableFSSymlink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/some/dir/other.ts": "NOTHING",
|
||||
"/other.ts": Symlink("/some/dir/other.ts"),
|
||||
"/some/dirlink": Symlink("/some/dir"),
|
||||
"/brokenlink": Symlink("/does/not/exist"),
|
||||
"/a": Symlink("/b"),
|
||||
"/b": Symlink("/c"),
|
||||
"/c": Symlink("/d"),
|
||||
"/d/existing.ts": "hello, world",
|
||||
}, false)
|
||||
|
||||
err := fs.WriteFile("/some/dirlink/file.ts", "hello, world")
|
||||
assert.NilError(t, err)
|
||||
|
||||
content, ok := fs.ReadFile("/some/dirlink/file.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/some/dir/file.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
err = fs.WriteFile("/some/dirlink/file.ts", "goodbye, world")
|
||||
assert.NilError(t, err)
|
||||
|
||||
content, ok = fs.ReadFile("/some/dirlink/file.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "goodbye, world")
|
||||
|
||||
err = fs.WriteFile("/other.ts", "hello, world")
|
||||
assert.NilError(t, err)
|
||||
|
||||
content, ok = fs.ReadFile("/other.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/some/dir/other.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
err = fs.WriteFile("/some/dirlink", "hello, world")
|
||||
assert.Error(t, err, `write "some/dirlink": path exists but is not a regular file`)
|
||||
|
||||
// Can't write inside a broken dir symlink
|
||||
err = fs.WriteFile("/brokenlink/file.ts", "hello, world")
|
||||
assert.Error(t, err, `broken symlink "brokenlink" -> "does/not/exist"`)
|
||||
|
||||
err = fs.WriteFile("/brokenlink/also/wrong/file.ts", "hello, world")
|
||||
assert.Error(t, err, `broken symlink "brokenlink" -> "does/not/exist"`)
|
||||
|
||||
// But we can write to a broken file symlink
|
||||
err = fs.WriteFile("/brokenlink", "hello, world")
|
||||
assert.NilError(t, err)
|
||||
content, ok = fs.ReadFile("/brokenlink")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
content, ok = fs.ReadFile("/does/not/exist")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
}
|
||||
|
||||
func TestWritableFSSymlinkChain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/a": Symlink("/b"),
|
||||
"/b": Symlink("/c"),
|
||||
"/c": Symlink("/d"),
|
||||
"/d/existing.ts": "hello, world",
|
||||
}, false)
|
||||
|
||||
err := fs.WriteFile("/a/foo/bar/new.ts", "this is new.ts")
|
||||
assert.NilError(t, err)
|
||||
content, ok := fs.ReadFile("/a/foo/bar/new.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "this is new.ts")
|
||||
content, ok = fs.ReadFile("/b/foo/bar/new.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "this is new.ts")
|
||||
content, ok = fs.ReadFile("/d/foo/bar/new.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "this is new.ts")
|
||||
}
|
||||
|
||||
func TestWritableFSSymlinkChainNotDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/a": Symlink("/b"),
|
||||
"/b": Symlink("/c"),
|
||||
"/c": Symlink("/d"),
|
||||
"/d": "hello, world",
|
||||
}, false)
|
||||
|
||||
err := fs.WriteFile("/a/foo/bar/new.ts", "this is new.ts")
|
||||
assert.Error(t, err, `mkdir "d": path exists but is not a directory`)
|
||||
}
|
||||
|
||||
func TestWritableFSSymlinkDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/some/dir/other.ts": "NOTHING",
|
||||
"/other.ts": Symlink("/some/dir/other.ts"),
|
||||
"/some/dirlink": Symlink("/some/dir"),
|
||||
"/brokenlink": Symlink("/does/not/exist"),
|
||||
"/a": Symlink("/b"),
|
||||
"/b": Symlink("/c"),
|
||||
"/c": Symlink("/d"),
|
||||
"/d/existing.ts": "hello, world",
|
||||
}, false)
|
||||
|
||||
err := fs.Remove("/a")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !fs.DirectoryExists("/a"))
|
||||
assert.Assert(t, fs.DirectoryExists("/b"))
|
||||
assert.Assert(t, fs.DirectoryExists("/c"))
|
||||
assert.Assert(t, fs.FileExists("/d/existing.ts"))
|
||||
|
||||
// symlinks should still exist even if underlying file/dir is deleted
|
||||
err = fs.Remove("/d")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !fs.DirectoryExists("/b"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/c"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/d"))
|
||||
assert.Assert(t, !fs.FileExists("/d/again.ts"))
|
||||
err = fs.WriteFile("/d/again.ts", "d exists again")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, fs.DirectoryExists("/b"))
|
||||
assert.Assert(t, fs.DirectoryExists("/c"))
|
||||
content, _ := fs.ReadFile("/b/again.ts")
|
||||
assert.Equal(t, content, "d exists again")
|
||||
|
||||
assert.Assert(t, !fs.FileExists("/brokenlink"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/brokenlink"))
|
||||
err = fs.Remove("/does/not/exist") // should do nothing
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !fs.FileExists("/brokenlink"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/brokenlink"))
|
||||
err = fs.WriteFile("/does/not/exist", "hello, world")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, fs.FileExists("/brokenlink"))
|
||||
}
|
||||
Reference in New Issue
Block a user