package editor import ( "bytes" "fmt" "testing" "time" "pad/internal/io/pool/mock" ) func TestAutoSaveE2E(t *testing.T) { // 1. Setup mockFS := mock.NewFileSystem() filename := "/test.txt" initialContent := "Hello" mockFS.AddFile(filename, []byte(initialContent), time.Now()) l := NewLogic(mockFS) go l.Run() defer l.Done() // Drain frameChan to prevent deadlocks go func() { for range l.FrameChan() { } }() l.state.Editor.Filename = filename TheState = l.state // 2. Open File OpenFile(filename) // Wait for the file to be loaded by checking if ChunkedBuffer is populated success := false for i := 0; i < 20; i++ { if l.state.Editor.ChunkedBuffer != nil && l.state.Editor.ChunkedBuffer.FileLen() > 0 { success = true break } time.Sleep(100 * time.Millisecond) } if !success { t.Fatal("Timed out waiting for file to load") } // 3. Edit l.state.Editor.CursorPosition = len(initialContent) HandleInsert(" World") // 4. Trigger auto-save l.markDirty() // 5. Wait for the write to complete success = false for i := 0; i < 20; i++ { content, _ := mockFS.ReadFile(filename) if string(content) == "Hello World" { success = true break } time.Sleep(200 * time.Millisecond) } // 6. Assert if !success { t.Errorf("Timed out waiting for file to save") } } // TestLargeFileChunkBoundary verifies that edits near and at chunk boundaries // are correctly persisted through the full editor pipeline (open → edit → save → // re-open → verify). It creates a 256 KB file (4 × 64 KB chunks) and performs // three edits: // // 1. Near the end of chunk 1 (position 65000) // 2. Right at the chunk 0 / chunk 1 boundary (position 65535) // 3. Near the end of chunk 3 (position 262000) // // After saving, the file is re-opened and the full content is compared // byte-for-byte against the expected result. func TestLargeFileChunkBoundary(t *testing.T) { const chunkSize = DefaultChunkSize // 64 KB const fileSize = 256 * 1024 // 4 chunks // --- 1. Setup: create a 256 KB file with a predictable pattern --- mockFS := mock.NewFileSystem() filename := "/large.txt" initialContent := make([]byte, fileSize) for i := range initialContent { initialContent[i] = byte(i % 256) } mockFS.AddFile(filename, initialContent, time.Now()) l := NewLogic(mockFS) go l.Run() defer l.Done() // Drain frameChan to prevent deadlocks go func() { for range l.FrameChan() { } }() l.state.Editor.Filename = filename TheState = l.state // --- 2. Open File --- OpenFile(filename) success := false for i := 0; i < 20; i++ { if l.state.Editor.ChunkedBuffer != nil && l.state.Editor.ChunkedBuffer.FileLen() > 0 { success = true break } time.Sleep(100 * time.Millisecond) } if !success { t.Fatal("Timed out waiting for large file to load") } cb := l.state.Editor.ChunkedBuffer if cb == nil { t.Fatal("ChunkedBuffer is nil after opening large file") } // --- 3. Edit near the end of chunk 1 (position 65000, inside chunk 1) --- l.state.Editor.CursorPosition = 65000 HandleInsert("CHUNK1") // --- 4. Edit right at the chunk 0 / chunk 1 boundary (position 65535) --- l.state.Editor.CursorPosition = 65535 HandleInsert("BOUNDARY") // --- 5. Edit near the end of chunk 3 (position 262000) --- l.state.Editor.CursorPosition = 262000 HandleInsert("CHUNK3") // --- 6. Trigger save --- l.FlushAll() // --- 7. Read back from mockFS and verify byte-for-byte --- savedContent, err := mockFS.ReadFile(filename) if err != nil { t.Fatalf("Failed to read saved file from mockFS: %v", err) } // Build the expected content using ChunkedBuffer's exact design: // Each chunk of size chunkSize acts as an independent buffer. // We divide the original content into chunks, apply the edits to the correct chunk, // and then concatenate them. chunks := make([][]byte, 4) for i := 0; i < 4; i++ { start := i * chunkSize end := start + chunkSize if end > fileSize { end = fileSize } chunks[i] = append([]byte(nil), initialContent[start:end]...) } // Helper to insert into a specific chunk insertInChunk := func(chunkIdx, offset int, txt string) { chunk := chunks[chunkIdx] newChunk := make([]byte, len(chunk)+len(txt)) copy(newChunk, chunk[:offset]) copy(newChunk[offset:], txt) copy(newChunk[offset+len(txt):], chunk[offset:]) chunks[chunkIdx] = newChunk } // 1. "CHUNK1" (pos 65000 -> chunk 0, offset 65000) insertInChunk(0, 65000, "CHUNK1") // 2. "BOUNDARY" (pos 65535 -> chunk 0, offset 65535) insertInChunk(0, 65535, "BOUNDARY") // 3. "CHUNK3" (pos 262000 -> chunk 3, offset 262000 - 3*chunkSize = 65392) insertInChunk(3, 262000-3*chunkSize, "CHUNK3") // Concatenate chunks to get expected content var expectedBuf bytes.Buffer for _, chunk := range chunks { expectedBuf.Write(chunk) } expected := expectedBuf.Bytes() if len(savedContent) != len(expected) { t.Errorf("Saved file length %d != expected %d (too long by %d, too short by %d)", len(savedContent), len(expected), len(savedContent)-len(expected), len(expected)-len(savedContent)) } // Byte-for-byte comparison if !bytes.Equal(savedContent, expected) { // Find the first differing byte minLen := len(savedContent) if len(expected) < minLen { minLen = len(expected) } diffPos := -1 for i := 0; i < minLen; i++ { if savedContent[i] != expected[i] { diffPos = i break } } if diffPos == -1 { t.Errorf("Saved content differs in length: got %d bytes, expected %d bytes", len(savedContent), len(expected)) } else { t.Errorf("Saved content differs at byte %d: got %d (%q), expected %d (%q)", diffPos, savedContent[diffPos], safeByte(savedContent[diffPos]), expected[diffPos], safeByte(expected[diffPos])) } } } // safeByte converts a byte to a printable representation for error messages. func safeByte(b byte) string { if b >= 32 && b < 127 { return string(b) } return fmt.Sprintf("\\x%02x", b) } func TestFlushOnExitE2E(t *testing.T) { // 1. Setup mockFS := mock.NewFileSystem() filename := "/test.txt" initialContent := "Hello" mockFS.AddFile(filename, []byte(initialContent), time.Now()) l := NewLogic(mockFS) go l.Run() defer l.Done() // Drain frameChan to prevent deadlocks go func() { for range l.FrameChan() { } }() l.state.Editor.Filename = filename l.state.Editor.Buffer = initialContent TheState = l.state // 2. Edit l.state.Editor.CursorPosition = len(initialContent) HandleInsert(" World") // 3. Trigger Flush l.FlushAll() // 4. Assert content, _ := mockFS.ReadFile(filename) if string(content) != "Hello World" { t.Errorf("Expected 'Hello World', got %q", string(content)) } }