using Xunit; namespace MrGameEng.Mods.Tests; public sealed class DefDatabaseTests : IDisposable { private sealed class AnimalDef : Def { public float Speed { get; init; } public int Legs { get; init; } = 4; public List Tags { get; init; } = []; } private readonly string _root = Directory.CreateTempSubdirectory("mrge-defs-tests-").FullName; private int _modCounter; public void Dispose() => Directory.Delete(_root, recursive: true); /// Создаёт мод с одним файлом дефов и возвращает его (порядок загрузки = порядок создания). private Mod WriteDefsMod(string defsJson) { var id = $"mod{_modCounter++:D2}"; var modDir = Path.Combine(_root, id); Directory.CreateDirectory(Path.Combine(modDir, "Defs")); File.WriteAllText(Path.Combine(modDir, "Defs", "animals.json"), defsJson); return new Mod(new ModInfo { Id = id }, modDir); } private DefDatabase LoadAnimals(params Mod[] mods) { var database = new DefDatabase(); database.RegisterType("Animal"); database.Load(mods); return database; } [Fact] public void Load_ParsesDefs_AndAllIsSortedByName() { var mod = WriteDefsMod( """ { "type": "Animal", "defs": [ { "defName": "Wolf", "label": "волк", "speed": 9 }, { "defName": "Bear", "speed": 6 } ]} """); var database = LoadAnimals(mod); Assert.Equal(["Bear", "Wolf"], database.All().Select(d => d.DefName)); Assert.Equal(9f, database.Get("Wolf").Speed); Assert.Equal("волк", database.Get("Wolf").Label); Assert.Equal(4, database.Get("Bear").Legs); // значение по умолчанию } [Fact] public void Load_ParentChain_MergesFields_ChildWins() { var mod = WriteDefsMod( """ { "type": "Animal", "defs": [ { "defName": "BaseAnimal", "abstract": true, "speed": 5, "tags": ["wild"] }, { "defName": "Hare", "parent": "BaseAnimal", "speed": 12 }, { "defName": "Snail", "parent": "BaseAnimal", "legs": 0, "tags": ["slow", "slimy"] } ]} """); var database = LoadAnimals(mod); var hare = database.Get("Hare"); Assert.Equal(12f, hare.Speed); // своё поле победило Assert.Equal(["wild"], hare.Tags); // унаследовано var snail = database.Get("Snail"); Assert.Equal(5f, snail.Speed); // унаследовано Assert.Equal(0, snail.Legs); Assert.Equal(["slow", "slimy"], snail.Tags); // массив заменён целиком Assert.False(database.TryGet("BaseAnimal", out _)); // абстрактный не эмитится } [Fact] public void Load_LaterMod_ReplacesSameDefName() { var core = WriteDefsMod( """{ "type": "Animal", "defs": [ { "defName": "Wolf", "speed": 9, "tags": ["wild"] } ] }"""); var patch = WriteDefsMod( """{ "type": "Animal", "defs": [ { "defName": "Wolf", "speed": 20 } ] }"""); var database = LoadAnimals(core, patch); var wolf = database.Get("Wolf"); Assert.Equal(20f, wolf.Speed); Assert.Empty(wolf.Tags); // полная замена, не слияние с дефом раннего мода } [Fact] public void Load_CyclicParents_Throws() { var mod = WriteDefsMod( """ { "type": "Animal", "defs": [ { "defName": "A", "parent": "B" }, { "defName": "B", "parent": "A" } ]} """); var exception = Assert.Throws(() => LoadAnimals(mod)); Assert.Contains("Cyclic", exception.Message); } [Fact] public void Load_UnknownParent_Throws() { var mod = WriteDefsMod("""{ "type": "Animal", "defs": [ { "defName": "X", "parent": "Ghost" } ] }"""); var exception = Assert.Throws(() => LoadAnimals(mod)); Assert.Contains("Ghost", exception.Message); } [Fact] public void Load_UnknownDefType_ThrowsWithFileAndMod() { var mod = WriteDefsMod("""{ "type": "Vehicle", "defs": [] }"""); var exception = Assert.Throws(() => LoadAnimals(mod)); Assert.Contains("Vehicle", exception.Message); Assert.Contains(mod.Id, exception.Message); } [Fact] public void Get_UnknownDef_Throws_TryGetReturnsFalse() { var database = LoadAnimals(WriteDefsMod("""{ "type": "Animal", "defs": [] }""")); Assert.Throws(() => database.Get("Dodo")); Assert.False(database.TryGet("Dodo", out _)); } }