mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
- 创建游戏内容配置系统详细文档,涵盖 YAML 配置、JSON Schema 结构、目录组织等 - 添加配置系统的当前能力说明,包括运行时查询、生成器支持等功能特性 - 完善 Schema 示例和 YAML 示例,提供完整的配置定义和数据样例 - 整理推荐接入模板,包含目录结构、csproj 配置、启动帮助器等最佳实践 - 补充运行时读取模板、生成查询辅助、Architecture 接入等高级使用方式 - 添加运行时校验行为说明,涵盖跨表引用、格式验证、约束检查等内容 - 提供开发期热重载功能文档,说明自动刷新运行时表的工作机制 - 创建 VS Code 工具使用指南,介绍浏览、编辑、校验等开发辅助功能 - 生成分析器规则清单文件,记录所有配置相关的诊断规则和严重级别
393 lines
13 KiB
C#
393 lines
13 KiB
C#
using System.IO;
|
|
using GFramework.Game.Abstractions.Config;
|
|
using GFramework.Game.Config;
|
|
|
|
namespace GFramework.Game.Tests.Config;
|
|
|
|
/// <summary>
|
|
/// 验证 YAML 配置加载器对对象级 <c>dependentSchemas</c> 约束的运行时行为。
|
|
/// </summary>
|
|
[TestFixture]
|
|
public sealed class YamlConfigLoaderDependentSchemasTests
|
|
{
|
|
private string _rootPath = null!;
|
|
|
|
/// <summary>
|
|
/// 为每个用例创建隔离的临时目录,避免不同 dependentSchemas 场景互相污染。
|
|
/// </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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证触发字段出现但条件 schema 未满足时,运行时会拒绝当前对象。
|
|
/// </summary>
|
|
[Test]
|
|
public void LoadAsync_Should_Throw_When_DependentSchema_Is_Not_Satisfied()
|
|
{
|
|
CreateConfigFile(
|
|
"monster/slime.yaml",
|
|
"""
|
|
id: 1
|
|
reward:
|
|
itemId: potion
|
|
""");
|
|
CreateSchemaFile(
|
|
"schemas/monster.schema.json",
|
|
"""
|
|
{
|
|
"type": "object",
|
|
"required": ["id", "reward"],
|
|
"properties": {
|
|
"id": { "type": "integer" },
|
|
"reward": {
|
|
"type": "object",
|
|
"properties": {
|
|
"itemId": { "type": "string" },
|
|
"itemCount": { "type": "integer" },
|
|
"bonus": { "type": "integer" }
|
|
},
|
|
"dependentSchemas": {
|
|
"itemId": {
|
|
"type": "object",
|
|
"required": ["itemCount"],
|
|
"properties": {
|
|
"itemCount": { "type": "integer" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""");
|
|
|
|
var loader = CreateMonsterRewardLoader();
|
|
var registry = CreateRegistry();
|
|
|
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(exception, Is.Not.Null);
|
|
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.ConstraintViolation));
|
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("reward"));
|
|
Assert.That(exception.Message, Does.Contain("dependentSchemas"));
|
|
Assert.That(exception.Message, Does.Contain("reward.itemId"));
|
|
Assert.That(registry.Count, Is.EqualTo(0));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证触发字段缺席时,不会误触发 dependentSchemas 检查。
|
|
/// </summary>
|
|
[Test]
|
|
public async Task LoadAsync_Should_Accept_When_DependentSchemas_Trigger_Is_Absent()
|
|
{
|
|
CreateConfigFile(
|
|
"monster/slime.yaml",
|
|
"""
|
|
id: 1
|
|
reward:
|
|
bonus: 2
|
|
""");
|
|
CreateSchemaFile(
|
|
"schemas/monster.schema.json",
|
|
"""
|
|
{
|
|
"type": "object",
|
|
"required": ["id", "reward"],
|
|
"properties": {
|
|
"id": { "type": "integer" },
|
|
"reward": {
|
|
"type": "object",
|
|
"properties": {
|
|
"itemId": { "type": "string" },
|
|
"itemCount": { "type": "integer" },
|
|
"bonus": { "type": "integer" }
|
|
},
|
|
"dependentSchemas": {
|
|
"itemId": {
|
|
"type": "object",
|
|
"required": ["itemCount"],
|
|
"properties": {
|
|
"itemCount": { "type": "integer" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""");
|
|
|
|
var loader = CreateMonsterRewardLoader();
|
|
var registry = CreateRegistry();
|
|
|
|
await loader.LoadAsync(registry);
|
|
|
|
var table = registry.GetTable<int, MonsterDependentSchemasConfigStub>("monster");
|
|
Assert.That(table.Count, Is.EqualTo(1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证触发字段出现且条件 schema 满足时,可以保留对象上的额外同级字段并正常通过加载。
|
|
/// </summary>
|
|
[Test]
|
|
public async Task LoadAsync_Should_Accept_When_DependentSchema_Is_Satisfied()
|
|
{
|
|
CreateConfigFile(
|
|
"monster/slime.yaml",
|
|
"""
|
|
id: 1
|
|
reward:
|
|
itemId: potion
|
|
itemCount: 3
|
|
bonus: 1
|
|
""");
|
|
CreateSchemaFile(
|
|
"schemas/monster.schema.json",
|
|
"""
|
|
{
|
|
"type": "object",
|
|
"required": ["id", "reward"],
|
|
"properties": {
|
|
"id": { "type": "integer" },
|
|
"reward": {
|
|
"type": "object",
|
|
"properties": {
|
|
"itemId": { "type": "string" },
|
|
"itemCount": { "type": "integer" },
|
|
"bonus": { "type": "integer" }
|
|
},
|
|
"dependentSchemas": {
|
|
"itemId": {
|
|
"type": "object",
|
|
"required": ["itemCount"],
|
|
"properties": {
|
|
"itemCount": { "type": "integer" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""");
|
|
|
|
var loader = CreateMonsterRewardLoader();
|
|
var registry = CreateRegistry();
|
|
|
|
await loader.LoadAsync(registry);
|
|
|
|
var table = registry.GetTable<int, MonsterDependentSchemasConfigStub>("monster");
|
|
var reward = table.Get(1).Reward;
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(table.Count, Is.EqualTo(1));
|
|
Assert.That(reward.ItemId, Is.EqualTo("potion"));
|
|
Assert.That(reward.ItemCount, Is.EqualTo(3));
|
|
Assert.That(reward.Bonus, Is.EqualTo(1));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证非对象 dependentSchemas 声明会在 schema 解析阶段被拒绝。
|
|
/// </summary>
|
|
[Test]
|
|
public void LoadAsync_Should_Throw_When_DependentSchemas_Is_Not_An_Object()
|
|
{
|
|
CreateConfigFile(
|
|
"monster/slime.yaml",
|
|
"""
|
|
id: 1
|
|
reward:
|
|
itemId: potion
|
|
""");
|
|
CreateSchemaFile(
|
|
"schemas/monster.schema.json",
|
|
"""
|
|
{
|
|
"type": "object",
|
|
"required": ["id", "reward"],
|
|
"properties": {
|
|
"id": { "type": "integer" },
|
|
"reward": {
|
|
"type": "object",
|
|
"properties": {
|
|
"itemId": { "type": "string" },
|
|
"itemCount": { "type": "integer" }
|
|
},
|
|
"dependentSchemas": ["itemId"]
|
|
}
|
|
}
|
|
}
|
|
""");
|
|
|
|
var loader = CreateMonsterRewardLoader();
|
|
var registry = CreateRegistry();
|
|
|
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(exception, Is.Not.Null);
|
|
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
|
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("reward"));
|
|
Assert.That(exception.Message, Does.Contain("must declare 'dependentSchemas' as an object"));
|
|
Assert.That(registry.Count, Is.EqualTo(0));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证 dependentSchemas 只接受 object-typed 条件子 schema。
|
|
/// </summary>
|
|
[Test]
|
|
public void LoadAsync_Should_Throw_When_DependentSchemas_Schema_Is_Not_Object_Typed()
|
|
{
|
|
CreateConfigFile(
|
|
"monster/slime.yaml",
|
|
"""
|
|
id: 1
|
|
reward:
|
|
itemId: potion
|
|
""");
|
|
CreateSchemaFile(
|
|
"schemas/monster.schema.json",
|
|
"""
|
|
{
|
|
"type": "object",
|
|
"required": ["id", "reward"],
|
|
"properties": {
|
|
"id": { "type": "integer" },
|
|
"reward": {
|
|
"type": "object",
|
|
"properties": {
|
|
"itemId": { "type": "string" }
|
|
},
|
|
"dependentSchemas": {
|
|
"itemId": {
|
|
"type": "string",
|
|
"const": "potion"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""");
|
|
|
|
var loader = CreateMonsterRewardLoader();
|
|
var registry = CreateRegistry();
|
|
|
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(exception, Is.Not.Null);
|
|
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
|
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("reward[dependentSchemas:itemId]"));
|
|
Assert.That(exception.Message, Does.Contain("object-typed 'dependentSchemas' schema"));
|
|
Assert.That(registry.Count, Is.EqualTo(0));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 在测试目录下写入配置文件,并自动创建缺失目录。
|
|
/// </summary>
|
|
/// <param name="relativePath">相对根目录的配置文件路径。</param>
|
|
/// <param name="content">要写入的 YAML 或 schema 内容。</param>
|
|
private void CreateConfigFile(string relativePath, string content)
|
|
{
|
|
var filePath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
|
var directoryPath = Path.GetDirectoryName(filePath);
|
|
if (!string.IsNullOrEmpty(directoryPath))
|
|
{
|
|
Directory.CreateDirectory(directoryPath);
|
|
}
|
|
|
|
File.WriteAllText(filePath, content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 写入测试 schema 文件,复用统一的测试文件创建逻辑。
|
|
/// </summary>
|
|
/// <param name="relativePath">schema 相对路径。</param>
|
|
/// <param name="content">schema JSON 内容。</param>
|
|
private void CreateSchemaFile(string relativePath, string content)
|
|
{
|
|
CreateConfigFile(relativePath, content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 创建用于对象 dependentSchemas 场景的加载器。
|
|
/// </summary>
|
|
/// <returns>已注册测试表与 schema 路径的加载器。</returns>
|
|
private YamlConfigLoader CreateMonsterRewardLoader()
|
|
{
|
|
return new YamlConfigLoader(_rootPath)
|
|
.RegisterTable<int, MonsterDependentSchemasConfigStub>(
|
|
"monster",
|
|
"monster",
|
|
"schemas/monster.schema.json",
|
|
static config => config.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 创建新的配置注册表,确保每个用例从干净状态开始。
|
|
/// </summary>
|
|
/// <returns>空的配置注册表。</returns>
|
|
private static ConfigRegistry CreateRegistry()
|
|
{
|
|
return new ConfigRegistry();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 用于对象 dependentSchemas 回归测试的最小配置类型。
|
|
/// </summary>
|
|
private sealed class MonsterDependentSchemasConfigStub
|
|
{
|
|
/// <summary>
|
|
/// 获取或设置主键。
|
|
/// </summary>
|
|
public int Id { get; set; }
|
|
|
|
/// <summary>
|
|
/// 获取或设置奖励对象。
|
|
/// </summary>
|
|
public DependentSchemasRewardConfigStub Reward { get; set; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 表示对象 dependentSchemas 回归测试中的奖励节点。
|
|
/// </summary>
|
|
private sealed class DependentSchemasRewardConfigStub
|
|
{
|
|
/// <summary>
|
|
/// 获取或设置掉落物 ID。
|
|
/// </summary>
|
|
public string ItemId { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// 获取或设置掉落物数量。
|
|
/// </summary>
|
|
public int ItemCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// 获取或设置额外奖励值。
|
|
/// </summary>
|
|
public int Bonus { get; set; }
|
|
}
|
|
}
|