MemMapFs MkdirAll error in parallel tests after go 1.18
Creating a MemMapFs in a table driven test running in parallel can cause an internal compiler error.
Minimal reproduction test code:
func TestMkdirAll_fails(t *testing.T) {
var tests = []struct {
fs afero.Fs
}{
{
fs: func() afero.Fs {
fs := afero.NewMemMapFs()
fs.MkdirAll("/home/alice", 0755)
return fs
}(),
},
}
for _, tc := range tests {
tc := tc // capture range variable
t.Run("sub_test", func(t *testing.T) {
_ = tc.fs
})
}
}
Produces the error:
./mkdirall_test.go:16:16: internal compiler error: order.stmt CALLMETH
Please file a bug report including a short program that triggers the error.
https://go.dev/issue/new
Versions
This works without error on golang 1.17.13. It fails for me on go 1.18.8 and go 1.19.3.
I have tried on an older afero 1.2.2 as well as the current afero 1.9.3 with the same results.
Workaround
A workaround is to make sure the fs is created inside the test Run() rather than in the declaration of the test cases. Such as:
func TestMkdirAll_ok(t *testing.T) {
var tests = []struct {
path string
createFs func() afero.Fs
}{
{
createFs: func() afero.Fs {
fs := afero.NewMemMapFs()
fs.MkdirAll("/home/alice", 0755)
return fs
},
},
}
for _, tc := range tests {
tc := tc // capture range variable
t.Run("sub_test", func(t *testing.T) {
_ = tc.createFs()
})
}
}
To be honest, I'm not convinced my original code that was trying to share the fs between different goroutines was sensible. If this is not to be fixed, perhaps it would be worth mentioning in the readme that a MemMapFs cannot be passed between goroutines.
package afero
import (
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
)
// MemFs is an in-memory filesystem.
type MemFs struct {
configDir string
files map[string]*memFileData
mu sync.RWMutex
}
// memFileData holds in-memory file metadata and content.
type memFileData struct {
name string
mode os.FileMode
modTime time.Time
data []byte
isDir bool
symlink string
}
// memFileInfo implements os.FileInfo for memFileData.
type memFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
isDir bool
}
// Name returns the base name of the file.
func (m *memFileInfo) Name() string { return m.name }
// Size returns the file size.
func (m *memFileInfo) Size() int64 { return m.size }
// Mode returns the file mode and permissions.
func (m *memFileInfo) Mode() os.FileMode {
return m.mode
}
// ModTime returns the modification time.
func (m *memFileInfo) ModTime() time.Time { return m.modTime }
// IsDir returns true if the file is a directory.
func (m *memFileInfo) IsDir() bool { return m.isDir }
// Sys returns nil (no underlying system-specific info).
func (m *memFileInfo) Sys() interface{} { return nil }
// NewMemFs creates a new in-memory filesystem.
func NewMemFs() *MemFs {
return &MemFs{
configDir: "/home/user/.config",
files: make(map[string]*memFileData),
}
}
// Create creates a new file with default permissions (0644).
func (m *MemFs) Create(name string) (File, error) {
name = filepath.Clean(name)
m.mu.Lock()
defer m.mu.Unlock()
file := &memFileData{
name: path.Base(name),
mode: 0644, // Default file mode: regular file, rw-r--r--
modTime: time.Now(),
isDir: false,
data: []byte{},
}
m.files[name] = file
return &memFile{name: name, data: file, fs: m}, nil
}
// Mkdir creates a directory with default permissions (0755).
func (m *MemFs) Mkdir(name string, perm os.FileMode) error {
name = filepath.Clean(name)
m.mu.Lock()
defer m.mu.Unlock()
if _, exists := m.files[name]; exists {
return &os.PathError{Op: "mkdir", Path: name, Err: os.ErrExist}
}
file := &memFileData{
name: path.Base(name),
mode: os.ModeDir | (perm & os.ModePerm), // Preserve dir bit
modTime: time.Now(),
isDir: true,
}
m.files[name] = file
return nil
}
// Chmod changes the file's permissions, preserving type bits.
func (m *MemFs) Chmod(name string, mode os.FileMode) error {
name = filepath.Clean(name)
m.mu.Lock()
defer m.mu.Unlock()
file, exists := m.files[name]
if !exists {
return &os.PathError{Op: "chmod", Path: name, Err: os.ErrNotExist}
}
// Preserve type bits (e.g., os.ModeDir, os.ModeSymlink) and update permissions
typeBits := file.mode &^ os.ModePerm // Keep type bits (e.g., dir, symlink)
file.mode = typeBits | (mode & os.ModePerm) // Apply new permissions
return nil
}
// Stat returns file information.
func (m *MemFs) Stat(name string) (os.FileInfo, error) {
name = filepath.Clean(name)
m.mu.RLock()
defer m.mu.RUnlock()
file, exists := m.files[name]
if !exists {
return nil, &os.PathError{Op: "stat", Path: name, Err: os.ErrNotExist}
}
return &memFileInfo{
name: file.name,
size: int64(len(file.data)),
mode: file.mode,
modTime: file.modTime,
isDir: file.isDir,
}, nil
}
// Lstat returns file information, handling symlinks (same as Stat for MemFs).
func (m *MemFs) Lstat(name string) (os.FileInfo, error) {
return m.Stat(name) // MemFs treats symlinks as regular files for simplicity
}
// ... (other MemFs methods unchanged, e.g., Open, Remove, etc.)