Integrate real filesystem with interface abstraction

- Define pool.FileSystem interface in internal/io/pool/filesystem.go
- Move DirEntry interface to internal/io/pool/types/types.go to break
  the pool <-> mock import cycle
- Implement real.FileSystem with atomic writes (temp file + os.Rename)
- Update mock.FileSystem to satisfy pool.FileSystem interface
- Update worker pool tasks to accept pool.FileSystem interface
- Update main.go to use RealFileSystem by default, with -root flag
- Update all browser/editor/test references to types.DirEntry
This commit is contained in:
Greg Pomerantz 2026-06-04 16:19:38 -04:00
parent 672dcbe6b3
commit d73cb0be2c
16 changed files with 217 additions and 78 deletions

View File

@ -1,8 +1,10 @@
package main package main
import ( import (
"flag"
"log" "log"
"os" "os"
"path/filepath"
"sync" "sync"
"gioui.org/app" "gioui.org/app"
@ -13,6 +15,7 @@ import (
"gioui.org/io/key" "gioui.org/io/key"
"pad/internal/editor" "pad/internal/editor"
"pad/internal/io/pool/real"
"pad/internal/ui" "pad/internal/ui"
) )
@ -33,7 +36,19 @@ func run(w *app.Window) error {
log.Printf("run: starting") log.Printf("run: starting")
var ops op.Ops var ops op.Ops
shaper := text.NewShaper(text.WithCollection(gofont.Collection())) shaper := text.NewShaper(text.WithCollection(gofont.Collection()))
logic := editor.NewLogic(nil)
// Determine root directory for real filesystem
rootDir := flag.String("root", ".", "root directory for the filesystem")
flag.Parse()
abs, err := filepath.Abs(*rootDir)
if err != nil {
log.Fatalf("invalid root directory: %v", err)
}
fs := &real.RealFileSystem{Root: abs}
log.Printf("using filesystem at %s", abs)
logic := editor.NewLogic(fs)
renderer := ui.New(ui.Theme{FontSize: 14}, shaper, logic.State()) renderer := ui.New(ui.Theme{FontSize: 14}, shaper, logic.State())
var mu sync.Mutex var mu sync.Mutex
var elems []ui.Element var elems []ui.Element

View File

@ -224,6 +224,24 @@ Key design points:
- [ ] Undo/Redo stack implementation. — **NOT STARTED** (`UndoStack` field is commented out in `EditorState`) - [ ] Undo/Redo stack implementation. — **NOT STARTED** (`UndoStack` field is commented out in `EditorState`)
- [ ] Performance testing with large files (>1MB). — **NOT STARTED** - [ ] Performance testing with large files (>1MB). — **NOT STARTED**
### 4.5 Real Filesystem Integration
To support real filesystem operations with minimal changes, we are abstracting the filesystem behind an interface.
### 4.5.1 Design
- **Interface Definition**: Define a `FileSystem` interface in `internal/io/pool/` that captures the required operations (`ReadDir`, `ReadFile`, `WriteFile`, `DirExists`, etc.).
- **Mock Update**: Update the existing `internal/io/pool/mock` to satisfy this interface.
- **Real Implementation**: Create `internal/io/pool/real` that implements the `FileSystem` interface using standard `os` and `path/filepath` packages.
- **Dependency Injection**: Update worker pool tasks to accept the `FileSystem` interface instead of the concrete `*mock.FileSystem` type.
- **Atomic Writes**: The `RealFileSystem` must also implement atomic writes via temporary files and `os.Rename` to maintain the consistency guarantees of the mock.
### 4.5.2 Implementation Roadmap
1. Define `pool.FileSystem` interface.
2. Update `mock.FileSystem` to satisfy the interface.
3. Update `pool` tasks to accept the interface.
4. Implement `real.FileSystem`.
5. Update `main.go` to inject the correct implementation based on build/runtime flags.
--- ---
## 5. Interaction Flow (Example: Keyboard/Mouse Input) ## 5. Interaction Flow (Example: Keyboard/Mouse Input)

View File

@ -17,17 +17,17 @@ func TestLazyLoadingLargeDirectory(t *testing.T) {
state.TotalEntries = 10000 state.TotalEntries = 10000
state.EntryHeight = 48.0 state.EntryHeight = 48.0
state.VisibleCount = 10 state.VisibleCount = 10
fs := mock.NewFileSystem() mfs := mock.NewFileSystem()
wp := pool.NewWorkerPool(4) wp := pool.NewWorkerPool(4)
wp.Start() wp.Start()
// Populate mock filesystem with 10k entries // Populate mock filesystem with 10k entries
fs.AddDir("/dir", time.Now()) mfs.AddDir("/dir", time.Now())
for i := 0; i < 10000; i++ { for i := 0; i < 10000; i++ {
fs.AddFile(fmt.Sprintf("/dir/file_%d.txt", i), []byte("data"), time.Now()) mfs.AddFile(fmt.Sprintf("/dir/file_%d.txt", i), []byte("data"), time.Now())
} }
bm, err := NewBrowserManager(state, wp, fs) bm, err := NewBrowserManager(state, wp, mfs)
if err != nil { if err != nil {
t.Fatalf("NewBrowserManager failed: %v", err) t.Fatalf("NewBrowserManager failed: %v", err)
} }
@ -88,17 +88,17 @@ func TestPrefetchOnScroll(t *testing.T) {
state.TotalEntries = 1000 state.TotalEntries = 1000
state.EntryHeight = 48.0 state.EntryHeight = 48.0
state.VisibleCount = 10 state.VisibleCount = 10
fs := mock.NewFileSystem() mfs := mock.NewFileSystem()
wp := pool.NewWorkerPool(4) wp := pool.NewWorkerPool(4)
wp.Start() wp.Start()
// Populate mock filesystem // Populate mock filesystem
fs.AddDir("/dir", time.Now()) mfs.AddDir("/dir", time.Now())
for i := 0; i < 1000; i++ { for i := 0; i < 1000; i++ {
fs.AddFile(fmt.Sprintf("/dir/file_%d.txt", i), []byte("data"), time.Now()) mfs.AddFile(fmt.Sprintf("/dir/file_%d.txt", i), []byte("data"), time.Now())
} }
bm, err := NewBrowserManager(state, wp, fs) bm, err := NewBrowserManager(state, wp, mfs)
if err != nil { if err != nil {
t.Fatalf("NewBrowserManager failed: %v", err) t.Fatalf("NewBrowserManager failed: %v", err)
} }

View File

@ -6,7 +6,7 @@ import (
"time" "time"
"pad/internal/io/pool" "pad/internal/io/pool"
"pad/internal/io/pool/mock" "pad/internal/io/pool/types"
) )
var ErrDirNotFound = fmt.Errorf("directory not found") var ErrDirNotFound = fmt.Errorf("directory not found")
@ -14,10 +14,10 @@ var ErrDirNotFound = fmt.Errorf("directory not found")
type BrowserManager struct { type BrowserManager struct {
state *BrowserState state *BrowserState
workerPool *pool.WorkerPool workerPool *pool.WorkerPool
fs *mock.FileSystem fs pool.FileSystem
} }
func NewBrowserManager(state *BrowserState, wp *pool.WorkerPool, fs *mock.FileSystem) (*BrowserManager, error) { func NewBrowserManager(state *BrowserState, wp *pool.WorkerPool, fs pool.FileSystem) (*BrowserManager, error) {
return &BrowserManager{ return &BrowserManager{
state: state, state: state,
workerPool: wp, workerPool: wp,
@ -86,12 +86,12 @@ func (bm *BrowserManager) HandleResult(result pool.Result) {
} }
func (bm *BrowserManager) handleBuildIndexSuccess(result pool.Result) { func (bm *BrowserManager) handleBuildIndexSuccess(result pool.Result) {
fmt.Printf("handleBuildIndexSuccess: TotalEntries=%d\n", len(result.Data.([]mock.DirEntry))) fmt.Printf("handleBuildIndexSuccess: TotalEntries=%d\n", len(result.Data.([]types.DirEntry)))
bm.state.Loading = false bm.state.Loading = false
// In the mock implementation, BuildIndexTask returns []mock.DirEntry. // In the mock implementation, BuildIndexTask returns []types.DirEntry.
// We need to convert these to browser.Entry and build the SortIndex. // We need to convert these to browser.Entry and build the SortIndex.
entries := result.Data.([]mock.DirEntry) entries := result.Data.([]types.DirEntry)
var browserEntries []Entry var browserEntries []Entry
// Add ".." entry if not at root // Add ".." entry if not at root

View File

@ -6,6 +6,7 @@ import (
"pad/internal/io/pool" "pad/internal/io/pool"
"pad/internal/io/pool/mock" "pad/internal/io/pool/mock"
"pad/internal/io/pool/types"
) )
// TestNewBrowserManager verifies that a BrowserManager can be constructed // TestNewBrowserManager verifies that a BrowserManager can be constructed
@ -109,7 +110,7 @@ func TestHandleResult_BuildIndexSuccess(t *testing.T) {
result := pool.Result{ result := pool.Result{
TaskType: pool.TypeBuildIndex, TaskType: pool.TypeBuildIndex,
Success: true, Success: true,
Data: []mock.DirEntry{}, Data: []types.DirEntry{},
} }
bm.HandleResult(result) bm.HandleResult(result)

View File

@ -12,9 +12,9 @@ import (
// BrowserState.CurrentPath. // BrowserState.CurrentPath.
func TestTapDirectory(t *testing.T) { func TestTapDirectory(t *testing.T) {
state := NewBrowserState() state := NewBrowserState()
fs := mock.NewFileSystem() mfs := mock.NewFileSystem()
wp := pool.NewWorkerPool(1) wp := pool.NewWorkerPool(1)
bm, _ := NewBrowserManager(state, wp, fs) bm, _ := NewBrowserManager(state, wp, mfs)
state.CurrentPath = "/root" state.CurrentPath = "/root"
// Mock entries: a subdirectory "sub" and a file // Mock entries: a subdirectory "sub" and a file
@ -38,9 +38,9 @@ func TestTapDirectory(t *testing.T) {
// BrowserState.CurrentPath to the parent directory. // BrowserState.CurrentPath to the parent directory.
func TestTapUp(t *testing.T) { func TestTapUp(t *testing.T) {
state := NewBrowserState() state := NewBrowserState()
fs := mock.NewFileSystem() mfs := mock.NewFileSystem()
wp := pool.NewWorkerPool(1) wp := pool.NewWorkerPool(1)
bm, _ := NewBrowserManager(state, wp, fs) bm, _ := NewBrowserManager(state, wp, mfs)
state.CurrentPath = "/root/sub" state.CurrentPath = "/root/sub"
// Mock entries: ".." and a file // Mock entries: ".." and a file
@ -64,9 +64,9 @@ func TestTapUp(t *testing.T) {
// in the full directory listing. // in the full directory listing.
func TestTapWithSearchResults(t *testing.T) { func TestTapWithSearchResults(t *testing.T) {
state := NewBrowserState() state := NewBrowserState()
fs := mock.NewFileSystem() mfs := mock.NewFileSystem()
wp := pool.NewWorkerPool(1) wp := pool.NewWorkerPool(1)
bm, _ := NewBrowserManager(state, wp, fs) bm, _ := NewBrowserManager(state, wp, mfs)
state.CurrentPath = "/root" state.CurrentPath = "/root"
// Mock entries: 5 files, sorted by name // Mock entries: 5 files, sorted by name
@ -98,9 +98,9 @@ func TestTapWithSearchResults(t *testing.T) {
// search results resolves to the correct entries. // search results resolves to the correct entries.
func TestTapWithMultipleSearchResults(t *testing.T) { func TestTapWithMultipleSearchResults(t *testing.T) {
state := NewBrowserState() state := NewBrowserState()
fs := mock.NewFileSystem() mfs := mock.NewFileSystem()
wp := pool.NewWorkerPool(1) wp := pool.NewWorkerPool(1)
bm, _ := NewBrowserManager(state, wp, fs) bm, _ := NewBrowserManager(state, wp, mfs)
state.CurrentPath = "/root" state.CurrentPath = "/root"
// Mock entries: 5 files // Mock entries: 5 files

View File

@ -56,7 +56,7 @@ type Logic struct {
openFileChan chan string openFileChan chan string
retryChan chan string // Added for auto-save retries retryChan chan string // Added for auto-save retries
workerPool *pool.WorkerPool workerPool *pool.WorkerPool
mockFS *mock.FileSystem mockFS pool.FileSystem
mu sync.Mutex mu sync.Mutex
done chan struct{} done chan struct{}
saveTimer *time.Timer // Added for auto-save debounce saveTimer *time.Timer // Added for auto-save debounce
@ -64,7 +64,7 @@ type Logic struct {
} }
// NewLogic creates a new Logic instance, accepting an optional mockFS. // NewLogic creates a new Logic instance, accepting an optional mockFS.
func NewLogic(mfs *mock.FileSystem) *Logic { func NewLogic(mfs pool.FileSystem) *Logic {
state := NewState() state := NewState()
TheState = state TheState = state

View File

@ -6,17 +6,23 @@ import (
"time" "time"
"pad/internal/browser" "pad/internal/browser"
"pad/internal/io/pool"
"pad/internal/io/pool/mock" "pad/internal/io/pool/mock"
"pad/internal/io/pool/types"
) )
// populateMockFileSystem seeds the mock filesystem with sample data for testing. // populateMockFileSystem seeds the mock filesystem with sample data for testing.
// Creates a realistic directory structure with subdirectories, various file types, // Creates a realistic directory structure with subdirectories, various file types,
// and varied sizes/dates to exercise the browser functionality. // and varied sizes/dates to exercise the browser functionality.
func populateMockFileSystem(fs *mock.FileSystem) { func populateMockFileSystem(fs pool.FileSystem) {
mfs, ok := fs.(*mock.FileSystem)
if !ok {
panic("populateMockFileSystem requires a *mock.FileSystem")
}
baseTime := time.Date(2026, 5, 15, 10, 0, 0, 0, time.UTC) baseTime := time.Date(2026, 5, 15, 10, 0, 0, 0, time.UTC)
// Create root directory - required for ReadDir("/") to succeed // Create root directory - required for ReadDir("/") to succeed
fs.AddDir("/", baseTime) mfs.AddDir("/", baseTime)
// --- Root directories --- // --- Root directories ---
dirs := []string{ dirs := []string{
@ -32,7 +38,7 @@ func populateMockFileSystem(fs *mock.FileSystem) {
"/Pictures/Vacation", "/Pictures/Vacation",
} }
for i, d := range dirs { for i, d := range dirs {
fs.AddDir(d, baseTime.AddDate(0, 0, -i)) mfs.AddDir(d, baseTime.AddDate(0, 0, -i))
} }
// --- Root files --- // --- Root files ---
@ -48,7 +54,7 @@ func populateMockFileSystem(fs *mock.FileSystem) {
{".gitignore", "*.log\n.pad/\n.git/\n*.tmp\n", -90}, {".gitignore", "*.log\n.pad/\n.git/\n*.tmp\n", -90},
} }
for _, f := range rootFiles { for _, f := range rootFiles {
fs.AddFile("/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days)) mfs.AddFile("/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
} }
// --- Documents/Work files --- // --- Documents/Work files ---
@ -64,7 +70,7 @@ func populateMockFileSystem(fs *mock.FileSystem) {
{"Presentation.pptx", "Pad Presentation Mock\n\nSlide 1: Title - No Practical Limits\nSlide 2: Virtualization is the key\nSlide 3: Single-Owner thread design\n", -21}, {"Presentation.pptx", "Pad Presentation Mock\n\nSlide 1: Title - No Practical Limits\nSlide 2: Virtualization is the key\nSlide 3: Single-Owner thread design\n", -21},
} }
for _, f := range workFiles { for _, f := range workFiles {
fs.AddFile("/Documents/Work/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days)) mfs.AddFile("/Documents/Work/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
} }
// --- Documents/Personal files --- // --- Documents/Personal files ---
@ -79,7 +85,7 @@ func populateMockFileSystem(fs *mock.FileSystem) {
{"Letter_to_Friend.txt", "Hey friend,\n\nI've been working on a really cool Go text editor called Pad. It uses Gio for rendering and handles massive files without blocking the GUI thread. Talk soon!\n", -5}, {"Letter_to_Friend.txt", "Hey friend,\n\nI've been working on a really cool Go text editor called Pad. It uses Gio for rendering and handles massive files without blocking the GUI thread. Talk soon!\n", -5},
} }
for _, f := range personalFiles { for _, f := range personalFiles {
fs.AddFile("/Documents/Personal/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days)) mfs.AddFile("/Documents/Personal/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
} }
// --- Projects/pad source files --- // --- Projects/pad source files ---
@ -100,7 +106,7 @@ func populateMockFileSystem(fs *mock.FileSystem) {
{"go.sum", "gioui.org v0.9.0 h1:...\n", -30}, {"go.sum", "gioui.org v0.9.0 h1:...\n", -30},
} }
for _, f := range padFiles { for _, f := range padFiles {
fs.AddFile("/Projects/pad/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days)) mfs.AddFile("/Projects/pad/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
} }
// --- Pictures/Vacation files --- // --- Pictures/Vacation files ---
@ -116,7 +122,7 @@ func populateMockFileSystem(fs *mock.FileSystem) {
{"IMG_0005.jpg", "[Binary JPEG Image Data Mock]", -178}, {"IMG_0005.jpg", "[Binary JPEG Image Data Mock]", -178},
} }
for _, f := range vacationFiles { for _, f := range vacationFiles {
fs.AddFile("/Pictures/Vacation/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days)) mfs.AddFile("/Pictures/Vacation/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
} }
// --- Downloads files --- // --- Downloads files ---
@ -130,7 +136,7 @@ func populateMockFileSystem(fs *mock.FileSystem) {
{"patch.diff", "--- a/internal/browser/handlers.go\n+++ b/internal/browser/handlers.go\n@@ -65,3 +65,3 @@\n-bm.NavigateTo(newPath)\n+bm.NavigateTo(entry.Path)\n", -3}, {"patch.diff", "--- a/internal/browser/handlers.go\n+++ b/internal/browser/handlers.go\n@@ -65,3 +65,3 @@\n-bm.NavigateTo(newPath)\n+bm.NavigateTo(entry.Path)\n", -3},
} }
for _, f := range downloads { for _, f := range downloads {
fs.AddFile("/Downloads/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days)) mfs.AddFile("/Downloads/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
} }
} }
@ -155,9 +161,9 @@ func modeCount() int {
return 4 return 4
} }
// buildBrowserIndex converts mock DirEntry results into a browser.DirectoryIndex // buildBrowserIndex converts types.DirEntry results into a browser.DirectoryIndex
// with pre-computed position maps for all sort modes. // with pre-computed position maps for all sort modes.
func buildBrowserIndex(entries []mock.DirEntry) *browser.DirectoryIndex { func buildBrowserIndex(entries []types.DirEntry) *browser.DirectoryIndex {
var browserEntries []browser.Entry var browserEntries []browser.Entry
for _, e := range entries { for _, e := range entries {
info, err := e.Info() info, err := e.Info()

View File

@ -234,6 +234,7 @@ func TestWorkerPool_ContextPropagation(t *testing.T) {
} }
} }
// TestWorkerPool_DirPathInResult verifies that the task's DirPath is correctly reflected in the result.
func TestWorkerPool_DirPathInResult(t *testing.T) { func TestWorkerPool_DirPathInResult(t *testing.T) {
fs := mock.NewFileSystem() fs := mock.NewFileSystem()
fs.CreateDir("/test/dir1") fs.CreateDir("/test/dir1")

View File

@ -0,0 +1,15 @@
package pool
import "pad/internal/io/pool/types"
// FileSystem defines the interface for filesystem operations
// used by the worker pool.
type FileSystem interface {
ReadDir(path string) ([]types.DirEntry, error)
DirExists(path string) bool
FileExists(path string) bool
ReadFile(path string) ([]byte, error)
WriteFile(path string, content []byte) error
DeleteFile(path string) error
CreateDir(path string) error
}

View File

@ -9,17 +9,9 @@ import (
"path/filepath" "path/filepath"
"sync" "sync"
"time" "time"
)
// File represents a file or directory in the mock filesystem. "pad/internal/io/pool/types"
type File struct { )
Path string
Name string
Content []byte
ModTime time.Time
Size int64
IsDir bool
}
// DirEntry mimics fs.DirEntry behavior for the mock. // DirEntry mimics fs.DirEntry behavior for the mock.
type DirEntry struct { type DirEntry struct {
@ -35,6 +27,16 @@ func (d DirEntry) Info() (os.FileInfo, error) {
return &mockFileInfo{name: d.name, isDir: d.isDir, modTime: d.modTime, size: d.size}, nil return &mockFileInfo{name: d.name, isDir: d.isDir, modTime: d.modTime, size: d.size}, nil
} }
// File represents a file or directory in the mock filesystem.
type File struct {
Path string
Name string
Content []byte
ModTime time.Time
Size int64
IsDir bool
}
// mockFileInfo implements os.FileInfo for the mock. // mockFileInfo implements os.FileInfo for the mock.
type mockFileInfo struct { type mockFileInfo struct {
name string name string
@ -273,7 +275,7 @@ func (fs *FileSystem) CreateDir(path string) error {
} }
// ReadDir returns a list of entries in the given directory. // ReadDir returns a list of entries in the given directory.
func (fs *FileSystem) ReadDir(path string) ([]DirEntry, error) { func (fs *FileSystem) ReadDir(path string) ([]types.DirEntry, error) {
fs.mu.Lock() fs.mu.Lock()
if fs.delay > 0 { if fs.delay > 0 {
@ -288,7 +290,7 @@ func (fs *FileSystem) ReadDir(path string) ([]DirEntry, error) {
return nil, fmt.Errorf("directory not found: %s", path) return nil, fmt.Errorf("directory not found: %s", path)
} }
var entries []DirEntry var entries []types.DirEntry
for _, f := range fs.files { for _, f := range fs.files {
if f.Path == dirPath { if f.Path == dirPath {
@ -308,7 +310,7 @@ func (fs *FileSystem) ReadDir(path string) ([]DirEntry, error) {
// Sort by name // Sort by name
for i := 0; i < len(entries); i++ { for i := 0; i < len(entries); i++ {
for j := i + 1; j < len(entries); j++ { for j := i + 1; j < len(entries); j++ {
if entries[i].name > entries[j].name { if entries[i].Name() > entries[j].Name() {
entries[i], entries[j] = entries[j], entries[i] entries[i], entries[j] = entries[j], entries[i]
} }
} }

View File

@ -0,0 +1,71 @@
package real
import (
"os"
"path/filepath"
"pad/internal/io/pool/types"
)
// RealFileSystem implements the pool.FileSystem interface using the real OS filesystem.
type RealFileSystem struct {
Root string
}
func (fs *RealFileSystem) ReadDir(path string) ([]types.DirEntry, error) {
entries, err := os.ReadDir(filepath.Join(fs.Root, path))
if err != nil {
return nil, err
}
var res []types.DirEntry
for _, e := range entries {
res = append(res, &realDirEntry{e})
}
return res, nil
}
func (fs *RealFileSystem) DirExists(path string) bool {
info, err := os.Stat(filepath.Join(fs.Root, path))
return err == nil && info.IsDir()
}
func (fs *RealFileSystem) FileExists(path string) bool {
info, err := os.Stat(filepath.Join(fs.Root, path))
return err == nil && !info.IsDir()
}
func (fs *RealFileSystem) ReadFile(path string) ([]byte, error) {
return os.ReadFile(filepath.Join(fs.Root, path))
}
func (fs *RealFileSystem) WriteFile(path string, content []byte) error {
// Atomic write implementation
fullPath := filepath.Join(fs.Root, path)
tmpPath := filepath.Join(fs.Root, ".tmp", filepath.Base(path))
if err := os.MkdirAll(filepath.Dir(tmpPath), 0755); err != nil {
return err
}
if err := os.WriteFile(tmpPath, content, 0644); err != nil {
return err
}
return os.Rename(tmpPath, fullPath)
}
func (fs *RealFileSystem) DeleteFile(path string) error {
return os.Remove(filepath.Join(fs.Root, path))
}
func (fs *RealFileSystem) CreateDir(path string) error {
return os.MkdirAll(filepath.Join(fs.Root, path), 0755)
}
// realDirEntry wraps os.DirEntry
type realDirEntry struct {
entry os.DirEntry
}
func (e *realDirEntry) Name() string { return e.entry.Name() }
func (e *realDirEntry) IsDir() bool { return e.entry.IsDir() }
func (e *realDirEntry) Info() (os.FileInfo, error) { return e.entry.Info() }

View File

@ -9,8 +9,6 @@ import (
"path/filepath" "path/filepath"
"sync/atomic" "sync/atomic"
"time" "time"
"pad/internal/io/pool/mock"
) )
// taskCounter generates unique task IDs. // taskCounter generates unique task IDs.
@ -95,10 +93,10 @@ type Task interface {
type ReadDirTask struct { type ReadDirTask struct {
taskID string taskID string
Dir string Dir string
FS *mock.FileSystem FS FileSystem
} }
func NewReadDirTask(dir string, fs *mock.FileSystem) *ReadDirTask { func NewReadDirTask(dir string, fs FileSystem) *ReadDirTask {
return &ReadDirTask{ return &ReadDirTask{
taskID: fmt.Sprintf("read_dir_%s_%d", filepath.Base(dir), taskCounter.Add(1)), taskID: fmt.Sprintf("read_dir_%s_%d", filepath.Base(dir), taskCounter.Add(1)),
Dir: dir, Dir: dir,
@ -136,10 +134,10 @@ func (t *ReadDirTask) Timeout() time.Duration { return 5 * time.Second }
type BuildIndexTask struct { type BuildIndexTask struct {
taskID string taskID string
Dir string Dir string
FS *mock.FileSystem FS FileSystem
} }
func NewBuildIndexTask(dir string, fs *mock.FileSystem) *BuildIndexTask { func NewBuildIndexTask(dir string, fs FileSystem) *BuildIndexTask {
return &BuildIndexTask{ return &BuildIndexTask{
taskID: fmt.Sprintf("build_index_%s_%d", filepath.Base(dir), taskCounter.Add(1)), taskID: fmt.Sprintf("build_index_%s_%d", filepath.Base(dir), taskCounter.Add(1)),
Dir: dir, Dir: dir,
@ -180,10 +178,10 @@ func (t *BuildIndexTask) Timeout() time.Duration { return 5 * time.Second }
type LoadIndexTask struct { type LoadIndexTask struct {
taskID string taskID string
Dir string Dir string
FS *mock.FileSystem FS FileSystem
} }
func NewLoadIndexTask(dir string, fs *mock.FileSystem) *LoadIndexTask { func NewLoadIndexTask(dir string, fs FileSystem) *LoadIndexTask {
return &LoadIndexTask{ return &LoadIndexTask{
taskID: fmt.Sprintf("load_index_%s_%d", filepath.Base(dir), taskCounter.Add(1)), taskID: fmt.Sprintf("load_index_%s_%d", filepath.Base(dir), taskCounter.Add(1)),
Dir: dir, Dir: dir,
@ -215,10 +213,10 @@ type LoadPagesTask struct {
taskID string taskID string
Dir string Dir string
PageIndices []int PageIndices []int
FS *mock.FileSystem FS FileSystem
} }
func NewLoadPagesTask(dir string, pageIndices []int, fs *mock.FileSystem) *LoadPagesTask { func NewLoadPagesTask(dir string, pageIndices []int, fs FileSystem) *LoadPagesTask {
return &LoadPagesTask{ return &LoadPagesTask{
taskID: fmt.Sprintf("load_pages_%s_%d", filepath.Base(dir), taskCounter.Add(1)), taskID: fmt.Sprintf("load_pages_%s_%d", filepath.Base(dir), taskCounter.Add(1)),
Dir: dir, Dir: dir,
@ -249,10 +247,10 @@ func (t *LoadPagesTask) Timeout() time.Duration { return 5 * time.Second }
type StatDirTask struct { type StatDirTask struct {
taskID string taskID string
Dir string Dir string
FS *mock.FileSystem FS FileSystem
} }
func NewStatDirTask(dir string, fs *mock.FileSystem) *StatDirTask { func NewStatDirTask(dir string, fs FileSystem) *StatDirTask {
return &StatDirTask{ return &StatDirTask{
taskID: fmt.Sprintf("stat_dir_%s_%d", filepath.Base(dir), taskCounter.Add(1)), taskID: fmt.Sprintf("stat_dir_%s_%d", filepath.Base(dir), taskCounter.Add(1)),
Dir: dir, Dir: dir,
@ -284,10 +282,10 @@ func (t *StatDirTask) Timeout() time.Duration { return 30 * time.Second }
type ReadFileTask struct { type ReadFileTask struct {
taskID string taskID string
Path string Path string
FS *mock.FileSystem FS FileSystem
} }
func NewReadFileTask(path string, fs *mock.FileSystem) *ReadFileTask { func NewReadFileTask(path string, fs FileSystem) *ReadFileTask {
return &ReadFileTask{ return &ReadFileTask{
taskID: fmt.Sprintf("read_file_%s_%d", filepath.Base(path), taskCounter.Add(1)), taskID: fmt.Sprintf("read_file_%s_%d", filepath.Base(path), taskCounter.Add(1)),
Path: path, Path: path,
@ -325,11 +323,11 @@ func (t *ReadFileTask) Timeout() time.Duration { return 5 * time.Second }
type WriteFileTask struct { type WriteFileTask struct {
taskID string taskID string
Path string Path string
FS *mock.FileSystem FS FileSystem
Data []byte Data []byte
} }
func NewWriteFileTask(path string, data []byte, fs *mock.FileSystem) *WriteFileTask { func NewWriteFileTask(path string, data []byte, fs FileSystem) *WriteFileTask {
return &WriteFileTask{ return &WriteFileTask{
taskID: fmt.Sprintf("write_file_%s_%d", filepath.Base(path), taskCounter.Add(1)), taskID: fmt.Sprintf("write_file_%s_%d", filepath.Base(path), taskCounter.Add(1)),
Path: path, Path: path,
@ -371,11 +369,11 @@ func (t *WriteFileTask) Timeout() time.Duration { return 30 * time.Second }
type WriteCacheTask struct { type WriteCacheTask struct {
taskID string taskID string
Path string Path string
FS *mock.FileSystem FS FileSystem
Data []byte Data []byte
} }
func NewWriteCacheTask(path string, data []byte, fs *mock.FileSystem) *WriteCacheTask { func NewWriteCacheTask(path string, data []byte, fs FileSystem) *WriteCacheTask {
return &WriteCacheTask{ return &WriteCacheTask{
taskID: fmt.Sprintf("write_cache_%s_%d", filepath.Base(path), taskCounter.Add(1)), taskID: fmt.Sprintf("write_cache_%s_%d", filepath.Base(path), taskCounter.Add(1)),
Path: path, Path: path,
@ -415,11 +413,11 @@ func (t *WriteCacheTask) Timeout() time.Duration { return 30 * time.Second }
type SaveStateTask struct { type SaveStateTask struct {
taskID string taskID string
Path string Path string
FS *mock.FileSystem FS FileSystem
Data []byte Data []byte
} }
func NewSaveStateTask(path string, data []byte, fs *mock.FileSystem) *SaveStateTask { func NewSaveStateTask(path string, data []byte, fs FileSystem) *SaveStateTask {
return &SaveStateTask{ return &SaveStateTask{
taskID: fmt.Sprintf("save_state_%s_%d", filepath.Base(path), taskCounter.Add(1)), taskID: fmt.Sprintf("save_state_%s_%d", filepath.Base(path), taskCounter.Add(1)),
Path: path, Path: path,
@ -457,11 +455,11 @@ func (t *SaveStateTask) Timeout() time.Duration { return 30 * time.Second }
type SaveUndoTask struct { type SaveUndoTask struct {
taskID string taskID string
Path string Path string
FS *mock.FileSystem FS FileSystem
Data []byte Data []byte
} }
func NewSaveUndoTask(path string, data []byte, fs *mock.FileSystem) *SaveUndoTask { func NewSaveUndoTask(path string, data []byte, fs FileSystem) *SaveUndoTask {
return &SaveUndoTask{ return &SaveUndoTask{
taskID: fmt.Sprintf("save_undo_%s_%d", filepath.Base(path), taskCounter.Add(1)), taskID: fmt.Sprintf("save_undo_%s_%d", filepath.Base(path), taskCounter.Add(1)),
Path: path, Path: path,

View File

@ -5,6 +5,7 @@ import (
"time" "time"
"pad/internal/io/pool/mock" "pad/internal/io/pool/mock"
"pad/internal/io/pool/types"
) )
func TestReadDirTask_Execute_Success(t *testing.T) { func TestReadDirTask_Execute_Success(t *testing.T) {
@ -26,9 +27,9 @@ func TestReadDirTask_Execute_Success(t *testing.T) {
t.Error("TaskID should not be empty") t.Error("TaskID should not be empty")
} }
entries, ok := result.Data.([]mock.DirEntry) entries, ok := result.Data.([]types.DirEntry)
if !ok { if !ok {
t.Fatalf("Data is not []mock.DirEntry, got %T", result.Data) t.Fatalf("Data is not []types.DirEntry, got %T", result.Data)
} }
if len(entries) != 2 { if len(entries) != 2 {
t.Errorf("Expected 2 entries, got %d", len(entries)) t.Errorf("Expected 2 entries, got %d", len(entries))

View File

@ -0,0 +1,10 @@
package types
import "os"
// DirEntry interface defines the structure for directory entries.
type DirEntry interface {
Name() string
IsDir() bool
Info() (os.FileInfo, error)
}

View File

@ -9,6 +9,7 @@ import (
"time" "time"
"pad/internal/io/pool/mock" "pad/internal/io/pool/mock"
"pad/internal/io/pool/types"
) )
// stubTask is a minimal task implementation for testing the worker pool. // stubTask is a minimal task implementation for testing the worker pool.
@ -471,9 +472,9 @@ func TestWorkerPool_ResultWithRealTasks(t *testing.T) {
if !result.IsSuccess() { if !result.IsSuccess() {
t.Errorf("ReadDir task failed: %v", result.Error) t.Errorf("ReadDir task failed: %v", result.Error)
} }
entries, ok := result.Data.([]mock.DirEntry) entries, ok := result.Data.([]types.DirEntry)
if !ok { if !ok {
t.Fatalf("Result data is not []mock.DirEntry, got %T", result.Data) t.Fatalf("Result data is not []types.DirEntry, got %T", result.Data)
} }
if len(entries) != 2 { if len(entries) != 2 {
t.Errorf("Entries count = %d, want 2", len(entries)) t.Errorf("Entries count = %d, want 2", len(entries))