using System; using System.Collections.Generic; using Server; using Server.Engines.Events; using Xunit; namespace UOContent.Tests; [Collection("Sequential UOContent 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 ); Assert.Equal(Core._now, evt.NextOccurrence); Timer.Slice(8); Assert.True(called); Assert.Equal(DateTime.MaxValue, evt.NextOccurrence); evt.Cancel(); } finally { Finish(); } } [Fact] public void Scheduler_OrdersEventsByNextOccurrence() { Init(); try { var executionOrder = new List(); // 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); evt1.Cancel(); evt2.Cancel(); evt3.Cancel(); } finally { Finish(); } } [Fact] public void Scheduler_HandlesRecurringEvents() { for (var i = 0; i < 100_000; i++) { 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); evt.Cancel(); } 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); failedEvt.Cancel(); laterEvt.Cancel(); } 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 evt.Cancel(); // 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(); } } }