refactor(ax): add memory medium aliases
Some checks failed
CI / test (push) Failing after 2s
CI / auto-fix (push) Failing after 1s
CI / auto-merge (push) Failing after 0s

This commit is contained in:
Virgil 2026-03-30 22:00:45 +00:00
parent c0ee58201b
commit 25b12a22a4
7 changed files with 93 additions and 87 deletions

View file

@ -4,31 +4,31 @@ import (
"testing"
)
func BenchmarkMockMedium_Write(b *testing.B) {
m := NewMockMedium()
func BenchmarkMemoryMedium_Write(b *testing.B) {
medium := NewMemoryMedium()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = m.Write("test.txt", "some content")
_ = medium.Write("test.txt", "some content")
}
}
func BenchmarkMockMedium_Read(b *testing.B) {
m := NewMockMedium()
_ = m.Write("test.txt", "some content")
func BenchmarkMemoryMedium_Read(b *testing.B) {
medium := NewMemoryMedium()
_ = medium.Write("test.txt", "some content")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = m.Read("test.txt")
_, _ = medium.Read("test.txt")
}
}
func BenchmarkMockMedium_List(b *testing.B) {
m := NewMockMedium()
_ = m.EnsureDir("dir")
func BenchmarkMemoryMedium_List(b *testing.B) {
medium := NewMemoryMedium()
_ = medium.EnsureDir("dir")
for i := 0; i < 100; i++ {
_ = m.Write("dir/file"+string(rune(i))+".txt", "content")
_ = medium.Write("dir/file"+string(rune(i))+".txt", "content")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = m.List("dir")
_, _ = medium.List("dir")
}
}

View file

@ -9,15 +9,15 @@ import (
"github.com/stretchr/testify/require"
)
// --- MockMedium Tests ---
// --- MemoryMedium Compatibility Tests ---
func TestClient_NewMockMedium_Good(t *testing.T) {
m := NewMockMedium()
assert.NotNil(t, m)
assert.NotNil(t, m.Files)
assert.NotNil(t, m.Dirs)
assert.Empty(t, m.Files)
assert.Empty(t, m.Dirs)
func TestClient_NewMemoryMedium_Good(t *testing.T) {
medium := NewMemoryMedium()
assert.NotNil(t, medium)
assert.NotNil(t, medium.Files)
assert.NotNil(t, medium.Dirs)
assert.Empty(t, medium.Files)
assert.Empty(t, medium.Dirs)
}
func TestClient_MockMedium_Read_Good(t *testing.T) {

View file

@ -37,7 +37,7 @@ var (
type Medium struct {
dataNode *borgdatanode.DataNode
directorySet map[string]bool // explicit directories that exist without file contents
mu sync.RWMutex
lock sync.RWMutex
}
func New() *Medium {
@ -63,8 +63,8 @@ func FromTar(data []byte) (*Medium, error) {
// Example: snapshot, _ := medium.Snapshot()
func (medium *Medium) Snapshot() ([]byte, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
medium.lock.RLock()
defer medium.lock.RUnlock()
data, err := medium.dataNode.ToTar()
if err != nil {
return nil, core.E("datanode.Snapshot", "tar failed", err)
@ -78,8 +78,8 @@ func (medium *Medium) Restore(data []byte) error {
if err != nil {
return core.E("datanode.Restore", "tar failed", err)
}
medium.mu.Lock()
defer medium.mu.Unlock()
medium.lock.Lock()
defer medium.lock.Unlock()
medium.dataNode = dataNode
medium.directorySet = make(map[string]bool)
return nil
@ -87,8 +87,8 @@ func (medium *Medium) Restore(data []byte) error {
// Example: dataNode := medium.DataNode()
func (medium *Medium) DataNode() *borgdatanode.DataNode {
medium.mu.RLock()
defer medium.mu.RUnlock()
medium.lock.RLock()
defer medium.lock.RUnlock()
return medium.dataNode
}
@ -105,8 +105,8 @@ func normaliseEntryPath(filePath string) string {
// --- io.Medium interface ---
func (medium *Medium) Read(filePath string) (string, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
medium.lock.RLock()
defer medium.lock.RUnlock()
filePath = normaliseEntryPath(filePath)
file, err := medium.dataNode.Open(filePath)
@ -131,8 +131,8 @@ func (medium *Medium) Read(filePath string) (string, error) {
}
func (medium *Medium) Write(filePath, content string) error {
medium.mu.Lock()
defer medium.mu.Unlock()
medium.lock.Lock()
defer medium.lock.Unlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
@ -150,8 +150,8 @@ func (medium *Medium) WriteMode(filePath, content string, mode fs.FileMode) erro
}
func (medium *Medium) EnsureDir(filePath string) error {
medium.mu.Lock()
defer medium.mu.Unlock()
medium.lock.Lock()
defer medium.lock.Unlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
@ -162,7 +162,7 @@ func (medium *Medium) EnsureDir(filePath string) error {
}
// ensureDirsLocked marks a directory and all ancestors as existing.
// Caller must hold medium.mu.
// Caller must hold medium.lock.
func (medium *Medium) ensureDirsLocked(directoryPath string) {
for directoryPath != "" && directoryPath != "." {
medium.directorySet[directoryPath] = true
@ -174,8 +174,8 @@ func (medium *Medium) ensureDirsLocked(directoryPath string) {
}
func (medium *Medium) IsFile(filePath string) bool {
medium.mu.RLock()
defer medium.mu.RUnlock()
medium.lock.RLock()
defer medium.lock.RUnlock()
filePath = normaliseEntryPath(filePath)
info, err := medium.dataNode.Stat(filePath)
@ -191,20 +191,17 @@ func (medium *Medium) FileSet(filePath, content string) error {
}
func (medium *Medium) Delete(filePath string) error {
medium.mu.Lock()
defer medium.mu.Unlock()
medium.lock.Lock()
defer medium.lock.Unlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
return core.E("datanode.Delete", "cannot delete root", fs.ErrPermission)
}
// Check if it's a file in the DataNode
info, err := medium.dataNode.Stat(filePath)
if err != nil {
// Check explicit directories
if medium.directorySet[filePath] {
// Check if dir is empty
hasChildren, err := medium.hasPrefixLocked(filePath + "/")
if err != nil {
return core.E("datanode.Delete", core.Concat("failed to inspect directory: ", filePath), err)
@ -238,8 +235,8 @@ func (medium *Medium) Delete(filePath string) error {
}
func (medium *Medium) DeleteAll(filePath string) error {
medium.mu.Lock()
defer medium.mu.Unlock()
medium.lock.Lock()
defer medium.lock.Unlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
@ -249,7 +246,6 @@ func (medium *Medium) DeleteAll(filePath string) error {
prefix := filePath + "/"
found := false
// Check if filePath itself is a file
info, err := medium.dataNode.Stat(filePath)
if err == nil && !info.IsDir() {
if err := medium.removeFileLocked(filePath); err != nil {
@ -287,20 +283,18 @@ func (medium *Medium) DeleteAll(filePath string) error {
}
func (medium *Medium) Rename(oldPath, newPath string) error {
medium.mu.Lock()
defer medium.mu.Unlock()
medium.lock.Lock()
defer medium.lock.Unlock()
oldPath = normaliseEntryPath(oldPath)
newPath = normaliseEntryPath(newPath)
// Check if source is a file
info, err := medium.dataNode.Stat(oldPath)
if err != nil {
return core.E("datanode.Rename", core.Concat("not found: ", oldPath), fs.ErrNotExist)
}
if !info.IsDir() {
// Read old, write new, delete old
data, err := medium.readFileLocked(oldPath)
if err != nil {
return core.E("datanode.Rename", core.Concat("failed to read source file: ", oldPath), err)
@ -313,7 +307,6 @@ func (medium *Medium) Rename(oldPath, newPath string) error {
return nil
}
// Directory rename: move all files under oldPath to newPath
oldPrefix := oldPath + "/"
newPrefix := newPath + "/"
@ -335,7 +328,6 @@ func (medium *Medium) Rename(oldPath, newPath string) error {
}
}
// Move explicit directories
dirsToMove := make(map[string]string)
for directoryPath := range medium.directorySet {
if directoryPath == oldPath || core.HasPrefix(directoryPath, oldPrefix) {
@ -352,14 +344,13 @@ func (medium *Medium) Rename(oldPath, newPath string) error {
}
func (medium *Medium) List(filePath string) ([]fs.DirEntry, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
medium.lock.RLock()
defer medium.lock.RUnlock()
filePath = normaliseEntryPath(filePath)
entries, err := medium.dataNode.ReadDir(filePath)
if err != nil {
// Check explicit directories
if filePath == "" || medium.directorySet[filePath] {
return []fs.DirEntry{}, nil
}
@ -399,8 +390,8 @@ func (medium *Medium) List(filePath string) ([]fs.DirEntry, error) {
}
func (medium *Medium) Stat(filePath string) (fs.FileInfo, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
medium.lock.RLock()
defer medium.lock.RUnlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
@ -419,8 +410,8 @@ func (medium *Medium) Stat(filePath string) (fs.FileInfo, error) {
}
func (medium *Medium) Open(filePath string) (fs.File, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
medium.lock.RLock()
defer medium.lock.RUnlock()
filePath = normaliseEntryPath(filePath)
return medium.dataNode.Open(filePath)
@ -440,25 +431,24 @@ func (medium *Medium) Append(filePath string) (goio.WriteCloser, error) {
return nil, core.E("datanode.Append", "empty path", fs.ErrInvalid)
}
// Read existing content
var existing []byte
medium.mu.RLock()
medium.lock.RLock()
if medium.IsFile(filePath) {
data, err := medium.readFileLocked(filePath)
if err != nil {
medium.mu.RUnlock()
medium.lock.RUnlock()
return nil, core.E("datanode.Append", core.Concat("failed to read existing content: ", filePath), err)
}
existing = data
}
medium.mu.RUnlock()
medium.lock.RUnlock()
return &writeCloser{medium: medium, path: filePath, buf: existing}, nil
}
func (medium *Medium) ReadStream(filePath string) (goio.ReadCloser, error) {
medium.mu.RLock()
defer medium.mu.RUnlock()
medium.lock.RLock()
defer medium.lock.RUnlock()
filePath = normaliseEntryPath(filePath)
file, err := medium.dataNode.Open(filePath)
@ -473,8 +463,8 @@ func (medium *Medium) WriteStream(filePath string) (goio.WriteCloser, error) {
}
func (medium *Medium) Exists(filePath string) bool {
medium.mu.RLock()
defer medium.mu.RUnlock()
medium.lock.RLock()
defer medium.lock.RUnlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
@ -488,8 +478,8 @@ func (medium *Medium) Exists(filePath string) bool {
}
func (medium *Medium) IsDir(filePath string) bool {
medium.mu.RLock()
defer medium.mu.RUnlock()
medium.lock.RLock()
defer medium.lock.RUnlock()
filePath = normaliseEntryPath(filePath)
if filePath == "" {
@ -556,7 +546,7 @@ func (medium *Medium) readFileLocked(filePath string) ([]byte, error) {
// removeFileLocked removes a single file by rebuilding the DataNode.
// This is necessary because Borg's DataNode doesn't expose a Remove method.
// Caller must hold medium.mu write lock.
// Caller must hold medium.lock write lock.
func (medium *Medium) removeFileLocked(target string) error {
entries, err := medium.collectAllLocked()
if err != nil {
@ -591,8 +581,8 @@ func (writer *writeCloser) Write(data []byte) (int, error) {
}
func (writer *writeCloser) Close() error {
writer.medium.mu.Lock()
defer writer.medium.mu.Unlock()
writer.medium.lock.Lock()
defer writer.medium.lock.Unlock()
writer.medium.dataNode.AddData(writer.path, writer.buf)
writer.medium.ensureDirsLocked(path.Dir(writer.path))

22
io.go
View file

@ -165,11 +165,17 @@ type MockMedium struct {
ModTimes map[string]time.Time
}
var _ Medium = (*MockMedium)(nil)
// Example: medium := io.NewMemoryMedium()
// _ = medium.Write("config/app.yaml", "port: 8080")
type MemoryMedium = MockMedium
var _ Medium = (*MemoryMedium)(nil)
// NewMockMedium returns MemoryMedium for compatibility.
//
// Example: medium := io.NewMockMedium()
// _ = medium.Write("config/app.yaml", "port: 8080")
func NewMockMedium() *MockMedium {
func NewMockMedium() *MemoryMedium {
return &MockMedium{
Files: make(map[string]string),
Dirs: make(map[string]bool),
@ -177,6 +183,12 @@ func NewMockMedium() *MockMedium {
}
}
// Example: medium := io.NewMemoryMedium()
// _ = medium.Write("config/app.yaml", "port: 8080")
func NewMemoryMedium() *MemoryMedium {
return NewMockMedium()
}
func (medium *MockMedium) Read(path string) (string, error) {
content, ok := medium.Files[path]
if !ok {
@ -369,6 +381,9 @@ type MockFile struct {
offset int64
}
// MemoryFile is the preferred alias for MockFile.
type MemoryFile = MockFile
func (file *MockFile) Stat() (fs.FileInfo, error) {
return FileInfo{
name: file.name,
@ -396,6 +411,9 @@ type MockWriteCloser struct {
data []byte
}
// MemoryWriteCloser is the preferred alias for MockWriteCloser.
type MemoryWriteCloser = MockWriteCloser
func (writeCloser *MockWriteCloser) Write(data []byte) (int, error) {
writeCloser.data = append(writeCloser.data, data...)
return len(data), nil

View file

@ -209,7 +209,7 @@ func (node *Node) CopyFile(sourcePath, destinationPath string, perm fs.FileMode)
return coreio.Local.WriteMode(destinationPath, string(file.content), perm)
}
// Example: _ = nodeTree.CopyTo(io.NewMockMedium(), "config", "backup/config")
// Example: _ = nodeTree.CopyTo(io.NewMemoryMedium(), "config", "backup/config")
func (node *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) error {
sourcePath = core.TrimPrefix(sourcePath, "/")
info, err := node.Stat(sourcePath)

View file

@ -420,12 +420,12 @@ func TestNode_CopyTo_Good(t *testing.T) {
n.AddData("config/app.yaml", []byte("port: 8080"))
n.AddData("config/env/app.env", []byte("MODE=test"))
fileTarget := coreio.NewMockMedium()
fileTarget := coreio.NewMemoryMedium()
err := n.CopyTo(fileTarget, "config/app.yaml", "backup/app.yaml")
require.NoError(t, err)
assert.Equal(t, "port: 8080", fileTarget.Files["backup/app.yaml"])
dirTarget := coreio.NewMockMedium()
dirTarget := coreio.NewMemoryMedium()
err = n.CopyTo(dirTarget, "config", "backup/config")
require.NoError(t, err)
assert.Equal(t, "port: 8080", dirTarget.Files["backup/config/app.yaml"])
@ -434,7 +434,7 @@ func TestNode_CopyTo_Good(t *testing.T) {
func TestNode_CopyTo_Bad(t *testing.T) {
n := New()
err := n.CopyTo(coreio.NewMockMedium(), "missing", "backup/missing")
err := n.CopyTo(coreio.NewMemoryMedium(), "missing", "backup/missing")
assert.Error(t, err)
}

View file

@ -34,12 +34,11 @@ type Options struct {
// Example: service, _ := workspace.New(workspace.Options{Core: core.New(), Crypt: cryptProvider})
type Service struct {
core *core.Core
crypt CryptProvider
activeWorkspaceID string
rootPath string
medium io.Medium
mu sync.RWMutex
lock sync.RWMutex
}
var _ Workspace = (*Service)(nil)
@ -58,7 +57,6 @@ func New(options Options) (*Service, error) {
}
service := &Service{
core: options.Core,
rootPath: rootPath,
medium: io.Local,
}
@ -76,8 +74,8 @@ func New(options Options) (*Service, error) {
// Example: workspaceID, _ := service.CreateWorkspace("alice", "pass123")
func (service *Service) CreateWorkspace(identifier, password string) (string, error) {
service.mu.Lock()
defer service.mu.Unlock()
service.lock.Lock()
defer service.lock.Unlock()
if service.crypt == nil {
return "", core.E("workspace.CreateWorkspace", "crypt service not available", nil)
@ -114,8 +112,8 @@ func (service *Service) CreateWorkspace(identifier, password string) (string, er
// Example: _ = service.SwitchWorkspace(workspaceID)
func (service *Service) SwitchWorkspace(workspaceID string) error {
service.mu.Lock()
defer service.mu.Unlock()
service.lock.Lock()
defer service.lock.Unlock()
workspaceDirectory, err := service.resolveWorkspaceDirectory("workspace.SwitchWorkspace", workspaceID)
if err != nil {
@ -148,8 +146,8 @@ func (service *Service) resolveActiveWorkspaceFilePath(operation, workspaceFileP
// Example: content, _ := service.WorkspaceFileGet("notes/todo.txt")
func (service *Service) WorkspaceFileGet(workspaceFilePath string) (string, error) {
service.mu.RLock()
defer service.mu.RUnlock()
service.lock.RLock()
defer service.lock.RUnlock()
filePath, err := service.resolveActiveWorkspaceFilePath("workspace.WorkspaceFileGet", workspaceFilePath)
if err != nil {
@ -160,8 +158,8 @@ func (service *Service) WorkspaceFileGet(workspaceFilePath string) (string, erro
// Example: _ = service.WorkspaceFileSet("notes/todo.txt", "ship it")
func (service *Service) WorkspaceFileSet(workspaceFilePath, content string) error {
service.mu.Lock()
defer service.mu.Unlock()
service.lock.Lock()
defer service.lock.Unlock()
filePath, err := service.resolveActiveWorkspaceFilePath("workspace.WorkspaceFileSet", workspaceFilePath)
if err != nil {