Pad/internal/editor/e2e_test.go
Greg Pomerantz 615957196e feat: virtual scrolling with chunked buffer for large files
- Add ChunkedBuffer for 64KB chunked file access with dirty-chunk
  eviction protection
- Add LineIndex for precise byte-offset-to-line-number mapping
- Refactor IO task system with context cancellation, typed priorities,
  and new task types (ReadChunk, BuildLineIndex, StatFile)
- Add ReadFileAt to FileSystem interface (mock + real implementations)
- Integrate virtual scrolling into editor layout
- Add comprehensive tests for chunked buffer eviction, dirty-chunk
  safety, and full edit lifecycle
2026-06-05 09:11:32 -04:00

262 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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))
}
}