Navigating into an unreadable directory failed cleanly in the header (handleError reverts CurrentPath to the previous path) but left the list empty forever: the recovery re-read of the previous directory found it unchanged, so handleBuildIndexSuccess took the no-change fast path — which assumes the view already shows that content — while the failed navigation's navigateToDirectory reset had just wiped TotalEntries/Pages. No frame was emitted and the browser sat on a correct header over a dead list (seen on device tapping ".." into /storage/emulated, which is media_rw:media_rw 0750; only /storage/emulated/0 is app-exposed). The fast path now requires a live view (TotalEntries != 0); a torn-down view always takes the full rebuild, restoring the rows. Regression test TestHandleResult_NavigationFailureRecoveryReloads (red without the fix, green with it); verified on emulator and phone.
554 lines
17 KiB
Go
554 lines
17 KiB
Go
package browser
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"pad/internal/io/pool"
|
|
"pad/internal/io/pool/mock"
|
|
"pad/internal/io/pool/types"
|
|
)
|
|
|
|
// testDirEntry / testFileInfo are minimal types.DirEntry implementations so a
|
|
// browser BuildIndex result can be constructed directly (no worker-pool round
|
|
// trip, which avoids the follow-up LoadPages tasks that a real landing result
|
|
// triggers via OnScroll).
|
|
type testDirEntry struct {
|
|
name string
|
|
isDir bool
|
|
size int64
|
|
mod time.Time
|
|
}
|
|
|
|
func (e testDirEntry) Name() string { return e.name }
|
|
func (e testDirEntry) IsDir() bool { return e.isDir }
|
|
func (e testDirEntry) Info() (os.FileInfo, error) { return testFileInfo{e}, nil }
|
|
|
|
type testFileInfo struct{ e testDirEntry }
|
|
|
|
func (f testFileInfo) Name() string { return f.e.name }
|
|
func (f testFileInfo) Size() int64 { return f.e.size }
|
|
func (f testFileInfo) Mode() os.FileMode { return 0o644 }
|
|
func (f testFileInfo) ModTime() time.Time { return f.e.mod }
|
|
func (f testFileInfo) IsDir() bool { return f.e.isDir }
|
|
func (f testFileInfo) Sys() any { return nil }
|
|
|
|
// TestNewBrowserManager verifies that a BrowserManager can be constructed
|
|
// with valid parameters and has correct initial state.
|
|
func TestNewBrowserManager(t *testing.T) {
|
|
state := NewBrowserState()
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
if bm == nil {
|
|
t.Fatal("NewBrowserManager returned nil")
|
|
}
|
|
|
|
// Verify initial state
|
|
if bm.state != state {
|
|
t.Errorf("BrowserManager.state = %p, want %p", bm.state, state)
|
|
}
|
|
}
|
|
|
|
// TestNavigateTo verifies that NavigateTo dispatches a BuildIndexTask
|
|
// and resets browser state appropriately.
|
|
func TestNavigateTo(t *testing.T) {
|
|
state := NewBrowserState()
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
// Create some mock entries in the filesystem
|
|
now := time.Now()
|
|
fs.AddDir("/test/docs", now)
|
|
fs.AddFile("/test/docs/file1.txt", []byte("content1"), now)
|
|
fs.AddFile("/test/docs/file2.txt", []byte("content2"), now)
|
|
fs.AddDir("/test/docs/subdir", now)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Navigate to a directory
|
|
bm.NavigateTo("/test/docs")
|
|
|
|
// Verify state was reset
|
|
if state.CurrentPath != "/test/docs" {
|
|
t.Errorf("CurrentPath = %q, want %q", state.CurrentPath, "/test/docs")
|
|
}
|
|
|
|
if state.ScrollOffset != 0 {
|
|
t.Errorf("ScrollOffset = %f, want 0", state.ScrollOffset)
|
|
}
|
|
|
|
if state.SelectedIndex != -1 {
|
|
t.Errorf("SelectedIndex = %d, want -1", state.SelectedIndex)
|
|
}
|
|
|
|
// Verify a task was dispatched (should have pending work)
|
|
// The manager should have queued a BuildIndexTask
|
|
if !state.Loading {
|
|
t.Error("Expected Loading to be true after NavigateTo")
|
|
}
|
|
}
|
|
|
|
// TestNavigateTo_NonExistentDirectory verifies behavior when navigating
|
|
// to a directory that doesn't exist in the filesystem.
|
|
func TestNavigateTo_NonExistentDirectory(t *testing.T) {
|
|
state := NewBrowserState()
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Navigate to non-existent directory
|
|
bm.NavigateTo("/non/existent/path")
|
|
|
|
// Should still set the path (error handling happens async)
|
|
if state.CurrentPath != "/non/existent/path" {
|
|
t.Errorf("CurrentPath = %q, want %q", state.CurrentPath, "/non/existent/path")
|
|
}
|
|
}
|
|
|
|
// TestHandleResult_BuildIndexSuccess verifies that a successful BuildIndex
|
|
// result populates the SortIndex and triggers initial page loading.
|
|
func TestHandleResult_BuildIndexSuccess(t *testing.T) {
|
|
state := NewBrowserState()
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Simulate a successful BuildIndex result
|
|
result := pool.Result{
|
|
TaskType: pool.TypeBuildIndex,
|
|
Success: true,
|
|
Data: []types.DirEntry{},
|
|
}
|
|
|
|
bm.HandleResult(result)
|
|
|
|
// After successful BuildIndex, Loading should be false
|
|
if state.Loading {
|
|
t.Error("Expected Loading to be false after successful BuildIndex result")
|
|
}
|
|
}
|
|
|
|
// TestHandleResult_BuildIndexRefreshFailure verifies that a failed BACKGROUND
|
|
// REFRESH keeps the current (last good) view rather than teleporting the user
|
|
// or resetting the list over a transient read error.
|
|
func TestHandleResult_BuildIndexRefreshFailure(t *testing.T) {
|
|
state := NewBrowserState()
|
|
state.CurrentPath = "/dir"
|
|
state.TotalEntries = 5
|
|
state.Pages[0] = NewPage(0, []Entry{{Name: "a.txt"}})
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Model an in-flight background refresh of the current directory (as
|
|
// BrowserManager.Refresh would), then feed it a failed result.
|
|
bm.indexInFlightPath = "/dir"
|
|
bm.indexInFlightIsNav = false
|
|
result := pool.Result{
|
|
TaskType: pool.TypeBuildIndex,
|
|
Success: false,
|
|
Error: ErrDirNotFound,
|
|
DirPath: "/dir",
|
|
}
|
|
|
|
bm.HandleResult(result)
|
|
|
|
// The view must be preserved: same directory, same pages, no reset.
|
|
if state.CurrentPath != "/dir" {
|
|
t.Errorf("CurrentPath = %q after refresh failure, want /dir (view must be kept)", state.CurrentPath)
|
|
}
|
|
if len(state.Pages) != 1 {
|
|
t.Errorf("Pages = %d after refresh failure, want 1 (view must be kept)", len(state.Pages))
|
|
}
|
|
if bm.indexInFlightPath != "" {
|
|
t.Errorf("indexInFlightPath = %q after refresh failure, want cleared", bm.indexInFlightPath)
|
|
}
|
|
}
|
|
|
|
// waitBrowserResult is a test helper: read one result from the pool's channel
|
|
// with a deadline, fatal on timeout.
|
|
func waitBrowserResult(t *testing.T, wp *pool.WorkerPool) pool.Result {
|
|
t.Helper()
|
|
select {
|
|
case res := <-wp.ResultChan():
|
|
return res
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timeout waiting for browser result")
|
|
return pool.Result{}
|
|
}
|
|
}
|
|
|
|
// TestRefresh_PicksUpExternalChanges verifies the periodic-refresh contract:
|
|
// the initial load reports a change; an unchanged refresh reports NO change
|
|
// (so no frame is emitted while idle) and preserves the scroll offset; and a
|
|
// refresh after an external add picks up the new file and re-sorts.
|
|
func TestRefresh_PicksUpExternalChanges(t *testing.T) {
|
|
t.Parallel()
|
|
fs := mock.NewFileSystem()
|
|
fs.AddDir("/Notes", time.Now()) // only used by the OnScroll LoadPages, harmless
|
|
wp := pool.NewWorkerPool(2)
|
|
wp.Start()
|
|
defer wp.Stop()
|
|
|
|
state := NewBrowserState()
|
|
state.CurrentPath = "/Notes"
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
mod := time.Date(2026, 5, 1, 12, 0, 0, 0, time.UTC)
|
|
buildIndex := func(dirPath string, files ...string) pool.Result {
|
|
entries := make([]types.DirEntry, 0, len(files))
|
|
for _, f := range files {
|
|
entries = append(entries, testDirEntry{name: f, size: 1, mod: mod})
|
|
}
|
|
return pool.Result{TaskType: pool.TypeBuildIndex, Success: true, Data: entries, DirPath: dirPath}
|
|
}
|
|
|
|
// Initial load: reports a change.
|
|
if !bm.HandleResult(buildIndex("/Notes", "a.txt")) {
|
|
t.Fatal("initial load should report a change")
|
|
}
|
|
if state.TotalEntries != 2 { // ".." + a.txt
|
|
t.Fatalf("TotalEntries = %d after load, want 2", state.TotalEntries)
|
|
}
|
|
|
|
// Unchanged refresh: reports NO change and preserves the scroll offset.
|
|
state.ScrollOffset = 123
|
|
if bm.HandleResult(buildIndex("/Notes", "a.txt")) {
|
|
t.Fatal("unchanged refresh should report no change")
|
|
}
|
|
if state.ScrollOffset != 123 {
|
|
t.Errorf("ScrollOffset = %v after no-op refresh, want 123 (preserved)", state.ScrollOffset)
|
|
}
|
|
|
|
// An external add: the next refresh picks it up and re-sorts.
|
|
if !bm.HandleResult(buildIndex("/Notes", "a.txt", "b.txt")) {
|
|
t.Fatal("refresh after external add should report a change")
|
|
}
|
|
if state.TotalEntries != 3 { // ".." + a.txt + b.txt
|
|
t.Fatalf("TotalEntries = %d after add, want 3", state.TotalEntries)
|
|
}
|
|
}
|
|
|
|
// TestRefresh_CoalescesInFlight verifies that a refresh requested while a read
|
|
// of the same directory is in flight does not dispatch a second read.
|
|
func TestRefresh_CoalescesInFlight(t *testing.T) {
|
|
t.Parallel()
|
|
fs := mock.NewFileSystem()
|
|
fs.AddDir("/Notes", time.Now())
|
|
fs.SetDelay(300 * time.Millisecond) // slow the read so we can observe it
|
|
wp := pool.NewWorkerPool(2)
|
|
wp.Start()
|
|
defer wp.Stop()
|
|
|
|
state := NewBrowserState()
|
|
state.CurrentPath = "/Notes"
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
bm.Load("/Notes") // dispatches one read; it is in flight for ~300ms
|
|
if bm.indexInFlightPath != "/Notes" {
|
|
t.Fatalf("expected an in-flight read of /Notes, got %q", bm.indexInFlightPath)
|
|
}
|
|
// A refresh while that read is in flight must coalesce (no second read).
|
|
bm.Refresh("/Notes")
|
|
if bm.indexInFlightPath != "/Notes" {
|
|
t.Fatalf("refresh should not replace the in-flight path, got %q", bm.indexInFlightPath)
|
|
}
|
|
// Only ONE result should arrive for the coalesced reads.
|
|
res := waitBrowserResult(t, wp)
|
|
bm.HandleResult(res)
|
|
if bm.indexInFlightPath != "" {
|
|
t.Fatalf("in-flight marker should be cleared after the result, got %q", bm.indexInFlightPath)
|
|
}
|
|
}
|
|
|
|
// TestHandleResult_BuildIndexNavFailureRestoresPrevious verifies that a failed
|
|
// NAVIGATION restores the previous directory (the existing recovery behavior)
|
|
// instead of leaving the user on a directory that could not be read.
|
|
func TestHandleResult_BuildIndexNavFailureRestoresPrevious(t *testing.T) {
|
|
state := NewBrowserState()
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Model a navigation from "/" to a directory that then fails to read.
|
|
state.History = append(state.History, "/")
|
|
state.CurrentPath = "/missing"
|
|
bm.indexInFlightPath = "/missing"
|
|
bm.indexInFlightIsNav = true
|
|
|
|
result := pool.Result{
|
|
TaskType: pool.TypeBuildIndex,
|
|
Success: false,
|
|
Error: ErrDirNotFound,
|
|
DirPath: "/missing",
|
|
}
|
|
bm.HandleResult(result)
|
|
|
|
// The user is sent back to the previous directory.
|
|
if state.CurrentPath != "/" {
|
|
t.Errorf("CurrentPath = %q after nav failure, want / (previous dir restored)", state.CurrentPath)
|
|
}
|
|
}
|
|
|
|
// TestHandleResult_LoadPagesSuccess verifies that a successful LoadPages
|
|
// result populates the Pages map.
|
|
func TestHandleResult_LoadPagesSuccess(t *testing.T) {
|
|
state := NewBrowserState()
|
|
state.TotalEntries = 500
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Simulate a successful LoadPages result
|
|
pageIndices := []int{0, 1}
|
|
result := pool.Result{
|
|
TaskType: pool.TypeLoadPages,
|
|
Success: true,
|
|
Data: pageIndices,
|
|
}
|
|
|
|
bm.HandleResult(result)
|
|
|
|
// Pages should be populated (or at least not cause a panic)
|
|
// The exact behavior depends on implementation
|
|
}
|
|
|
|
// TestOnScroll_Prefetch verifies that scrolling triggers prefetch for
|
|
// pages beyond the visible region.
|
|
func TestOnScroll_Prefetch(t *testing.T) {
|
|
state := NewBrowserState()
|
|
state.TotalEntries = 1000
|
|
state.EntryHeight = 48.0
|
|
state.VisibleCount = 10
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Set initial scroll position
|
|
state.ScrollOffset = 0
|
|
|
|
// Simulate scrolling down
|
|
state.ScrollOffset = 5000.0 // ~100 entries down
|
|
|
|
bm.OnScroll()
|
|
|
|
// Should trigger prefetch (exact behavior depends on implementation)
|
|
// At minimum, should not panic
|
|
}
|
|
|
|
// TestOnScroll_Eviction verifies that scrolling triggers eviction of
|
|
// pages far from the visible region.
|
|
func TestOnScroll_Eviction(t *testing.T) {
|
|
state := NewBrowserState()
|
|
state.TotalEntries = 1000
|
|
state.EntryHeight = 48.0
|
|
state.VisibleCount = 10
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Pre-populate some pages
|
|
state.Pages[0] = NewPage(0, make([]Entry, 100))
|
|
state.Pages[5] = NewPage(5, make([]Entry, 100))
|
|
state.Pages[10] = NewPage(10, make([]Entry, 100))
|
|
|
|
// Scroll far down
|
|
state.ScrollOffset = 50000.0 // ~1000 entries down
|
|
|
|
bm.OnScroll()
|
|
|
|
// Pages far from visible region should be evicted
|
|
// (exact behavior depends on implementation)
|
|
}
|
|
|
|
// TestBrowserManager_StateOwnership verifies that the BrowserManager
|
|
// does not directly modify state outside of designated methods.
|
|
func TestBrowserManager_StateOwnership(t *testing.T) {
|
|
state := NewBrowserState()
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Verify that the manager has a reference to the state
|
|
// but does not own it (state is owned by the editor)
|
|
if bm.state != state {
|
|
t.Error("BrowserManager should have reference to BrowserState")
|
|
}
|
|
}
|
|
|
|
// TestNavigateTo_ChangesDirectory verifies that navigating to a new
|
|
// directory clears the previous directory's state.
|
|
func TestNavigateTo_ChangesDirectory(t *testing.T) {
|
|
state := NewBrowserState()
|
|
state.CurrentPath = "/old/path"
|
|
state.TotalEntries = 500
|
|
state.Pages[0] = NewPage(0, make([]Entry, 100))
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Navigate to new directory
|
|
bm.NavigateTo("/new/path")
|
|
|
|
if state.CurrentPath != "/new/path" {
|
|
t.Errorf("CurrentPath = %q, want %q", state.CurrentPath, "/new/path")
|
|
}
|
|
|
|
// Pages should be cleared for new directory
|
|
if len(state.Pages) != 0 {
|
|
t.Errorf("Expected Pages to be cleared, got %d pages", len(state.Pages))
|
|
}
|
|
}
|
|
|
|
// TestHandleResult_UnknownType verifies that unknown result types
|
|
// are handled gracefully without panicking.
|
|
func TestHandleResult_UnknownType(t *testing.T) {
|
|
state := NewBrowserState()
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// Simulate an unknown result type
|
|
result := pool.Result{
|
|
TaskType: pool.TypeReadFile, // Not a browser task
|
|
Success: true,
|
|
Data: nil,
|
|
}
|
|
|
|
// Should not panic
|
|
bm.HandleResult(result)
|
|
}
|
|
|
|
// TestHandleResult_NavigationFailureRecoveryReloads reproduces the empty-list
|
|
// bug: navigating into an unreadable directory fails, the manager reverts to
|
|
// the previous path and re-reads it; that re-read finds the directory
|
|
// UNCHANGED, and the no-change fast path must not skip restoring the rows the
|
|
// failed navigation's reset wiped — or the browser sits on an empty list
|
|
// forever (header correct, list dead).
|
|
func TestHandleResult_NavigationFailureRecoveryReloads(t *testing.T) {
|
|
state := NewBrowserState()
|
|
fs := mock.NewFileSystem()
|
|
wp := pool.NewWorkerPool(1)
|
|
// Started (unlike the direct-result tests above): the recovery path
|
|
// dispatches REAL follow-up tasks (BuildIndex, LoadPages), and three of
|
|
// them would overflow the unstarted pool's high-priority channel.
|
|
wp.Start()
|
|
defer wp.Stop()
|
|
|
|
bm, err := NewBrowserManager(state, wp, fs)
|
|
if err != nil {
|
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
|
}
|
|
|
|
// A FIXED mtime: the two index results must carry byte-identical content
|
|
// (same directorySignature) so the recovery re-read genuinely hits the
|
|
// no-change fast path that the bug lived in.
|
|
fixed := time.Date(2026, 5, 15, 10, 0, 0, 0, time.UTC)
|
|
entries := func() []types.DirEntry {
|
|
return []types.DirEntry{
|
|
testDirEntry{name: "file1.txt", size: 8, mod: fixed},
|
|
testDirEntry{name: "subdir", isDir: true, mod: fixed},
|
|
}
|
|
}
|
|
indexResult := func(dirPath string) pool.Result {
|
|
return pool.Result{TaskType: pool.TypeBuildIndex, Success: true, DirPath: dirPath, Data: entries()}
|
|
}
|
|
|
|
// Land in /test/docs with a populated view.
|
|
state.CurrentPath = "/test/docs"
|
|
bm.indexInFlightPath = "/test/docs"
|
|
bm.indexInFlightIsNav = true
|
|
if !bm.HandleResult(indexResult("/test/docs")) {
|
|
t.Fatal("initial index did not change the view")
|
|
}
|
|
if state.TotalEntries != 3 { // 2 entries + ".."
|
|
t.Fatalf("TotalEntries = %d, want 3", state.TotalEntries)
|
|
}
|
|
|
|
// The user taps into an unreadable directory (as NavigateTo would set up).
|
|
state.History = append(state.History, state.CurrentPath)
|
|
navigateToDirectory(state, "/missing")
|
|
state.Loading = true
|
|
bm.indexInFlightPath = "/missing"
|
|
bm.indexInFlightIsNav = true
|
|
|
|
// That read fails: the manager must revert to /test/docs.
|
|
failed := pool.Result{TaskType: pool.TypeBuildIndex, Success: false, Error: ErrDirNotFound, DirPath: "/missing"}
|
|
bm.HandleResult(failed)
|
|
if state.CurrentPath != "/test/docs" {
|
|
t.Fatalf("CurrentPath = %q, want reverted /test/docs", state.CurrentPath)
|
|
}
|
|
if state.TotalEntries != 0 {
|
|
t.Fatalf("TotalEntries = %d after failed navigation, want 0 (rows reset)", state.TotalEntries)
|
|
}
|
|
|
|
// The recovery re-read of /test/docs lands with the directory unchanged.
|
|
// This is where the bug lived: the no-change fast path returned early and
|
|
// the view stayed empty.
|
|
if !bm.HandleResult(indexResult("/test/docs")) {
|
|
t.Fatal("recovery re-read reported no view change; the list would stay empty")
|
|
}
|
|
if state.TotalEntries != 3 {
|
|
t.Fatalf("TotalEntries = %d after recovery re-read, want 3", state.TotalEntries)
|
|
}
|
|
if page, ok := state.Pages[0]; !ok || page == nil || !page.Loaded {
|
|
t.Fatal("page 0 not restored after recovery re-read")
|
|
}
|
|
}
|