afero icon indicating copy to clipboard operation
afero copied to clipboard

MemMapFs MkdirAll error in parallel tests after go 1.18

Open biinari opened this issue 3 years ago • 1 comments

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.

biinari avatar Nov 17 '22 04:11 biinari


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.)

ljluestc avatar May 25 '25 11:05 ljluestc