diff --git a/ErsatzTV.Core.Tests/Scheduling/SequentialScheduleSelectorTests.cs b/ErsatzTV.Core.Tests/Scheduling/SequentialScheduleSelectorTests.cs new file mode 100644 index 000000000..e1eba0a41 --- /dev/null +++ b/ErsatzTV.Core.Tests/Scheduling/SequentialScheduleSelectorTests.cs @@ -0,0 +1,168 @@ +using ErsatzTV.Core.Scheduling.YamlScheduling; +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Core.Tests.Scheduling; + +public static class SequentialScheduleSelectorTests +{ + [TestFixture] + public class GetActiveSchedule + { + private static readonly TimeSpan Offset = TimeSpan.FromHours(-5); + + [Test] + public void Should_Return_None_When_No_Schedules() + { + Option result = SequentialScheduleSelector.GetActiveSchedule( + new List(), + new DateTimeOffset(2025, 4, 15, 12, 0, 0, Offset)); + + result.IsNone.ShouldBeTrue(); + } + + [Test] + public void Should_Match_Annual_Range_Every_Year() + { + var april = new YamlPlayoutScheduleItem + { + Name = "April", + StartDate = "04-01", + EndDate = "04-30" + }; + + SequentialScheduleSelector + .GetActiveSchedule([april], new DateTimeOffset(2025, 4, 15, 12, 0, 0, Offset)) + .IfNone(() => null).ShouldBe(april); + + SequentialScheduleSelector + .GetActiveSchedule([april], new DateTimeOffset(2030, 4, 1, 0, 0, 0, Offset)) + .IfNone(() => null).ShouldBe(april); + } + + [Test] + public void Should_Not_Match_Annual_Range_Outside_Dates() + { + var april = new YamlPlayoutScheduleItem + { + Name = "April", + StartDate = "04-01", + EndDate = "04-30" + }; + + SequentialScheduleSelector + .GetActiveSchedule([april], new DateTimeOffset(2025, 5, 1, 0, 0, 0, Offset)) + .IsNone.ShouldBeTrue(); + } + + [Test] + public void Should_Match_Specific_Year_Only() + { + var christmas = new YamlPlayoutScheduleItem + { + Name = "Christmas 2025", + StartDate = "2025-12-25", + EndDate = "2025-12-25" + }; + + SequentialScheduleSelector + .GetActiveSchedule([christmas], new DateTimeOffset(2025, 12, 25, 8, 0, 0, Offset)) + .IfNone(() => null).ShouldBe(christmas); + + SequentialScheduleSelector + .GetActiveSchedule([christmas], new DateTimeOffset(2026, 12, 25, 8, 0, 0, Offset)) + .IsNone.ShouldBeTrue(); + } + + [Test] + public void Should_Match_Annual_Range_That_Wraps_Year_Boundary() + { + var winter = new YamlPlayoutScheduleItem + { + Name = "Winter", + StartDate = "12-01", + EndDate = "01-15" + }; + + SequentialScheduleSelector + .GetActiveSchedule([winter], new DateTimeOffset(2025, 12, 20, 0, 0, 0, Offset)) + .IfNone(() => null).ShouldBe(winter); + + SequentialScheduleSelector + .GetActiveSchedule([winter], new DateTimeOffset(2026, 1, 10, 0, 0, 0, Offset)) + .IfNone(() => null).ShouldBe(winter); + + SequentialScheduleSelector + .GetActiveSchedule([winter], new DateTimeOffset(2026, 6, 1, 0, 0, 0, Offset)) + .IsNone.ShouldBeTrue(); + } + + [Test] + public void Should_Prefer_Higher_Priority_On_Overlap() + { + var december = new YamlPlayoutScheduleItem + { + Name = "December", + StartDate = "12-01", + EndDate = "12-31", + Priority = 0 + }; + + var christmasDay = new YamlPlayoutScheduleItem + { + Name = "Christmas Day", + StartDate = "12-25", + EndDate = "12-25", + Priority = 10 + }; + + SequentialScheduleSelector + .GetActiveSchedule([december, christmasDay], new DateTimeOffset(2025, 12, 25, 12, 0, 0, Offset)) + .IfNone(() => null).ShouldBe(christmasDay); + + SequentialScheduleSelector + .GetActiveSchedule([december, christmasDay], new DateTimeOffset(2025, 12, 20, 12, 0, 0, Offset)) + .IfNone(() => null).ShouldBe(december); + } + + [Test] + public void Should_Break_Priority_Ties_By_Definition_Order() + { + var first = new YamlPlayoutScheduleItem + { + Name = "First", + StartDate = "04-01", + EndDate = "04-30", + Priority = 5 + }; + + var second = new YamlPlayoutScheduleItem + { + Name = "Second", + StartDate = "04-01", + EndDate = "04-30", + Priority = 5 + }; + + SequentialScheduleSelector + .GetActiveSchedule([first, second], new DateTimeOffset(2025, 4, 15, 12, 0, 0, Offset)) + .IfNone(() => null).ShouldBe(first); + } + + [Test] + public void Should_Not_Match_Invalid_Or_Missing_Dates() + { + var invalid = new YamlPlayoutScheduleItem + { + Name = "Invalid", + StartDate = "not-a-date", + EndDate = "04-30" + }; + + SequentialScheduleSelector + .GetActiveSchedule([invalid], new DateTimeOffset(2025, 4, 15, 12, 0, 0, Offset)) + .IsNone.ShouldBeTrue(); + } + } +} diff --git a/ErsatzTV.Core.Tests/Scheduling/YamlPlayoutContextTests.cs b/ErsatzTV.Core.Tests/Scheduling/YamlPlayoutContextTests.cs new file mode 100644 index 000000000..107fdd4e4 --- /dev/null +++ b/ErsatzTV.Core.Tests/Scheduling/YamlPlayoutContextTests.cs @@ -0,0 +1,77 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling.YamlScheduling; +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Core.Tests.Scheduling; + +public static class YamlPlayoutContextTests +{ + [TestFixture] + public class ScheduleSwitching + { + private static YamlPlayoutContext CreateContext() + { + var definition = new YamlPlayoutDefinition + { + Playout = [new YamlPlayoutInstruction()], + Schedules = + [ + new YamlPlayoutScheduleItem + { + Name = "Christmas", + StartDate = "12-25", + EndDate = "12-25", + Playout = [new YamlPlayoutInstruction()] + } + ] + }; + + return new YamlPlayoutContext(new Playout(), definition, 1); + } + + [Test] + public void Switching_Should_Not_Leak_Graphics_Elements_Across_Lists() + { + YamlPlayoutContext context = CreateContext(); + + // default playout turns a graphics element on + context.SetGraphicsElement(1, null); + context.GetGraphicsElements().ShouldContainKey(1); + + // crossing into the schedule should start with a clean ambient state + context.SwitchToSchedule("Christmas"); + context.GetGraphicsElements().ShouldNotContainKey(1); + + // the schedule can turn on the same element without throwing + Should.NotThrow(() => context.SetGraphicsElement(1, null)); + context.GetGraphicsElements().ShouldContainKey(1); + } + + [Test] + public void Returning_To_Default_Should_Restore_Its_Graphics_Elements() + { + YamlPlayoutContext context = CreateContext(); + + context.SetGraphicsElement(1, "default-vars"); + context.SwitchToSchedule("Christmas"); + context.SetGraphicsElement(1, "christmas-vars"); + + // returning to the default playout restores its ambient state + context.SwitchToSchedule(null); + context.GetGraphicsElements().ShouldContainKey(1); + context.GetGraphicsElements()[1].ShouldBe("default-vars"); + } + + [Test] + public void SetGraphicsElement_Should_Be_Idempotent() + { + YamlPlayoutContext context = CreateContext(); + + context.SetGraphicsElement(1, "a"); + Should.NotThrow(() => context.SetGraphicsElement(1, "b")); + context.GetGraphicsElements()[1].ShouldBe("b"); + } + } +}