PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m25s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m21s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m36s
PR Gates / decisions lifecycle (pull_request) Failing after 14s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been cancelled
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Has been cancelled
Extract the byte-identical PlaybackOrder -> IMediaCollectionEnumerator switch shared by SchedulingEngine.EnumeratorForContent (Scripted) and EnumeratorCache.GetEnumeratorForContent (Sequential/YAML) into one static per-family seam, mirroring #380's ShuffleSourceBuilder. Each engine keeps its own "not supported" warning on the None branch, so the per-engine message is unchanged. Adds ContentEnumeratorBuilderTests pinning the block-shuffle-not-classic trap and the unsupported-order -> None (#70) contract across all 8 unsupported orders. Corrects the testing.scripted-playout-golden-deferred decision record: Scripted's external-process + HTTP pipeline is integration-only (deferred to #563), but the in-process SchedulingEngine it drives IS unit-testable (ScriptedScheduleController is a 1:1 pass-through) -- the earlier "un-golden-able by construction" framing conflated transport with engine. docs/testing.md reframed to match. [decisions-edit] fixes #395 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
4.7 KiB
C#
120 lines
4.7 KiB
C#
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Interfaces.Scheduling;
|
|
using ErsatzTV.Core.Scheduling;
|
|
using ErsatzTV.Core.Scheduling.BlockScheduling;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Core.Tests.Scheduling;
|
|
|
|
// Direct unit coverage for the enumerator-construction helper extracted from the Scripted
|
|
// (SchedulingEngine.EnumeratorForContent) and Sequential/YAML (EnumeratorCache.GetEnumeratorForContent)
|
|
// engines (#395). These pin the two behaviors the issue flags as traps:
|
|
// - Shuffle must build the *block* enumerator, NOT Classic's ShuffledMediaCollectionEnumerator (a
|
|
// different algorithm keyed on the same PlaybackOrder), and
|
|
// - every order the two engines don't support returns None, so each caller logs its own #70 warning
|
|
// instead of silently scheduling nothing.
|
|
// The Scripted engine has no golden (its external-process/HTTP transport is integration-only, #563), so
|
|
// this direct helper test is the in-process regression net for the shared construction it drives.
|
|
[TestFixture]
|
|
public class ContentEnumeratorBuilderTests
|
|
{
|
|
[Test]
|
|
public void Chronological_Builds_Chronological_Enumerator()
|
|
{
|
|
Option<IMediaCollectionEnumerator> result = ContentEnumeratorBuilder.ForContent(
|
|
[FakeMovie(1), FakeMovie(2)],
|
|
new CollectionEnumeratorState(),
|
|
PlaybackOrder.Chronological,
|
|
multiPart: false);
|
|
|
|
result.IsSome.ShouldBeTrue();
|
|
foreach (IMediaCollectionEnumerator enumerator in result)
|
|
{
|
|
enumerator.ShouldBeOfType<ChronologicalMediaCollectionEnumerator>();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Shuffle_Builds_Block_Shuffle_Enumerator_Not_Classic()
|
|
{
|
|
Option<IMediaCollectionEnumerator> result = ContentEnumeratorBuilder.ForContent(
|
|
[FakeMovie(1), FakeMovie(2)],
|
|
new CollectionEnumeratorState(),
|
|
PlaybackOrder.Shuffle,
|
|
multiPart: false);
|
|
|
|
result.IsSome.ShouldBeTrue();
|
|
foreach (IMediaCollectionEnumerator enumerator in result)
|
|
{
|
|
// The documented trap: Shuffle here is the block algorithm, never Classic's shuffle.
|
|
enumerator.ShouldBeOfType<BlockPlayoutShuffledMediaCollectionEnumerator>();
|
|
enumerator.ShouldNotBeOfType<ShuffledMediaCollectionEnumerator>();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Shuffle_MultiPart_Also_Builds_Block_Shuffle_Enumerator()
|
|
{
|
|
// "(1)"/"(2)" are a two-part episode MultiPartEpisodeGrouper keeps together; multiPart routes the
|
|
// items through it before the block enumerator. Grouping mechanics are covered by
|
|
// MultiPartEpisodeGrouper's / ShuffleSourceBuilder's own tests; here we pin that the flag path still
|
|
// produces the block enumerator (and doesn't throw on the grouped list).
|
|
List<MediaItem> parts =
|
|
[
|
|
NamedEpisode("Episode 1 (1)", 1),
|
|
NamedEpisode("Episode 2 (2)", 2)
|
|
];
|
|
|
|
Option<IMediaCollectionEnumerator> result = ContentEnumeratorBuilder.ForContent(
|
|
parts,
|
|
new CollectionEnumeratorState(),
|
|
PlaybackOrder.Shuffle,
|
|
multiPart: true);
|
|
|
|
result.IsSome.ShouldBeTrue();
|
|
foreach (IMediaCollectionEnumerator enumerator in result)
|
|
{
|
|
enumerator.ShouldBeOfType<BlockPlayoutShuffledMediaCollectionEnumerator>();
|
|
}
|
|
}
|
|
|
|
[TestCase(PlaybackOrder.None)]
|
|
[TestCase(PlaybackOrder.Random)]
|
|
[TestCase(PlaybackOrder.ShuffleInOrder)]
|
|
[TestCase(PlaybackOrder.MultiEpisodeShuffle)]
|
|
[TestCase(PlaybackOrder.SeasonEpisode)]
|
|
[TestCase(PlaybackOrder.RandomRotation)]
|
|
[TestCase(PlaybackOrder.Marathon)]
|
|
[TestCase(PlaybackOrder.WeightedShuffle)]
|
|
public void Unsupported_Order_Returns_None(PlaybackOrder order)
|
|
{
|
|
// #70: these two engines support only Chronological + Shuffle; every other order returns None so the
|
|
// caller logs a "not supported" warning instead of silently scheduling nothing.
|
|
Option<IMediaCollectionEnumerator> result = ContentEnumeratorBuilder.ForContent(
|
|
[FakeMovie(1)],
|
|
new CollectionEnumeratorState(),
|
|
order,
|
|
multiPart: false);
|
|
|
|
result.IsNone.ShouldBeTrue();
|
|
}
|
|
|
|
private static Episode NamedEpisode(string title, int id) => new()
|
|
{
|
|
Id = id,
|
|
EpisodeMetadata = [new EpisodeMetadata { Title = title, EpisodeNumber = id }],
|
|
Season = new Season { SeasonNumber = 1, Show = new Show { Id = 1 }, ShowId = 1 }
|
|
};
|
|
|
|
private static Movie FakeMovie(int id) => new()
|
|
{
|
|
Id = id,
|
|
MediaVersions = [],
|
|
MovieMetadata =
|
|
[
|
|
new MovieMetadata { ReleaseDate = new DateTime(2020, 1, id) }
|
|
]
|
|
};
|
|
}
|