GFramework/GFramework.Game.Tests/Config/YamlConfigTextValidatorTests.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

221 lines
7.1 KiB
C#

using System.IO;
using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config;
namespace GFramework.Game.Tests.Config;
/// <summary>
/// 验证公开的 YAML 文本校验入口可以在保存前复用运行时同一套 schema 规则。
/// </summary>
[TestFixture]
public sealed class YamlConfigTextValidatorTests
{
/// <summary>
/// 为每个测试准备独立临时目录。
/// </summary>
[SetUp]
public void SetUp()
{
_rootPath = Path.Combine(Path.GetTempPath(), "GFramework.TextValidatorTests", 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>
/// 验证合法 YAML 文本会通过公开校验入口。
/// </summary>
[Test]
public void Validate_Should_Succeed_When_Yaml_Matches_Schema()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "hp"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" }
}
}
""");
Assert.DoesNotThrow(() =>
YamlConfigTextValidator.Validate(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
name: Slime
hp: 10
"""));
}
/// <summary>
/// 验证结构错误会继续通过稳定的配置异常类型暴露给宿主。
/// </summary>
[Test]
public void Validate_Should_Throw_ConfigLoadException_When_Yaml_Contains_Unknown_Field()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
}
""");
var exception = Assert.Throws<ConfigLoadException>(() =>
YamlConfigTextValidator.Validate(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
name: Slime
hp: 10
"""));
Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Diagnostic.TableName, Is.EqualTo("monster"));
Assert.That(exception.Diagnostic.SchemaPath, Is.EqualTo(schemaPath));
Assert.That(exception.Diagnostic.YamlPath, Is.EqualTo("monster/generated.yaml"));
Assert.That(exception.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.UnknownProperty));
});
}
/// <summary>
/// 验证异步入口与同步入口共享相同校验语义。
/// </summary>
[Test]
public async Task ValidateAsync_Should_Throw_ConfigLoadException_When_Required_Field_Is_Missing()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
}
""");
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () =>
await YamlConfigTextValidator.ValidateAsync(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
""").ConfigureAwait(false));
Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.MissingRequiredProperty));
Assert.That(exception.Diagnostic.SchemaPath, Is.EqualTo(schemaPath));
Assert.That(exception.Diagnostic.YamlPath, Is.EqualTo("monster/generated.yaml"));
});
}
/// <summary>
/// 验证公开校验入口会在 schema 文件发生变化后失效旧缓存,
/// 避免保存路径持续沿用过期的字段约束。
/// </summary>
[Test]
public void Validate_Should_Refresh_Cached_Schema_When_File_Timestamp_Changes()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "hp"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" }
}
}
""");
var yaml = """
id: 1
name: Slime
hp: 10
""";
Assert.DoesNotThrow(() =>
YamlConfigTextValidator.Validate("monster", schemaPath, "monster/generated.yaml", yaml));
File.WriteAllText(
schemaPath,
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
}
""".Replace("\n", Environment.NewLine, StringComparison.Ordinal));
File.SetLastWriteTimeUtc(schemaPath, new DateTime(2040, 1, 1, 0, 0, 1, DateTimeKind.Utc));
var exception = Assert.Throws<ConfigLoadException>(() =>
YamlConfigTextValidator.Validate("monster", schemaPath, "monster/generated.yaml", yaml));
Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.UnknownProperty));
Assert.That(exception.Diagnostic.SchemaPath, Is.EqualTo(schemaPath));
});
}
/// <summary>
/// 在临时目录中创建 schema 文件。
/// </summary>
/// <param name="relativePath">相对根目录的路径。</param>
/// <param name="content">文件内容。</param>
/// <returns>写入后的绝对路径。</returns>
private string CreateSchemaFile(
string relativePath,
string content)
{
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;
}
}