GFramework/GFramework.Game.Tests/Config/YamlConfigSchemaValidatorTests.cs
GeWuYou 6ed4d8da1a feat(config): 添加配置 schema 诊断和验证功能
- 新增 ConfigSchemaDiagnostics 类提供配置 schema 代码生成相关诊断
- 添加 JSON 解析错误、根对象检查、ID字段要求等12种诊断规则
- 实现配置验证逻辑,支持整数、数字、布尔值、邮箱等格式校验
- 添加日期时间、持续时间、URI、UUID 等字符串格式验证
- 实现 YAML 解析和注释提取功能
- 提供配置样本生成和批量编辑更新功能
- 添加模式匹配和格式验证的正则表达式支持
2026-04-17 14:17:12 +08:00

207 lines
6.5 KiB
C#

using System.IO;
using GFramework.Game.Config;
namespace GFramework.Game.Tests.Config;
/// <summary>
/// 验证内部 schema 解析器会输出稳定且可预期的运行时依赖元数据。
/// </summary>
[TestFixture]
public sealed class YamlConfigSchemaValidatorTests
{
private string? _rootPath;
/// <summary>
/// 为每个测试准备独立临时目录。
/// </summary>
[SetUp]
public void SetUp()
{
_rootPath = Path.Combine(Path.GetTempPath(), "GFramework.SchemaValidatorTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_rootPath);
}
/// <summary>
/// 清理测试临时目录。
/// </summary>
[TearDown]
public void TearDown()
{
if (!string.IsNullOrEmpty(_rootPath) &&
Directory.Exists(_rootPath))
{
Directory.Delete(_rootPath, true);
}
}
/// <summary>
/// 验证 schema 中声明的跨表引用名称会以序数排序形式输出,
/// 避免热重载依赖推导与测试快照受哈希集合枚举顺序影响。
/// </summary>
[Test]
public void Load_Should_Return_Referenced_Table_Names_In_Ordinal_Sorted_Order()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"properties": {
"weaponId": {
"type": "string",
"x-gframework-ref-table": "weapon"
},
"allies": {
"type": "array",
"items": {
"type": "integer",
"x-gframework-ref-table": "ally"
}
},
"itemId": {
"type": "string",
"x-gframework-ref-table": "item"
}
}
}
""");
var schema = YamlConfigSchemaValidator.Load("monster", schemaPath);
Assert.That(schema.ReferencedTableNames, Is.EqualTo(new[] { "ally", "item", "weapon" }));
}
/// <summary>
/// 验证条件子 schema 复用同一条 ref-table 字段时,不会把同一引用重复写入结果。
/// </summary>
[Test]
public void ValidateAndCollectReferences_Should_Not_Duplicate_Reference_Usages_From_DependentSchemas()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"properties": {
"reward": {
"type": "object",
"properties": {
"itemId": {
"type": "string",
"x-gframework-ref-table": "item"
}
},
"dependentSchemas": {
"itemId": {
"type": "object",
"properties": {
"itemId": {
"type": "string",
"x-gframework-ref-table": "item"
}
}
}
}
}
}
}
""");
var schema = YamlConfigSchemaValidator.Load("monster", schemaPath);
var references = YamlConfigSchemaValidator.ValidateAndCollectReferences(
"monster",
schema,
"monster/slime.yaml",
"""
reward:
itemId: potion
""");
Assert.That(references, Has.Count.EqualTo(1));
Assert.Multiple(() =>
{
Assert.That(references[0].DisplayPath, Is.EqualTo("reward.itemId"));
Assert.That(references[0].ReferencedTableName, Is.EqualTo("item"));
Assert.That(references[0].RawValue, Is.EqualTo("potion"));
});
}
/// <summary>
/// 验证 <c>allOf</c> focused block 复用同一条 ref-table 字段时,不会把同一引用重复写入结果。
/// </summary>
[Test]
public void ValidateAndCollectReferences_Should_Not_Duplicate_Reference_Usages_From_AllOf()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"properties": {
"reward": {
"type": "object",
"properties": {
"itemId": {
"type": "string",
"x-gframework-ref-table": "item"
}
},
"allOf": [
{
"type": "object",
"properties": {
"itemId": {
"type": "string",
"x-gframework-ref-table": "item"
}
}
}
]
}
}
}
""");
var schema = YamlConfigSchemaValidator.Load("monster", schemaPath);
var references = YamlConfigSchemaValidator.ValidateAndCollectReferences(
"monster",
schema,
"monster/slime.yaml",
"""
reward:
itemId: potion
""");
Assert.That(references, Has.Count.EqualTo(1));
Assert.Multiple(() =>
{
Assert.That(references[0].DisplayPath, Is.EqualTo("reward.itemId"));
Assert.That(references[0].ReferencedTableName, Is.EqualTo("item"));
Assert.That(references[0].RawValue, Is.EqualTo("potion"));
});
}
/// <summary>
/// 在临时目录中创建 schema 文件。
/// </summary>
/// <param name="relativePath">相对根目录的路径。</param>
/// <param name="content">文件内容。</param>
/// <returns>写入后的绝对路径。</returns>
private string CreateSchemaFile(
string relativePath,
string content)
{
ArgumentNullException.ThrowIfNull(_rootPath);
var fullPath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
var directoryPath = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrWhiteSpace(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
File.WriteAllText(fullPath, content.Replace("\n", Environment.NewLine, StringComparison.Ordinal));
return fullPath;
}
}