Files
TeslaSuite/Console/tests/TeslaConsole.DiffTests/BehavioralEquivalenceTests.cs
T
CydandClaude Opus 5 2a5a387381 Console: remember Enable Custom Bitmaps, ship the art; bump suite to 4.11.4.5
Settings -> Enable Custom Bitmaps was a static bool with no backing store:
it defaulted to off on every launch, so an operator had to re-tick it each
session and any custom plasma art was silently ignored until they did. New
ConsoleSettings persists machine-level menu toggles as XML in
%ProgramData%\Tesla Console\console.settings, alongside RPDefaults.rpd /
BTDefaults.btd / local.siteconfig. It is loaded once from Main and never
from a static initializer: the differential suite drives PlasmaBitmaps
directly and must keep seeing the original defaults rather than whatever
this machine has saved. A missing file is the first-run case; a corrupt one
is ignored and rewritten by the next toggle, because losing a menu setting
must never stop the console starting.

Custom art is now version-controlled and rolls with the release. New
Console\Plasma Images\ is copied into the package, and the lookup searches
%ProgramData%\Tesla Console\Plasma Images first and the exe-relative folder
second, so a release can never clobber a site's own name bitmaps.
install.bat creates the data-dir folder before the icacls grant so an
unelevated operator can write it. Ships with three 128x32 name bitmaps
(Deadmeat, Muerte, Phrogg); Muerte arrived as "Muerte_128x32-2.bmp", a name
the lookup can never build, so it is renamed to match its pilot.

Two fixes fell out of making the flag sticky. Path.Combine ran on the raw
participant name outside the try block, so with the option on a pilot named
"A:B" threw ArgumentException straight out of egg generation; names that
cannot be a Windows file name now just render procedurally. And the art was
loaded with Image.FromFile, which keeps the bitmap backed by the file and
locked for its whole lifetime — it is copied out through a stream now, so
art can be swapped between missions without restarting the console.

Verified against the built net40 exe: all three shipped bitmaps resolve by
pilot name, ProgramData wins over the shipped copy, a wrong-size file is
ignored, an illegal-character name does not throw, the file is not left
locked, and the settings round-trip and corrupt-file tolerance both hold.
Diff suite 106/106.

Version bumped 4.11.4.4 -> 4.11.4.5 across Console, Launcher, vPOD, the
install/build banners, the diff-suite version assertion and the README's
latest-release pointer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:16:11 -05:00

140 lines
6.5 KiB
C#

using System;
using Xunit;
namespace TeslaConsole.DiffTests
{
/// <summary>
/// For every input below, the original and the recovered build must return the
/// exact same result. Each [Theory] case runs the same method in both assemblies
/// (in their own AppDomains) and asserts byte-for-byte string equality.
/// </summary>
public class BehavioralEquivalenceTests : IClassFixture<DifferentialFixture>
{
private readonly DifferentialFixture _fx;
public BehavioralEquivalenceTests(DifferentialFixture fx) => _fx = fx;
private void AssertSame(string caseName, params string[] args)
{
var (original, recovered) = _fx.RunBoth(caseName, args);
Assert.Equal(original, recovered);
// Guard against both silently throwing the same way for a case that should succeed.
Assert.False(original == null,
$"{caseName}({string.Join(",", args)}) returned null from both assemblies");
}
[Fact]
public void Harness_Distinguishes_Different_Outputs()
{
// Negative control: a passing suite must be capable of seeing a difference.
// Same method, different inputs, must yield different results in each assembly.
string ninetySec = (90L * TimeSpan.TicksPerSecond).ToString();
var zero = _fx.Original.Run("GetTimeString", new[] { "0" });
var ninety = _fx.Original.Run("GetTimeString", new[] { ninetySec });
Assert.NotEqual(zero, ninety);
var rZero = _fx.Recovered.Run("GetTimeString", new[] { "0" });
var rNinety = _fx.Recovered.Run("GetTimeString", new[] { ninetySec });
Assert.NotEqual(rZero, rNinety);
}
[Fact]
public void Loaded_Assemblies_Are_The_Expected_Builds()
{
// The original is the untouched 4.11.3.37076 baseline; the recovered build is
// the modernized line, intentionally bumped to 4.11.4.x (same assembly name,
// new version). Both must be TeslaConsole — guards against testing the wrong files.
Assert.Contains("TeslaConsole", _fx.Original.AssemblyFullName);
Assert.Contains("TeslaConsole", _fx.Recovered.AssemblyFullName);
Assert.Contains("4.11.3.37076", _fx.Original.AssemblyFullName);
Assert.Contains("4.11.4.5", _fx.Recovered.AssemblyFullName);
}
// ---- RPStrings.GetTimeString: mm:ss formatting with 0.5s rounding ----
[Theory]
[InlineData(0L)] // 0s -> 00:00
[InlineData(4_000_000L)] // 0.4s -> rounds down
[InlineData(5_000_000L)] // 0.5s -> rounds up to 1s
[InlineData(6_000_000L)] // 0.6s
[InlineData(595_000_000L)] // 59.5s -> rolls to 01:00
[InlineData(600_000_000L)] // 60s -> 01:00
[InlineData(900_000_000L)] // 90s -> 01:30
[InlineData(5_990_000_000L)] // 599s
[InlineData(6_000_000_000L)] // 600s -> 10:00
[InlineData(36_000_000_000L)] // 3600s -> 60:00
[InlineData(-50_000_000L)] // negative
[InlineData(864_000_000_000L)] // 1 day
public void GetTimeString_Matches(long ticks)
=> AssertSame("GetTimeString", ticks.ToString());
// ---- HostTypeHelper.Parse(...).ToString() including invalid input ----
[Theory]
[InlineData("Game Machine")]
[InlineData("Mission Review")]
[InlineData("Console")]
[InlineData("GameMachineHostType")]
[InlineData("MissionReviewHostType")]
[InlineData("ConsoleHostType")]
[InlineData("console")] // case-sensitive: should throw in both
[InlineData("Nonsense")] // invalid: ArgumentException in both
[InlineData("")] // empty
public void HostTypeParse_Matches(string input)
=> AssertSame("HostTypeParse", input);
// ---- PlasmaBitmaps.ConvertBitmap: 1bpp packing of a known pixel pattern ----
[Theory]
[InlineData(8, 8, 0)]
[InlineData(8, 8, 1)]
[InlineData(8, 8, 2)]
[InlineData(16, 16, 0)]
[InlineData(16, 16, 3)]
[InlineData(32, 8, 5)]
[InlineData(7, 8, 0)] // width not multiple of 8 -> throws in both
[InlineData(8, 7, 0)] // height not multiple of 8 -> throws in both
public void ConvertBitmap_Matches(int width, int height, int seed)
=> AssertSame("ConvertBitmap", width.ToString(), height.ToString(), seed.ToString());
// ---- PlasmaBitmaps.GenerateString: full GDI text -> 1bpp pipeline ----
[Theory]
[InlineData(128, 32, "Microsoft Sans Serif", "RED")]
[InlineData(64, 16, "Microsoft Sans Serif", "RED")]
[InlineData(128, 32, "Microsoft Sans Serif", "1")]
[InlineData(128, 32, "Microsoft Sans Serif", "ABCDEFGH")]
[InlineData(128, 32, "Microsoft Sans Serif", "")]
[InlineData(128, 32, "Arial", "GO")]
public void GenerateString_Matches(int width, int height, string font, string text)
=> AssertSame("GenerateString", width.ToString(), height.ToString(), font, text);
// ---- RedPlanet RPMap / RPVehicle XML parsing ----
[Theory]
[InlineData("<map key=\"m1\" name=\"Blade's Edge\" image=\"images/x.bmp\" />")]
[InlineData("<map key=\"m2\" name=\"Lyz's Lane\" />")] // no image attr
[InlineData("<map key=\"\" name=\"\" />")] // empty values
public void RPMapParse_Matches(string xml)
=> AssertSame("RPMapParse", xml);
[Theory]
[InlineData("<vehicle key=\"v1\" name=\"Armadillo\" image=\"images/a.bmp\" />")]
[InlineData("<vehicle key=\"v2\" name=\"Skeeter\" />")]
public void RPVehicleParse_Matches(string xml)
=> AssertSame("RPVehicleParse", xml);
// ---- SiteManagement well-known application GUIDs (constants) ----
[Theory]
[InlineData("RPAppGuid")]
[InlineData("RPMRAppGuid")]
[InlineData("RPLCAppGuid")]
[InlineData("MW4AppGuid")]
[InlineData("MW4LCAppGuid")]
public void SiteManagementGuid_Matches(string fieldName)
=> AssertSame("SiteManagementGuid", fieldName);
// ---- Tuple.Create<int,string> generic factory ----
[Theory]
[InlineData("1", "alpha")]
[InlineData("-7", "")]
public void TupleCreate_Matches(string a, string b)
=> AssertSame("TupleCreate", a, b);
}
}