diff --git a/cmd/pad/main.go b/cmd/pad/main.go index 5ad868b..aba37cf 100644 --- a/cmd/pad/main.go +++ b/cmd/pad/main.go @@ -1,8 +1,10 @@ package main import ( + "flag" "log" "os" + "path/filepath" "sync" "gioui.org/app" @@ -13,6 +15,7 @@ import ( "gioui.org/io/key" "pad/internal/editor" + "pad/internal/io/pool/real" "pad/internal/ui" ) @@ -33,7 +36,19 @@ func run(w *app.Window) error { log.Printf("run: starting") var ops op.Ops 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()) var mu sync.Mutex var elems []ui.Element diff --git a/doc/editor_implementation_plan.md b/doc/editor_implementation_plan.md index 6abfc42..a2994c7 100644 --- a/doc/editor_implementation_plan.md +++ b/doc/editor_implementation_plan.md @@ -224,6 +224,24 @@ Key design points: - [ ] Undo/Redo stack implementation. — **NOT STARTED** (`UndoStack` field is commented out in `EditorState`) - [ ] 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) diff --git a/internal/browser/lazy_loading_test.go b/internal/browser/lazy_loading_test.go index 6e53b21..4b325b4 100644 --- a/internal/browser/lazy_loading_test.go +++ b/internal/browser/lazy_loading_test.go @@ -17,17 +17,17 @@ func TestLazyLoadingLargeDirectory(t *testing.T) { state.TotalEntries = 10000 state.EntryHeight = 48.0 state.VisibleCount = 10 - fs := mock.NewFileSystem() + mfs := mock.NewFileSystem() wp := pool.NewWorkerPool(4) wp.Start() // Populate mock filesystem with 10k entries - fs.AddDir("/dir", time.Now()) + mfs.AddDir("/dir", time.Now()) 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 { t.Fatalf("NewBrowserManager failed: %v", err) } @@ -88,17 +88,17 @@ func TestPrefetchOnScroll(t *testing.T) { state.TotalEntries = 1000 state.EntryHeight = 48.0 state.VisibleCount = 10 - fs := mock.NewFileSystem() + mfs := mock.NewFileSystem() wp := pool.NewWorkerPool(4) wp.Start() // Populate mock filesystem - fs.AddDir("/dir", time.Now()) + mfs.AddDir("/dir", time.Now()) 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 { t.Fatalf("NewBrowserManager failed: %v", err) } diff --git a/internal/browser/manager.go b/internal/browser/manager.go index a8de223..2796de9 100644 --- a/internal/browser/manager.go +++ b/internal/browser/manager.go @@ -6,7 +6,7 @@ import ( "time" "pad/internal/io/pool" - "pad/internal/io/pool/mock" + "pad/internal/io/pool/types" ) var ErrDirNotFound = fmt.Errorf("directory not found") @@ -14,10 +14,10 @@ var ErrDirNotFound = fmt.Errorf("directory not found") type BrowserManager struct { state *BrowserState 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{ state: state, workerPool: wp, @@ -86,12 +86,12 @@ func (bm *BrowserManager) HandleResult(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 - // 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. - entries := result.Data.([]mock.DirEntry) + entries := result.Data.([]types.DirEntry) var browserEntries []Entry // Add ".." entry if not at root diff --git a/internal/browser/manager_test.go b/internal/browser/manager_test.go index cd4bf6c..c220a11 100644 --- a/internal/browser/manager_test.go +++ b/internal/browser/manager_test.go @@ -6,6 +6,7 @@ import ( "pad/internal/io/pool" "pad/internal/io/pool/mock" + "pad/internal/io/pool/types" ) // TestNewBrowserManager verifies that a BrowserManager can be constructed @@ -109,7 +110,7 @@ func TestHandleResult_BuildIndexSuccess(t *testing.T) { result := pool.Result{ TaskType: pool.TypeBuildIndex, Success: true, - Data: []mock.DirEntry{}, + Data: []types.DirEntry{}, } bm.HandleResult(result) diff --git a/internal/browser/navigation_test.go b/internal/browser/navigation_test.go index 4867ca2..5e3ffe0 100644 --- a/internal/browser/navigation_test.go +++ b/internal/browser/navigation_test.go @@ -12,9 +12,9 @@ import ( // BrowserState.CurrentPath. func TestTapDirectory(t *testing.T) { state := NewBrowserState() - fs := mock.NewFileSystem() + mfs := mock.NewFileSystem() wp := pool.NewWorkerPool(1) - bm, _ := NewBrowserManager(state, wp, fs) + bm, _ := NewBrowserManager(state, wp, mfs) state.CurrentPath = "/root" // Mock entries: a subdirectory "sub" and a file @@ -38,9 +38,9 @@ func TestTapDirectory(t *testing.T) { // BrowserState.CurrentPath to the parent directory. func TestTapUp(t *testing.T) { state := NewBrowserState() - fs := mock.NewFileSystem() + mfs := mock.NewFileSystem() wp := pool.NewWorkerPool(1) - bm, _ := NewBrowserManager(state, wp, fs) + bm, _ := NewBrowserManager(state, wp, mfs) state.CurrentPath = "/root/sub" // Mock entries: ".." and a file @@ -64,9 +64,9 @@ func TestTapUp(t *testing.T) { // in the full directory listing. func TestTapWithSearchResults(t *testing.T) { state := NewBrowserState() - fs := mock.NewFileSystem() + mfs := mock.NewFileSystem() wp := pool.NewWorkerPool(1) - bm, _ := NewBrowserManager(state, wp, fs) + bm, _ := NewBrowserManager(state, wp, mfs) state.CurrentPath = "/root" // Mock entries: 5 files, sorted by name @@ -98,9 +98,9 @@ func TestTapWithSearchResults(t *testing.T) { // search results resolves to the correct entries. func TestTapWithMultipleSearchResults(t *testing.T) { state := NewBrowserState() - fs := mock.NewFileSystem() + mfs := mock.NewFileSystem() wp := pool.NewWorkerPool(1) - bm, _ := NewBrowserManager(state, wp, fs) + bm, _ := NewBrowserManager(state, wp, mfs) state.CurrentPath = "/root" // Mock entries: 5 files diff --git a/internal/editor/logic.go b/internal/editor/logic.go index 41b7740..e5cfe04 100644 --- a/internal/editor/logic.go +++ b/internal/editor/logic.go @@ -56,7 +56,7 @@ type Logic struct { openFileChan chan string retryChan chan string // Added for auto-save retries workerPool *pool.WorkerPool - mockFS *mock.FileSystem + mockFS pool.FileSystem mu sync.Mutex done chan struct{} 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. -func NewLogic(mfs *mock.FileSystem) *Logic { +func NewLogic(mfs pool.FileSystem) *Logic { state := NewState() TheState = state diff --git a/internal/editor/mock_setup.go b/internal/editor/mock_setup.go index b03b428..9d78a9f 100644 --- a/internal/editor/mock_setup.go +++ b/internal/editor/mock_setup.go @@ -6,17 +6,23 @@ import ( "time" "pad/internal/browser" + "pad/internal/io/pool" "pad/internal/io/pool/mock" + "pad/internal/io/pool/types" ) // populateMockFileSystem seeds the mock filesystem with sample data for testing. // Creates a realistic directory structure with subdirectories, various file types, // 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) // Create root directory - required for ReadDir("/") to succeed - fs.AddDir("/", baseTime) + mfs.AddDir("/", baseTime) // --- Root directories --- dirs := []string{ @@ -32,7 +38,7 @@ func populateMockFileSystem(fs *mock.FileSystem) { "/Pictures/Vacation", } for i, d := range dirs { - fs.AddDir(d, baseTime.AddDate(0, 0, -i)) + mfs.AddDir(d, baseTime.AddDate(0, 0, -i)) } // --- Root files --- @@ -48,7 +54,7 @@ func populateMockFileSystem(fs *mock.FileSystem) { {".gitignore", "*.log\n.pad/\n.git/\n*.tmp\n", -90}, } 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 --- @@ -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}, } 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 --- @@ -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}, } 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 --- @@ -100,7 +106,7 @@ func populateMockFileSystem(fs *mock.FileSystem) { {"go.sum", "gioui.org v0.9.0 h1:...\n", -30}, } 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 --- @@ -116,7 +122,7 @@ func populateMockFileSystem(fs *mock.FileSystem) { {"IMG_0005.jpg", "[Binary JPEG Image Data Mock]", -178}, } 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 --- @@ -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}, } 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 } -// 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. -func buildBrowserIndex(entries []mock.DirEntry) *browser.DirectoryIndex { +func buildBrowserIndex(entries []types.DirEntry) *browser.DirectoryIndex { var browserEntries []browser.Entry for _, e := range entries { info, err := e.Info() diff --git a/internal/io/pool/context_test.go b/internal/io/pool/context_test.go index c0a5e59..db7702d 100644 --- a/internal/io/pool/context_test.go +++ b/internal/io/pool/context_test.go @@ -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) { fs := mock.NewFileSystem() fs.CreateDir("/test/dir1") diff --git a/internal/io/pool/filesystem.go b/internal/io/pool/filesystem.go new file mode 100644 index 0000000..212b70a --- /dev/null +++ b/internal/io/pool/filesystem.go @@ -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 +} diff --git a/internal/io/pool/mock/filesystem.go b/internal/io/pool/mock/filesystem.go index c277707..21ba036 100644 --- a/internal/io/pool/mock/filesystem.go +++ b/internal/io/pool/mock/filesystem.go @@ -9,17 +9,9 @@ import ( "path/filepath" "sync" "time" -) -// 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 -} + "pad/internal/io/pool/types" +) // DirEntry mimics fs.DirEntry behavior for the mock. type DirEntry struct { @@ -29,12 +21,22 @@ type DirEntry struct { size int64 } -func (d DirEntry) Name() string { return d.name } -func (d DirEntry) IsDir() bool { return d.isDir } +func (d DirEntry) Name() string { return d.name } +func (d DirEntry) IsDir() bool { return d.isDir } func (d DirEntry) Info() (os.FileInfo, error) { 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. type mockFileInfo struct { name string @@ -273,7 +275,7 @@ func (fs *FileSystem) CreateDir(path string) error { } // 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() 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) } - var entries []DirEntry + var entries []types.DirEntry for _, f := range fs.files { if f.Path == dirPath { @@ -308,7 +310,7 @@ func (fs *FileSystem) ReadDir(path string) ([]DirEntry, error) { // Sort by name for i := 0; i < len(entries); i++ { 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] } } diff --git a/internal/io/pool/real/filesystem.go b/internal/io/pool/real/filesystem.go new file mode 100644 index 0000000..915ed1a --- /dev/null +++ b/internal/io/pool/real/filesystem.go @@ -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() } diff --git a/internal/io/pool/task.go b/internal/io/pool/task.go index 76c5a20..f0728b8 100644 --- a/internal/io/pool/task.go +++ b/internal/io/pool/task.go @@ -9,8 +9,6 @@ import ( "path/filepath" "sync/atomic" "time" - - "pad/internal/io/pool/mock" ) // taskCounter generates unique task IDs. @@ -95,10 +93,10 @@ type Task interface { type ReadDirTask struct { taskID 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{ taskID: fmt.Sprintf("read_dir_%s_%d", filepath.Base(dir), taskCounter.Add(1)), Dir: dir, @@ -136,10 +134,10 @@ func (t *ReadDirTask) Timeout() time.Duration { return 5 * time.Second } type BuildIndexTask struct { taskID 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{ taskID: fmt.Sprintf("build_index_%s_%d", filepath.Base(dir), taskCounter.Add(1)), Dir: dir, @@ -180,10 +178,10 @@ func (t *BuildIndexTask) Timeout() time.Duration { return 5 * time.Second } type LoadIndexTask struct { taskID 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{ taskID: fmt.Sprintf("load_index_%s_%d", filepath.Base(dir), taskCounter.Add(1)), Dir: dir, @@ -215,10 +213,10 @@ type LoadPagesTask struct { taskID string Dir string 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{ taskID: fmt.Sprintf("load_pages_%s_%d", filepath.Base(dir), taskCounter.Add(1)), Dir: dir, @@ -249,10 +247,10 @@ func (t *LoadPagesTask) Timeout() time.Duration { return 5 * time.Second } type StatDirTask struct { taskID 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{ taskID: fmt.Sprintf("stat_dir_%s_%d", filepath.Base(dir), taskCounter.Add(1)), Dir: dir, @@ -284,10 +282,10 @@ func (t *StatDirTask) Timeout() time.Duration { return 30 * time.Second } type ReadFileTask struct { taskID 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{ taskID: fmt.Sprintf("read_file_%s_%d", filepath.Base(path), taskCounter.Add(1)), Path: path, @@ -325,11 +323,11 @@ func (t *ReadFileTask) Timeout() time.Duration { return 5 * time.Second } type WriteFileTask struct { taskID string Path string - FS *mock.FileSystem + FS FileSystem Data []byte } -func NewWriteFileTask(path string, data []byte, fs *mock.FileSystem) *WriteFileTask { +func NewWriteFileTask(path string, data []byte, fs FileSystem) *WriteFileTask { return &WriteFileTask{ taskID: fmt.Sprintf("write_file_%s_%d", filepath.Base(path), taskCounter.Add(1)), Path: path, @@ -371,11 +369,11 @@ func (t *WriteFileTask) Timeout() time.Duration { return 30 * time.Second } type WriteCacheTask struct { taskID string Path string - FS *mock.FileSystem + FS FileSystem Data []byte } -func NewWriteCacheTask(path string, data []byte, fs *mock.FileSystem) *WriteCacheTask { +func NewWriteCacheTask(path string, data []byte, fs FileSystem) *WriteCacheTask { return &WriteCacheTask{ taskID: fmt.Sprintf("write_cache_%s_%d", filepath.Base(path), taskCounter.Add(1)), Path: path, @@ -415,11 +413,11 @@ func (t *WriteCacheTask) Timeout() time.Duration { return 30 * time.Second } type SaveStateTask struct { taskID string Path string - FS *mock.FileSystem + FS FileSystem Data []byte } -func NewSaveStateTask(path string, data []byte, fs *mock.FileSystem) *SaveStateTask { +func NewSaveStateTask(path string, data []byte, fs FileSystem) *SaveStateTask { return &SaveStateTask{ taskID: fmt.Sprintf("save_state_%s_%d", filepath.Base(path), taskCounter.Add(1)), Path: path, @@ -457,11 +455,11 @@ func (t *SaveStateTask) Timeout() time.Duration { return 30 * time.Second } type SaveUndoTask struct { taskID string Path string - FS *mock.FileSystem + FS FileSystem Data []byte } -func NewSaveUndoTask(path string, data []byte, fs *mock.FileSystem) *SaveUndoTask { +func NewSaveUndoTask(path string, data []byte, fs FileSystem) *SaveUndoTask { return &SaveUndoTask{ taskID: fmt.Sprintf("save_undo_%s_%d", filepath.Base(path), taskCounter.Add(1)), Path: path, diff --git a/internal/io/pool/task_test.go b/internal/io/pool/task_test.go index 349a9dd..f82862a 100644 --- a/internal/io/pool/task_test.go +++ b/internal/io/pool/task_test.go @@ -5,6 +5,7 @@ import ( "time" "pad/internal/io/pool/mock" + "pad/internal/io/pool/types" ) 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") } - entries, ok := result.Data.([]mock.DirEntry) + entries, ok := result.Data.([]types.DirEntry) 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 { t.Errorf("Expected 2 entries, got %d", len(entries)) diff --git a/internal/io/pool/types/types.go b/internal/io/pool/types/types.go new file mode 100644 index 0000000..7836b8b --- /dev/null +++ b/internal/io/pool/types/types.go @@ -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) +} diff --git a/internal/io/pool/worker_pool_test.go b/internal/io/pool/worker_pool_test.go index 5d2a5b9..6ba1c50 100644 --- a/internal/io/pool/worker_pool_test.go +++ b/internal/io/pool/worker_pool_test.go @@ -9,6 +9,7 @@ import ( "time" "pad/internal/io/pool/mock" + "pad/internal/io/pool/types" ) // stubTask is a minimal task implementation for testing the worker pool. @@ -471,9 +472,9 @@ func TestWorkerPool_ResultWithRealTasks(t *testing.T) { if !result.IsSuccess() { t.Errorf("ReadDir task failed: %v", result.Error) } - entries, ok := result.Data.([]mock.DirEntry) + entries, ok := result.Data.([]types.DirEntry) 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 { t.Errorf("Entries count = %d, want 2", len(entries))