Files

288 lines
6.1 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package window
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/timer/mockable"
)
const (
testTTL = 10 * time.Second
testMaxSize = 10
)
// TestAdd tests that elements are populated as expected, ignoring
// any eviction.
func TestAdd(t *testing.T) {
tests := []struct {
name string
window []int
newlyAdded int
expectedOldest int
}{
{
name: "empty",
window: []int{},
newlyAdded: 1,
expectedOldest: 1,
},
{
name: "populated",
window: []int{1, 2, 3, 4},
newlyAdded: 5,
expectedOldest: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
clock.Set(time.Now())
window := New[int](
Config{
Clock: clock,
MaxSize: testMaxSize,
TTL: testTTL,
},
)
for _, element := range test.window {
window.Add(element)
}
window.Add(test.newlyAdded)
require.Equal(len(test.window)+1, window.Length())
oldest, ok := window.Oldest()
require.True(ok)
require.Equal(test.expectedOldest, oldest)
})
}
}
// TestTTLAdd tests the case where an element is stale in the window
// and needs to be evicted on Add.
func TestTTLAdd(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
start := time.Now()
clock.Set(start)
window := New[int](
Config{
Clock: clock,
MaxSize: testMaxSize,
TTL: testTTL,
},
)
// Now the window looks like this:
// [1, 2, 3]
window.Add(1)
window.Add(2)
window.Add(3)
require.Equal(3, window.Length())
oldest, ok := window.Oldest()
require.True(ok)
require.Equal(1, oldest)
// Now we're one second past the ttl of 10 seconds as defined in testTTL,
// so all existing elements need to be evicted.
clock.Set(start.Add(testTTL + time.Second))
// Now the window should look like this:
// [4]
window.Add(4)
require.Equal(1, window.Length())
oldest, ok = window.Oldest()
require.True(ok)
require.Equal(4, oldest)
// Now we're one second before the ttl of 10 seconds of when [4] was added,
// no element should be evicted
// [4, 5]
clock.Set(start.Add(2 * testTTL))
window.Add(5)
require.Equal(2, window.Length())
oldest, ok = window.Oldest()
require.True(ok)
require.Equal(4, oldest)
// Now the window is still containing 4:
// [4, 5]
// we only evict on Add method because the window is calculated in the last element added
require.Equal(2, window.Length())
oldest, ok = window.Oldest()
require.True(ok)
require.Equal(4, oldest)
}
// TestTTLLength tests that elements are evicted on Length
func TestTTLLength(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
start := time.Now()
clock.Set(start)
window := New[int](
Config{
Clock: clock,
MaxSize: testMaxSize,
TTL: testTTL,
},
)
// Now the window looks like this:
// [1, 2, 3]
window.Add(1)
window.Add(2)
window.Add(3)
require.Equal(3, window.Length())
// Now we're one second past the ttl of 10 seconds as defined in testTTL,
// so all existing elements need to be evicted.
clock.Set(start.Add(testTTL + time.Second))
// No more elements should be present in the window.
require.Equal(0, window.Length())
}
// TestTTLOldest tests that stale elements are evicted on calling Oldest
func TestTTLOldest(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
start := time.Now()
clock.Set(start)
windowIntf := New[int](
Config{
Clock: clock,
MaxSize: testMaxSize,
TTL: testTTL,
},
)
require.IsType(&window[int]{}, windowIntf)
window := windowIntf.(*window[int])
// Now the window looks like this:
// [1, 2, 3]
window.Add(1)
window.Add(2)
window.Add(3)
oldest, ok := window.Oldest()
require.True(ok)
require.Equal(1, oldest)
require.Equal(3, window.elements.Len())
// Now we're one second before the ttl of 10 seconds as defined in testTTL,
// so all existing elements shoud still exist.
// Add 4 to the window to make it:
// [1, 2, 3, 4]
clock.Set(start.Add(testTTL - time.Second))
window.Add(4)
oldest, ok = window.Oldest()
require.True(ok)
require.Equal(1, oldest)
require.Equal(4, window.elements.Len())
// Now we're one second past the ttl of the initial 3 elements
// call to oldest should now evict 1,2,3 and return 4.
clock.Set(start.Add(testTTL + time.Second))
oldest, ok = window.Oldest()
require.True(ok)
require.Equal(4, oldest)
require.Equal(1, window.elements.Len())
}
// Tests that we bound the amount of elements in the window
func TestMaxCapacity(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
clock.Set(time.Now())
window := New[int](
Config{
Clock: clock,
MaxSize: 3,
TTL: testTTL,
},
)
// Now the window looks like this:
// [1, 2, 3]
window.Add(1)
window.Add(2)
window.Add(3)
// We should evict 1 and replace it with 4.
// Now the window should look like this:
// [2, 3, 4]
window.Add(4)
// We should evict 2 and replace it with 5.
// Now the window should look like this:
// [3, 4, 5]
window.Add(5)
// We should evict 3 and replace it with 6.
// Now the window should look like this:
// [4, 5, 6]
window.Add(6)
require.Equal(3, window.Length())
oldest, ok := window.Oldest()
require.True(ok)
require.Equal(4, oldest)
}
// Tests that we do not evict past the minimum window size
func TestMinCapacity(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
start := time.Now()
clock.Set(start)
window := New[int](
Config{
Clock: clock,
MaxSize: 3,
MinSize: 2,
TTL: testTTL,
},
)
// Now the window looks like this:
// [1, 2, 3]
window.Add(1)
window.Add(2)
window.Add(3)
clock.Set(start.Add(testTTL + time.Second))
// All of [1, 2, 3] are past the ttl now, but we don't evict 3 because of
// the minimum length.
// Now the window should look like this:
// [3, 4]
window.Add(4)
require.Equal(2, window.Length())
oldest, ok := window.Oldest()
require.True(ok)
require.Equal(3, oldest)
}