test(381): golden coverage for Sequential (YAML) playout builder; document Scripted deferral
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 28s
PR Gates / Docs update reminder (pull_request) Successful in 46s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m53s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 18s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m29s
PR Gates / decisions lifecycle (pull_request) Successful in 18s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 21m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 28s
PR Gates / Docs update reminder (pull_request) Successful in 46s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m53s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 18s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m29s
PR Gates / decisions lifecycle (pull_request) Successful in 18s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 21m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Extends the #163 PlayoutBuildGoldenTests in-memory net to the Sequential (YAML) builder: a committed fixture (Goldens/Fixtures/sequential-schedule.yml) with two `count: 2` instructions over one chronological collection, built via SequentialPlayoutBuilder over the pinned window. The count/all/duration handlers do UTC-only arithmetic off the caller-supplied start, so the case is TZ-independent (passes, not skips, under a non-UTC TZ) and needs no Assume guard. Non-vacuity: a fixture count tweak flips the golden + the contiguity assertion. Scripted is deliberately excluded from the golden net — ScriptedPlayoutBuilder shells out via Cli.Wrap to an external process that drives SchedulingEngine over HTTP, which no in-memory golden can characterize. Recorded as the Done-when "documented decision" arm in docs/decisions.md (testing.scripted-playout-golden-deferred) + docs/testing.md; the scripted integration harness is tracked as follow-up #563. fixes #381 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Deterministic Sequential (YAML) schedule fixture for
|
||||
# PlayoutBuildGoldenTests.Sequential_yaml (ersatztv#381).
|
||||
#
|
||||
# Two `count` instructions over ONE chronological collection. The content enumerator is cached by key,
|
||||
# so it continues across the two instructions: items 1-2 come from the first `count`, items 3-4 from the
|
||||
# second. `order: chronological` + literal integer counts keep the build free of shuffle-seed, RNG, and
|
||||
# wall-clock/local-time dependence, so the snapshot of raw UTC Start/Finish is machine-timezone-independent
|
||||
# (no Assume guard needed, unlike the Block golden). Do not introduce `wait_until` / `pad_to_next` /
|
||||
# `pad_until` (local-time-of-day) or a `shuffle` order without revisiting that determinism claim.
|
||||
content:
|
||||
- collection: Sequential Test Collection
|
||||
key: movies
|
||||
order: chronological
|
||||
playout:
|
||||
- count: 2
|
||||
content: movies
|
||||
- count: 2
|
||||
content: movies
|
||||
@@ -0,0 +1,4 @@
|
||||
000 | 2026-01-15 06:00:00 - 2026-01-15 06:30:00 | None | Sequential Movie 01
|
||||
001 | 2026-01-15 06:30:00 - 2026-01-15 07:15:00 | None | Sequential Movie 02
|
||||
002 | 2026-01-15 07:15:00 - 2026-01-15 08:15:00 | None | Sequential Movie 03
|
||||
003 | 2026-01-15 08:15:00 - 2026-01-15 08:45:00 | None | Sequential Movie 04
|
||||
@@ -6,10 +6,12 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Core.Scheduling.BlockScheduling;
|
||||
using ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
using ErsatzTV.Infrastructure;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Data.Repositories;
|
||||
@@ -134,6 +136,36 @@ public class PlayoutBuildGoldenTests
|
||||
[Test]
|
||||
public Task Classic_weighted() => Verify("classic-weighted.txt", BuildWeightedPlayout);
|
||||
|
||||
// Sequential (YAML) builder (#381, follow-up to #163). SequentialPlayoutBuilder reads a YAML schedule
|
||||
// file (Playout.ScheduleFile) instead of a ProgramSchedule/Block calendar. The committed fixture
|
||||
// Goldens/Fixtures/sequential-schedule.yml schedules two `count: 2` instructions over one chronological
|
||||
// collection, so the builder lays exactly four items back-to-back from the pinned start (the enumerator
|
||||
// is cached by content key and continues across the two instructions). Besides the golden we assert the
|
||||
// contiguity invariant: it is what "sequential" means here, and a deliberate change to a count or a
|
||||
// duration flips both the assertion and the golden (#12 non-vacuity). The count/all/duration handlers do
|
||||
// pure UTC arithmetic off the caller-supplied start (no TimeZoneInfo.Local / ToLocalTime), so this case
|
||||
// is TZ-independent and needs no Assume guard — unlike Block, and unlike the wait_until/pad_* handlers
|
||||
// the fixture deliberately avoids.
|
||||
[Test]
|
||||
public async Task Sequential_yaml()
|
||||
{
|
||||
(List<PlayoutItem> items, Dictionary<int, string> titles) = await BuildSequentialPlayout();
|
||||
|
||||
List<PlayoutItem> ordered = items.OrderBy(i => i.Start).ToList();
|
||||
|
||||
ordered.Count.ShouldBe(4);
|
||||
ordered.ShouldAllBe(i => i.FillerKind == FillerKind.None);
|
||||
ordered[0].Start.ShouldBe(Start.UtcDateTime);
|
||||
for (var i = 1; i < ordered.Count; i++)
|
||||
{
|
||||
ordered[i].Start.ShouldBe(
|
||||
ordered[i - 1].Finish,
|
||||
$"sequential item {i} at {ordered[i].Start:HH:mm:ss} is not contiguous with the previous finish");
|
||||
}
|
||||
|
||||
await CompareGolden("sequential-yaml.txt", items, titles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit("Regenerates all playout goldens from current output; review the diff before committing.")]
|
||||
public async Task Regenerate_goldens()
|
||||
@@ -142,7 +174,10 @@ public class PlayoutBuildGoldenTests
|
||||
try
|
||||
{
|
||||
foreach (Func<Task> regen in new Func<Task>[]
|
||||
{ Classic_chronological, Block_playout, Classic_clock_padded, Classic_shuffle })
|
||||
{
|
||||
Classic_chronological, Block_playout, Classic_clock_padded, Classic_shuffle,
|
||||
Classic_weighted, Sequential_yaml
|
||||
})
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -1141,6 +1176,166 @@ public class PlayoutBuildGoldenTests
|
||||
TimeSpan.Zero);
|
||||
}
|
||||
|
||||
// --- Sequential (YAML) builder (issue #381) ---
|
||||
//
|
||||
// SequentialPlayoutBuilder reads its schedule from a YAML file at Playout.ScheduleFile. It checks the
|
||||
// file's existence through the injected IFileSystem but reads the bytes with the static System.IO.File,
|
||||
// so the test writes a REAL committed fixture on disk (Goldens/Fixtures/sequential-schedule.yml) and
|
||||
// only stubs IFileSystem.File.Exists -> true. The schema validator is stubbed (the real one loads a JSON
|
||||
// schema from a runtime cache folder that a unit test has no reason to populate); this golden locks the
|
||||
// BUILDER's PlayoutItem output, not the validator, which is a separate surface.
|
||||
private async Task<(List<PlayoutItem> Items, Dictionary<int, string> Titles)> BuildSequentialPlayout()
|
||||
{
|
||||
var cancellationToken = CancellationToken.None;
|
||||
|
||||
var (playoutId, titles) = await SeedSequentialData(cancellationToken);
|
||||
|
||||
var fileSystem = Substitute.For<System.IO.Abstractions.IFileSystem>();
|
||||
fileSystem.File.Exists(Arg.Any<string>()).Returns(true);
|
||||
|
||||
var validator = Substitute.For<ISequentialScheduleValidator>();
|
||||
validator.ValidateSchedule(Arg.Any<string>(), Arg.Any<bool>()).Returns(Task.FromResult(true));
|
||||
|
||||
var builder = new SequentialPlayoutBuilder(
|
||||
fileSystem,
|
||||
new ConfigElementRepository(_dbContextFactory),
|
||||
new MediaCollectionRepository(Substitute.For<ISearchIndex>(), _dbContextFactory),
|
||||
Substitute.For<IChannelRepository>(),
|
||||
Substitute.For<IGraphicsElementRepository>(),
|
||||
validator,
|
||||
NullLogger<SequentialPlayoutBuilder>.Instance);
|
||||
|
||||
await using TvContext context = _dbContextFactory.CreateDbContext();
|
||||
|
||||
Playout playout = await context.Playouts
|
||||
.Include(p => p.ProgramScheduleAnchors)
|
||||
.ThenInclude(a => a.EnumeratorState)
|
||||
.Include(p => p.FillGroupIndices)
|
||||
.ThenInclude(fgi => fgi.EnumeratorState)
|
||||
.SingleAsync(p => p.Id == playoutId, cancellationToken);
|
||||
|
||||
PlayoutReferenceData referenceData = await GetSequentialReferenceData(context, playoutId);
|
||||
|
||||
// Reset over the pinned window: with no prior Anchor, Reset avoids the YamlPlayoutContext.Reset path
|
||||
// (its ToLocalTime() only runs on a saved-anchor Continue), keeping the build TZ-independent.
|
||||
Either<BaseError, PlayoutBuildResult> result = await builder.Build(
|
||||
Start,
|
||||
playout,
|
||||
referenceData,
|
||||
PlayoutBuildMode.Reset,
|
||||
cancellationToken);
|
||||
|
||||
PlayoutBuildResult buildResult = result.Match(
|
||||
r => r,
|
||||
error => throw new AssertionException($"Build returned error: {error.Value}"));
|
||||
|
||||
return (buildResult.AddedItems, titles);
|
||||
}
|
||||
|
||||
private async Task<(int PlayoutId, Dictionary<int, string> Titles)> SeedSequentialData(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext context = _dbContextFactory.CreateDbContext();
|
||||
|
||||
var path = new LibraryPath { Path = "Sequential LibraryPath" };
|
||||
var library = new LocalLibrary
|
||||
{
|
||||
MediaKind = LibraryMediaKind.Movies,
|
||||
Paths = new List<LibraryPath> { path },
|
||||
MediaSource = new LocalMediaSource()
|
||||
};
|
||||
await context.Libraries.AddAsync(library, cancellationToken);
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Six movies, distinct release dates (so chronological order is unambiguous) and varied durations so
|
||||
// item boundaries are visible. Only the first four are scheduled (2 + 2 counts).
|
||||
int[] durationsMinutes = [30, 45, 60, 30, 45, 60];
|
||||
var movies = new List<Movie>();
|
||||
for (var i = 1; i <= 6; i++)
|
||||
{
|
||||
var movie = new Movie
|
||||
{
|
||||
MediaVersions = new List<MediaVersion>
|
||||
{
|
||||
new() { Duration = TimeSpan.FromMinutes(durationsMinutes[i - 1]) }
|
||||
},
|
||||
MovieMetadata = new List<MovieMetadata>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Title = $"Sequential Movie {i:D2}",
|
||||
ReleaseDate = new DateTime(2005, 1, 1).AddDays(i)
|
||||
}
|
||||
},
|
||||
LibraryPath = path,
|
||||
LibraryPathId = path.Id
|
||||
};
|
||||
movies.Add(movie);
|
||||
}
|
||||
|
||||
await context.Movies.AddRangeAsync(movies, cancellationToken);
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var titles = movies.ToDictionary(m => m.Id, m => m.MovieMetadata[0].Title);
|
||||
|
||||
// Name must match the fixture YAML's `collection:` value — EnumeratorCache resolves content by
|
||||
// Collection.Name.
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Sequential Test Collection",
|
||||
MediaItems = movies.Cast<MediaItem>().ToList()
|
||||
};
|
||||
await context.Collections.AddAsync(collection, cancellationToken);
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var ffmpegProfile = new FFmpegProfile { Name = "Sequential FFmpeg Profile" };
|
||||
await context.FFmpegProfiles.AddAsync(ffmpegProfile, cancellationToken);
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Number/GUID must be globally unique: every golden fixture shares one in-memory DB.
|
||||
var channel = new Channel(Guid.Parse("00000000-0000-0000-0000-000000000006"))
|
||||
{
|
||||
Name = "Sequential Test Channel",
|
||||
Number = "6",
|
||||
FFmpegProfile = ffmpegProfile,
|
||||
FFmpegProfileId = ffmpegProfile.Id
|
||||
};
|
||||
await context.Channels.AddAsync(channel, cancellationToken);
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Sequential playout: no ProgramSchedule; content comes from the YAML ScheduleFile.
|
||||
var playout = new Playout
|
||||
{
|
||||
Channel = channel,
|
||||
ChannelId = channel.Id,
|
||||
ScheduleKind = PlayoutScheduleKind.Sequential,
|
||||
ScheduleFile = Path.Combine(FixtureDir(), "sequential-schedule.yml")
|
||||
};
|
||||
await context.Playouts.AddAsync(playout, cancellationToken);
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return (playout.Id, titles);
|
||||
}
|
||||
|
||||
private static async Task<PlayoutReferenceData> GetSequentialReferenceData(TvContext dbContext, int playoutId)
|
||||
{
|
||||
Channel channel = await dbContext.Channels
|
||||
.AsNoTracking()
|
||||
.Where(c => c.Playouts.Any(p => p.Id == playoutId))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
// Sequential reads content from the YAML file, not a ProgramSchedule; empty history + a fresh build.
|
||||
return new PlayoutReferenceData(
|
||||
channel,
|
||||
Option<Deco>.None,
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
[],
|
||||
TimeSpan.Zero);
|
||||
}
|
||||
|
||||
// One line per PlayoutItem, ordered by Start then MediaItemId (stable tiebreak). Raw UTC Start/Finish
|
||||
// serialized invariant — NOT the *Offset properties (those localize). Title resolved from the seed map.
|
||||
private static string Snapshot(List<PlayoutItem> items, Dictionary<int, string> titles)
|
||||
@@ -1176,6 +1371,12 @@ public class PlayoutBuildGoldenTests
|
||||
private static string GoldenDir([CallerFilePath] string thisFile = "") =>
|
||||
Path.Combine(Path.GetDirectoryName(thisFile) ?? ".", "Goldens");
|
||||
|
||||
// Committed YAML input fixtures (not golden outputs) for builders that read a schedule file. Note
|
||||
// GoldenDir nests as Goldens/Goldens (this test file already lives under Goldens/), so fixtures sit in a
|
||||
// sibling Goldens/Fixtures to keep inputs and snapshot outputs visually separate.
|
||||
private static string FixtureDir([CallerFilePath] string thisFile = "") =>
|
||||
Path.Combine(Path.GetDirectoryName(thisFile) ?? ".", "Fixtures");
|
||||
|
||||
private sealed class TestTvContextFactory(DbContextOptions<TvContext> options) : IDbContextFactory<TvContext>
|
||||
{
|
||||
public TvContext CreateDbContext() =>
|
||||
|
||||
@@ -3473,3 +3473,31 @@ enabled`) because `/iptv/*` does not accept the SPA's `ctv-session` cookie and n
|
||||
for the browser. #552 closed that: the SPA now mints a short-lived token and appends it as
|
||||
`?access_token=`, so this projection no longer inspects JWT status at all. See
|
||||
`security.iptv-browser-token`.
|
||||
## 2026-07-22 — Sequential (YAML) playout gets a golden; Scripted is excluded from the golden net by construction (#381)
|
||||
|
||||
`key: testing.scripted-playout-golden-deferred` · `status: active` · `since: 2026-07-22` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** The `PlayoutBuildGoldenTests` in-memory golden net covers Sequential (YAML) as of #381, but Scripted playout is deliberately excluded from it — Scripted shells out to an external process that calls back over HTTP, which no in-memory golden can characterize; its integration harness is tracked separately.
|
||||
**Signals:** why is there no scripted golden; SequentialPlayoutBuilder golden; scripted playout is not deterministic in-process; Cli.Wrap external process; SchedulingEngine HTTP callback · paths: `ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs`, `ErsatzTV.Core.Tests/Scheduling/Goldens/Fixtures/sequential-schedule.yml`, `ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs`, `ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs` · issues: #381, #163, #563
|
||||
**Mechanics:** docs/testing.md → Golden-file nets
|
||||
|
||||
**Sequential (YAML) is golden-able and TZ-independent.** `SequentialPlayoutBuilder` reads a YAML schedule
|
||||
file (`Playout.ScheduleFile`) rather than a `ProgramSchedule`/Block calendar, but it is still a pure
|
||||
in-process build: content resolves from an in-memory-SQLite `Collection` by name, and the `count`/`all`/
|
||||
`duration` handlers do UTC-only arithmetic off the caller-supplied `start`. So the `Sequential_yaml` case
|
||||
uses the exact same harness as Classic/Block — a committed input fixture
|
||||
(`Goldens/Fixtures/sequential-schedule.yml`) with two `count: 2` instructions over one `chronological`
|
||||
collection — and needs **no** `Assume`/TZ guard (verified: it passes, not skips, under a non-UTC `TZ`,
|
||||
unlike Block). The fixture deliberately avoids the local-time-of-day handlers (`wait_until`, `pad_to_next`,
|
||||
`pad_until`) and shuffle order, which would reintroduce TZ- or seed-dependence. The builder checks the
|
||||
file via the injected `IFileSystem` but reads bytes with the static `System.IO.File`, so the test commits a
|
||||
real fixture on disk and stubs only `IFileSystem.File.Exists`; the JSON-schema validator is stubbed (it
|
||||
loads its schema from a runtime cache folder, irrelevant to characterizing builder output).
|
||||
|
||||
**Scripted is not golden-able.** `ScriptedPlayoutBuilder` builds nothing in-process: it `Cli.Wrap`-executes
|
||||
an external script process that drives `SchedulingEngine` over `http://localhost:{Settings.UiPort}`. An
|
||||
external-process + HTTP-callback flow cannot be pinned by the in-memory golden harness (shared SQLite, no
|
||||
I/O, deterministic clock). The deterministic core the scripts drive (`SchedulingEngine`) is in-process and
|
||||
unit-testable — there is a seed `SchedulingEngineTests` — but the scripted *builder* is process orchestration
|
||||
plus HTTP transport, i.e. integration-test territory. Rather than force a golden onto it, the scripted
|
||||
integration harness is deferred to **#563**; this is the Done-when-sanctioned "documented decision" arm of
|
||||
#381, not an omission.
|
||||
|
||||
@@ -157,4 +157,5 @@ the link for rationale. Superseded/retired history lives in `archive/`. Regenera
|
||||
| `testing.e2e-local-fresh-config-dir` | Always point `scripts/e2e-local.sh` at a fresh config dir — a reused one never emits the probe's ready line and the script kills a healthy server. | 2026-07-21 | [link](workflow-process.md#2026-07-21--run-scriptse2e-localsh-against-a-fresh-config-dir-a-reused-one-hangs-the-readiness-probe-542) |
|
||||
| `testing.live-e2e-prepush-timing` | Run live-E2E via `scripts/e2e-local.sh` before pushing a write-path or UI change, and exercise download endpoints with curl, never a browser tab. | 2026-07-21 | [link](workflow-process.md#2026-07-21--live-e2e-runs-before-the-push-and-downloads-are-curled-not-browsed-542) |
|
||||
| `testing.playwright-mcp-download-and-recovery` | In Playwright-MCP E2E, fetch file-download endpoints with curl — never a browser tab or `window.open` — and if browser tools stall repeatedly, `pkill -f ms-playwright-mcp` and drive a fresh session. | 2026-07-21 | [link](workflow-process.md#2026-07-21--playwright-mcp-curl-download-endpoints-never-open-a-tab-or-windowopen-542) |
|
||||
| `testing.scripted-playout-golden-deferred` | The `PlayoutBuildGoldenTests` in-memory golden net covers Sequential (YAML) as of #381, but Scripted playout is deliberately excluded from it — Scripted shells out to an external process that calls back over HTTP, which no in-memory golden can characterize; its integration harness is tracked separately. | 2026-07-22 | [link](../decisions.md#2026-07-22--sequential-yaml-playout-gets-a-golden-scripted-is-excluded-from-the-golden-net-by-construction-381) |
|
||||
| `testing.troubleshoot-path-cannot-test-branding` | Verify logo/watermark/bug changes through a real channel playout — a green troubleshoot run proves nothing about branding. | 2026-07-21 | [link](workflow-process.md#2026-07-21--channel-branding-is-not-testable-through-the-troubleshooting-playback-api-542) |
|
||||
|
||||
+9
-3
@@ -8,7 +8,7 @@ before adding tests, not just `docs/contributing.md` §8 (which now just points
|
||||
| Project | Covers | Notes |
|
||||
|---|---|---|
|
||||
| `ErsatzTV.Tests` | API controllers + MediatR handlers | In-memory SQLite fixture: a shared `SqliteConnection("Data Source=:memory:;Foreign Keys=False")` kept open + `EnsureCreatedAsync()` (**not** full migration replay) + `PRAGMA foreign_keys=OFF`, then seed; a tiny `IDbContextFactory` wraps `new TvContext(...)`. 828 tests currently. |
|
||||
| `ErsatzTV.Core.Tests` | Domain logic, scheduling, IPTV/XMLTV generation | References `ErsatzTV.Application` directly — there is no separate `Application.Tests` project. 542 tests + 1 skipped under `TZ=UTC` (the Block playout golden additionally skips under a non-UTC `TZ`; see Golden-file nets). |
|
||||
| `ErsatzTV.Core.Tests` | Domain logic, scheduling, IPTV/XMLTV generation | References `ErsatzTV.Application` directly — there is no separate `Application.Tests` project. 543 tests + 1 skipped under `TZ=UTC` (the Block playout golden additionally skips under a non-UTC `TZ`; see Golden-file nets). |
|
||||
| `ErsatzTV.Scanner.Tests` | Library scanning: scan handlers, folder scanners, NFO readers | Handler tests substitute the folder scanners + `ILibraryRepository` and assert the resulting repository writes (e.g. `ScanLocalLibraryHandlerTests` pins which `LastScan` levels a scan records — ersatztv#264). Fakes/`Testably` back the file-system-facing scanners. 1471 tests. Additionally contains `Core/FFmpeg/TranscodingTests` — `[Explicit]` + `[Combinatorial]`, so it never runs in CI or a plain `dotnet test` (it needs real ffmpeg/hardware) and contributes 0 to that count; run it by name when touching the transcoding pipeline. |
|
||||
| `ErsatzTV.Architecture.Tests` | Layering rules via NetArchTest.eNhancedEdition | Core↛Infra/App/EF; FFmpeg↛all; App↛concrete providers. 5 tests. See `docs/contributing.md` §1. |
|
||||
| `ErsatzTV.FFmpeg.Tests` | FFmpeg command construction | Build a pipeline, assert the exact rendered arg string (`PipelineBuilderBaseTests.cs`). |
|
||||
@@ -23,8 +23,14 @@ Three golden-file suites guard the highest-value, most-subtle output:
|
||||
- **Playout build**: `ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs` (ersatztv#163)
|
||||
— env var **`ETV_UPDATE_PLAYOUT_GOLDENS`** (deliberately separate from `ETV_UPDATE_GOLDENS` so
|
||||
regenerating one net can't silently rewrite the other). Snapshots the `PlayoutItem`s each builder
|
||||
produces over a pinned build window. Covers the **Classic** (`PlaybackOrder.Chronological`) and
|
||||
**Block** builders; **Sequential (YAML)** + **Scripted** are tracked in ersatztv#381. A third case,
|
||||
produces over a pinned build window. Covers the **Classic** (`PlaybackOrder.Chronological`),
|
||||
**Block**, and **Sequential (YAML)** builders. **Scripted** is deliberately excluded from this net —
|
||||
`ScriptedPlayoutBuilder` shells out to an external process that drives `SchedulingEngine` over HTTP, so
|
||||
no in-memory golden can characterize it; its integration harness is tracked in ersatztv#563 (decision:
|
||||
`testing.scripted-playout-golden-deferred`). The **Sequential** case (`Sequential_yaml`, ersatztv#381)
|
||||
builds from a committed YAML fixture (`Goldens/Fixtures/sequential-schedule.yml`) instead of a
|
||||
`ProgramSchedule`; it is TZ-independent (the `count`/`all`/`duration` handlers do UTC-only arithmetic —
|
||||
it passes, not skips, under a non-UTC `TZ`) so needs no `Assume` guard. A third case,
|
||||
`Classic_clock_padded` (ersatztv#77), locks clock-boundary padding: a `FillerMode.Pad` +
|
||||
`PadToNearestMinute=15` PostRoll preset snaps content to `:15`, and the test both goldens the output and
|
||||
asserts every content item after the first starts on a quarter-hour. Its EPG counterpart is
|
||||
|
||||
Reference in New Issue
Block a user