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