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