Files
ersatztv/ErsatzTV.Core.Tests/Scheduling/ContentEnumeratorBuilderTests.cs
T
timothyandClaude Fable 5.1 d6e4426aa9 docs(563): retire the forward-references the supersession falsified, and trim the record to what only it says
Two pointers still named #563 as the open reason scripted playout has no
coverage. The ContentEnumeratorBuilderTests header now points at the
successor record and docs/testing.md instead of the issue this branch
closes. docs/decisions.md carried an orphaned fragment, "external-process
pipeline remains #563's", in the residual block under ## Index -- with the
pipeline now permanently outside the automated suite rather than deferred,
the fragment states something false and has no recoverable subject to
rewrite it around, so it goes.

The record's rule gains the two things measurement settled: that
ApiJsonSettings shares production's configuration and never MVC's settings
object (MaxDepth 32, the two ProblemDetails converters, pinned by
ApiJsonSettingsTests), and that a fixture must aim every trimming
instruction between two content boundaries or that action's trim argument
is witnessed by nothing. Its body loses the mutant table and the
extraction paragraph, which the mechanics doc its own frontmatter points
at carries verbatim; what remains is the conclusion plus the reasoning
that exists nowhere else. 70 prose lines to 56, under the advisory ceiling
without dropping a distinct finding.

Refs #563

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-05 09:25:11 +02:00

122 lines
4.8 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 permanently outside the
// automated suite, and the engine behind it is covered in-process instead (decision
// testing.scripted-engine-in-process-net, docs/testing.md -> "Scripted playout coverage"). This direct
// helper test is the regression net for the shared construction that engine 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) }
]
};
}