#77's core (pad/snap schedules to :00/:15/:30 via filler) already exists — FillerMode.Pad + PadToNearestMinute (Classic), pad_to_next/pad_until (Sequential); Block is inherently time-anchored. No production code change; this locks the behaviour end-to-end and documents that it exists. - PlayoutBuildGoldenTests.Classic_clock_padded: a PostRoll FillerMode.Pad(15) preset through the real PlayoutBuilder snaps content to :15 (golden + explicit quarter-hour assertion). Splits Verify -> CompareGolden for reuse. - ChannelGuideProjectorClockPadTests: the guide projection coalesces trailing filler so programmes STOP on the padded boundary (XMLTV half). - docs: decisions.md (2026-07-17 entry), domain-model.md (clock-boundary row), testing.md (test map + count 540->542). Deferred (UI, blocked on #388): one-click per-channel/schedule clock-align toggle + 60-min increment option. Refs #77 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.2 KiB
Testing map
Purpose: authoritative map of what each test project/suite covers and how to run it. Read this
before adding tests, not just docs/contributing.md §8 (which now just points here).
Test projects
| 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.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). |
web/ (vitest) |
React SPA unit tests | 330 tests; run alongside typecheck + build (see below). |
Golden-file nets
Three golden-file suites guard the highest-value, most-subtle output:
- M3U:
ErsatzTV.Core.Tests/Iptv/ChannelPlaylistGoldenTests.cs(ersatztv#11) — env varETV_UPDATE_GOLDENS - XMLTV:
ChannelGuideGoldenTests(ersatztv#28) — env varETV_UPDATE_GOLDENS - Playout build:
ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs(ersatztv#163) — env varETV_UPDATE_PLAYOUT_GOLDENS(deliberately separate fromETV_UPDATE_GOLDENSso regenerating one net can't silently rewrite the other). Snapshots thePlayoutItems 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,Classic_clock_padded(ersatztv#77), locks clock-boundary padding: aFillerMode.Pad+PadToNearestMinute=15PostRoll 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 isErsatzTV.Core.Tests/Channels/ChannelGuideProjectorClockPadTests.cs(guide programmes stop on the padded boundary). The build reads no wall clock — time enters only via the caller-suppliedstart— so a pinnedstartis fully deterministic. Snapshots the rawPlayoutItem.Start/Finish(UTC), not the*Offsetproperties (those call.ToLocalTime()and would make the golden machine-TZ dependent). The Block case is TZ-sensitive by construction (BlockPlayoutBuildermaps template times viaTimeZoneInfo.Local), so it is guarded withAssume.That(TimeZoneInfo.Local.BaseUtcOffset == Zero): it runs underTZ=UTC(CI) and reports inconclusive (a graceful skip, not a failure) under any other TZ. A real TZ seam for the block builder is ersatztv#380's scope.
All three locate their golden files via [CallerFilePath]. A missing golden is a hard fail, not a
skip. Regenerate via ETV_UPDATE_GOLDENS=1 dotnet test ... (M3U/XMLTV) or
ETV_UPDATE_PLAYOUT_GOLDENS=1 dotnet test ... (playout build).
Never set ETV_UPDATE_GOLDENS / ETV_UPDATE_PLAYOUT_GOLDENS in CI or from an agent. A golden
diff during normal test runs means the code changed the output — regenerating to make the diff go
away hides the change instead of surfacing it. Only a human who has confirmed the change is
intentional should regenerate.
Timezone independence
The suite is timezone-independent (ersatztv#24). When constructing test PlayoutItems, always
set a real Start (e.g. startState.CurrentTime.UtcDateTime) — never rely on the default
DateTime.MinValue, which underflows DateTimeOffset.MinValue once a non-UTC local offset is
applied (StartOffset calls ToLocalTime()). CI runs in UTC; local runs may not.
Running tests
Full .NET gate:
dotnet build ErsatzTV.sln
TZ=UTC dotnet test
CI adds --blame-hang-timeout 2m to catch hangs.
Fast subsets:
# single project
dotnet test ErsatzTV.Tests
# filtered
dotnet test ErsatzTV.Core.Tests --filter FullyQualifiedName~ChannelPlaylistGoldenTests
Web (web/):
npm test # vitest
npm run typecheck # tsc -b --pretty false
npm run lint # eslint .
npm run build # tsc -b && vite build
Per-PR verification gate
Before opening a PR: build the solution, run both .NET test projects (plus
ErsatzTV.Architecture.Tests and ErsatzTV.FFmpeg.Tests if touched), and run the web test/lint/
typecheck/build steps above. All must be green. A golden-file diff or an architecture-test
failure is a hard stop — fix the code, don't regenerate/relax the test.
See also
docs/ci-cd.md— CI pipeline (test → migrations → build), versioning, dependency management.docs/contributing.md§1 — layering rules enforced byErsatzTV.Architecture.Tests.docs/contributing.md§8 — short pointer back to this doc.