github.com/safing/portbase@v0.19.5/database/database_test.go (about) 1 package database 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "log" 8 "os" 9 "reflect" 10 "runtime/pprof" 11 "testing" 12 "time" 13 14 q "github.com/safing/portbase/database/query" 15 "github.com/safing/portbase/database/record" 16 "github.com/safing/portbase/database/storage" 17 _ "github.com/safing/portbase/database/storage/badger" 18 _ "github.com/safing/portbase/database/storage/bbolt" 19 _ "github.com/safing/portbase/database/storage/fstree" 20 _ "github.com/safing/portbase/database/storage/hashmap" 21 ) 22 23 func TestMain(m *testing.M) { 24 testDir, err := os.MkdirTemp("", "portbase-database-testing-") 25 if err != nil { 26 panic(err) 27 } 28 29 err = InitializeWithPath(testDir) 30 if err != nil { 31 panic(err) 32 } 33 34 exitCode := m.Run() 35 36 // Clean up the test directory. 37 // Do not defer, as we end this function with a os.Exit call. 38 _ = os.RemoveAll(testDir) 39 40 os.Exit(exitCode) 41 } 42 43 func makeKey(dbName, key string) string { 44 return fmt.Sprintf("%s:%s", dbName, key) 45 } 46 47 func testDatabase(t *testing.T, storageType string, shadowDelete bool) { //nolint:maintidx,thelper 48 t.Run(fmt.Sprintf("TestStorage_%s_%v", storageType, shadowDelete), func(t *testing.T) { 49 dbName := fmt.Sprintf("testing-%s-%v", storageType, shadowDelete) 50 fmt.Println(dbName) 51 _, err := Register(&Database{ 52 Name: dbName, 53 Description: fmt.Sprintf("Unit Test Database for %s", storageType), 54 StorageType: storageType, 55 ShadowDelete: shadowDelete, 56 }) 57 if err != nil { 58 t.Fatal(err) 59 } 60 dbController, err := getController(dbName) 61 if err != nil { 62 t.Fatal(err) 63 } 64 65 // hook 66 hook, err := RegisterHook(q.New(dbName).MustBeValid(), &HookBase{}) 67 if err != nil { 68 t.Fatal(err) 69 } 70 71 // interface 72 db := NewInterface(&Options{ 73 Local: true, 74 Internal: true, 75 }) 76 77 // sub 78 sub, err := db.Subscribe(q.New(dbName).MustBeValid()) 79 if err != nil { 80 t.Fatal(err) 81 } 82 83 A := NewExample(dbName+":A", "Herbert", 411) 84 err = A.Save() 85 if err != nil { 86 t.Fatal(err) 87 } 88 89 B := NewExample(makeKey(dbName, "B"), "Fritz", 347) 90 err = B.Save() 91 if err != nil { 92 t.Fatal(err) 93 } 94 95 C := NewExample(makeKey(dbName, "C"), "Norbert", 217) 96 err = C.Save() 97 if err != nil { 98 t.Fatal(err) 99 } 100 101 exists, err := db.Exists(makeKey(dbName, "A")) 102 if err != nil { 103 t.Fatal(err) 104 } 105 if !exists { 106 t.Fatalf("record %s should exist!", makeKey(dbName, "A")) 107 } 108 109 A1, err := GetExample(makeKey(dbName, "A")) 110 if err != nil { 111 t.Fatal(err) 112 } 113 if !reflect.DeepEqual(A, A1) { 114 log.Fatalf("A and A1 mismatch, A1: %v", A1) 115 } 116 117 cnt := countRecords(t, db, q.New(dbName).Where( 118 q.And( 119 q.Where("Name", q.EndsWith, "bert"), 120 q.Where("Score", q.GreaterThan, 100), 121 ), 122 )) 123 if cnt != 2 { 124 t.Fatalf("expected two records, got %d", cnt) 125 } 126 127 // test putmany 128 if _, ok := dbController.storage.(storage.Batcher); ok { 129 batchPut := db.PutMany(dbName) 130 records := []record.Record{A, B, C, nil} // nil is to signify finish 131 for _, r := range records { 132 err = batchPut(r) 133 if err != nil { 134 t.Fatal(err) 135 } 136 } 137 } 138 139 // test maintenance 140 if _, ok := dbController.storage.(storage.Maintainer); ok { 141 now := time.Now().UTC() 142 nowUnix := now.Unix() 143 144 // we start with 3 records without expiry 145 cnt := countRecords(t, db, q.New(dbName)) 146 if cnt != 3 { 147 t.Fatalf("expected three records, got %d", cnt) 148 } 149 // delete entry 150 A.Meta().Deleted = nowUnix - 61 151 err = A.Save() 152 if err != nil { 153 t.Fatal(err) 154 } 155 // expire entry 156 B.Meta().Expires = nowUnix - 1 157 err = B.Save() 158 if err != nil { 159 t.Fatal(err) 160 } 161 162 // one left 163 cnt = countRecords(t, db, q.New(dbName)) 164 if cnt != 1 { 165 t.Fatalf("expected one record, got %d", cnt) 166 } 167 168 // run maintenance 169 err = dbController.MaintainRecordStates(context.TODO(), now.Add(-60*time.Second)) 170 if err != nil { 171 t.Fatal(err) 172 } 173 // one left 174 cnt = countRecords(t, db, q.New(dbName)) 175 if cnt != 1 { 176 t.Fatalf("expected one record, got %d", cnt) 177 } 178 179 // check status individually 180 _, err = dbController.storage.Get("A") 181 if !errors.Is(err, storage.ErrNotFound) { 182 t.Errorf("A should be deleted and purged, err=%s", err) 183 } 184 B1, err := dbController.storage.Get("B") 185 if err != nil { 186 t.Fatalf("should exist: %s, original meta: %+v", err, B.Meta()) 187 } 188 if B1.Meta().Deleted == 0 { 189 t.Errorf("B should be deleted") 190 } 191 192 // delete last entry 193 C.Meta().Deleted = nowUnix - 1 194 err = C.Save() 195 if err != nil { 196 t.Fatal(err) 197 } 198 199 // run maintenance 200 err = dbController.MaintainRecordStates(context.TODO(), now) 201 if err != nil { 202 t.Fatal(err) 203 } 204 205 // check status individually 206 B2, err := dbController.storage.Get("B") 207 if err == nil { 208 t.Errorf("B should be deleted and purged, meta: %+v", B2.Meta()) 209 } else if !errors.Is(err, storage.ErrNotFound) { 210 t.Errorf("B should be deleted and purged, err=%s", err) 211 } 212 C2, err := dbController.storage.Get("C") 213 if err == nil { 214 t.Errorf("C should be deleted and purged, meta: %+v", C2.Meta()) 215 } else if !errors.Is(err, storage.ErrNotFound) { 216 t.Errorf("C should be deleted and purged, err=%s", err) 217 } 218 219 // none left 220 cnt = countRecords(t, db, q.New(dbName)) 221 if cnt != 0 { 222 t.Fatalf("expected no records, got %d", cnt) 223 } 224 } 225 226 err = hook.Cancel() 227 if err != nil { 228 t.Fatal(err) 229 } 230 err = sub.Cancel() 231 if err != nil { 232 t.Fatal(err) 233 } 234 }) 235 } 236 237 func TestDatabaseSystem(t *testing.T) { //nolint:tparallel 238 t.Parallel() 239 240 // panic after 10 seconds, to check for locks 241 finished := make(chan struct{}) 242 defer close(finished) 243 go func() { 244 select { 245 case <-finished: 246 case <-time.After(10 * time.Second): 247 fmt.Println("===== TAKING TOO LONG - PRINTING STACK TRACES =====") 248 _ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) 249 os.Exit(1) 250 } 251 }() 252 253 for _, shadowDelete := range []bool{false, true} { 254 testDatabase(t, "bbolt", shadowDelete) 255 testDatabase(t, "hashmap", shadowDelete) 256 testDatabase(t, "fstree", shadowDelete) 257 // testDatabase(t, "badger", shadowDelete) 258 // TODO: Fix badger tests 259 } 260 261 err := MaintainRecordStates(context.TODO()) 262 if err != nil { 263 t.Fatal(err) 264 } 265 266 err = Maintain(context.TODO()) 267 if err != nil { 268 t.Fatal(err) 269 } 270 271 err = MaintainThorough(context.TODO()) 272 if err != nil { 273 t.Fatal(err) 274 } 275 276 err = Shutdown() 277 if err != nil { 278 t.Fatal(err) 279 } 280 } 281 282 func countRecords(t *testing.T, db *Interface, query *q.Query) int { 283 t.Helper() 284 285 _, err := query.Check() 286 if err != nil { 287 t.Fatal(err) 288 } 289 290 it, err := db.Query(query) 291 if err != nil { 292 t.Fatal(err) 293 } 294 295 cnt := 0 296 for range it.Next { 297 cnt++ 298 } 299 if it.Err() != nil { 300 t.Fatal(it.Err()) 301 } 302 return cnt 303 }