Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 46s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
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 / Build & test (.NET) (pull_request) Successful in 7m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 14m22s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Third-round review caught that Stale_Playlist_Guard_Should_Take_Precedence_Over_An_Otherwise_Valid_Three_Way describes an impossible case: ThreeWay requires processLaunched <= playlistExists, which is exactly the negation of the guard condition, so the guard can never preempt a ThreeWay. What the test really pins is precedence over the progress branches (TwoWayLateProgress) — still the ordering that matters. That is the same "rationale misstates the mechanism" defect the previous commit existed to fix, landed inside the fix itself. Renaming rather than leaving a test whose name teaches the next reader something false. Also broadens the escape-hatch caveat: a stale playlist that slips past the guard lands as TwoWay more often than ThreeWay, since FFmpeg has usually not reported progress that early. Test name and comments only; no logic change.
172 lines
6.5 KiB
C#
172 lines
6.5 KiB
C#
using System.Diagnostics;
|
|
using ErsatzTV.Core.FFmpeg;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Core.Tests.FFmpeg;
|
|
|
|
[TestFixture]
|
|
public class ColdStartStartupSplitTests
|
|
{
|
|
// milestones are Stopwatch.GetTimestamp() values; build them from a base + millisecond offsets
|
|
private const long Base = 1_000_000_000;
|
|
|
|
private static long At(double milliseconds) =>
|
|
Base + (long)(milliseconds / 1000.0 * Stopwatch.Frequency);
|
|
|
|
[Test]
|
|
public void Should_Split_Three_Ways_When_All_Milestones_Present()
|
|
{
|
|
ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
|
|
At(0),
|
|
At(150),
|
|
At(1200),
|
|
At(1600));
|
|
|
|
split.Kind.ShouldBe(ColdStartStartupSplitKind.ThreeWay);
|
|
split.Prep.TotalMilliseconds.ShouldBe(150, 1);
|
|
split.FFmpegInit.TotalMilliseconds.ShouldBe(1050, 1);
|
|
split.FirstGop.TotalMilliseconds.ShouldBe(400, 1);
|
|
}
|
|
|
|
[Test]
|
|
public void Sub_Phases_Should_Sum_To_Run_Entry_Through_Playlist()
|
|
{
|
|
// deliberately NOT "should sum to startup": the buckets span the worker's Run entry, which
|
|
// begins before the request thread's startup stopwatch, so prep overlaps the tail of setup
|
|
ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
|
|
At(0),
|
|
At(150),
|
|
At(1200),
|
|
At(1600));
|
|
|
|
(split.Prep + split.FFmpegInit + split.FirstGop).TotalMilliseconds.ShouldBe(1600, 1);
|
|
}
|
|
|
|
[Test]
|
|
public void Should_Be_Unavailable_When_The_Playlist_Predates_The_Process_Launch()
|
|
{
|
|
// a stale live.m3u8 survives when the handler's pre-session folder wipe fails (EmptyFolder
|
|
// swallows the failure into a warning). Every bucket would be meaningless, so report nothing
|
|
// rather than a plausible-looking sample with a prep that exceeds the whole measured phase
|
|
ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
|
|
At(0),
|
|
At(1600),
|
|
0,
|
|
At(150));
|
|
|
|
split.ShouldBe(ColdStartStartupSplit.Unavailable);
|
|
}
|
|
|
|
[Test]
|
|
public void Stale_Playlist_Guard_Should_Take_Precedence_Over_The_Progress_Branches()
|
|
{
|
|
// without the guard, this input would be classified TwoWayLateProgress; the guard must be
|
|
// evaluated first. (It can never preempt a ThreeWay: that requires processLaunched <=
|
|
// playlistExists, which is exactly the negation of the guard condition.)
|
|
ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
|
|
At(0),
|
|
At(1600),
|
|
At(1700),
|
|
At(150));
|
|
|
|
split.ShouldBe(ColdStartStartupSplit.Unavailable);
|
|
}
|
|
|
|
[Test]
|
|
public void Should_Fall_Back_To_Two_Way_Split_When_Progress_Predates_The_Process_Launch()
|
|
{
|
|
// a progress timestamp older than the launch cannot belong to this process
|
|
ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
|
|
At(100),
|
|
At(150),
|
|
At(120),
|
|
At(1600));
|
|
|
|
split.Kind.ShouldBe(ColdStartStartupSplitKind.TwoWay);
|
|
split.FFmpegInit.TotalMilliseconds.ShouldBe(1450, 1);
|
|
split.FirstGop.ShouldBe(TimeSpan.Zero);
|
|
}
|
|
|
|
[Test]
|
|
public void Should_Stay_Three_Way_When_Progress_Coincides_With_A_Boundary()
|
|
{
|
|
ColdStartStartupSplit atLaunch = ColdStartStartupSplit.FromTimestamps(At(0), At(150), At(150), At(1600));
|
|
atLaunch.Kind.ShouldBe(ColdStartStartupSplitKind.ThreeWay);
|
|
atLaunch.FFmpegInit.ShouldBe(TimeSpan.Zero);
|
|
atLaunch.FirstGop.TotalMilliseconds.ShouldBe(1450, 1);
|
|
|
|
ColdStartStartupSplit atPlaylist = ColdStartStartupSplit.FromTimestamps(At(0), At(150), At(1600), At(1600));
|
|
atPlaylist.Kind.ShouldBe(ColdStartStartupSplitKind.ThreeWay);
|
|
atPlaylist.FFmpegInit.TotalMilliseconds.ShouldBe(1450, 1);
|
|
atPlaylist.FirstGop.ShouldBe(TimeSpan.Zero);
|
|
}
|
|
|
|
[Test]
|
|
public void Should_Fall_Back_To_Two_Way_Split_When_FFmpeg_Never_Reported_Progress()
|
|
{
|
|
// no -progress output before the playlist appeared: ffmpegInit must absorb the remainder
|
|
// rather than the split inventing a firstGop boundary that was never observed
|
|
ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
|
|
At(0),
|
|
At(150),
|
|
0,
|
|
At(1600));
|
|
|
|
split.Kind.ShouldBe(ColdStartStartupSplitKind.TwoWay);
|
|
split.Prep.TotalMilliseconds.ShouldBe(150, 1);
|
|
split.FFmpegInit.TotalMilliseconds.ShouldBe(1450, 1);
|
|
split.FirstGop.ShouldBe(TimeSpan.Zero);
|
|
}
|
|
|
|
[Test]
|
|
public void Should_Report_Late_Progress_Distinctly_When_Progress_Arrived_After_The_Playlist()
|
|
{
|
|
// the playlist is observed on the request thread while progress is recorded on the worker
|
|
// thread; a progress milestone outside the phase must not produce a negative bucket
|
|
ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
|
|
At(0),
|
|
At(150),
|
|
At(1800),
|
|
At(1600));
|
|
|
|
split.Kind.ShouldBe(ColdStartStartupSplitKind.TwoWayLateProgress);
|
|
split.FFmpegInit.TotalMilliseconds.ShouldBe(1450, 1);
|
|
split.FirstGop.ShouldBe(TimeSpan.Zero);
|
|
}
|
|
|
|
[TestCase(0L, 150L, 1200L, 1600L, TestName = "Run never started")]
|
|
[TestCase(100L, 0L, 0L, 1600L, TestName = "Process never launched")]
|
|
[TestCase(100L, 150L, 1200L, 0L, TestName = "Playlist never appeared")]
|
|
public void Should_Be_Unavailable_When_A_Required_Milestone_Is_Missing(
|
|
long runStarted,
|
|
long processLaunched,
|
|
long firstProgress,
|
|
long playlistExists)
|
|
{
|
|
ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
|
|
runStarted == 0 ? 0 : At(runStarted),
|
|
processLaunched == 0 ? 0 : At(processLaunched),
|
|
firstProgress == 0 ? 0 : At(firstProgress),
|
|
playlistExists == 0 ? 0 : At(playlistExists));
|
|
|
|
split.ShouldBe(ColdStartStartupSplit.Unavailable);
|
|
}
|
|
|
|
[Test]
|
|
public void Should_Clamp_Rather_Than_Report_A_Negative_Prep()
|
|
{
|
|
// defensive: launch cannot precede Run entry, but telemetry must never show a negative
|
|
ColdStartStartupSplit split = ColdStartStartupSplit.FromTimestamps(
|
|
At(500),
|
|
At(150),
|
|
At(1200),
|
|
At(1600));
|
|
|
|
split.Prep.ShouldBe(TimeSpan.Zero);
|
|
split.Kind.ShouldBe(ColdStartStartupSplitKind.ThreeWay);
|
|
split.FFmpegInit.TotalMilliseconds.ShouldBe(1050, 1);
|
|
split.FirstGop.TotalMilliseconds.ShouldBe(400, 1);
|
|
}
|
|
}
|