Files
LlamaCasty/ytLive.Tests/OutputProfileTests.cs
T
gramps 1708667602 feat(quality): retarget the ladder — 1080p60@12, 1080p30@10, 720p@6
Every 1080 tier carried 8 Mbps, inherited from TASK 4's "mainstream ceiling,
upload headroom stays comfortable". It was never derived from anything: the same
number served 30 fps and 60 fps, landscape and vertical, which no per-tier
derivation would produce. With CBR now actually holding the rate (700c7a8) that
number becomes the picture quality, so it has to be chosen rather than inherited.

New defaults sit between YouTube's H.264 recommendation (17 at 1080p60, 14 at
1080p30, 8 at 720p — support.google.com/youtube/answer/2853702) and the rate
practitioner OBS guides converge on for YouTube (9–12 Mbps at 1080p60). Vertical
mirrors its landscape pair: 1080x1920 is the same pixel count as 1920x1080.

The free-profile fallback in OutputProfile.Resolve moves with it (8.0 -> 10.0) so
a build with no shipped tiers still emits the 1080p30 rate rather than a stale
one.

668 tests (1 audio-pipe flake under full-suite load, passes alone), 0 warnings.
2026-11-12 08:01:01 -08:00

109 lines
4.0 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Xunit;
using ytLive.Models;
namespace ytLive.Tests;
/// <summary>
/// The output-profile rule: a free build may emit every shipped resolution at 30 fps;
/// 60 fps requires Pro, independent of resolution.
///
/// <para>The one thing that must never break: a cracked or unlicensed build silently gets a
/// *smaller frame*, never a smaller feature set. If a future tier is added above the budget,
/// the last test here fails instead of shipping it to everyone.</para>
/// </summary>
public class OutputProfileTests
{
/// <summary>Mirror of <c>MainViewModel.QualityOptions</c> — the shipped tiers. Kept in
/// step deliberately: this is the table the budget has to cover.</summary>
private static readonly QualityOption[] Shipped =
{
new("1080p60", 60, 12.0, 1920, 1080),
new("1080p30", 30, 10.0, 1920, 1080),
new("720p60", 60, 6.0, 1280, 720),
new("720p30", 30, 6.0, 1280, 720),
new("Vertical 1080p30", 30, 10.0, 1080, 1920),
new("Vertical 1080p60", 60, 12.0, 1080, 1920),
};
private static QualityOption Tier(string name) => Shipped.Single(t => t.Display == name);
[Fact]
public void A_free_build_resolves_1080p60_to_1080p30()
{
var resolved = OutputProfile.Resolve(Tier("1080p60"), Shipped, pro: false);
Assert.Equal("1080p30", resolved.Display);
Assert.Equal(1920, resolved.Width);
Assert.Equal(1080, resolved.Height);
Assert.Equal(30, resolved.Fps);
Assert.Equal(10.0, resolved.Bitrate);
}
[Fact]
public void A_free_build_leaves_its_own_tier_untouched()
{
var requested = Tier("1080p30");
Assert.Same(requested, OutputProfile.Resolve(requested, Shipped, pro: false));
}
[Fact]
public void Pro_passes_every_shipped_tier_through()
{
foreach (var tier in Shipped)
Assert.Same(tier, OutputProfile.Resolve(tier, Shipped, pro: true));
}
[Fact]
public void Free_access_depends_on_frame_rate_not_resolution()
{
foreach (var tier in Shipped)
{
var freeAllowed = tier.Fps <= OutputProfile.FreeMaxFps;
Assert.Equal(freeAllowed, OutputProfile.IsAllowed(tier, pro: false));
Assert.Equal(!freeAllowed, tier.RequiresPro);
}
}
[Fact]
public void An_empty_tier_table_still_yields_a_legal_frame()
{
var resolved = OutputProfile.Resolve(Tier("1080p60"), Array.Empty<QualityOption>(), pro: false);
Assert.Equal("1080p30", resolved.Display);
Assert.Equal(1920, resolved.Width);
Assert.Equal(1080, resolved.Height);
Assert.Equal(30, resolved.Fps);
Assert.Equal(10.0, resolved.Bitrate);
}
/// <summary>
/// The developer override must not exist in a shipped binary. A runtime test cannot prove
/// that (the test host is a Debug build), so this asserts the build rule itself: the
/// panel's XAML and code-behind are removed whenever the configuration is not Debug.
/// </summary>
[Fact]
public void The_developer_panel_is_excluded_from_the_release_compile()
{
var project = XDocument.Load(SourceAssert.RepoPath("ytLive.csproj"));
var guards = project.Descendants()
.Where(e => e.Name.LocalName is "Page" or "Compile")
.Where(e => (string?)e.Attribute("Remove") is string remove
&& remove.Contains("DeveloperPanel", StringComparison.OrdinalIgnoreCase))
.ToArray();
Assert.Equal(2, guards.Length);
foreach (var guard in guards)
{
// The condition may sit on the item or on its ItemGroup — either excludes it.
var condition = (string?)guard.Attribute("Condition")
?? (string?)guard.Parent?.Attribute("Condition");
Assert.NotNull(condition);
Assert.Contains("'$(Configuration)' != 'Debug'", condition, StringComparison.Ordinal);
}
}
}