Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 8s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m20s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m1s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 14m21s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14m18s
Review follow-ups (cold review of PR #457): - Marathon can reach PlayoutBuilder's default arm via `goto default` when its enumerator can't be built; the new warning claimed "Marathon is not supported by classic scheduling", which is false. Distinguish supported-but-failed-to-build (logs "could not build") from genuinely unsupported using PlaybackOrderSupport, which also makes Classic a runtime consumer of the matrix. - PlaybackOrderSupportTests now asserts every SchedulingEngineKind has a matrix entry, so a new engine kind fails the test instead of throwing KeyNotFoundException at runtime. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
94 lines
3.8 KiB
C#
94 lines
3.8 KiB
C#
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<PlaybackOrder>();
|
||
|
||
// 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<SchedulingEngineKind>())
|
||
{
|
||
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<PlaybackOrder> supported = PlaybackOrderSupport.SupportedBy(engine);
|
||
IReadOnlySet<PlaybackOrder> 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)");
|
||
}
|
||
}
|
||
}
|