Files
UnrealEngine/Engine/Source/Runtime/Online/Experimental/EventLoopTests/Tests/EventLoop/EventLoopTimerTests.cpp
2025-05-18 13:04:45 +08:00

606 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "EventLoop/EventLoopTimer.h"
#include "TestHarness.h"
namespace UE::EventLoop
{
struct FMockTimerHandleTraits
{
static constexpr const TCHAR* Name = TEXT("MockTimerHandle");
};
struct FEventLoopTimerManagerMockTraits : public FTimerManagerTraitsBase
{
static void CheckIsManagerThread(bool bIsManagerThread)
{
bCheckIsManagerThreadTriggered = !bIsManagerThread;
}
using FTimerHeapAllocatorType = TInlineAllocator<32>;
using FTimerRepeatAllocatorType = TInlineAllocator<32>;
struct FStorageTraits
{
static uint32 GetCurrentThreadId()
{
return CurrentThreadId;
}
static bool IsManagerThread(uint32 ManagerThreadId)
{
return ManagerThreadId == GetCurrentThreadId();
}
static void CheckNotInitialized(uint32 ManagerThreadId)
{
bCheckNotInitializedTriggered = (ManagerThreadId != 0);
}
static void CheckIsManagerThread(uint32 ManagerThreadId)
{
bCheckIsManagerThreadTriggered = !IsManagerThread(ManagerThreadId);
}
static constexpr bool bStorageAccessThreadChecksEnabled = true;
using InternalHandleArryAllocatorType = TInlineAllocator<32>;
using FExternalHandle = TResourceHandle<FMockTimerHandleTraits>;
static void ResetTestConditions()
{
CurrentThreadId = 1;
bCheckNotInitializedTriggered = false;
bCheckIsManagerThreadTriggered = false;
}
static uint32 CurrentThreadId;
static bool bCheckNotInitializedTriggered;
static bool bCheckIsManagerThreadTriggered;
};
static void ResetTestConditions()
{
FStorageTraits::ResetTestConditions();
bCheckIsManagerThreadTriggered = false;
}
static bool bCheckIsManagerThreadTriggered;
};
bool FEventLoopTimerManagerMockTraits::bCheckIsManagerThreadTriggered = false;
uint32 FEventLoopTimerManagerMockTraits::FStorageTraits::CurrentThreadId = 1;
bool FEventLoopTimerManagerMockTraits::FStorageTraits::bCheckNotInitializedTriggered = false;
bool FEventLoopTimerManagerMockTraits::FStorageTraits::bCheckIsManagerThreadTriggered = false;
TEST_CASE("EventLoop::Timer", "[Online][EventLoop][Smoke]")
{
using FMockEventLoopTimerManager = TTimerManager<FEventLoopTimerManagerMockTraits>;
using FMockEventLoopTimerHandle = FMockEventLoopTimerManager::FTimerHandle;
FMockEventLoopTimerManager TimerManager;
const uint32 ManagerThreadId = 1;
FEventLoopTimerManagerMockTraits::ResetTestConditions();
FEventLoopTimerManagerMockTraits::FStorageTraits::CurrentThreadId = ManagerThreadId;
SECTION("Single init call")
{
TimerManager.Init();
CHECK(!FEventLoopTimerManagerMockTraits::FStorageTraits::bCheckNotInitializedTriggered);
}
SECTION("Double init call")
{
TimerManager.Init();
TimerManager.Init();
CHECK(FEventLoopTimerManagerMockTraits::FStorageTraits::bCheckNotInitializedTriggered);
}
SECTION("Call Tick before Init")
{
TimerManager.Tick(FTimespan());
CHECK(FEventLoopTimerManagerMockTraits::bCheckIsManagerThreadTriggered);
}
SECTION("Initialized")
{
uint32 TimerRunCount1 = 0;
auto TimerFunc1 = [&]() { ++TimerRunCount1; };
uint32 TimerRunCount2 = 0;
auto TimerFunc2 = [&]() { ++TimerRunCount2; };
TimerManager.Init();
SECTION("Call Tick after Init")
{
TimerManager.Tick(FTimespan());
CHECK(!FEventLoopTimerManagerMockTraits::bCheckIsManagerThreadTriggered);
}
SECTION("Timer paramater validation.")
{
SECTION("Timer with zero rate.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::Zero());
CHECK(Handle.IsValid());
}
SECTION("Timer with positive rate.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1));
CHECK(Handle.IsValid());
}
SECTION("Timer with negative rate.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(-1));
CHECK(!Handle.IsValid());
}
SECTION("Timer with zero first delay.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1), true, FTimespan::Zero());
CHECK(Handle.IsValid());
}
SECTION("Timer with positive first delay.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1), true, FTimespan::FromSeconds(1));
CHECK(Handle.IsValid());
}
SECTION("Timer with negative first delay.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1), true, FTimespan::FromSeconds(-1));
CHECK(!Handle.IsValid());
}
SECTION("Timer with invalid first delay.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1), false, FTimespan::FromSeconds(1));
CHECK(!Handle.IsValid());
}
SECTION("Timer with invalid callback.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(nullptr, FTimespan::FromSeconds(1));
CHECK(!Handle.IsValid());
}
}
SECTION("Non-repeating")
{
SECTION("Simple timer")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1));
REQUIRE(Handle.IsValid());
// Tick to add the timer.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
// Tick just before expiration time.
TimerManager.Tick(FTimespan::FromMilliseconds(999));
CHECK(TimerRunCount1 == 0);
// Tick to make the timer run.
TimerManager.Tick(FTimespan::FromMilliseconds(1));
CHECK(TimerRunCount1 == 1);
// Make sure timer does not run again.
TimerRunCount1 = 0;
TimerManager.Tick(FTimespan::FromSeconds(10));
CHECK(TimerRunCount1 == 0);
}
SECTION("Multiple timers fired during tick.")
{
FMockEventLoopTimerHandle Handle1 = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1));
REQUIRE(Handle1.IsValid());
FMockEventLoopTimerHandle Handle2 = TimerManager.SetTimer(MoveTemp(TimerFunc2), FTimespan::FromSeconds(2));
REQUIRE(Handle2.IsValid());
// Tick to add the timers.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
CHECK(TimerRunCount2 == 0);
// Tick to fire the timers.
TimerManager.Tick(FTimespan::FromSeconds(5));
CHECK(TimerRunCount1 == 1);
CHECK(TimerRunCount2 == 1);
}
SECTION("Tiemr with zero rate called when scheduled.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::Zero());
REQUIRE(Handle.IsValid());
// Tick to add and fire the timer.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 1);
}
SECTION("Timer removed before firing.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1));
REQUIRE(Handle.IsValid());
// Tick to add the timer.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
TimerManager.ClearTimer(Handle);
// Make sure timer does not run.
TimerManager.Tick(FTimespan::FromSeconds(10));
CHECK(TimerRunCount1 == 0);
}
SECTION("Timer not fired more than once.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1));
REQUIRE(Handle.IsValid());
// Tick to add the timer.
TimerManager.Tick(FTimespan::Zero());
// Check that timer ran.
TimerManager.Tick(FTimespan::FromSeconds(10));
CHECK(TimerRunCount1 == 1);
}
SECTION("Timer pending reschedule is not triggered.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1));
REQUIRE(Handle.IsValid());
// Tick to add the timer.
TimerManager.Tick(FTimespan::Zero());
// Check that no timer is pending reschedule.
CHECK(!TimerManager.HasPendingRepeatTimer());
// Check that timer ran.
TimerManager.Tick(FTimespan::FromSeconds(10));
CHECK(TimerRunCount1 == 1);
// Check that no timer is pending reschedule.
CHECK(!TimerManager.HasPendingRepeatTimer());
}
SECTION("Timer storage")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1));
REQUIRE(Handle.IsValid());
// Tick to add the timer.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
CHECK(TimerManager.GetNumTimers() == 1);
// Tick to fire the timer.
TimerRunCount1 = 0;
TimerManager.Tick(FTimespan::FromSeconds(10));
CHECK(TimerRunCount1 == 1);
CHECK(TimerManager.GetNumTimers() == 0);
}
}
SECTION("Repeating")
{
SECTION("Timer with zero rate does not block tick")
{
FMockEventLoopTimerHandle Handle1 = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1));
REQUIRE(Handle1.IsValid());
// Tick to add first timer.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
FMockEventLoopTimerHandle Handle2 = TimerManager.SetTimer(MoveTemp(TimerFunc2), FTimespan::Zero());
REQUIRE(Handle2.IsValid());
// Check that both timers ran.
TimerManager.Tick(FTimespan::FromSeconds(10));
CHECK(TimerRunCount1 == 1);
CHECK(TimerRunCount2 == 1);
}
SECTION("Timer with zero rate called each tick")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::Zero(), true);
REQUIRE(Handle.IsValid());
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 1);
TimerRunCount1 = 0;
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 1);
TimerManager.ClearTimer(Handle);
TimerRunCount1 = 0;
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
}
SECTION("Timer with positive rate called when elapsed")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1), true);
REQUIRE(Handle.IsValid());
// Tick to add the timer.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
// Tick not enough time to fire.
TimerManager.Tick(FTimespan::FromMilliseconds(500));
CHECK(TimerRunCount1 == 0);
// Tick enough time to fire.
TimerManager.Tick(FTimespan::FromMilliseconds(500));
CHECK(TimerRunCount1 == 1);
TimerRunCount1 = 0;
// Tick to reschedule timer.
// This looks funky, the intent is that a timer will never fire early.
// The way to prevent the extra call to tick to reschedule while providing that
// guarantee is to get the current time again when rescheduling which this
// implementation wants to avoid.
TimerManager.Tick(FTimespan::FromMilliseconds(500));
CHECK(TimerRunCount1 == 0);
// Tick not enough time to fire.
TimerManager.Tick(FTimespan::FromMilliseconds(500));
CHECK(TimerRunCount1 == 0);
// Tick enough time to fire.
TimerManager.Tick(FTimespan::FromMilliseconds(500));
CHECK(TimerRunCount1 == 1);
// Timer not fired after cleared.
TimerManager.ClearTimer(Handle);
TimerRunCount1 = 0;
TimerManager.Tick(FTimespan::FromSeconds(10));
CHECK(TimerRunCount1 == 0);
}
SECTION("Timer with zero first delay fires as expected.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1), true, FTimespan::Zero());
REQUIRE(Handle.IsValid());
// Tick to add and fire the timer.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 1);
// Tick to reschedule the timer.
TimerRunCount1 = 0;
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
// Tick to see timer does not fire again at zero interval.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
// Tick to fire the timer.
TimerManager.Tick(FTimespan::FromSeconds(1));
CHECK(TimerRunCount1 == 1);
}
SECTION("Timer with positive first delay fires as expected.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1), true, FTimespan::FromSeconds(5));
REQUIRE(Handle.IsValid());
// Tick to add the timer.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
// Tick to see timer not fire once rate has elapsed.
TimerManager.Tick(FTimespan::FromSeconds(1));
CHECK(TimerRunCount1 == 0);
// Tick near remaining time to see timer has still not fired.
TimerManager.Tick(FTimespan::FromMilliseconds(3999));
CHECK(TimerRunCount1 == 0);
// Tick remaining time to see timer has fired.
TimerManager.Tick(FTimespan::FromMilliseconds(1));
CHECK(TimerRunCount1 == 1);
}
SECTION("Timer pending reschedule is triggered.")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1), true);
REQUIRE(Handle.IsValid());
// Tick to add the timer.
TimerManager.Tick(FTimespan::Zero());
// Check that no timer is pending reschedule.
CHECK(!TimerManager.HasPendingRepeatTimer());
// Check that timer ran.
TimerManager.Tick(FTimespan::FromSeconds(10));
CHECK(TimerRunCount1 == 1);
// Check that timer is pending reschedule.
CHECK(TimerManager.HasPendingRepeatTimer());
}
SECTION("Timer storage")
{
FMockEventLoopTimerHandle Handle = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1), true);
REQUIRE(Handle.IsValid());
// Tick to add the timer.
TimerRunCount1 = 0;
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
CHECK(TimerManager.GetNumTimers() == 1);
// Tick to fire the timer.
TimerRunCount1 = 0;
TimerManager.Tick(FTimespan::FromSeconds(2));
CHECK(TimerRunCount1 == 1);
CHECK(TimerManager.GetNumTimers() == 1);
SECTION("Remove before reschedule")
{
// Tick to clear the timer.
TimerRunCount1 = 0;
TimerManager.ClearTimer(Handle);
TimerManager.Tick(FTimespan::FromSeconds(2));
CHECK(TimerRunCount1 == 0);
CHECK(TimerManager.GetNumTimers() == 0);
}
SECTION("Remove after reschedule")
{
// Tick to reschedule the timer.
TimerRunCount1 = 0;
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
CHECK(TimerManager.GetNumTimers() == 1);
// Tick to clear the timer.
TimerRunCount1 = 0;
TimerManager.ClearTimer(Handle);
TimerManager.Tick(FTimespan::FromSeconds(2));
CHECK(TimerRunCount1 == 0);
CHECK(TimerManager.GetNumTimers() == 0);
}
}
}
SECTION("Primed timer removed before it is processed during tick.")
{
FMockEventLoopTimerHandle Handle1 = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(2));
REQUIRE(Handle1.IsValid());
bool Timer1Removed = false;
auto Timer1RemovedCallback = [&]()
{
Timer1Removed = true;
};
auto Timer2Callback = [&]()
{
TimerManager.ClearTimer(Handle1, MoveTemp(Timer1RemovedCallback));
};
FMockEventLoopTimerHandle Handle2 = TimerManager.SetTimer(MoveTemp(Timer2Callback), FTimespan::FromSeconds(1));
REQUIRE(Handle1.IsValid());
// Tick to add the timers.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
// Tick so that both timers are expired.
// See that timer 1 did not fire, but will not be removed until the following tick.
TimerManager.Tick(FTimespan::FromSeconds(2));
CHECK(TimerRunCount1 == 0);
CHECK(Timer1Removed == false);
// Tick again to see that timer has been removed.
TimerManager.Tick(FTimespan::Zero());
CHECK(Timer1Removed);
}
SECTION("Timer with zero rate added during tick is not fired until the next tick.")
{
FMockEventLoopTimerHandle Handle1;
FMockEventLoopTimerHandle Handle2;
auto Timer1Callback = [&]()
{
Handle2 = TimerManager.SetTimer(MoveTemp(TimerFunc2), FTimespan::Zero());
REQUIRE(Handle2.IsValid());
};
FString value = Handle1.ToString();
Handle1 = TimerManager.SetTimer(MoveTemp(Timer1Callback), FTimespan::Zero());
REQUIRE(Handle1.IsValid());
value = Handle1.ToString();
// Tick to add and fire the timers.
TimerManager.Tick(FTimespan::Zero());
// Timer 2 has been set, but has not fired.
CHECK(Handle2.IsValid());
CHECK(TimerRunCount2 == 0);
// Tick to fire timer 2.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount2 == 1);
}
SECTION("GetNextTimeout.")
{
FTimespan NextTimeout = FTimespan::Zero();
FMockEventLoopTimerHandle Handle1 = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::FromSeconds(1));
REQUIRE(Handle1.IsValid());
FMockEventLoopTimerHandle Handle2 = TimerManager.SetTimer(MoveTemp(TimerFunc2), FTimespan::FromSeconds(2));
REQUIRE(Handle2.IsValid());
// No next timeout set yet.
CHECK(!TimerManager.GetNextTimeout(NextTimeout));
// Tick to add the timers.
TimerManager.Tick(FTimespan::Zero());
CHECK(TimerRunCount1 == 0);
CHECK(TimerRunCount2 == 0);
CHECK(TimerManager.GetNextTimeout(NextTimeout));
CHECK(NextTimeout == FTimespan::FromSeconds(1));
TimerManager.Tick(FTimespan::FromMilliseconds(500));
CHECK(TimerRunCount1 == 0);
CHECK(TimerRunCount2 == 0);
CHECK(TimerManager.GetNextTimeout(NextTimeout));
CHECK(NextTimeout == FTimespan::FromMilliseconds(500));
TimerManager.Tick(NextTimeout);
CHECK(TimerRunCount1 == 1);
CHECK(TimerRunCount2 == 0);
CHECK(TimerManager.GetNextTimeout(NextTimeout));
CHECK(NextTimeout == FTimespan::FromSeconds(1));
TimerManager.Tick(NextTimeout);
CHECK(TimerRunCount2 == 1);
// There is no longer any valid timer.
CHECK(!TimerManager.GetNextTimeout(NextTimeout));
}
SECTION("Handle string conversion.")
{
FMockEventLoopTimerHandle Handle1 = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::Zero());
REQUIRE(Handle1.IsValid());
// Test that ToString resolves name from traits.
FString StringValue = Handle1.ToString();
CHECK(StringValue.Contains(FEventLoopTimerManagerMockTraits::FStorageTraits::FExternalHandle::GetTypeName()));
}
SECTION("Timer handle is cleared when timer is removed.")
{
FMockEventLoopTimerHandle Handle1 = TimerManager.SetTimer(MoveTemp(TimerFunc1), FTimespan::Zero());
REQUIRE(Handle1.IsValid());
TimerManager.ClearTimer(Handle1);
CHECK(!Handle1.IsValid());
}
}
}
}