// 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;
///
/// 验证公开的 YAML 文本校验入口可以在保存前复用运行时同一套 schema 规则。
///
[TestFixture]
public sealed class YamlConfigTextValidatorTests
{
///
/// 为每个测试准备独立临时目录。
///
[SetUp]
public void SetUp()
{
_rootPath = Path.Combine(Path.GetTempPath(), "GFramework.TextValidatorTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_rootPath);
}
///
/// 清理测试临时目录。
///
[TearDown]
public void TearDown()
{
if (Directory.Exists(_rootPath))
{
Directory.Delete(_rootPath, true);
}
}
private string _rootPath = null!;
///
/// 验证合法 YAML 文本会通过公开校验入口。
///
[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" }
}
}
""");
YamlConfigTextValidator.Validate(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
name: Slime
hp: 10
""");
}
///
/// 验证结构错误会继续通过稳定的配置异常类型暴露给宿主。
///
[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(() =>
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));
});
}
///
/// 验证异步入口与同步入口共享相同校验语义。
///
[Test]
public void 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(() =>
YamlConfigTextValidator.ValidateAsync(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
"""));
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"));
});
}
///
/// 验证公开校验入口会在 schema 文件发生变化后失效旧缓存,
/// 避免保存路径持续沿用过期的字段约束。
///
[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
""";
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(() =>
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));
});
}
///
/// 在临时目录中创建 schema 文件。
///
/// 相对根目录的路径。
/// 文件内容。
/// 写入后的绝对路径。
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;
}
}