using System.IO; using GFramework.Game.Abstractions.Config; using GFramework.Game.Config; namespace GFramework.Game.Tests.Config; /// /// 验证 YAML 配置加载器对对象 / 数组 enum 约束的运行时行为。 /// [TestFixture] public class YamlConfigLoaderEnumTests { /// /// 为每个测试创建独立临时目录,避免文件系统状态互相污染。 /// [SetUp] public void SetUp() { _rootPath = Path.Combine(Path.GetTempPath(), "GFramework.ConfigTests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_rootPath); } /// /// 清理测试期间创建的临时目录。 /// [TearDown] public void TearDown() { if (Directory.Exists(_rootPath)) { Directory.Delete(_rootPath, true); } } private string _rootPath = null!; /// /// 验证对象 enum 会按字段名排序后的稳定比较键匹配,而不是依赖 schema 内的 JSON 字段顺序。 /// [Test] public async Task LoadAsync_Should_Accept_Object_Value_Declared_In_Schema_Enum_When_Property_Order_Differs() { CreateConfigFile( "monster/slime.yaml", """ id: 1 reward: gold: 10 itemId: potion """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "reward"], "properties": { "id": { "type": "integer" }, "reward": { "type": "object", "required": ["gold", "itemId"], "properties": { "gold": { "type": "integer" }, "itemId": { "type": "string" } }, "enum": [ { "itemId": "potion", "gold": 10 }, { "gold": 50, "itemId": "gem" } ] } } } """); var loader = CreateLoader(); var registry = new ConfigRegistry(); await loader.LoadAsync(registry).ConfigureAwait(false); var table = registry.GetTable("monster"); Assert.Multiple(() => { Assert.That(table.Count, Is.EqualTo(1)); Assert.That(table.Get(1).Reward.Gold, Is.EqualTo(10)); Assert.That(table.Get(1).Reward.ItemId, Is.EqualTo("potion")); }); } /// /// 验证对象 enum 不匹配时,运行时会拒绝整个对象值并输出候选 JSON 文本。 /// [Test] public void LoadAsync_Should_Throw_When_Object_Value_Is_Not_Declared_In_Schema_Enum() { CreateConfigFile( "monster/slime.yaml", """ id: 1 reward: gold: 10 itemId: elixir """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "reward"], "properties": { "id": { "type": "integer" }, "reward": { "type": "object", "required": ["gold", "itemId"], "properties": { "gold": { "type": "integer" }, "itemId": { "type": "string" } }, "enum": [ { "gold": 10, "itemId": "potion" }, { "gold": 50, "itemId": "gem" } ] } } } """); var loader = CreateLoader(); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(() => loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("reward")); Assert.That(exception.Message, Does.Contain("\"itemId\": \"potion\"")); Assert.That(exception.Message, Does.Contain("\"itemId\": \"gem\"")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证数组 enum 会保留元素顺序;同一批元素但顺序不同仍视为不同候选值。 /// [Test] public void LoadAsync_Should_Throw_When_Array_Value_Order_Does_Not_Match_Schema_Enum() { CreateConfigFile( "monster/slime.yaml", """ id: 1 dropItemIds: - ice - fire """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "dropItemIds"], "properties": { "id": { "type": "integer" }, "dropItemIds": { "type": "array", "items": { "type": "string" }, "enum": [ ["fire", "ice"], ["earth"] ] } } } """); var loader = CreateLoader(); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(() => loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("dropItemIds")); Assert.That(exception.Message, Does.Contain("[\"fire\", \"ice\"]")); Assert.That(exception.Message, Does.Contain("[\"earth\"]")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 创建带 schema 校验的测试加载器。 /// /// 配置类型。 /// 已注册 monster 表的加载器。 private YamlConfigLoader CreateLoader() where TConfig : IHasMonsterId { return new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); } /// /// 创建测试配置文件。 /// /// 相对路径。 /// 文件内容。 private void CreateConfigFile(string relativePath, string content) { var filePath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar)); var directoryPath = Path.GetDirectoryName(filePath); if (!string.IsNullOrWhiteSpace(directoryPath)) { Directory.CreateDirectory(directoryPath); } File.WriteAllText(filePath, content); } /// /// 创建测试 schema 文件。 /// /// 相对路径。 /// 文件内容。 private void CreateSchemaFile(string relativePath, string content) { var filePath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar)); var directoryPath = Path.GetDirectoryName(filePath); if (!string.IsNullOrWhiteSpace(directoryPath)) { Directory.CreateDirectory(directoryPath); } File.WriteAllText(filePath, content); } /// /// 为通用测试加载器暴露统一主键访问约定。 /// private interface IHasMonsterId { /// /// 获取配置主键。 /// int Id { get; } } /// /// 供对象 enum 测试使用的配置桩。 /// private sealed class MonsterRewardConfigStub : IHasMonsterId { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置奖励对象。 /// public RewardConfigStub Reward { get; set; } = new(); } /// /// 供数组 enum 测试使用的配置桩。 /// private sealed class MonsterDropItemIdsConfigStub : IHasMonsterId { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置掉落物数组。 /// public IReadOnlyList DropItemIds { get; set; } = Array.Empty(); } /// /// 供对象 enum 测试使用的奖励配置桩。 /// private sealed class RewardConfigStub { /// /// 获取或设置金币数量。 /// public int Gold { get; set; } /// /// 获取或设置道具标识。 /// public string ItemId { get; set; } = string.Empty; } }