GFramework/GFramework.Game.Tests/Config/YamlConfigLoaderEnumTests.cs
gewuyou 36507bbc52 fix(game-tests): 收敛 Game.Tests 测试告警
- 修复多组 YAML 与 persistence 测试中的 ConfigureAwait 使用与状态校验,清理低风险 analyzer 告警

- 重构 PersistenceTestUtilities 为单类型文件,消除测试辅助模型的文件命名告警

- 更新 analyzer-warning-reduction 跟踪与 trace,记录 RP-054 批次结果与 75 文件阈值停止点
2026-04-24 18:17:21 +08:00

296 lines
9.3 KiB
C#

using System.IO;
using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config;
namespace GFramework.Game.Tests.Config;
/// <summary>
/// 验证 YAML 配置加载器对对象 / 数组 <c>enum</c> 约束的运行时行为。
/// </summary>
[TestFixture]
public class YamlConfigLoaderEnumTests
{
/// <summary>
/// 为每个测试创建独立临时目录,避免文件系统状态互相污染。
/// </summary>
[SetUp]
public void SetUp()
{
_rootPath = Path.Combine(Path.GetTempPath(), "GFramework.ConfigTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_rootPath);
}
/// <summary>
/// 清理测试期间创建的临时目录。
/// </summary>
[TearDown]
public void TearDown()
{
if (Directory.Exists(_rootPath))
{
Directory.Delete(_rootPath, true);
}
}
private string _rootPath = null!;
/// <summary>
/// 验证对象 <c>enum</c> 会按字段名排序后的稳定比较键匹配,而不是依赖 schema 内的 JSON 字段顺序。
/// </summary>
[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<MonsterRewardConfigStub>();
var registry = new ConfigRegistry();
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterRewardConfigStub>("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"));
});
}
/// <summary>
/// 验证对象 <c>enum</c> 不匹配时,运行时会拒绝整个对象值并输出候选 JSON 文本。
/// </summary>
[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<MonsterRewardConfigStub>();
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
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));
});
}
/// <summary>
/// 验证数组 <c>enum</c> 会保留元素顺序;同一批元素但顺序不同仍视为不同候选值。
/// </summary>
[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<MonsterDropItemIdsConfigStub>();
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
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));
});
}
/// <summary>
/// 创建带 schema 校验的测试加载器。
/// </summary>
/// <typeparam name="TConfig">配置类型。</typeparam>
/// <returns>已注册 monster 表的加载器。</returns>
private YamlConfigLoader CreateLoader<TConfig>()
where TConfig : IHasMonsterId
{
return new YamlConfigLoader(_rootPath)
.RegisterTable<int, TConfig>("monster", "monster", "schemas/monster.schema.json", static config => config.Id);
}
/// <summary>
/// 创建测试配置文件。
/// </summary>
/// <param name="relativePath">相对路径。</param>
/// <param name="content">文件内容。</param>
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);
}
/// <summary>
/// 创建测试 schema 文件。
/// </summary>
/// <param name="relativePath">相对路径。</param>
/// <param name="content">文件内容。</param>
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);
}
/// <summary>
/// 为通用测试加载器暴露统一主键访问约定。
/// </summary>
private interface IHasMonsterId
{
/// <summary>
/// 获取配置主键。
/// </summary>
int Id { get; }
}
/// <summary>
/// 供对象 <c>enum</c> 测试使用的配置桩。
/// </summary>
private sealed class MonsterRewardConfigStub : IHasMonsterId
{
/// <summary>
/// 获取或设置主键。
/// </summary>
public int Id { get; set; }
/// <summary>
/// 获取或设置奖励对象。
/// </summary>
public RewardConfigStub Reward { get; set; } = new();
}
/// <summary>
/// 供数组 <c>enum</c> 测试使用的配置桩。
/// </summary>
private sealed class MonsterDropItemIdsConfigStub : IHasMonsterId
{
/// <summary>
/// 获取或设置主键。
/// </summary>
public int Id { get; set; }
/// <summary>
/// 获取或设置掉落物数组。
/// </summary>
public IReadOnlyList<string> DropItemIds { get; set; } = Array.Empty<string>();
}
/// <summary>
/// 供对象 <c>enum</c> 测试使用的奖励配置桩。
/// </summary>
private sealed class RewardConfigStub
{
/// <summary>
/// 获取或设置金币数量。
/// </summary>
public int Gold { get; set; }
/// <summary>
/// 获取或设置道具标识。
/// </summary>
public string ItemId { get; set; } = string.Empty;
}
}