@ -10,6 +10,7 @@ import (
"testing"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp"
xmaps "golang.org/x/exp/maps"
)
)
func TestLRU ( t * testing . T ) {
func TestLRU ( t * testing . T ) {
@ -48,6 +49,156 @@ func TestLRU(t *testing.T) {
}
}
}
}
func TestLRUDeleteCorruption ( t * testing . T ) {
// Regression test for tailscale/corp#14747
c := Cache [ int , bool ] { }
c . Set ( 1 , true )
c . Set ( 2 , true ) // now 2 is the head
c . Delete ( 2 ) // delete the head
c . check ( t )
}
func TestStressEvictions ( t * testing . T ) {
const (
cacheSize = 1_000
numKeys = 10_000
numProbes = 100_000
)
vm := map [ uint64 ] bool { }
for len ( vm ) < numKeys {
vm [ rand . Uint64 ( ) ] = true
}
vals := xmaps . Keys ( vm )
c := Cache [ uint64 , bool ] {
MaxEntries : cacheSize ,
}
for i := 0 ; i < numProbes ; i ++ {
v := vals [ rand . Intn ( len ( vals ) ) ]
c . Set ( v , true )
if l := c . Len ( ) ; l > cacheSize {
t . Fatalf ( "Cache size now %d, want max %d" , l , cacheSize )
}
}
}
func TestStressBatchedEvictions ( t * testing . T ) {
// One of Cache's consumers dynamically adjusts the cache size at
// runtime, and does batched evictions as needed. This test
// simulates that behavior.
const (
cacheSizeMin = 1_000
cacheSizeMax = 2_000
numKeys = 10_000
numProbes = 100_000
)
vm := map [ uint64 ] bool { }
for len ( vm ) < numKeys {
vm [ rand . Uint64 ( ) ] = true
}
vals := xmaps . Keys ( vm )
c := Cache [ uint64 , bool ] { }
for i := 0 ; i < numProbes ; i ++ {
v := vals [ rand . Intn ( len ( vals ) ) ]
c . Set ( v , true )
if c . Len ( ) == cacheSizeMax {
// Batch eviction down to cacheSizeMin
for c . Len ( ) > cacheSizeMin {
c . DeleteOldest ( )
}
}
if l := c . Len ( ) ; l > cacheSizeMax {
t . Fatalf ( "Cache size now %d, want max %d" , l , cacheSizeMax )
}
}
}
func TestLRUStress ( t * testing . T ) {
var c Cache [ int , int ]
const (
maxSize = 500
numProbes = 5_000
)
for i := 0 ; i < numProbes ; i ++ {
n := rand . Intn ( maxSize * 2 )
op := rand . Intn ( 4 )
switch op {
case 0 :
c . Get ( n )
case 1 :
c . Set ( n , n )
case 2 :
c . Delete ( n )
case 3 :
for c . Len ( ) > maxSize {
c . DeleteOldest ( )
}
}
c . check ( t )
}
}
// check verifies that c.lookup and c.head are consistent in size with
// each other, and that the ring has the same size when traversed in
// both directions.
func ( c * Cache [ K , V ] ) check ( t testing . TB ) {
size := c . Len ( )
nextLen := c . nextLen ( t , size )
prevLen := c . prevLen ( t , size )
if nextLen != size {
t . Fatalf ( "next list len %v != map len %v" , nextLen , size )
}
if prevLen != size {
t . Fatalf ( "prev list len %v != map len %v" , prevLen , size )
}
}
// nextLen returns the length of the ring at c.head when traversing
// the .next pointers.
func ( c * Cache [ K , V ] ) nextLen ( t testing . TB , limit int ) ( n int ) {
if c . head == nil {
return 0
}
n = 1
at := c . head . next
for at != c . head {
limit --
if limit < 0 {
t . Fatal ( "next list is too long" )
}
n ++
at = at . next
}
return n
}
// prevLen returns the length of the ring at c.head when traversing
// the .prev pointers.
func ( c * Cache [ K , V ] ) prevLen ( t testing . TB , limit int ) ( n int ) {
if c . head == nil {
return 0
}
n = 1
at := c . head . prev
for at != c . head {
limit --
if limit < 0 {
t . Fatal ( "next list is too long" )
}
n ++
at = at . prev
}
return n
}
func TestDumpHTML ( t * testing . T ) {
func TestDumpHTML ( t * testing . T ) {
c := Cache [ int , string ] { MaxEntries : 3 }
c := Cache [ int , string ] { MaxEntries : 3 }