using System; using Xunit; using ytLive.Models; namespace ytLive.Tests; /// /// TASK 33 — the step-down ladders as pure policy. The two rules that shaped the design and are easy /// to undo by "tidying" later: /// /// /// Two levers, never mixed. Machine starvation buys less work (fps, then resolution); /// upload congestion buys fewer bits. Resolution at an unchanged bitrate relieves no congestion, and /// an fps drop at an unchanged bitrate relieves no upload. /// One-way by default. Climbing is a constructor flag; the ceiling is carried either way /// so recovery is a policy change, not a rewrite. /// /// public sealed class StepDownPolicyTests { private static StepDownSample Machine(double renderMs = 40, int stalls = 10) => new(WindowSeconds: 5, PumpStalls: stalls, PumpDrops: 0, EncoderDrops: 0, AvgRenderMs: renderMs, AvgSubmitWaitMs: 1); private static StepDownSample Upload(int encoderDrops = 30) => new(WindowSeconds: 5, PumpStalls: 0, PumpDrops: 0, EncoderDrops: encoderDrops, AvgRenderMs: 1, AvgSubmitWaitMs: 2); private static StepDownSample Clean() => new(WindowSeconds: 5, PumpStalls: 0, PumpDrops: 0, EncoderDrops: 0, AvgRenderMs: 1, AvgSubmitWaitMs: 1); /// Feeds windows every 5 s until the policy changes QUALITY; the capture-load shed is a /// pre-step (no budget, nothing the viewer sees) so it is skipped here, and tested on its own. private static StepDownDecision Run(StepDownPolicy policy, StepDownSample sample, ref double t, int maxWindows = 24) { var last = new StepDownDecision(StepCause.None, StepAction.None, null); for (var i = 0; i < maxWindows; i++) { t += 5; last = policy.Observe(sample, TimeSpan.FromSeconds(t)); if (last.Acted && last.Action != StepAction.ShedCaptureCadence) return last; } return last; } [Fact] public void MachineStarvation_ShedsOurOwnCaptureCostBeforeLoweringQuality() { var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0); var t = 0.0; StepDownDecision first = default; for (var i = 0; i < 6 && !first.Acted; i++) { t += 5; first = policy.Observe(Machine(), TimeSpan.FromSeconds(t)); } Assert.Equal(StepAction.ShedCaptureCadence, first.Action); Assert.Equal(60, policy.Fps); Assert.Equal(0, policy.StepsUsed); // shedding is not a quality step Assert.Equal(1920, policy.Width); } [Fact] public void MachineStarvation_LowersFrameRateBeforeAnythingElse() { var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0); var t = 0.0; var decision = Run(policy, Machine(), ref t); Assert.Equal(StepCause.Machine, decision.Cause); Assert.Equal(StepAction.LowerFrameRate, decision.Action); Assert.Equal(30, policy.Fps); Assert.Equal(8.0, policy.BitrateMbps); // bits were never the problem Assert.Equal(1920, policy.Width); } [Fact] public void MachineStarvationAtThirty_DropsResolutionNext() { var policy = new StepDownPolicy(vertical: false, startFps: 30, startBitrateMbps: 8.0); var t = 0.0; var decision = Run(policy, Machine(renderMs: 40), ref t); Assert.Equal(StepAction.LowerResolution, decision.Action); Assert.Equal(1280, policy.Width); Assert.Equal(720, policy.Height); Assert.Equal(30, policy.Fps); } [Fact] public void MachineStarvationAtTheFloor_HoldsAndSaysSo() { var policy = new StepDownPolicy(vertical: false, startFps: 30, startBitrateMbps: 3.0); var t = 0.0; Run(policy, Machine(), ref t); // 1080p30 → 720p30 var t2 = t; var next = Run(policy, Machine(renderMs: 90), ref t2); Assert.False(next.Acted); Assert.Contains("floor", next.Why ?? string.Empty); Assert.Equal(1280, policy.Width); } [Fact] public void UploadCongestion_StepsBitrateOnly_NeverGeometry() { var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0); var t = 0.0; var decision = Run(policy, Upload(encoderDrops: 30), ref t); // 30 / (60×5) = 10% loss Assert.Equal(StepCause.Upload, decision.Cause); Assert.Equal(StepAction.LowerBitrate, decision.Action); Assert.Equal(7.2, policy.BitrateMbps); // 8.0 − max(0.5, 0.8) Assert.Equal(60, policy.Fps); Assert.Equal(1920, policy.Width); } [Fact] public void UploadStep_IsSizedByLossAndStopsAtTheFloor() { var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 6.0); var t = 0.0; for (var i = 0; i < 8; i++) { t += 40; // clear the cooldown policy.Observe(Upload(encoderDrops: 60), TimeSpan.FromSeconds(t)); // 20% loss } Assert.True(policy.BitrateMbps >= 3.0); Assert.Equal(60, policy.Fps); // congested upload never buys an fps drop Assert.Equal(1920, policy.Width); } [Fact] public void UploadStep_StopsAtTheTiersYoutubeMinimumWhenOneIsGiven() { // 1080p60: YouTube's floor is 6 Mbps, not the historical 3. From 8 the ladder reaches it in two // steps (8 → 6.4 → 6.0) and must then hold rather than cut into the minimum. var floor = RateLadder.For(1920, 1080, 60).Minimum; Assert.Equal(6.0, floor); var policy = new StepDownPolicy(1920, 1080, 60, 8.0, floorBitrateMbps: floor); var t = 0.0; for (var i = 0; i < 12; i++) { t += 40; policy.Observe(Upload(encoderDrops: 60), TimeSpan.FromSeconds(t)); // 20% loss } Assert.Equal(6.0, policy.BitrateMbps); } [Fact] public void BothKindsOfEvidenceInOneWindow_DoesNothing() { var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0); var t = 0.0; var mixed = new StepDownSample(5, PumpStalls: 20, PumpDrops: 3, EncoderDrops: 40, AvgRenderMs: 50, AvgSubmitWaitMs: 30); var decision = Run(policy, mixed, ref t, maxWindows: 6); Assert.False(decision.Acted); Assert.Equal(StepCause.Ambiguous, decision.Cause); Assert.Equal(60, policy.Fps); Assert.Equal(8.0, policy.BitrateMbps); } [Fact] public void StepBudget_IsThreeForAFullHdStart_AndOneForSevenTwenty() { var full = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0); var hd = new StepDownPolicy(startWidth: 1280, startHeight: 720, startFps: 60, startBitrateMbps: 6.0); var vertical = new StepDownPolicy(vertical: true, startFps: 60, startBitrateMbps: 8.0); Assert.Equal(3, full.StepsAllowed); Assert.Equal(1, hd.StepsAllowed); Assert.Equal(1, vertical.StepsAllowed); } [Fact] public void VerticalTier_HasNoResolutionStepOnlyFrameRateAndBits() { var policy = new StepDownPolicy(vertical: true, startFps: 60, startBitrateMbps: 8.0); var t = 0.0; var first = Run(policy, Machine(), ref t); Assert.Equal(StepAction.LowerFrameRate, first.Action); Assert.Equal(1080, policy.Width); Assert.Equal(1920, policy.Height); var t2 = t; var second = Run(policy, Machine(), ref t2); Assert.False(second.Acted); // one step on a vertical start, then done } [Fact] public void Cooldown_StopsASecondStepImmediatelyAfterTheFirst() { var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0); var t = 0.0; Run(policy, Upload(encoderDrops: 30), ref t); // Three more qualifying windows, still inside the 30 s cooldown. var acted = false; for (var i = 0; i < 3; i++) { t += 5; if (policy.Observe(Upload(encoderDrops: 30), TimeSpan.FromSeconds(t)).Acted) acted = true; } Assert.False(acted); Assert.Equal(7.2, policy.BitrateMbps); } [Fact] public void Climbing_IsOffByDefault_EvenAfterMinutesOfCleanWindows() { var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0); var t = 0.0; Run(policy, Upload(encoderDrops: 30), ref t); var lowered = policy.BitrateMbps; for (var i = 0; i < 60; i++) { t += 5; policy.Observe(Clean(), TimeSpan.FromSeconds(t)); } Assert.Equal(lowered, policy.BitrateMbps); Assert.Equal(1, policy.StepsUsed); } [Fact] public void WithRecoveryEnabled_ALongCleanStretchClimbsOneStep() { var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0, allowClimb: true); var t = 0.0; Run(policy, Upload(encoderDrops: 30), ref t); StepDownDecision raised = default; for (var i = 0; i < 60 && !raised.Acted; i++) { t += 5; raised = policy.Observe(Clean(), TimeSpan.FromSeconds(t)); } Assert.Equal(StepAction.RaiseBitrate, raised.Action); Assert.True(policy.BitrateMbps > 7.2); Assert.True(policy.BitrateMbps <= 8.0); Assert.Equal(0, policy.StepsUsed); } [Fact] public void AClimbFollowedByARelapse_FreezesTheSessionAtTheLowValue() { var policy = new StepDownPolicy(vertical: false, startFps: 60, startBitrateMbps: 8.0, allowClimb: true); var t = 0.0; Run(policy, Upload(encoderDrops: 30), ref t); for (var i = 0; i < 60; i++) { t += 5; if (policy.Observe(Clean(), TimeSpan.FromSeconds(t)).Acted) break; } var raisedTo = policy.BitrateMbps; // The link gives way again shortly after the climb. for (var i = 0; i < 60; i++) { t += 5; if (policy.Observe(Upload(encoderDrops: 60), TimeSpan.FromSeconds(t)).Acted) break; } Assert.True(policy.Frozen); // Frozen: a much longer clean stretch must not climb again. var afterRelapse = policy.BitrateMbps; for (var i = 0; i < 120; i++) { t += 5; policy.Observe(Clean(), TimeSpan.FromSeconds(t)); } Assert.Equal(afterRelapse, policy.BitrateMbps); Assert.True(afterRelapse <= raisedTo); } }