//go:build testing // +build testing package agent import ( "testing" "testing/synctest" "time" "github.com/henrygd/beszel/internal/entities/container" "github.com/henrygd/beszel/internal/entities/system" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func createTestCacheData() *system.CombinedData { return &system.CombinedData{ Stats: system.Stats{ Cpu: 50.5, Mem: 8192, DiskTotal: 100000, }, Info: system.Info{ Hostname: "test-host", }, Containers: []*container.Stats{ { Name: "test-container", Cpu: 25.0, }, }, } } func TestNewSystemDataCache(t *testing.T) { cache := NewSystemDataCache() require.NotNil(t, cache) assert.NotNil(t, cache.cache) assert.Empty(t, cache.cache) } func TestCacheGetSet(t *testing.T) { cache := NewSystemDataCache() data := createTestCacheData() // Test setting data cache.Set(data, 1000) // 1 second cache // Test getting fresh data retrieved, isCached := cache.Get(1000) assert.True(t, isCached) assert.Equal(t, data, retrieved) // Test getting non-existent cache key _, isCached = cache.Get(2000) assert.False(t, isCached) } func TestCacheFreshness(t *testing.T) { cache := NewSystemDataCache() data := createTestCacheData() testCases := []struct { name string cacheTimeMs uint16 sleepMs time.Duration expectFresh bool }{ { name: "fresh data - well within cache time", cacheTimeMs: 1000, // 1 second sleepMs: 100, // 100ms expectFresh: true, }, { name: "fresh data - at 50% of cache time boundary", cacheTimeMs: 1000, // 1 second, 50% = 500ms sleepMs: 499, // just under 500ms expectFresh: true, }, { name: "stale data - exactly at 50% cache time", cacheTimeMs: 1000, // 1 second, 50% = 500ms sleepMs: 500, // exactly 500ms expectFresh: false, }, { name: "stale data - well beyond cache time", cacheTimeMs: 1000, // 1 second sleepMs: 800, // 800ms expectFresh: false, }, { name: "short cache time", cacheTimeMs: 200, // 200ms, 50% = 100ms sleepMs: 150, // 150ms > 100ms expectFresh: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { synctest.Test(t, func(t *testing.T) { // Set data cache.Set(data, tc.cacheTimeMs) // Wait for the specified duration if tc.sleepMs > 0 { time.Sleep(tc.sleepMs * time.Millisecond) } // Check freshness _, isCached := cache.Get(tc.cacheTimeMs) assert.Equal(t, tc.expectFresh, isCached) }) }) } } func TestCacheMultipleIntervals(t *testing.T) { synctest.Test(t, func(t *testing.T) { cache := NewSystemDataCache() data1 := createTestCacheData() data2 := &system.CombinedData{ Stats: system.Stats{ Cpu: 75.0, Mem: 16384, }, Info: system.Info{ Hostname: "test-host-2", }, Containers: []*container.Stats{}, } // Set data for different intervals cache.Set(data1, 500) // 500ms cache cache.Set(data2, 1000) // 1000ms cache // Both should be fresh immediately retrieved1, isCached1 := cache.Get(500) assert.True(t, isCached1) assert.Equal(t, data1, retrieved1) retrieved2, isCached2 := cache.Get(1000) assert.True(t, isCached2) assert.Equal(t, data2, retrieved2) // Wait 300ms - 500ms cache should be stale (250ms threshold), 1000ms should still be fresh (500ms threshold) time.Sleep(300 * time.Millisecond) _, isCached1 = cache.Get(500) assert.False(t, isCached1) _, isCached2 = cache.Get(1000) assert.True(t, isCached2) // Wait another 300ms (total 600ms) - now 1000ms cache should also be stale time.Sleep(300 * time.Millisecond) _, isCached2 = cache.Get(1000) assert.False(t, isCached2) }) } func TestCacheOverwrite(t *testing.T) { cache := NewSystemDataCache() data1 := createTestCacheData() data2 := &system.CombinedData{ Stats: system.Stats{ Cpu: 90.0, Mem: 32768, }, Info: system.Info{ Hostname: "updated-host", }, Containers: []*container.Stats{}, } // Set initial data cache.Set(data1, 1000) retrieved, isCached := cache.Get(1000) assert.True(t, isCached) assert.Equal(t, data1, retrieved) // Overwrite with new data cache.Set(data2, 1000) retrieved, isCached = cache.Get(1000) assert.True(t, isCached) assert.Equal(t, data2, retrieved) assert.NotEqual(t, data1, retrieved) } func TestCacheMiss(t *testing.T) { synctest.Test(t, func(t *testing.T) { cache := NewSystemDataCache() // Test getting from empty cache _, isCached := cache.Get(1000) assert.False(t, isCached) // Set data for one interval data := createTestCacheData() cache.Set(data, 1000) // Test getting different interval _, isCached = cache.Get(2000) assert.False(t, isCached) // Test getting after data has expired time.Sleep(600 * time.Millisecond) // 600ms > 500ms (50% of 1000ms) _, isCached = cache.Get(1000) assert.False(t, isCached) }) } func TestCacheZeroInterval(t *testing.T) { cache := NewSystemDataCache() data := createTestCacheData() // Set with zero interval - should allow immediate cache cache.Set(data, 0) // With 0 interval, 50% is 0, so it should never be considered fresh // (time.Since(lastUpdate) >= 0, which is not < 0) _, isCached := cache.Get(0) assert.False(t, isCached) } func TestCacheLargeInterval(t *testing.T) { synctest.Test(t, func(t *testing.T) { cache := NewSystemDataCache() data := createTestCacheData() // Test with maximum uint16 value cache.Set(data, 65535) // ~65 seconds // Should be fresh immediately _, isCached := cache.Get(65535) assert.True(t, isCached) // Should still be fresh after a short time time.Sleep(100 * time.Millisecond) _, isCached = cache.Get(65535) assert.True(t, isCached) }) }