Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 47s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m28s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 3m29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m3s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adversarial review (MERGEABLE, no blockers) raised two items worth folding in rather than deferring: Medium — ScanLocalLibraryHandler silently swallowed path scan errors, while the three remote scanners it mirrors all log result.LeftToSeq(). That mattered less when a failed path only skipped the path-level LastScan, but the previous commit makes a failed path suppress the library-level scan time too — so the user would see exactly the #264 symptom ("Never scanned") with nothing in the log explaining why. That is a diagnosis dead-end of the same class as the bug being fixed, so log it here rather than file a follow-up. No test: the sibling Synchronize*LibraryByIdHandlerTests don't assert on logging either, and LogError is an extension method that NSubstitute can't cleanly verify. Low — docs/testing.md bills itself as the authoritative map of what each test project covers but omitted ErsatzTV.Scanner.Tests entirely (1471 pre-existing tests). This PR adds a file to that project, so add the row and include it in the per-PR verification gate.
99 lines
5.6 KiB
Markdown
99 lines
5.6 KiB
Markdown
# 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.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. |
|
|
| `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 var `ETV_UPDATE_GOLDENS`
|
|
- **XMLTV**: `ChannelGuideGoldenTests` (ersatztv#28) — env var `ETV_UPDATE_GOLDENS`
|
|
- **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,
|
|
`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
|
|
`ErsatzTV.Core.Tests/Channels/ChannelGuideProjectorClockPadTests.cs` (guide programmes stop on the padded
|
|
boundary). The build reads
|
|
no wall clock — time enters only via the caller-supplied `start` — so a pinned `start` is fully
|
|
deterministic. Snapshots the raw `PlayoutItem.Start`/`Finish` (UTC), **not** the `*Offset` properties
|
|
(those call `.ToLocalTime()` and would make the golden machine-TZ dependent). The **Block** case is
|
|
TZ-sensitive by construction (`BlockPlayoutBuilder` maps template times via `TimeZoneInfo.Local`), so
|
|
it is guarded with `Assume.That(TimeZoneInfo.Local.BaseUtcOffset == Zero)`: it runs under `TZ=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 `PlayoutItem`s, 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:
|
|
|
|
```bash
|
|
dotnet build ErsatzTV.sln
|
|
TZ=UTC dotnet test
|
|
```
|
|
|
|
CI adds `--blame-hang-timeout 2m` to catch hangs.
|
|
|
|
Fast subsets:
|
|
|
|
```bash
|
|
# single project
|
|
dotnet test ErsatzTV.Tests
|
|
|
|
# filtered
|
|
dotnet test ErsatzTV.Core.Tests --filter FullyQualifiedName~ChannelPlaylistGoldenTests
|
|
```
|
|
|
|
Web (`web/`):
|
|
|
|
```bash
|
|
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.Scanner.Tests`, `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 by `ErsatzTV.Architecture.Tests`.
|
|
- `docs/contributing.md` §8 — short pointer back to this doc.
|