The tree was formatted with an older gofmt; go1.27's gofmt additionally wants: EOF exactly one newline (no trailing blank lines), imports sorted alphabetically within a block, mixed-precedence binary expressions re-spaced for grouping ((a+b)/c), single-field composite literals un-aligned, adjacent one-line method signatures aligned, and one-line bodies containing a compound statement expanded. Applied repo-wide (31 files under internal/); pure formatting, no semantic changes — build and the full test suite pass.
275 lines
7.0 KiB
Go
275 lines
7.0 KiB
Go
package editor
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"testing"
|
||
"time"
|
||
|
||
"pad/internal/io/pool/mock"
|
||
)
|
||
|
||
// withState runs fn on the logic goroutine (the sole state owner) and fails
|
||
// the test on timeout. Test-only helper (architecture.md §1): tests must
|
||
// never touch l.state directly while Run() is active.
|
||
func withState(t *testing.T, l *Logic, fn func(*State)) {
|
||
t.Helper()
|
||
_, ok := l.Inspect(func(st *State) any {
|
||
fn(st)
|
||
return nil
|
||
})
|
||
if !ok {
|
||
t.Fatalf("withState: inspect timed out")
|
||
}
|
||
}
|
||
|
||
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, "/", func(string) {})
|
||
go l.Run()
|
||
defer l.Shutdown()
|
||
|
||
// Drain frameChan to prevent deadlocks
|
||
go func() {
|
||
for range l.FrameChan() {
|
||
}
|
||
}()
|
||
|
||
withState(t, l, func(st *State) {
|
||
TheState = st
|
||
})
|
||
|
||
// 2. Open File (OpenFile also sets Editor.Filename on the owner)
|
||
withState(t, l, func(st *State) {
|
||
TheState = st
|
||
OpenFile(filename)
|
||
})
|
||
|
||
// Wait for the file to be loaded by checking if ChunkedBuffer is populated
|
||
success := false
|
||
for i := 0; i < 20; i++ {
|
||
v, ok := l.Inspect(func(st *State) any {
|
||
cb := st.Editor.ChunkedBuffer
|
||
return cb != nil && cb.FileLen() > 0
|
||
})
|
||
if ok && v.(bool) {
|
||
success = true
|
||
break
|
||
}
|
||
time.Sleep(100 * time.Millisecond)
|
||
}
|
||
if !success {
|
||
t.Fatal("Timed out waiting for file to load")
|
||
}
|
||
|
||
// 3. Edit (owner-side)
|
||
withState(t, l, func(st *State) {
|
||
st.Editor.CursorPosition = len(initialContent)
|
||
HandleInsert(" World")
|
||
})
|
||
|
||
// 4. Trigger auto-save (owner-side)
|
||
withState(t, l, func(st *State) {
|
||
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 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, "/", func(string) {})
|
||
go l.Run()
|
||
defer l.Shutdown()
|
||
|
||
// Drain frameChan to prevent deadlocks
|
||
go func() {
|
||
for range l.FrameChan() {
|
||
}
|
||
}()
|
||
|
||
withState(t, l, func(st *State) {
|
||
TheState = st
|
||
})
|
||
|
||
// --- 2. Open File ---
|
||
withState(t, l, func(st *State) {
|
||
TheState = st
|
||
OpenFile(filename)
|
||
})
|
||
|
||
success := false
|
||
for i := 0; i < 20; i++ {
|
||
v, ok := l.Inspect(func(st *State) any {
|
||
cb := st.Editor.ChunkedBuffer
|
||
return cb != nil && cb.FileLen() > 0
|
||
})
|
||
if ok && v.(bool) {
|
||
success = true
|
||
break
|
||
}
|
||
time.Sleep(100 * time.Millisecond)
|
||
}
|
||
if !success {
|
||
t.Fatal("Timed out waiting for large file to load")
|
||
}
|
||
|
||
// --- 3. Edit near the end of chunk 1 (position 65000, inside chunk 1) ---
|
||
withState(t, l, func(st *State) {
|
||
st.Editor.CursorPosition = 65000
|
||
HandleInsert("CHUNK1")
|
||
})
|
||
|
||
// --- 4. Edit right at the chunk 0 / chunk 1 boundary (position 65535) ---
|
||
withState(t, l, func(st *State) {
|
||
st.Editor.CursorPosition = 65535
|
||
HandleInsert("BOUNDARY")
|
||
})
|
||
|
||
// --- 5. Edit near the end of chunk 3 (position 262000) ---
|
||
withState(t, l, func(st *State) {
|
||
st.Editor.CursorPosition = 262000
|
||
HandleInsert("CHUNK3")
|
||
})
|
||
|
||
// --- 6. Trigger save (owner-side) ---
|
||
withState(t, l, func(st *State) {
|
||
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 by applying the same three inserts to the
|
||
// original content as a whole, in order. This is the shift-correct ground
|
||
// truth: each insert at an absolute position pushes all later bytes right.
|
||
// (The old fixed-slot model treated each chunk as an independent buffer and
|
||
// did NOT shift later chunks; that was the bug this design replaces.)
|
||
expectedStr := string(initialContent)
|
||
expectedStr = expectedStr[:65000] + "CHUNK1" + expectedStr[65000:] // insert 1 @65000
|
||
expectedStr = expectedStr[:65535] + "BOUNDARY" + expectedStr[65535:] // insert 2 @65535
|
||
expectedStr = expectedStr[:262000] + "CHUNK3" + expectedStr[262000:] // insert 3 @262000
|
||
expected := []byte(expectedStr)
|
||
|
||
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, "/", func(string) {})
|
||
go l.Run()
|
||
defer l.Shutdown()
|
||
|
||
// Drain frameChan to prevent deadlocks
|
||
go func() {
|
||
for range l.FrameChan() {
|
||
}
|
||
}()
|
||
|
||
withState(t, l, func(st *State) {
|
||
st.Editor.Filename = filename
|
||
st.Editor.Buffer = initialContent
|
||
TheState = st
|
||
})
|
||
|
||
// 2. Edit (owner-side)
|
||
withState(t, l, func(st *State) {
|
||
st.Editor.CursorPosition = len(initialContent)
|
||
HandleInsert(" World")
|
||
})
|
||
|
||
// 3. Trigger Flush (owner-side)
|
||
withState(t, l, func(st *State) {
|
||
l.FlushAll()
|
||
})
|
||
|
||
// 4. Assert
|
||
content, _ := mockFS.ReadFile(filename)
|
||
if string(content) != "Hello World" {
|
||
t.Errorf("Expected 'Hello World', got %q", string(content))
|
||
}
|
||
}
|