fix: Streamlines event scheduler API. Adds months to weekly recurrence (#2178)

This commit is contained in:
Kamron Batman 2025-05-07 22:42:44 -07:00 committed by GitHub
parent 990e86b983
commit f95b3a8b3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 347 additions and 137 deletions

View file

@ -41,8 +41,8 @@ public class EventSchedulerTests
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)
public TestScheduledEvent(TimeOnly time, Action callback, IRecurrencePattern recurrence = null)
: base(time, recurrence)
{
CallCount = 0;
Callback = callback;
@ -105,25 +105,28 @@ public class EventSchedulerTests
var executionOrder = new List<int>();
// Create events with staggered occurrences
var evt3 = new TestScheduledEvent(
Core._now.AddSeconds(30),
() => executionOrder.Add(3)
);
var evt1Start = Core._now.AddSeconds(10);
var evt1 = new TestScheduledEvent(
Core._now.AddSeconds(10),
TimeOnly.FromDateTime(Core._now.AddSeconds(10)),
() => executionOrder.Add(1)
);
var evt2Start = Core._now.AddSeconds(20);
var evt2 = new TestScheduledEvent(
Core._now.AddSeconds(20),
TimeOnly.FromDateTime(evt2Start),
() => executionOrder.Add(2)
);
var evt3Start = Core._now.AddSeconds(30);
var evt3 = new TestScheduledEvent(
TimeOnly.FromDateTime(evt3Start),
() => executionOrder.Add(3)
);
// Add out of order
EventScheduler.Shared.ScheduleEvent(evt3);
EventScheduler.Shared.ScheduleEvent(evt1);
EventScheduler.Shared.ScheduleEvent(evt2);
evt3.Schedule(evt3Start);
evt1.Schedule(evt1Start);
evt2.Schedule(evt2Start);
// Advance time to after all events
Core._now = Core._now.AddSeconds(40);
@ -157,12 +160,12 @@ public class EventSchedulerTests
var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3);
var evt = new TestScheduledEvent(
Core._now,
TimeOnly.FromDateTime(Core._now),
() => callCount++,
recurrence
);
EventScheduler.Shared.ScheduleEvent(evt);
evt.Schedule(Core._now);
// Advance time to after all occurrences should have happened
Core._now = Core._now.AddSeconds(50);
@ -271,14 +274,13 @@ public class EventSchedulerTests
var endTime = Core._now.AddSeconds(36);
var customEvent = new CustomEvent(
startTime,
endTime,
TimeOnly.FromDateTime(startTime),
endTime,
recurrence,
() => callCount++
);
EventScheduler.Shared.ScheduleEvent(customEvent);
customEvent.Schedule(startTime);
Core._now = Core._now.AddSeconds(50);
Timer.Slice(8);
@ -297,12 +299,11 @@ public class EventSchedulerTests
private readonly Action _callback;
public CustomEvent(
DateTime startAfter,
DateTime endOn,
TimeOnly time,
DateTime endOn,
IRecurrencePattern recurrence,
Action callback
) : base(startAfter, endOn, time, recurrence, TimeZoneInfo.Utc) => _callback = callback;
) : base(time, endOn, recurrence) => _callback = callback;
public override void OnEvent()
{

View file

@ -51,6 +51,152 @@ public class RecurrencePatternTests
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 5, 15, 12, 30, 1, AllowedDays.All, 2024, 5, 16, 12, 30)] // Next day (Thursday)
[InlineData(2024, 5, 15, 12, 30, 1, AllowedDays.Monday, 2024, 5, 20, 12, 30)] // Next Monday
[InlineData(2024, 5, 15, 12, 30, 2, AllowedDays.Wednesday, 2024, 5, 29, 12, 30)] // Biweekly Wednesday
public void WeeklyRecurrencePattern_BasicPatterns_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 12, 30, 12, 30, 1, AllowedDays.Monday, 2025, 1, 6, 12, 30)] // Across year boundary
[InlineData(2024, 5, 31, 12, 30, 1, AllowedDays.Saturday, 2024, 6, 1, 12, 30)] // Across month boundary
public void WeeklyRecurrencePattern_CrossBoundaries_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 5, 15, 12, 30, AllowedMonths.January | AllowedMonths.February, AllowedDays.Wednesday, 2025, 1, 1, 12, 30)] // Skip to January
[InlineData(2024, 12, 15, 12, 30, AllowedMonths.January | AllowedMonths.December, AllowedDays.Wednesday, 2024, 12, 18, 12, 30)] // Same month
[InlineData(2024, 1, 31, 12, 30, AllowedMonths.February | AllowedMonths.April, AllowedDays.Saturday, 2024, 2, 3, 12, 30)] // Next month allowed
public void WeeklyRecurrencePattern_MonthFiltering_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
AllowedMonths allowedMonths, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(1, allowedMonths, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData("America/New_York", 2024, 3, 9, 12, 0, 1, AllowedDays.Sunday, 2024, 3, 10, 12, 0)] // Before spring forward
[InlineData("America/New_York", 2024, 3, 10, 2, 30, 1, AllowedDays.Sunday, 2024, 3, 17, 2, 30)] // During spring forward (invalid time)
[InlineData("America/New_York", 2024, 11, 2, 12, 0, 1, AllowedDays.Sunday, 2024, 11, 3, 12, 0)] // Before fall back
[InlineData("America/New_York", 2024, 11, 3, 1, 30, 1, AllowedDays.Sunday, 2024, 11, 10, 1, 30)] // During fall back (ambiguous time)
public void WeeklyRecurrencePattern_DSTTransitions_HandlesCorrectly(
string tzId, int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
var startLocal = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0);
var afterUtc = startLocal.LocalToUtc(tz);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(result, tz);
Assert.Equal(new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0), resultLocal);
}
[Theory]
[InlineData(2024, 5, 29, 12, 0, AllowedDays.None, 2024, 5, 29, 12, 30)] // Default to current day
[InlineData(2024, 5, 29, 12, 0, AllowedDays.Wednesday | AllowedDays.Friday, 2024, 5, 29, 12, 30)] // Current day is Wednesday
[InlineData(2024, 5, 29, 12, 0, AllowedDays.Monday | AllowedDays.Friday, 2024, 5, 31, 12, 30)] // Next allowed day (Friday)
public void WeeklyRecurrencePattern_DaysOfWeekHandling_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(1, AllowedMonths.All, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 1, 29, 12, 0, 1, 2024, 1, 29, 12, 30, AllowedMonths.January | AllowedMonths.March, 2024, 1, 30, 12, 30)] // Current month allowed
[InlineData(2024, 1, 31, 12, 0, 1, 2024, 1, 31, 12, 30, AllowedMonths.January | AllowedMonths.March, 2024, 3, 1, 12, 30)] // Skip February
public void WeeklyRecurrencePattern_WeekSpanningMonths_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute,
AllowedMonths allowedMonths,
int nextExpectedYear, int nextExpectedMonth, int nextExpectedDay, int nextExpectedHour, int nextExpectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(intervalWeeks, allowedMonths);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act - first call should hit expectedDate
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
// Call again - should move to next available day in allowed months
var nextResult = pattern.GetNextOccurrence(result, time, tz);
var nextExpected = new DateTime(nextExpectedYear, nextExpectedMonth, nextExpectedDay, nextExpectedHour, nextExpectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(nextExpected, nextResult);
}
[Theory]
[InlineData(2024, 11, 3, 1, 30, DayOfWeek.Sunday, OrdinalDayOccurrence.First, "America/New_York", 2024, 11, 3)]
[InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Last, "America/New_York", 2024, 3, 31)]
@ -145,7 +291,6 @@ public class RecurrencePatternTests
Assert.Equal(new DateTime(2024, 3, 31, 12, 0, 0), result);
}
// Additional utility method tests
[Theory]
[InlineData("America/New_York", 2024, 11, 3, 1, 30)] // Fall back - ambiguous time
public void LocalToUtc_AmbiguousTime_HandlesConsistently(