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:
parent
672dcbe6b3
commit
d73cb0be2c
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
15
internal/io/pool/filesystem.go
Normal file
15
internal/io/pool/filesystem.go
Normal 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
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
internal/io/pool/real/filesystem.go
Normal file
71
internal/io/pool/real/filesystem.go
Normal 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() }
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
10
internal/io/pool/types/types.go
Normal file
10
internal/io/pool/types/types.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user