Files
ersatztv/ErsatzTV.FFmpeg.Tests/Filter/OverlayWatermarkFilterTests.cs
T
timothy 39873811da
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 18s
PR Gates / decisions lifecycle (pull_request) Successful in 21s
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 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 16m21s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 16m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
test(503): guard WatermarkLocation exhaustiveness and cover source-content margins
Review follow-up. The 9-element ExpectedPositions table meant a future enum
member would pass every case and silently render bottom-right again -- the
exact bug #503 fixes. Assert the table covers Enum.GetValues instead, and
exercise the previously untested SourceContentMargins() branch.
2026-07-25 15:19:23 +02:00

132 lines
4.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.Filter;
using ErsatzTV.FFmpeg.Filter.Cuda;
using ErsatzTV.FFmpeg.Filter.Qsv;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.State;
using LanguageExt;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.FFmpeg.Tests.Filter;
[TestFixture]
public class OverlayWatermarkFilterTests
{
private static readonly FrameSize Resolution = new(1920, 1080);
private static readonly FrameSize SquarePixelFrameSize = new(1920, 1080);
// a pillarboxed source: 1440x1080 of real content inside a 1920x1080 frame, so
// SourceContentMargins() adds half the 480px horizontal padding to the margin
private static readonly FrameSize PillarboxedSquarePixelFrameSize = new(1440, 1080);
private static WatermarkState StateFor(WatermarkLocation location, bool placeWithinSourceContent = false) =>
new(
Option<List<WatermarkFadePoint>>.None,
location,
WatermarkSize.ActualSize,
10,
10,
10,
100,
placeWithinSourceContent);
private static readonly (WatermarkLocation Location, string Expected)[] ExpectedPositions =
[
(WatermarkLocation.BottomRight, "x=W-w-192:y=H-h-108"),
(WatermarkLocation.BottomLeft, "x=192:y=H-h-108"),
(WatermarkLocation.TopRight, "x=W-w-192:y=108"),
(WatermarkLocation.TopLeft, "x=192:y=108"),
(WatermarkLocation.TopMiddle, "x=(W-w)/2:y=108"),
(WatermarkLocation.RightMiddle, "x=W-w-192:y=(H-h)/2"),
(WatermarkLocation.BottomMiddle, "x=(W-w)/2:y=H-h-108"),
(WatermarkLocation.LeftMiddle, "x=192:y=(H-h)/2"),
(WatermarkLocation.MiddleCenter, "x=(W-w)/2:y=(H-h)/2")
];
private static IEnumerable<TestCaseData> AllLocations() =>
ExpectedPositions.Select(x => new TestCaseData(x.Location, x.Expected).SetName(
$"{{m}}({x.Location})"));
[TestCaseSource(nameof(AllLocations))]
public void Software_Overlay_Maps_Every_Location_To_Exact_Position(WatermarkLocation location, string expectedPosition)
{
var filter = new OverlayWatermarkFilter(
StateFor(location),
Resolution,
SquarePixelFrameSize,
new PixelFormatUnknown(),
NullLogger.Instance);
filter.Filter.ShouldBe($"overlay={expectedPosition}:format=0");
}
[TestCaseSource(nameof(AllLocations))]
public void Cuda_Overlay_Maps_Every_Location_To_Exact_Position(WatermarkLocation location, string expectedPosition)
{
var filter = new OverlayWatermarkCudaFilter(
StateFor(location),
Resolution,
SquarePixelFrameSize,
NullLogger.Instance);
filter.Filter.ShouldBe($"overlay_cuda={expectedPosition}");
}
[TestCaseSource(nameof(AllLocations))]
public void Qsv_Overlay_Maps_Every_Location_To_Exact_Position(WatermarkLocation location, string expectedPosition)
{
var filter = new OverlayWatermarkQsvFilter(
StateFor(location),
Resolution,
SquarePixelFrameSize,
NullLogger.Instance);
filter.Filter.ShouldBe($"overlay_qsv={expectedPosition}");
}
// #503: the position switch has a catch-all default that silently renders bottom-right, so a
// newly added WatermarkLocation would compile, pass every case above, and reintroduce the exact
// bug this fixture exists to prevent. Fail here instead, at the point the enum grows.
[Test]
public void Every_WatermarkLocation_Has_An_Expected_Position()
{
IEnumerable<WatermarkLocation> covered = ExpectedPositions.Select(x => x.Location);
Enum.GetValues<WatermarkLocation>().Except(covered).ShouldBeEmpty(
"every WatermarkLocation needs an explicit arm in OverlayWatermarkFilter.Position and a "
+ "matching entry in ExpectedPositions; otherwise it silently renders bottom-right");
}
[Test]
public void Source_Content_Margins_Offset_By_Half_The_Pillarbox_Padding()
{
var filter = new OverlayWatermarkFilter(
StateFor(WatermarkLocation.BottomRight, placeWithinSourceContent: true),
Resolution,
PillarboxedSquarePixelFrameSize,
new PixelFormatUnknown(),
NullLogger.Instance);
// horizontal: round(0.10 * 1440 + 480 / 2) = 384; vertical: round(0.10 * 1080 + 0) = 108
filter.Filter.ShouldBe("overlay=x=W-w-384:y=H-h-108:format=0");
}
[Test]
public void Unmapped_Location_Falls_Back_To_BottomRight_Position()
{
var filter = new OverlayWatermarkFilter(
StateFor((WatermarkLocation)999),
Resolution,
SquarePixelFrameSize,
new PixelFormatUnknown(),
NullLogger.Instance);
filter.Filter.ShouldBe("overlay=x=W-w-192:y=H-h-108:format=0");
}
}