### 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
);
```
308 lines
7.9 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|