GFramework/GFramework.Game.Tests/Config/YamlConfigLoaderEnumTests.cs
gewuyou ff553977e3 chore(license): 补齐 Apache-2.0 文件头治理
- 新增许可证文件头检查与修复脚本

- 补充维护者手动修复 PR 工作流和 CI 校验

- 更新贡献指南中的文件头说明

- 补齐仓库维护源码和配置文件的许可证声明
2026-05-03 19:39:49 +08:00

299 lines
9.3 KiB
C#

// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
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>(() => 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));
});
}
/// <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>(() => 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));
});
}
/// <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;
}
}