codeberg.org/gruf/go-cache/v3@v3.5.7/result/cache_test.go (about)

     1  package result_test
     2  
     3  import (
     4  	"errors"
     5  	"math"
     6  	"reflect"
     7  	"testing"
     8  	"time"
     9  
    10  	"codeberg.org/gruf/go-cache/v3/result"
    11  	"github.com/google/go-cmp/cmp"
    12  )
    13  
    14  const (
    15  	testLookupField1      = "Field1"
    16  	testLookupField2      = "Field2"
    17  	testLookupField3      = "Field3"
    18  	testLookupField4      = "Field4"
    19  	testLookupField5And6  = "Field5.Field6"
    20  	testLookupField7      = "Field7"
    21  	testLookupField8      = "Field8"
    22  	testLookupField9And10 = "Field9.Field10"
    23  	testLookupField11     = "Field11"
    24  	testLookupField12     = "Field12"
    25  	testLookupField13     = "Field13"
    26  	testLookupField14     = "Field14"
    27  	testLookupField15     = "Field15"
    28  	testLookupField16     = "Field16"
    29  )
    30  
    31  var testLookups = []struct {
    32  	Lookup result.Lookup
    33  	Fields func(testType) []any
    34  }{
    35  	{
    36  		Lookup: result.Lookup{Name: testLookupField1, AllowZero: true},
    37  		Fields: func(tt testType) []any { return []any{tt.Field1} },
    38  	},
    39  	{
    40  		Lookup: result.Lookup{Name: testLookupField2, AllowZero: true},
    41  		Fields: func(tt testType) []any { return []any{tt.Field2} },
    42  	},
    43  	{
    44  		Lookup: result.Lookup{Name: testLookupField3, AllowZero: true},
    45  		Fields: func(tt testType) []any { return []any{tt.Field3} },
    46  	},
    47  	{
    48  		Lookup: result.Lookup{Name: testLookupField4, AllowZero: true},
    49  		Fields: func(tt testType) []any { return []any{tt.Field4} },
    50  	},
    51  	{
    52  		Lookup: result.Lookup{Name: testLookupField5And6, AllowZero: true},
    53  		Fields: func(tt testType) []any { return []any{tt.Field5, tt.Field6} },
    54  	},
    55  	{
    56  		Lookup: result.Lookup{Name: testLookupField7, AllowZero: true},
    57  		Fields: func(tt testType) []any { return []any{tt.Field7} },
    58  	},
    59  	{
    60  		Lookup: result.Lookup{Name: testLookupField8, AllowZero: true},
    61  		Fields: func(tt testType) []any { return []any{tt.Field8} },
    62  	},
    63  	{
    64  		Lookup: result.Lookup{Name: testLookupField9And10, AllowZero: true},
    65  		Fields: func(tt testType) []any { return []any{tt.Field9, tt.Field10} },
    66  	},
    67  	{
    68  		Lookup: result.Lookup{Name: testLookupField11, AllowZero: true},
    69  		Fields: func(tt testType) []any { return []any{tt.Field11} },
    70  	},
    71  	{
    72  		Lookup: result.Lookup{Name: testLookupField12, AllowZero: true},
    73  		Fields: func(tt testType) []any { return []any{tt.Field12} },
    74  	},
    75  	{
    76  		Lookup: result.Lookup{Name: testLookupField13, AllowZero: false},
    77  		Fields: func(tt testType) []any { return []any{tt.Field13} },
    78  	},
    79  	{
    80  		Lookup: result.Lookup{Name: testLookupField14, AllowZero: false},
    81  		Fields: func(tt testType) []any { return []any{tt.Field14} },
    82  	},
    83  	{
    84  		Lookup: result.Lookup{Name: testLookupField15, AllowZero: false},
    85  		Fields: func(tt testType) []any { return []any{tt.Field15} },
    86  	},
    87  	{
    88  		Lookup: result.Lookup{Name: testLookupField16, AllowZero: false},
    89  		Fields: func(tt testType) []any { return []any{tt.Field16} },
    90  	},
    91  }
    92  
    93  type testType struct {
    94  	// Each must be unique
    95  	Field1  string
    96  	Field2  int
    97  	Field3  uint
    98  	Field4  float32
    99  	Field7  time.Time
   100  	Field8  *time.Time
   101  	Field11 []byte
   102  	Field12 []rune
   103  
   104  	// Combined must be unique
   105  	Field5  string
   106  	Field6  string
   107  	Field9  time.Duration
   108  	Field10 *time.Duration
   109  
   110  	// Empty, should be ignored
   111  	Field13 int
   112  	Field14 float32
   113  	Field15 string
   114  	Field16 []byte
   115  }
   116  
   117  var testEntries = []testType{
   118  	{
   119  		Field1:  "i am medium",
   120  		Field2:  42,
   121  		Field3:  69,
   122  		Field4:  42.69,
   123  		Field5:  "hello",
   124  		Field6:  "world",
   125  		Field7:  time.Time{}.Add(time.Nanosecond),
   126  		Field8:  func() *time.Time { t := time.Time{}.Add(time.Nanosecond); return &t }(),
   127  		Field9:  time.Nanosecond,
   128  		Field10: func() *time.Duration { d := time.Nanosecond; return &d }(),
   129  		Field11: []byte{'0'},
   130  		Field12: []rune{'0'},
   131  	},
   132  	{
   133  		Field1:  "i am small",
   134  		Field2:  math.MinInt,
   135  		Field3:  1,
   136  		Field4:  math.SmallestNonzeroFloat32,
   137  		Field5:  "hello",
   138  		Field6:  "earth",
   139  		Field7:  time.Time{}.Add(time.Millisecond),
   140  		Field8:  func() *time.Time { t := time.Time{}.Add(time.Millisecond); return &t }(),
   141  		Field9:  time.Millisecond,
   142  		Field10: func() *time.Duration { d := time.Millisecond; return &d }(),
   143  		Field11: []byte("hello world"),
   144  		Field12: []rune("hello world"),
   145  	},
   146  	{
   147  		Field1:  "i am large",
   148  		Field2:  math.MaxInt,
   149  		Field3:  math.MaxUint,
   150  		Field4:  math.MaxFloat32,
   151  		Field5:  "hello",
   152  		Field6:  "moon",
   153  		Field7:  time.Time{}.Add(time.Second),
   154  		Field8:  func() *time.Time { t := time.Time{}.Add(time.Second); return &t }(),
   155  		Field9:  time.Second,
   156  		Field10: func() *time.Duration { d := time.Second; return &d }(),
   157  		Field11: []byte{'\n'},
   158  		Field12: []rune{'\n'},
   159  	},
   160  }
   161  
   162  func TestCache(t *testing.T) {
   163  	// Convert test lookups to lookup string slice
   164  	lookups := func() []result.Lookup {
   165  		var lookups []result.Lookup
   166  		for _, l := range testLookups {
   167  			lookups = append(lookups, l.Lookup)
   168  		}
   169  		return lookups
   170  	}()
   171  
   172  	// Prepare cache and schedule cleaning
   173  	c := result.New(lookups, func(tt *testType) *testType {
   174  		tt2 := new(testType)
   175  		*tt2 = *tt
   176  		return tt2
   177  	}, 3)
   178  
   179  	done := make(chan struct{})
   180  	go func() {
   181  		for {
   182  			// Return if done
   183  			select {
   184  			case <-done:
   185  				return
   186  			default:
   187  			}
   188  
   189  			// Continually loop checking keys
   190  			// (puts concurrent strain on cache)
   191  			for _, entry := range testEntries {
   192  				for _, lookup := range testLookups {
   193  					c.Has(lookup.Lookup.Name, lookup.Fields(entry)...)
   194  				}
   195  			}
   196  		}
   197  	}()
   198  	defer close(done)
   199  
   200  	// Allocate callbacks slice of length >= expected.
   201  	callbacks := make([]testType, 0, len(testEntries))
   202  
   203  	// Track callbacks performed
   204  	c.SetInvalidateCallback(func(tt *testType) {
   205  		t.Logf("-> Invalidate: %+v", tt)
   206  		callbacks = append(callbacks, *tt)
   207  	})
   208  	c.SetEvictionCallback(func(tt *testType) {
   209  		t.Logf("-> Evict: %+v", tt)
   210  		callbacks = append(callbacks, *tt)
   211  	})
   212  
   213  	// Prepare callback search function
   214  	findInCallbacks := func(cb []testType, tt testType) bool {
   215  		for _, entry := range cb {
   216  			if cmp.Equal(entry, tt) {
   217  				return true
   218  			}
   219  		}
   220  		return false
   221  	}
   222  
   223  	// Add all entries to cache
   224  	for i := range testEntries {
   225  		t.Logf("Cache.Store(%+v)", testEntries[i])
   226  
   227  		if err := c.Store(&(testEntries[i]), func() error {
   228  			return nil
   229  		}); err != nil {
   230  			t.Fatalf("placing entry failed: %v", err)
   231  		}
   232  	}
   233  
   234  	// Ensure all entries are expected
   235  	for _, entry := range testEntries {
   236  		for _, lookup := range testLookups {
   237  			key := lookup.Fields(entry)
   238  			zero := true
   239  
   240  			// Skip zero value keys
   241  			for _, field := range key {
   242  				zero = zero && reflect.ValueOf(field).IsZero()
   243  			}
   244  			if zero {
   245  				continue
   246  			}
   247  
   248  			check, err := c.Load(lookup.Lookup.Name, func() (*testType, error) {
   249  				return nil, errors.New("item SHOULD be cached")
   250  			}, lookup.Fields(entry)...)
   251  			if err != nil {
   252  				t.Errorf("key unexpectedly not found in cache: %v", err)
   253  			} else if !cmp.Equal(entry, *check) {
   254  				t.Errorf("value not as expected for key in cache: %s", lookup.Lookup.Name)
   255  			}
   256  		}
   257  	}
   258  
   259  	// Force invalidate, check callbacks
   260  	for _, entry := range testEntries {
   261  		lookup := testLookups[0].Lookup
   262  		key := testLookups[0].Fields(entry)
   263  
   264  		t.Logf("Cache.Invalidate(%s,%v)", lookup.Name, key)
   265  		c.Invalidate(lookup.Name, key...)
   266  
   267  		if !findInCallbacks(callbacks, entry) {
   268  			t.Errorf("invalidate callback unexpectedly not called for: %s,%v", lookup.Name, key)
   269  		}
   270  
   271  		for _, lookup := range testLookups {
   272  			key := lookup.Fields(entry)
   273  			if c.Has(lookup.Lookup.Name, key...) {
   274  				t.Errorf("key unexpected found in cache: %s,%v", lookup.Lookup.Name, key)
   275  			}
   276  		}
   277  	}
   278  
   279  	// Reset callbacks
   280  	callbacks = callbacks[:0]
   281  
   282  	// Re-add all entries to cache
   283  	for i := range testEntries {
   284  		t.Logf("Cache.Store(%+v)", testEntries[i])
   285  
   286  		if err := c.Store(&(testEntries[i]), func() error {
   287  			return nil
   288  		}); err != nil {
   289  			t.Fatalf("placing entry failed: %v", err)
   290  		}
   291  	}
   292  }