Files
TeslaSuite/Console/tests/TeslaConsole.DiffTests/BTGoldenEggTests.cs
T
CydandClaude Fable 5 9148ce2cca BattleTech egg builder, catalog, and golden-egg DiffTests (BT port phases 1-2)
New TeslaConsole.BattleTech namespace mirroring TeslaConsole.RedPlanet's
mission layer: BTMission/BTFreeForAllMission (egg serializer; RP wire framing
and ordinal bitmaps reused verbatim), BTParticipant/BTPlayer/BTCamera (BT
participant fields: advancedDamage, experience, vehicleValue, patch, role),
and the BTConfig/BTScenario/BTMap/BTVehicle/BTWeather/BTRole catalog loaded
from BattleTech\BTConfig.xml (reconstructed from the Mac Console 4.10 ini BT
tree). Free For All and No Return share one mission class - both send
scenario=freeforall; the mode is the role assigned to each pilot.

BTGoldenEggTests (7 tests, recovered-only) diff the generated egg
field-by-field against two eggs captured from the original consoles
(cavern.egg, TESTARN.EGG): order-independent per-section compare with
font-rendered bitmap pixel rows excluded, ordinal art byte-exact vs
TESTARN.EGG, EggFileMessage framing reassembly, role de-dup, No Return role
semantics, and shipped-catalog contents. Invoker gains BTEggString/
BTEggFraming reflection cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:28:07 -05:00

248 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Xunit;
namespace TeslaConsole.DiffTests
{
/// <summary>
/// Golden-egg tests for the new TeslaConsole.BattleTech mission builder. BattleTech
/// is absent from the original exe, so like CatalogTests these run against the
/// recovered build only; the baselines are two eggs captured from the original
/// consoles (BattleTech\cavern.egg, BattleTech\TESTARN.EGG — see the port spec in
/// 410console/battletech-port/).
///
/// The comparison is per-section and order-independent: the pod parses the egg
/// INI-style, and the two golden eggs themselves disagree with each other on
/// participant field order. Pixel rows of the font-rendered name bitmaps are
/// excluded — the original consoles rendered them with a different font, and
/// cavern.egg's ordinal art is Mac-rendered too (the spec's known false
/// byte-match). TESTARN.EGG's ordinal art, however, is identical to the
/// RP-inherited rows, so that one IS compared byte-exactly.
/// </summary>
public class BTGoldenEggTests : IClassFixture<DifferentialFixture>
{
private readonly DifferentialFixture _fx;
public BTGoldenEggTests(DifferentialFixture fx) => _fx = fx;
// Mission args: mapKey, timeOfDayKey, weatherKey, temperature, lengthSeconds,
// then per pilot: podHost, name, vehicle, camo, patch, emblem, experience,
// dropzone, roleKey, roleModel (matches Invoker.BuildBTMission).
private static readonly string[] CavernArgs =
{
"cavern", "night", "clear", "27", "120",
"200.0.0.113", "cyd", "madcat", "Grey", "Red", "VGL", "veteran", "one", "Default", "dfltrole",
};
private static readonly string[] TestarnArgs =
{
"arena1", "day", "clear", "27", "600",
"200.0.0.113", "cyd", "madcat", "White", "Yellow", "VGL", "veteran", "one", "Default", "dfltrole",
};
private string Generate(string[] args)
{
string egg = _fx.Recovered.Run("BTEggString", args);
Assert.StartsWith("[mission]", egg); // not an "EXC:<Type>" result
return egg;
}
private static string ReadGolden(string fileName)
{
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "BattleTech", fileName);
Assert.True(File.Exists(path), "Golden egg not found: " + path);
// On disk the egg is the wire form: NUL-delimited ASCII.
return Encoding.ASCII.GetString(File.ReadAllBytes(path));
}
// ---- egg parsing ----
private static Dictionary<string, List<string>> ParseEgg(string text)
{
var sections = new Dictionary<string, List<string>>();
List<string> current = null;
foreach (var raw in text.Split(new[] { '\0', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
// TESTARN.EGG was captured with a stray \r before each NUL delimiter.
var line = raw.TrimEnd('\r');
if (line.Length == 0)
continue;
if (line.StartsWith("[", StringComparison.Ordinal))
{
string name = line.Trim('[', ']');
Assert.False(sections.ContainsKey(name), "Duplicate egg section: " + name);
current = new List<string>();
sections.Add(name, current);
}
else
{
Assert.NotNull(current); // a key=value before any section header
current.Add(line);
}
}
return sections;
}
/// <summary>Sections whose bitmap= lines are pixel rows, not references.</summary>
private static bool IsPixelSection(string name)
=> name.StartsWith("BitMap::", StringComparison.Ordinal) ||
name.StartsWith("Ordinal::BitMap::", StringComparison.Ordinal);
private static Dictionary<string, List<string>> DropPixelRows(Dictionary<string, List<string>> egg)
=> egg.ToDictionary(
s => s.Key,
s => IsPixelSection(s.Key)
? s.Value.Where(l => !l.StartsWith("bitmap=", StringComparison.Ordinal)).ToList()
: s.Value);
private static void AssertFieldsMatchGolden(string generated, string golden,
params string[] acceptedExtraGoldenSections)
{
var gen = DropPixelRows(ParseEgg(generated));
var gold = DropPixelRows(ParseEgg(golden));
var missing = gold.Keys.Except(gen.Keys).Except(acceptedExtraGoldenSections)
.OrderBy(s => s, StringComparer.Ordinal).ToArray();
var extra = gen.Keys.Except(gold.Keys)
.OrderBy(s => s, StringComparer.Ordinal).ToArray();
Assert.True(missing.Length == 0,
"Golden egg sections missing from the generated egg: " + string.Join(", ", missing));
Assert.True(extra.Length == 0,
"Generated egg sections absent from the golden egg: " + string.Join(", ", extra));
foreach (var name in gen.Keys)
{
var g = gen[name].OrderBy(s => s, StringComparer.Ordinal).ToArray();
var r = gold[name].OrderBy(s => s, StringComparer.Ordinal).ToArray();
Assert.True(g.SequenceEqual(r),
"[" + name + "] fields differ.\n golden: " + string.Join(" | ", r) +
"\n generated: " + string.Join(" | ", g));
}
}
// ---- golden-egg field equivalence ----
[Fact]
public void Cavern_Egg_Fields_Match_Golden()
=> AssertFieldsMatchGolden(Generate(CavernArgs), ReadGolden("cavern.egg"));
[Fact]
public void Testarn_Egg_Fields_Match_Golden()
{
// The original console emitted a [Role::*] block for every role in its
// catalog; ours emits only the roles the pilots reference, so TESTARN's
// unreferenced [Role::NoReturn] is an accepted difference (extra blocks
// are harmless to the pod's INI-style parser; missing referenced ones
// would not be).
AssertFieldsMatchGolden(Generate(TestarnArgs), ReadGolden("TESTARN.EGG"),
"Role::NoReturn");
}
[Fact]
public void Ordinal_Bitmap_Art_Matches_Testarn_Byte_Exactly()
{
// TESTARN.EGG's 1st4th place art is identical to the RP ordinals the BT
// builder reuses, so here the pixel rows are compared exactly and in order.
// (cavern.egg is excluded on purpose: its ordinal art is Mac-font-rendered
// and legitimately differs — the spec's known false byte-match.)
var gen = ParseEgg(Generate(TestarnArgs));
var gold = ParseEgg(ReadGolden("TESTARN.EGG"));
for (int n = 1; n <= 4; n++)
{
string name = "Ordinal::BitMap::" + n;
Assert.True(gen.ContainsKey(name), "Generated egg lacks [" + name + "]");
Assert.Equal(gold[name], gen[name]);
}
}
// ---- wire framing ----
[Fact]
public void Egg_Wire_Framing_Chunks_And_Reassembles()
{
string result = _fx.Recovered.Run("BTEggFraming", CavernArgs);
var m = Regex.Match(result, @"^messages=(\d+);total=(\d+);framing=True;reassembled=True$");
Assert.True(m.Success, "Unexpected framing result: " + result);
int messages = int.Parse(m.Groups[1].Value);
int total = int.Parse(m.Groups[2].Value);
Assert.Equal((total + 999) / 1000, messages);
Assert.True(total > 5000, "Suspiciously small egg (" + total + " bytes)");
}
// ---- behavior pinned by the resolved spec questions ----
[Fact]
public void Role_Blocks_Are_Deduplicated_Across_Pilots()
{
var args = CavernArgs.Concat(new[]
{
"200.0.0.114", "kai", "loki", "Black", "Blue", "Davion", "novice", "two", "Default", "dfltrole",
}).ToArray();
string egg = Generate(args);
int roleRefs = Regex.Matches(egg, Regex.Escape("role=Role::Default\n")).Count;
int roleBlocks = Regex.Matches(egg, Regex.Escape("[Role::Default]\n")).Count;
Assert.Equal(2, roleRefs);
Assert.Equal(1, roleBlocks);
}
[Fact]
public void NoReturn_Mode_Sends_Freeforall_Scenario_With_NoReturn_Role()
{
// No Return is not a separate scenario on the wire: both golden eggs say
// scenario=freeforall, and the modes differ only by the pilots' role.
var args = (string[])CavernArgs.Clone();
args[13] = "NoReturn";
args[14] = "noretun";
var egg = ParseEgg(Generate(args));
Assert.Contains("scenario=freeforall", egg["mission"]);
Assert.Contains("role=Role::NoReturn", egg["200.0.0.113"]);
Assert.True(egg.ContainsKey("Role::NoReturn"), "Missing [Role::NoReturn] block");
Assert.Contains("model=noretun", egg["Role::NoReturn"]);
Assert.False(egg.ContainsKey("Role::Default"), "Unreferenced [Role::Default] emitted");
}
// ---- the shipped catalog ----
[Fact]
public void BTConfig_Catalog_Ships_With_Build_And_Has_Expected_Counts()
{
// Verifies the csproj Content include actually lands BTConfig.xml next to
// the exe (BTConfig loads it from there at runtime) and that the catalog
// carries the full Mac Console.ini BT tree.
string path = Path.Combine(Path.GetDirectoryName(AssemblyPaths.RecoveredExe),
"BattleTech", "BTConfig.xml");
Assert.True(File.Exists(path), "BTConfig.xml not copied next to the build: " + path);
var doc = new XmlDocument();
doc.Load(path);
XmlElement root = doc.DocumentElement;
Assert.Equal("BattleTech", root.Name);
Assert.Equal(8, root.SelectNodes("map").Count);
Assert.Equal(18, root.SelectNodes("vehicle").Count);
Assert.Equal(4, root.SelectNodes("time").Count);
Assert.Equal(3, root.SelectNodes("weather").Count);
foreach (XmlNode weather in root.SelectNodes("weather"))
Assert.Equal("27", weather.Attributes["temperature"].InnerText);
XmlNode ffa = root.SelectSingleNode("freeforall");
Assert.NotNull(ffa);
Assert.Equal(3, ffa.SelectNodes("experience").Count);
Assert.Equal(7, ffa.SelectNodes("color").Count);
Assert.Equal(8, ffa.SelectNodes("patch").Count);
Assert.Equal(7, ffa.SelectNodes("badge").Count);
Assert.Equal(2, ffa.SelectNodes("role").Count);
Assert.Equal("dfltrole", ffa.SelectSingleNode("role[@key='Default']").Attributes["model"].InnerText);
Assert.Equal("noretun", ffa.SelectSingleNode("role[@key='NoReturn']").Attributes["model"].InnerText);
}
}
}