diff --git a/MFiles.VAF.Extensions.Tests/Configuration/ScheduledExecution/DailyTriggerTests.cs b/MFiles.VAF.Extensions.Tests/Configuration/ScheduledExecution/DailyTriggerTests.cs index 209057d..518a654 100644 --- a/MFiles.VAF.Extensions.Tests/Configuration/ScheduledExecution/DailyTriggerTests.cs +++ b/MFiles.VAF.Extensions.Tests/Configuration/ScheduledExecution/DailyTriggerTests.cs @@ -154,6 +154,27 @@ public static IEnumerable GetNextExecutionData() new DateTimeOffset(2024, 04, 17, 10, 00, 00, TimeSpan.Zero), // Wednesday @ 10am TimeZoneInfo.Utc, }; + + // Regression: Positive UTC offset (UTC+8) where trigger time just passed. + // When the server is at UTC+8, 02:01 local = 18:01 UTC (previous day). + // The next execution should be the NEXT day, not immediate re-trigger. + var sgTimezone = TimeZoneInfo.FindSystemTimeZoneById("Singapore Standard Time"); + yield return new object[] + { + new []{ new TimeSpan(2, 0, 0) }, // Trigger at 02:00 local + new DateTimeOffset(2024, 04, 17, 2, 01, 00, TimeSpan.FromHours(8)), // 02:01 SGT (= 16 Apr 18:01 UTC) + new DateTimeOffset(2024, 04, 18, 2, 00, 00, TimeSpan.FromHours(8)), // Next day 02:00 SGT + sgTimezone, + }; + + // Regression: Positive UTC offset (UTC+8) where trigger time is still ahead today. + yield return new object[] + { + new []{ new TimeSpan(10, 0, 0) }, // Trigger at 10:00 local + new DateTimeOffset(2024, 04, 17, 2, 00, 00, TimeSpan.FromHours(8)), // 02:00 SGT (= 16 Apr 18:00 UTC) + new DateTimeOffset(2024, 04, 17, 10, 00, 00, TimeSpan.FromHours(8)), // Same day 10:00 SGT + sgTimezone, + }; } } } diff --git a/MFiles.VAF.Extensions.Tests/Configuration/ScheduledExecution/ScheduleTests.cs b/MFiles.VAF.Extensions.Tests/Configuration/ScheduledExecution/ScheduleTests.cs index ba5f572..c0a4661 100644 --- a/MFiles.VAF.Extensions.Tests/Configuration/ScheduledExecution/ScheduleTests.cs +++ b/MFiles.VAF.Extensions.Tests/Configuration/ScheduledExecution/ScheduleTests.cs @@ -383,6 +383,69 @@ public static IEnumerable GetNextExecutionData_AusEastern() }; } + [TestMethod] + [DynamicData(nameof(GetNextExecutionData_Singapore), DynamicDataSourceType.Method)] + public void GetNextExecution_Singapore + ( + IEnumerable triggers, + DateTime? after, + DateTime? expected + ) + { + var execution = new Schedule() + { + Enabled = true, + Triggers = triggers + .Select(t => new Trigger(t)) + .Where(t => t != null) + .ToList(), + TriggerTimeType = TriggerTimeType.Custom, + TriggerTimeCustomTimeZone = "Singapore Standard Time" + }.GetNextExecution(after); + Assert.AreEqual(expected?.ToUniversalTime(), execution?.ToUniversalTime()); + } + + /// + /// Returns data for a high positive UTC offset (UTC+8) where the UTC day differs from local day. + /// Regression test: ensures trigger time just passed does not cause immediate re-trigger. + /// + public static IEnumerable GetNextExecutionData_Singapore() + { + // Trigger at 02:00 SGT, current time is 02:01 SGT (= 18:01 UTC previous day). + // Next execution should be tomorrow at 02:00 SGT, NOT immediate. + yield return new object[] + { + new TriggerBase[] + { + new DailyTrigger(){ + TriggerTimes = new List() + { + new TimeSpan(2, 0, 0) + }.ToList() + } + }, + new DateTime(2024, 04, 16, 18, 01, 00, DateTimeKind.Utc), // 02:01 SGT on 17th = 18:01 UTC on 16th + new DateTime(2024, 04, 17, 18, 00, 00, DateTimeKind.Utc), // 02:00 SGT on 18th = 18:00 UTC on 17th + }; + + // Trigger at 10:00 SGT, current time is 02:00 SGT (= 18:00 UTC previous day). + // Next execution should be today at 10:00 SGT. + yield return new object[] + { + new TriggerBase[] + { + new DailyTrigger(){ + TriggerTimes = new List() + { + new TimeSpan(10, 0, 0) + }.ToList() + } + }, + new DateTime(2024, 04, 16, 18, 00, 00, DateTimeKind.Utc), // 02:00 SGT on 17th = 18:00 UTC on 16th + new DateTime(2024, 04, 17, 02, 00, 00, DateTimeKind.Utc), // 10:00 SGT on 17th = 02:00 UTC on 17th + }; + } + /// /// Returns data for a high UTC offset where the trigger times are the next day in UTC. /// diff --git a/MFiles.VAF.Extensions/Configuration/ScheduledExecution/DailyTrigger.cs b/MFiles.VAF.Extensions/Configuration/ScheduledExecution/DailyTrigger.cs index e11e28d..c4abc53 100644 --- a/MFiles.VAF.Extensions/Configuration/ScheduledExecution/DailyTrigger.cs +++ b/MFiles.VAF.Extensions/Configuration/ScheduledExecution/DailyTrigger.cs @@ -59,8 +59,7 @@ public DailyTrigger() timeZoneInfo = timeZoneInfo ?? TimeZoneInfo.Local; // When should we start looking? - var before = (after ?? DateTime.UtcNow); - after = before.ToUniversalTime(); + after = (after ?? DateTime.UtcNow).ToUniversalTime(); // Convert the time into the timezone we're after. after = TimeZoneInfo.ConvertTime(after.Value, timeZoneInfo); @@ -74,12 +73,7 @@ public DailyTrigger() // What is the potential time that this will run? DateTimeOffset potential; { - // If the timezone conversion changed the date then go back to the start of the date. - var date = after.Value.Date; - if (after.Value.Date != before.Date) - date = new DateTime(before.Date.Ticks); - - var dateTime = date.Add(t); + var dateTime = after.Value.Date.Add(t); potential = new DateTimeOffset(dateTime, timeZoneInfo.GetUtcOffset(dateTime)); } diff --git a/MFiles.VAF.Extensions/Configuration/ScheduledExecution/DayOfMonthTrigger.cs b/MFiles.VAF.Extensions/Configuration/ScheduledExecution/DayOfMonthTrigger.cs index 89c006f..95d3eb3 100644 --- a/MFiles.VAF.Extensions/Configuration/ScheduledExecution/DayOfMonthTrigger.cs +++ b/MFiles.VAF.Extensions/Configuration/ScheduledExecution/DayOfMonthTrigger.cs @@ -89,6 +89,7 @@ public DayOfMonthTrigger() ( d => GetNextDayOfMonth(after.Value, d, this.UnrepresentableDateHandling) ) + .Select(d => new DateTimeOffset(d.DateTime, timeZoneInfo.GetUtcOffset(d.DateTime))) .Select ( d => new DailyTrigger() { Type = ScheduleTriggerType.Daily, TriggerTimes = this.TriggerTimes } diff --git a/MFiles.VAF.Extensions/Configuration/ScheduledExecution/WeeklyTrigger.cs b/MFiles.VAF.Extensions/Configuration/ScheduledExecution/WeeklyTrigger.cs index 17ed8f3..1b1fe4e 100644 --- a/MFiles.VAF.Extensions/Configuration/ScheduledExecution/WeeklyTrigger.cs +++ b/MFiles.VAF.Extensions/Configuration/ScheduledExecution/WeeklyTrigger.cs @@ -65,6 +65,7 @@ public WeeklyTrigger() // Get the next execution time (this will not find run times today). var potentialMatches = this.TriggerDays .SelectMany(d => GetNextDayOfWeek(after.Value, d)) + .Select(d => new DateTimeOffset(d.DateTime, timeZoneInfo.GetUtcOffset(d.DateTime))) .Select ( d => new DailyTrigger() { Type = ScheduleTriggerType.Daily, TriggerTimes = this.TriggerTimes }