using ErsatzTV.Core.Domain; using ErsatzTV.Core.Scheduling; using NUnit.Framework; using Shouldly; namespace ErsatzTV.Core.Tests.Scheduling; [TestFixture] public class PlaybackOrderSupportTests { private static readonly PlaybackOrder[] AllOrders = Enum.GetValues(); // The tripwire (#403): every engine must classify every PlaybackOrder value as either supported or // explicitly unsupported. Adding a new order without classifying it here fails this test, which forces the // author to wire it into (or deliberately reject it from) each dispatch site instead of letting it degrade // silently. [Test] public void EveryOrder_IsClassified_ForEveryEngine() { foreach (SchedulingEngineKind engine in PlaybackOrderSupport.Engines) { foreach (PlaybackOrder order in AllOrders) { PlaybackOrderSupport.IsClassified(engine, order).ShouldBeTrue( $"PlaybackOrder.{order} is not classified for {engine}. Add it to PlaybackOrderSupport " + "(supported or unsupported) AND wire it into that engine's dispatch switch (#403)."); } } } // Every SchedulingEngineKind must have a matrix entry, or Matrix[engine] throws KeyNotFoundException at // runtime instead of failing here. This is the engine-axis counterpart to the order tripwire. [Test] public void EveryEngineKind_HasAMatrixEntry() { var classified = PlaybackOrderSupport.Engines.ToHashSet(); foreach (SchedulingEngineKind engine in Enum.GetValues()) { classified.ShouldContain(engine, $"SchedulingEngineKind.{engine} has no PlaybackOrderSupport matrix entry (#403)."); } } // The two sets must partition the enum: no order both supported and unsupported, and together they cover // exactly the enum (no stale entry for a removed value, no missing value). [Test] public void SupportedAndUnsupported_ArePartition_ForEveryEngine() { var all = AllOrders.ToHashSet(); foreach (SchedulingEngineKind engine in PlaybackOrderSupport.Engines) { IReadOnlySet supported = PlaybackOrderSupport.SupportedBy(engine); IReadOnlySet unsupported = PlaybackOrderSupport.UnsupportedBy(engine); supported.Intersect(unsupported).ShouldBeEmpty( $"{engine}: an order is listed as both supported and unsupported"); var union = supported.Concat(unsupported).ToHashSet(); union.ShouldBe(all, ignoreOrder: true, $"{engine}: supported ∪ unsupported does not equal the PlaybackOrder enum"); } } // Guards the specific fragility called out in #403: Random is in Block's allow-list, so it must be // supported by Block (it previously worked only via the switch's coincidental Random fallback). [Test] public void Block_Supports_Random() { PlaybackOrderSupport.IsSupported(SchedulingEngineKind.Block, PlaybackOrder.Random).ShouldBeTrue(); } // WeightedShuffle (#70) is Classic-only; the other engines must classify it as unsupported so the // write-path guards and this matrix agree. [Test] public void WeightedShuffle_IsClassicOnly() { PlaybackOrderSupport.IsSupported(SchedulingEngineKind.Classic, PlaybackOrder.WeightedShuffle) .ShouldBeTrue(); foreach (SchedulingEngineKind engine in PlaybackOrderSupport.Engines) { if (engine == SchedulingEngineKind.Classic) { continue; } PlaybackOrderSupport.IsSupported(engine, PlaybackOrder.WeightedShuffle).ShouldBeFalse( $"{engine} must not support WeightedShuffle (#70 is Classic-only)"); } } }