Files
mrgameeng/tests/MrGameEng.Content.Tests/Mods/DefDatabaseTests.cs
T
Leonid PershinandClaude Opus 4.8 d044cafad9
CI / build-test (push) Successful in 1m12s
Regex tooling: formula gene grouping, content patches, def validation
Three regex-powered content tools (phase G5):

- Formula group functions gsum/gcount/gavg/gmin/gmax('regex') aggregate
  over every context variable whose name matches the pattern, e.g.
  gsum('leaf_.*'). Adds string literals to the formula grammar and an
  IFormulaContext.ResolveMatching hook; GenomeContext enumerates matching
  genes, so a trait can sum/average a gene group.
- DefDatabase content patches: a { "type": "Patch", patches:[{ defType,
  match (regex on defName), set:{fields} }] } file sets fields on every
  matching raw def before resolution — mods patch Core in bulk.
- DefDatabase.RegisterValidator(typeKey, field, regex): load-time check
  that a string field matches a pattern, throwing otherwise (naming/format
  conventions).

Covered by FormulaTests (group aggregation, composition, bad calls) and
DefDatabaseTests (patch set/match/unknown-type, validator pass/reject).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:59:36 +03:00

216 lines
7.4 KiB
C#

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<string> Tags { get; init; } = [];
}
private readonly string _root = Directory.CreateTempSubdirectory("mrge-defs-tests-").FullName;
private int _modCounter;
public void Dispose() => Directory.Delete(_root, recursive: true);
/// <summary>Создаёт мод с одним файлом дефов и возвращает его (порядок загрузки = порядок создания).</summary>
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<AnimalDef>("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<AnimalDef>().Select(d => d.DefName));
Assert.Equal(9f, database.Get<AnimalDef>("Wolf").Speed);
Assert.Equal("волк", database.Get<AnimalDef>("Wolf").Label);
Assert.Equal(4, database.Get<AnimalDef>("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<AnimalDef>("Hare");
Assert.Equal(12f, hare.Speed); // своё поле победило
Assert.Equal(["wild"], hare.Tags); // унаследовано
var snail = database.Get<AnimalDef>("Snail");
Assert.Equal(5f, snail.Speed); // унаследовано
Assert.Equal(0, snail.Legs);
Assert.Equal(["slow", "slimy"], snail.Tags); // массив заменён целиком
Assert.False(database.TryGet<AnimalDef>("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<AnimalDef>("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<InvalidDataException>(() => 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<InvalidDataException>(() => LoadAnimals(mod));
Assert.Contains("Ghost", exception.Message);
}
[Fact]
public void Load_UnknownDefType_ThrowsWithFileAndMod()
{
var mod = WriteDefsMod("""{ "type": "Vehicle", "defs": [] }""");
var exception = Assert.Throws<InvalidDataException>(() => 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<KeyNotFoundException>(() => database.Get<AnimalDef>("Dodo"));
Assert.False(database.TryGet<AnimalDef>("Dodo", out _));
}
private Mod WriteMod(string fileName, string json)
{
var id = $"mod{_modCounter++:D2}";
var modDir = Path.Combine(_root, id);
Directory.CreateDirectory(Path.Combine(modDir, "Defs"));
File.WriteAllText(Path.Combine(modDir, "Defs", fileName), json);
return new Mod(new ModInfo { Id = id }, modDir);
}
[Fact]
public void Patch_SetsFields_OnDefsMatchingNamePattern()
{
var defs = WriteDefsMod(
"""
{ "type": "Animal", "defs": [
{ "defName": "Wolf", "speed": 9 },
{ "defName": "WolfPup", "speed": 4 },
{ "defName": "Bear", "speed": 6 }
]}
"""
);
var patch = WriteMod(
"patches.json",
"""{ "type": "Patch", "patches": [ { "defType": "Animal", "match": "Wolf.*", "set": { "legs": 6 } } ] }"""
);
var database = LoadAnimals(defs, patch);
Assert.Equal(6, database.Get<AnimalDef>("Wolf").Legs); // matched
Assert.Equal(6, database.Get<AnimalDef>("WolfPup").Legs); // matched
Assert.Equal(4, database.Get<AnimalDef>("Bear").Legs); // unmatched → default
}
[Fact]
public void Patch_UnknownDefType_Throws()
{
var patch = WriteMod(
"patches.json",
"""{ "type": "Patch", "patches": [ { "defType": "Ghost", "match": ".*", "set": {} } ] }"""
);
Assert.Throws<InvalidDataException>(() => LoadAnimals(patch));
}
[Fact]
public void Validator_RejectsField_NotMatchingPattern()
{
var database = new DefDatabase();
database.RegisterType<AnimalDef>("Animal");
database.RegisterValidator("Animal", "defName", "^[A-Z]");
var bad = WriteDefsMod("""{ "type": "Animal", "defs": [ { "defName": "wolf" } ] }""");
var error = Assert.Throws<InvalidDataException>(() => database.Load([bad]));
Assert.Contains("wolf", error.Message);
}
[Fact]
public void Validator_Passes_WhenFieldMatches()
{
var database = new DefDatabase();
database.RegisterType<AnimalDef>("Animal");
database.RegisterValidator("Animal", "defName", "^[A-Z]");
database.Load([
WriteDefsMod("""{ "type": "Animal", "defs": [ { "defName": "Wolf", "speed": 7 } ] }"""),
]);
Assert.Equal(7f, database.Get<AnimalDef>("Wolf").Speed);
}
}