mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 08:44:29 +08:00
- 修复多组 YAML 与 persistence 测试中的 ConfigureAwait 使用与状态校验,清理低风险 analyzer 告警 - 重构 PersistenceTestUtilities 为单类型文件,消除测试辅助模型的文件命名告警 - 更新 analyzer-warning-reduction 跟踪与 trace,记录 RP-054 批次结果与 75 文件阈值停止点
296 lines
9.3 KiB
C#
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;
|
|
}
|
|
}
|