Aetheria/tests/Aetheria.Shared.Tests/ProductLinesCatalogTests.cs
2026-10-21 20:34:50 +02:00

170 lines
8.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using MessagePack;
using Xunit;
// The weapon product lines the shipped catalog holds (campaign aetheria-release, cut product-lines-weapons; rulings
// catalog-grows-generic-designs-branded-products, maker-design-tiers-allowed): a sold weapon design has several brands, a
// brand name is one product's, and every row of the command's table is in the catalog with the quality its maker's profile gives.
// The rows come from the command's own table and the profiles from its own maker-profile table, so a row edited in one place
// and not shipped fails here.
public sealed class ProductLinesCatalogTests
{
// A design with any product is sold; the Smoke test designs have none and stay exempt.
[Fact]
public void EveryWeaponDesignWithAProductHasTwoMakers()
{
using var cache = RestoredHullsTests.OpenCatalog();
var products = cache.GetAll<FactionProductData>().ToArray();
var unsold = new List<string>();
foreach (var design in cache.GetAll<WeaponItemData>())
{
var key = cache.RefOf(design).Key;
var makers = products.Where(p => p.Design.Key.Equals(key)).Select(p => p.Manufacturer.Key).Distinct().Count();
if (makers == 0)
{
unsold.Add(design.Name);
continue;
}
Assert.True(makers >= 2, $"the weapon design \"{design.Name}\" is sold by {makers} maker");
}
// The designs no product sells are the Smoke test designs and no others: a design that lost all its products is a failure.
Assert.All(unsold, name => Assert.StartsWith("Smoke ", name));
}
[Fact]
public void NoTwoProductsShareAName()
{
using var cache = RestoredHullsTests.OpenCatalog();
var shared = cache.GetAll<FactionProductData>()
.GroupBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
.Where(group => group.Count() > 1)
.Select(group => group.Key)
.ToArray();
Assert.Empty(shared);
}
// Ruling gear-without-presence: a sold ship gear design with roles has two makers. The six reactor designs named here are
// station gear or Faction Play's NiteLife-only quirk, and keep their one maker.
private static readonly string[] OneMakerGear =
{
"Muon Fusion Reactor", "Muon Fusion Station Reactor", "Station Reactor", "Heavy Station Reactor",
"High-Yield Station Reactor", "Ring Station Reactor",
};
[Fact]
public void EveryRoleBearingGearDesignWithAProductHasTwoMakers()
{
using var cache = RestoredHullsTests.OpenCatalog();
var products = cache.GetAll<FactionProductData>().ToArray();
var gear = cache.GetAll<GearData>().Where(d => d is not WeaponItemData && (d.Roles?.Count ?? 0) > 0).ToArray();
Assert.All(OneMakerGear, name => Assert.Contains(gear, d => d.Name == name));
var sold = 0;
foreach (var design in gear.Where(d => !OneMakerGear.Contains(d.Name)))
{
var key = cache.RefOf(design).Key;
var makers = products.Where(p => p.Design.Key.Equals(key)).Select(p => p.Manufacturer.Key).Distinct().Count();
if (makers == 0) continue;
sold++;
Assert.True(makers >= 2, $"the gear design \"{design.Name}\" is sold by {makers} maker");
}
Assert.True(sold >= 20, "the gear designs with products should be many");
}
[Theory]
[InlineData("weapons", 20)]
[InlineData("gear", 22)]
[InlineData("minor-powers", 10)]
public void LineRowsAreShipped(string line, int count)
{
using var cache = RestoredHullsTests.OpenCatalog();
var table = ProductLinesCommand.Table(line);
Assert.Equal(count, table.Count);
foreach (var row in table)
{
var product = Assert.Single(cache.GetAll<FactionProductData>(), p => p.Name == row.Name);
var design = cache.Get(product.Design);
Assert.Equal(row.Design, design.Name);
Assert.Equal(row.Maker, cache.Get(product.Manufacturer).ShortName);
Assert.Equal(row.Description, product.Description);
var (mean, dev) = Program.MakerProfiles[row.Maker];
Assert.Equal(
(design.Roles ?? new()).Select(r => r.Name).OrderBy(n => n, StringComparer.Ordinal),
product.Roles.Select(r => r.Role).OrderBy(n => n, StringComparer.Ordinal));
foreach (var role in product.Roles)
{
Assert.Equal(mean, role.Mean, 4);
Assert.Equal(dev, role.StandardDeviation, 4);
}
}
}
// Cut product-tiers: the six bins of the command's tier table are in the catalog, each a single product that names its line.
[Fact]
public void TierRowsAreShipped()
{
using var cache = RestoredHullsTests.OpenCatalog();
var table = ProductLinesCommand.TierTable();
Assert.Equal(6, table.Count);
foreach (var row in table)
{
var bin = Assert.Single(cache.GetAll<FactionProductData>(), p => p.Name == row.Name);
var line = Assert.Single(cache.GetAll<FactionProductData>(), p => p.Name == row.Line);
Assert.Equal(cache.RefOf(line).Key, bin.Bin.Line.Key);
Assert.Equal(line.Design.Key, bin.Design.Key);
Assert.Equal(line.Manufacturer.Key, bin.Manufacturer.Key);
Assert.Equal(row.Floor, bin.Bin.Floor);
Assert.Equal(row.Description, bin.Description);
Assert.Empty(bin.Roles);
Assert.Null(bin.Assembly);
}
}
// A bin bins an unbinned line of its own design and maker, and the catalog holds no bin the table does not author.
[Fact]
public void BinsBinAnUnbinnedLineOfTheirOwnDesignAndMaker()
{
using var cache = RestoredHullsTests.OpenCatalog();
var bins = cache.GetAll<FactionProductData>().Where(p => p.Bin != null).ToArray();
Assert.Equal(ProductLinesCommand.TierTable().Select(r => r.Name).OrderBy(n => n, StringComparer.Ordinal), bins.Select(b => b.Name).OrderBy(n => n, StringComparer.Ordinal));
foreach (var bin in bins)
{
var line = cache.Get(bin.Bin.Line);
Assert.NotNull(line);
Assert.Null(line.Bin);
Assert.Equal(line.Design.Key, bin.Design.Key);
Assert.Equal(line.Manufacturer.Key, bin.Manufacturer.Key);
Assert.True(bin.Bin.Floor >= 0 && float.IsFinite(bin.Bin.Floor));
}
}
// FactionProductData slots 7 and 8 are additive under the same schema id: a bin and NotOffered round-trip, a record written
// before slot 8 existed (an array one element shorter) is on the market, and one written before slot 7 also reads no bin.
[Fact]
public void BinIsSlotSevenAndACatalogWithoutItReadsNoBin()
{
var options = CultDocumentMessagePackSerialization.OptionsFor(typeof(FactionProductData).Assembly);
var binned = MessagePackSerializer.Deserialize<FactionProductData>(
MessagePackSerializer.Serialize(new FactionProductData { Name = "Binned", Bin = new ProductBin { Floor = .67f } }, options), options);
Assert.Equal(.67f, binned.Bin.Floor);
Assert.True(MessagePackSerializer.Deserialize<FactionProductData>(
MessagePackSerializer.Serialize(new FactionProductData { Name = "Unsold", NotOffered = true }, options), options).NotOffered);
var bytes = MessagePackSerializer.Serialize(new FactionProductData { Name = "Old", Description = "d" }, options);
Assert.Equal(0x99, bytes[0]);
Assert.Equal(0xC0, bytes[^2]);
Assert.Equal(0xC2, bytes[^1]);
var beforeNotOffered = new byte[] { 0x98 }.Concat(bytes.Skip(1).Take(bytes.Length - 2)).ToArray();
var offered = MessagePackSerializer.Deserialize<FactionProductData>(beforeNotOffered, options);
Assert.Equal("Old", offered.Name);
Assert.False(offered.NotOffered);
var before = new byte[] { 0x97 }.Concat(bytes.Skip(1).Take(bytes.Length - 3)).ToArray();
var old = MessagePackSerializer.Deserialize<FactionProductData>(before, options);
Assert.Equal("Old", old.Name);
Assert.Null(old.Bin);
Assert.False(old.NotOffered);
}
}