Aetheria/tests/Aetheria.Shared.Tests/SafeAssert.cs
Meta Vens 178897ce7a Scenarios Cut 1: a failed staging reports what it admitted, not a crash
"Zone unchanged on failure" was defended only by xunit formatting an entity
sequence until the test host overflowed its stack. SafeAssert.Unchanged compares
the zone's entities and agents by identity and order and names what changed by
hull name; every Assert.Contains over zone.Entities in RunStartTests goes through
SafeAssert.In for the same reason.

AMissingDesignFailsNamedAndChangesNothing gains the partial-admission case: a
player and a piloted entity that build cleanly, then a later entity whose preset
is missing. Admitting the built player before the all-or-nothing return now fails
by name ("the zone's entities changed, 5 -> 6: added [Djinni]").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:59:50 +02:00

54 lines
2.7 KiB
C#

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System.Collections.Generic;
using System.Linq;
using Xunit;
// Assertions over collections of entities and shots. xunit formats a collection's contents into its failure
// message, and an Entity, a PendingShot or a ShotOutcome drags the whole zone graph in with it: the formatter
// recurses until the test host dies of a stack overflow, which a mutation tool scores as a kill of every test
// that was running. A failure here must be an ordinary red naming the test, so these compare counts and
// identities and never hand xunit an entity.
internal static class SafeAssert
{
public static void NoShots(Zone zone) =>
Assert.True(zone.PendingShots.Count == 0, $"{zone.PendingShots.Count} shot(s) still pending");
public static PendingShot OnlyShot(Zone zone)
{
Assert.True(zone.PendingShots.Count == 1, $"expected exactly one pending shot, found {zone.PendingShots.Count}");
return zone.PendingShots[0];
}
public static T Only<T>(List<T> list)
{
Assert.True(list.Count == 1, $"expected exactly one {typeof(T).Name}, found {list.Count}");
return list[0];
}
public static void Empty<T>(List<T> list) => Assert.True(list.Count == 0, $"expected no {typeof(T).Name}, found {list.Count}");
public static void In(Zone zone, Entity entity, string why = "entity should be in the zone") =>
Assert.True(zone.Entities.Contains(entity), why);
public static void NotIn(Zone zone, Entity entity, string why = "entity should have left the zone") =>
Assert.False(zone.Entities.Contains(entity), why);
// The zone holds exactly the entities and agents it held before, in the same order: nothing admitted, nothing
// lost. A failure names what changed by hull name.
public static void Unchanged(Zone zone, List<Entity> entities, List<Agent> agents)
{
Same(entities, zone.Entities.ToList(), entity => entity.Name, "entities");
Same(agents, zone.Agents, agent => agent.Ship.Name, "agents");
}
private static void Same<T>(List<T> before, List<T> now, System.Func<T, string> name, string what) where T : class
{
var added = now.Where(x => !before.Contains(x)).Select(name).ToList();
var lost = before.Where(x => !now.Contains(x)).Select(name).ToList();
var inOrder = now.Count == before.Count && now.Zip(before, ReferenceEquals).All(same => same);
Assert.True(inOrder, $"the zone's {what} changed, {before.Count} -> {now.Count}: added [{string.Join(", ", added)}], lost [{string.Join(", ", lost)}]");
}
}