Files

358 lines
13 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package uptime
import (
"testing"
"time"
"github.com/luxfi/evmgpu/plugin/evm/validators/uptime/interfaces"
"github.com/luxfi/ids"
"github.com/luxfi/timer/mockable"
"github.com/luxfi/validators/uptime"
"github.com/stretchr/testify/require"
)
// testUptimeState is a mock implementation of uptime.State for testing
type testUptimeState struct {
startTimes map[ids.NodeID]time.Time
uptimes map[ids.NodeID]time.Duration
}
func (t *testUptimeState) GetStartTime(nodeID ids.NodeID, netID ids.ID) (time.Time, error) {
if startTime, ok := t.startTimes[nodeID]; ok {
return startTime, nil
}
return time.Time{}, nil
}
func (t *testUptimeState) GetUptime(nodeID ids.NodeID, netID ids.ID) (time.Duration, time.Duration, error) {
uptime, ok := t.uptimes[nodeID]
if !ok {
uptime = 0
}
// Return uptime and total possible uptime
return uptime, uptime, nil
}
func (t *testUptimeState) SetUptime(nodeID ids.NodeID, netID ids.ID, uptime time.Duration, lastUpdated time.Time) error {
t.uptimes[nodeID] = uptime
return nil
}
func TestPausableManager(t *testing.T) {
vID := ids.GenerateTestID()
nodeID0 := ids.GenerateTestNodeID()
startTime := time.Now()
tests := []struct {
name string
testFunc func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State)
}{
{
name: "Case 1: Connect, pause, start tracking",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Connect before tracking
require.NoError(up.Connect(nodeID0))
addTime(clk, time.Second)
expectedUptime := 1 * time.Second
// Pause before tracking
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// Elapse Time
addTime(clk, time.Second)
// Since we have not started tracking this node yet, its observed uptime should
// be incremented even though it is actually paused.
expectedUptime += 1 * time.Second
// Start tracking
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Uptime here should not increase after start tracking
// since the node is still paused after we started tracking
currentTime := addTime(clk, time.Second)
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Disconnect
require.NoError(up.Disconnect(nodeID0))
// Uptime should not have increased
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
{
name: "Case 2: Start tracking, connect, pause, re-connect, resume",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Start tracking
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Connect
addTime(clk, 1*time.Second)
require.NoError(up.Connect(nodeID0))
// Pause
addTime(clk, 1*time.Second)
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// Elapse time
currentTime := addTime(clk, 2*time.Second)
// Uptime should be 1 second since the node was paused after 1 sec
expectedUptime := 1 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Disconnect and check uptime
currentTime = addTime(clk, 3*time.Second)
require.NoError(up.Disconnect(nodeID0))
// Uptime should not have increased since the node was paused
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Connect again and check uptime
addTime(clk, 4*time.Second)
require.NoError(up.Connect(nodeID0))
currentTime = addTime(clk, 5*time.Second)
// Uptime should not have increased since the node was paused
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Resume and check uptime
currentTime = addTime(clk, 6*time.Second)
up.OnValidatorStatusUpdated(vID, nodeID0, true)
require.False(up.IsPaused(nodeID0))
// Uptime should not have increased since the node was paused
// and we just resumed it
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Elapsed time check
currentTime = addTime(clk, 7*time.Second)
// Uptime should increase by 7 seconds above since the node was resumed
expectedUptime += 7 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
{
name: "Case 3: Pause, start tracking, connect, re-connect, resume",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Pause before tracking
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// Start tracking
addTime(clk, time.Second)
// Uptime should be 1 since the node was paused before we started tracking
expectedUptime := 1 * time.Second
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Connect and check uptime
addTime(clk, 1*time.Second)
checkUptime(t, up, nodeID0, expectedUptime, clk.Time())
require.NoError(up.Connect(nodeID0))
currentTime := addTime(clk, 2*time.Second)
// Uptime should not have increased since the node was paused after we started tracking
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Disconnect and check uptime
currentTime = addTime(clk, 3*time.Second)
require.NoError(up.Disconnect(nodeID0))
// Uptime should not have increased since the node was paused
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Connect again and resume
addTime(clk, 4*time.Second)
require.NoError(up.Connect(nodeID0))
addTime(clk, 5*time.Second)
up.OnValidatorStatusUpdated(vID, nodeID0, true)
require.False(up.IsPaused(nodeID0))
// Check uptime after resume
currentTime = addTime(clk, 6*time.Second)
// Uptime should have increased by 6 seconds since the node was resumed
expectedUptime += 6 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
{
name: "Case 4: Start tracking, connect, pause, stop tracking, resume tracking",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Start tracking and connect
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
addTime(clk, time.Second)
require.NoError(up.Connect(nodeID0))
// Pause and check uptime
currentTime := addTime(clk, 2*time.Second)
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// Uptime should be 2 seconds since the node was paused after 2 seconds
expectedUptime := 2 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Stop tracking and reinitialize manager
currentTime = addTime(clk, 3*time.Second)
require.NoError(up.StopTracking([]ids.NodeID{nodeID0}))
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
up = NewPausableManager(NewManager(s, clk))
// Uptime should not have increased since the node was paused
// and we have not started tracking again
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Pause and check uptime
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// Uptime should not have increased since the node was paused
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Resume and check uptime
currentTime = addTime(clk, 5*time.Second)
up.OnValidatorStatusUpdated(vID, nodeID0, true)
require.False(up.IsPaused(nodeID0))
// Uptime should have increased by 5 seconds since the node was resumed
expectedUptime += 5 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Start tracking and check elapsed time
currentTime = addTime(clk, 6*time.Second)
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Uptime should have increased by 6 seconds since we started tracking
// and node was resumed (we assume the node was online until we started tracking)
expectedUptime += 6 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Elapsed time
currentTime = addTime(clk, 7*time.Second)
// Uptime should not have increased since the node was not connected
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Connect and final uptime check
require.NoError(up.Connect(nodeID0))
currentTime = addTime(clk, 8*time.Second)
// Uptime should have increased by 8 seconds since the node was connected
expectedUptime += 8 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
{
name: "Case 5: Node paused after we stop tracking",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Start tracking and connect
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
addTime(clk, time.Second)
require.NoError(up.Connect(nodeID0))
// Stop tracking
currentTime := addTime(clk, 2*time.Second)
expectedUptime := 2 * time.Second
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
require.NoError(up.StopTracking([]ids.NodeID{nodeID0}))
// Pause after a while
addTime(clk, 3*time.Second)
// expectedUptime should increase since we stopped tracking
expectedUptime += 3 * time.Second
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
// expectedUptime should increase since we stopped tracking (even if the node was paused)
currentTime = addTime(clk, 4*time.Second)
expectedUptime += 4 * time.Second
// Start tracking and check elapsed time
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Uptime have increased since the node was paused before we started tracking
// We should be optimistic and assume the node was online and active until we start tracking
require.True(up.IsPaused(nodeID0))
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
{
name: "Case 6: Paused node got resumed after we stop tracking",
testFunc: func(t *testing.T, up interfaces.PausableManager, clk *mockable.Clock, s uptime.State) {
require := require.New(t)
// Start tracking and connect
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
addTime(clk, time.Second)
require.NoError(up.Connect(nodeID0))
// Pause after a while
currentTime := addTime(clk, 2*time.Second)
// expectedUptime should increase
expectedUptime := 2 * time.Second
up.OnValidatorStatusUpdated(vID, nodeID0, false)
require.True(up.IsPaused(nodeID0))
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
// Stop tracking
currentTime = addTime(clk, 3*time.Second)
// expectedUptime should not increase since the node was paused
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
require.NoError(up.StopTracking([]ids.NodeID{nodeID0}))
// Resume after a while
addTime(clk, 4*time.Second)
// expectedUptime should increase since we stopped tracking
expectedUptime += 4 * time.Second
up.OnValidatorStatusUpdated(vID, nodeID0, true)
require.False(up.IsPaused(nodeID0))
// expectedUptime should increase since we stopped tracking
currentTime = addTime(clk, 5*time.Second)
expectedUptime += 5 * time.Second
// Start tracking and check elapsed time
require.NoError(up.StartTracking([]ids.NodeID{nodeID0}))
// Uptime should have increased by 4 seconds since the node was resumed
// We should be optimistic and assume the node was online and active until we start tracking
checkUptime(t, up, nodeID0, expectedUptime, currentTime)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
up, clk, s := setupTestEnv(nodeID0, startTime)
test.testFunc(t, up, clk, s)
})
}
}
func setupTestEnv(nodeID ids.NodeID, startTime time.Time) (interfaces.PausableManager, *mockable.Clock, uptime.State) {
clk := mockable.Clock{}
clk.Set(startTime)
// Create a test state that implements uptime.State
s := &testUptimeState{
startTimes: make(map[ids.NodeID]time.Time),
uptimes: make(map[ids.NodeID]time.Duration),
}
up := NewPausableManager(NewManager(s, &clk))
return up, &clk, s
}
func addTime(clk *mockable.Clock, duration time.Duration) time.Time {
clk.Set(clk.Time().Add(duration))
return clk.Time()
}
func checkUptime(t *testing.T, up interfaces.PausableManager, nodeID ids.NodeID, expectedUptime time.Duration, expectedLastUpdate time.Time) {
t.Helper()
// Use a default chain ID for testing
chainID := ids.GenerateTestID()
uptime, totalUptime, err := up.CalculateUptime(nodeID, chainID)
require.NoError(t, err)
// Compare the duration directly, not Unix timestamps
require.Equal(t, expectedUptime, uptime)
// totalUptime should be at least as much as uptime
require.GreaterOrEqual(t, totalUptime, uptime)
}