store_paths_test.mx raw

   1  // The store paths the happy-path engine tests cannot reach: every failure
   2  // return in Open (a broken bit of the directory layout) and the stale
   3  // checkpoint recovery, which rebuilds the indexes from the WAL.
   4  package store
   5  
   6  import (
   7  	"os"
   8  	"testing"
   9  
  10  	"git.smesh.lol/nostr/pkg/filter"
  11  	"git.smesh.lol/morly/pkg/store/checkpoint"
  12  )
  13  
  14  // engIndexNames is the index file set Open creates, in Open's order.
  15  var engIndexNames []string
  16  
  17  func TestOpenFailsOnABrokenIndexFile(t *testing.T) {
  18  	for i := 0; i < len(engIndexNames); i++ {
  19  		dir, ok := engTmp(t)
  20  		if !ok {
  21  			return
  22  		}
  23  		// A directory where the index file belongs makes sorted.Open fail, so
  24  		// Open must report that index's error and not hand back an engine.
  25  		if err := os.MkdirAll(dir|"/idx/"|engIndexNames[i]|".dat", 0755); err != nil {
  26  			t.Fatal(err)
  27  		}
  28  		if g, oerr := Open(dir); oerr == nil {
  29  			g.Close()
  30  			t.Fatalf("Open must fail when idx/%s.dat is a directory", engIndexNames[i])
  31  		}
  32  		os.RemoveAll(dir)
  33  	}
  34  }
  35  
  36  func TestOpenFailsOnABrokenWAL(t *testing.T) {
  37  	// A regular file where the wal directory belongs: wal.Open's MkdirAll and
  38  	// then ReadDir/OpenFile must fail.
  39  	dir, ok := engTmp(t)
  40  	if !ok {
  41  		return
  42  	}
  43  	if err := os.WriteFile(dir|"/wal", []byte("not a directory"), 0644); err != nil {
  44  		t.Fatal(err)
  45  	}
  46  	if g, oerr := Open(dir); oerr == nil {
  47  		g.Close()
  48  		t.Fatal("Open must fail when the wal path is a file")
  49  	}
  50  	os.RemoveAll(dir)
  51  
  52  	// A segment that cannot be opened is reported too.
  53  	dir2, ok2 := engTmp(t)
  54  	if !ok2 {
  55  		return
  56  	}
  57  	if err := os.MkdirAll(dir2|"/wal/seg-0000000000000000.dat", 0755); err != nil {
  58  		t.Fatal(err)
  59  	}
  60  	if g, oerr := Open(dir2); oerr == nil {
  61  		g.Close()
  62  		t.Fatal("Open must fail when a wal segment cannot be opened")
  63  	}
  64  	os.RemoveAll(dir2)
  65  }
  66  
  67  func TestOpenFailsOnABrokenCheckpoint(t *testing.T) {
  68  	dir, ok := engTmp(t)
  69  	if !ok {
  70  		return
  71  	}
  72  	if err := os.MkdirAll(dir|"/checkpoint.dat", 0755); err != nil {
  73  		t.Fatal(err)
  74  	}
  75  	if g, oerr := Open(dir); oerr == nil {
  76  		g.Close()
  77  		t.Fatal("Open must fail when checkpoint.dat cannot be read")
  78  	}
  79  	os.RemoveAll(dir)
  80  }
  81  
  82  func TestOpenFailsWhenTheDirectoryCannotBeCreated(t *testing.T) {
  83  	dir, ok := engTmp(t)
  84  	if !ok {
  85  		return
  86  	}
  87  	// A path through a regular file cannot become a directory.
  88  	file := dir | "/file"
  89  	if err := os.WriteFile(file, []byte("x"), 0644); err != nil {
  90  		t.Fatal(err)
  91  	}
  92  	if g, oerr := Open(file | "/sub"); oerr == nil {
  93  		g.Close()
  94  		t.Fatal("Open must fail when the store directory cannot be created")
  95  	}
  96  	os.RemoveAll(dir)
  97  }
  98  
  99  func TestOpenRecoversFromAStaleCheckpoint(t *testing.T) {
 100  	g, dir, ok := engOpenTmp(t)
 101  	if !ok {
 102  		return
 103  	}
 104  	ev := engEvent(0x31, 0x32, 1, 1700000000, engTag("t", "stale"), "stale checkpoint")
 105  	if !engSaveAll(t, g, []*event.E{ev}) {
 106  		g.Close()
 107  		os.RemoveAll(dir)
 108  		return
 109  	}
 110  	if cerr := g.Close(); cerr != nil {
 111  		t.Fatal(cerr)
 112  	}
 113  
 114  	// A checkpoint offset beyond the segment's size is stale: Open must clear
 115  	// the indexes and rebuild them from the WAL instead of failing.
 116  	ck, cok := checkpoint.Open(dir | "/checkpoint.dat")
 117  	if cok != nil {
 118  		t.Fatal(cok)
 119  	}
 120  	if serr := ck.Set(1 << 20); serr != nil {
 121  		t.Fatal(serr)
 122  	}
 123  
 124  	g2, ok2 := engOpen(t, dir)
 125  	if !ok2 {
 126  		return
 127  	}
 128  	defer g2.Close()
 129  	res, qok := engQuery(t, g2, filter.New())
 130  	if !qok {
 131  		return
 132  	}
 133  	if len(res) != 1 {
 134  		t.Fatalf("the rebuilt store returned %d events, want the one in the WAL", int32(len(res)))
 135  	}
 136  	if string(res[0].Content) != "stale checkpoint" {
 137  		t.Fatalf("rebuilt event content = %s", res[0].Content)
 138  	}
 139  	os.RemoveAll(dir)
 140  }
 141  
 142  func init() {
 143  	engIndexNames = []string{
 144  		"eid", "fpc", "sei", "ca", "exp", "kc", "pc", "kpc",
 145  		"tc", "tkc", "tpc", "tkp", "wrd", "pks", "spk",
 146  		"epg", "peg", "eeg", "gee", "ppg", "gpp",
 147  	}
 148  }
 149