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;
}
}