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  }