ModernUO/Projects/UOContent.Tests/Tests/Engines/Events/EventSchedulerTests.cs
Kamron Batman 21a4092dd8
fix: Changes EventScheduler API so it is more explicit (#2164)
### Summary

Refactors ScheduledEvent and EventScheduler API to use TimeOnly so recurrence offset is explicit.

API:

```cs
public ScheduledEvent(
    DateTime startAfter,
    DateTime endOn,
    TimeOnly time,
    IRecurrencePattern recurrence,
    TimeZoneInfo timeZone = null
)
```

Example:
```cs
// Schedule a daily event at 8:00 AM UTC
EventScheduler.DailyAt(
    new DateTime(2024, 6, 1, 8, 0, 0, DateTimeKind.Utc),
    () => Console.WriteLine("Daily event triggered!")
);

// Schedule a custom recurring event at 3:30 PM UTC every Monday
var recurrence = new WeeklyRecurrencePattern(1, DaysOfWeek.Monday);
EventScheduler.Shared.ScheduleEvent(
    DateTime.UtcNow,
    new TimeOnly(15, 30),
    () => Console.WriteLine("Weekly Monday event!"),
    recurrence
);
```
2025-04-28 16:16:33 -07:00

308 lines
7.9 KiB
C#

using System;
using System.Collections.Generic;
using Server;
using Server.Engines.Events;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential Tests")]
public class EventSchedulerTests
{
// Test implementations for controlled testing
private class TestRecurrencePattern : IRecurrencePattern
{
private readonly TimeSpan _interval;
private readonly int _maxOccurrences;
private int _currentOccurrence;
public TestRecurrencePattern(TimeSpan interval, int maxOccurrences = int.MaxValue)
{
_interval = interval;
_maxOccurrences = maxOccurrences;
_currentOccurrence = 0;
}
public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone)
{
_currentOccurrence++;
if (_currentOccurrence > _maxOccurrences)
{
return DateTime.MaxValue;
}
return afterUtc + _interval;
}
}
private class TestScheduledEvent : ScheduledEvent
{
public int CallCount { get; private set; }
public Action Callback { get; }
public TestScheduledEvent(DateTime startAfter, Action callback, IRecurrencePattern recurrence = null)
: base(startAfter, TimeOnly.FromDateTime(startAfter), recurrence, TimeZoneInfo.Utc)
{
CallCount = 0;
Callback = callback;
}
public override void OnEvent()
{
CallCount++;
Callback?.Invoke();
}
}
private static void Init()
{
Core._now = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
Timer.Init(0);
EventScheduler.Configure();
}
private static void Finish()
{
EventScheduler.Shared.Stop();
}
[Fact]
public void ScheduleEvent_ExecutesCallback_HappyPath()
{
Init();
try
{
bool called = false;
var evt = EventScheduler.Shared.ScheduleEvent(
Core._now,
TimeOnly.FromDateTime(Core._now),
() => called = true
);
Timer.Slice(8);
Assert.True(called);
Assert.Equal(Core._now, evt.NextOccurrence);
EventScheduler.Shared.StopEvent(evt);
}
finally
{
Finish();
}
}
[Fact]
public void Scheduler_OrdersEventsByNextOccurrence()
{
Init();
try
{
var executionOrder = new List<int>();
// Create events with staggered occurrences
var evt3 = new TestScheduledEvent(
Core._now.AddSeconds(30),
() => executionOrder.Add(3)
);
var evt1 = new TestScheduledEvent(
Core._now.AddSeconds(10),
() => executionOrder.Add(1)
);
var evt2 = new TestScheduledEvent(
Core._now.AddSeconds(20),
() => executionOrder.Add(2)
);
// Add out of order
EventScheduler.Shared.ScheduleEvent(evt3);
EventScheduler.Shared.ScheduleEvent(evt1);
EventScheduler.Shared.ScheduleEvent(evt2);
// Advance time to after all events
Core._now = Core._now.AddSeconds(40);
Timer.Slice(8);
// Verify they executed in time order, not addition order
Assert.Equal([1, 2, 3], executionOrder);
EventScheduler.Shared.StopEvent(evt1);
EventScheduler.Shared.StopEvent(evt2);
EventScheduler.Shared.StopEvent(evt3);
}
finally
{
Finish();
}
}
[Fact]
public void Scheduler_HandlesRecurringEvents()
{
Init();
try
{
int callCount = 0;
// Create a recurrence pattern that fires every 10 seconds, up to 3 times
var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3);
var evt = new TestScheduledEvent(
Core._now,
() => callCount++,
recurrence
);
EventScheduler.Shared.ScheduleEvent(evt);
// Advance time to after all occurrences should have happened
Core._now = Core._now.AddSeconds(50);
Timer.Slice(8);
// Should have fired 4 times (3 recurrences)
Assert.Equal(3, callCount);
Assert.Equal(3, evt.CallCount);
EventScheduler.Shared.StopEvent(evt);
}
finally
{
Finish();
}
}
[Fact]
public void Scheduler_HandlesExceptionsInEvents()
{
Init();
try
{
bool failedEventCalled = false;
bool laterEventCalled = false;
// Event that throws exception
var failedEvt = EventScheduler.Shared.ScheduleEvent(
Core._now.AddSeconds(10),
() =>
{
failedEventCalled = true;
throw new Exception("Test exception");
}
);
// Later event that should still execute
var laterEvt = EventScheduler.Shared.ScheduleEvent(
Core._now.AddSeconds(20),
() => laterEventCalled = true
);
// Advance time to after both events
Core._now = Core._now.AddSeconds(30);
Timer.Slice(8);
// Both should have been called despite the exception
Assert.True(failedEventCalled);
Assert.True(laterEventCalled);
EventScheduler.Shared.StopEvent(failedEvt);
EventScheduler.Shared.StopEvent(laterEvt);
}
finally
{
Finish();
}
}
[Fact]
public void Scheduler_RemovesEventsCorrectly()
{
Init();
try
{
bool eventCalled = false;
var evt = EventScheduler.Shared.ScheduleEvent(
Core._now.AddSeconds(10),
() => eventCalled = true
);
// Remove before execution
EventScheduler.Shared.StopEvent(evt);
// Advance time
Core._now = Core._now.AddSeconds(20);
Timer.Slice(8);
// Event should not have executed
Assert.False(eventCalled);
}
finally
{
Finish();
}
}
[Fact]
public void Scheduler_HandlesEventsWithEndDates()
{
Init();
try
{
int callCount = 0;
// Create a recurrence pattern that fires every 10 seconds
var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10));
// Create a custom event with an end date
var startTime = Core._now;
var endTime = Core._now.AddSeconds(36);
var customEvent = new CustomEvent(
startTime,
endTime,
TimeOnly.FromDateTime(startTime),
recurrence,
() => callCount++
);
EventScheduler.Shared.ScheduleEvent(customEvent);
Core._now = Core._now.AddSeconds(50);
Timer.Slice(8);
// Should have fired 3 times only (initial + 2 within the timeframe)
Assert.Equal(3, callCount);
}
finally
{
Finish();
}
}
private class CustomEvent : ScheduledEvent
{
private readonly Action _callback;
public CustomEvent(
DateTime startAfter,
DateTime endOn,
TimeOnly time,
IRecurrencePattern recurrence,
Action callback
) : base(startAfter, endOn, time, recurrence, TimeZoneInfo.Utc) => _callback = callback;
public override void OnEvent()
{
_callback?.Invoke();
}
}
}