GFramework/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs
GeWuYou fac2453766 docs(config): 添加游戏内容配置系统完整文档与集成测试
- 新增游戏内容配置系统完整使用文档,涵盖 YAML 配置、JSON Schema 结构、目录组织等
- 添加推荐的项目接入模板,包括目录结构、csproj 配置、启动帮助器和运行时读取入口
- 实现官方启动帮助器 GameConfigBootstrap 与 GameConfigBootstrapOptions 集成
- 添加强类型配置表访问、查询辅助和跨表引用功能说明
- 实现开发期热重载、运行时校验和 VS Code 工具集成指南
- 添加完整的单元测试验证生成器绑定、聚合注册和强类型访问功能
- 提供 Architecture 接入模板和热重载管理最佳实践
- 包含运行时校验行为、诊断对象和配置加载异常处理说明
2026-04-09 10:01:32 +08:00

352 lines
16 KiB
C#

using System;
using System.IO;
using System.Linq;
using GFramework.Game.Config;
using GFramework.Game.Config.Generated;
namespace GFramework.Game.Tests.Config;
/// <summary>
/// 验证消费者项目通过 `schemas/**/*.schema.json` 自动拾取 schema 后,
/// 可以直接编译并使用生成的聚合注册辅助、强类型访问入口、查询辅助与运行时加载链路。
/// </summary>
[TestFixture]
public class GeneratedConfigConsumerIntegrationTests
{
/// <summary>
/// 为每个端到端测试准备独立的配置根目录,避免编译期 schema 资产与运行时写入互相污染。
/// </summary>
[SetUp]
public void SetUp()
{
_rootPath = Path.Combine(Path.GetTempPath(), "GFramework.GeneratedConfigTests", 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>
/// 验证生成器自动拾取消费者项目的 schema 后,
/// 可以用生成的聚合注册辅助完成加载,并通过强类型表包装访问运行时数据与查询辅助。
/// </summary>
[Test]
public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Project()
{
CreateMonsterFiles();
CreateItemFiles();
var registry = new ConfigRegistry();
var loader = new YamlConfigLoader(_rootPath)
.RegisterAllGeneratedConfigTables();
await loader.LoadAsync(registry);
var monsterTable = registry.GetMonsterTable();
var dungeonMonsters = monsterTable.FindByFaction("dungeon");
var itemTable = registry.GetItemTable();
Assert.Multiple(() =>
{
Assert.That(
GeneratedConfigCatalog.Tables.Select(static metadata => metadata.TableName),
Is.SupersetOf(new[] { "item", "monster" }));
Assert.That(GeneratedConfigCatalog.TryGetByTableName("item", out var itemCatalogEntry), Is.True);
Assert.That(itemCatalogEntry.ConfigDomain, Is.EqualTo("item"));
Assert.That(itemCatalogEntry.ConfigRelativePath, Is.EqualTo("item"));
Assert.That(itemCatalogEntry.SchemaRelativePath, Is.EqualTo("schemas/item.schema.json"));
Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var catalogEntry), Is.True);
Assert.That(catalogEntry.ConfigDomain, Is.EqualTo("monster"));
Assert.That(catalogEntry.ConfigRelativePath, Is.EqualTo("monster"));
Assert.That(catalogEntry.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
Assert.That(ItemConfigBindings.ConfigDomain, Is.EqualTo("item"));
Assert.That(ItemConfigBindings.Metadata.TableName, Is.EqualTo("item"));
Assert.That(MonsterConfigBindings.ConfigDomain, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.TableName, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.ConfigRelativePath, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
Assert.That(MonsterConfigBindings.Metadata.ConfigDomain, Is.EqualTo(MonsterConfigBindings.ConfigDomain));
Assert.That(MonsterConfigBindings.Metadata.TableName, Is.EqualTo(MonsterConfigBindings.TableName));
Assert.That(MonsterConfigBindings.Metadata.ConfigRelativePath,
Is.EqualTo(MonsterConfigBindings.ConfigRelativePath));
Assert.That(MonsterConfigBindings.Metadata.SchemaRelativePath,
Is.EqualTo(MonsterConfigBindings.SchemaRelativePath));
Assert.That(MonsterConfigBindings.References.All, Is.Empty);
Assert.That(MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out _), Is.False);
Assert.That(monsterTable.Count, Is.EqualTo(2));
Assert.That(monsterTable.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(monsterTable.Get(2).Hp, Is.EqualTo(30));
Assert.That(monsterTable.FindByName("Slime").Select(static config => config.Id), Is.EqualTo(new[] { 1 }));
Assert.That(dungeonMonsters.Select(static config => config.Name), Is.EquivalentTo(new[] { "Slime", "Goblin" }));
Assert.That(monsterTable.TryFindFirstByName("Goblin", out var goblin), Is.True);
Assert.That(goblin, Is.Not.Null);
Assert.That(goblin!.Id, Is.EqualTo(2));
Assert.That(monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster), Is.True);
Assert.That(firstDungeonMonster, Is.Not.Null);
Assert.That(firstDungeonMonster!.Name, Is.AnyOf("Slime", "Goblin"));
Assert.That(monsterTable.TryFindFirstByFaction("forest", out var missingMonster), Is.False);
Assert.That(missingMonster, Is.Null);
Assert.That(registry.TryGetMonsterTable(out var generatedTable), Is.True);
Assert.That(generatedTable, Is.Not.Null);
Assert.That(generatedTable!.All().Select(static config => config.Name),
Is.EquivalentTo(new[] { "Slime", "Goblin" }));
Assert.That(itemTable.Count, Is.EqualTo(2));
Assert.That(itemTable.Get("potion").Name, Is.EqualTo("Potion"));
Assert.That(itemTable.FindByCategory("consumable").Select(static config => config.Id),
Is.EquivalentTo(new[] { "potion", "ether" }));
Assert.That(registry.TryGetItemTable(out var generatedItemTable), Is.True);
Assert.That(generatedItemTable, Is.Not.Null);
Assert.That(generatedItemTable!.Get("ether").Name, Is.EqualTo("Ether"));
});
}
/// <summary>
/// 验证项目级生成目录既能按配置域枚举元数据,也能直接复用聚合注册筛选规则产出启动诊断视图。
/// </summary>
[Test]
public void GeneratedConfigCatalog_Should_Expose_Domain_And_Registration_Diagnostic_Views()
{
Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var monsterMetadata), Is.True);
Assert.That(GeneratedConfigCatalog.TryGetByTableName("item", out var itemMetadata), Is.True);
var monsterDomainTables = GeneratedConfigCatalog.GetTablesInConfigDomain(MonsterConfigBindings.ConfigDomain);
var missingDomainTables = GeneratedConfigCatalog.GetTablesInConfigDomain("missing");
var itemOnlyRegistrationTables = GeneratedConfigCatalog.GetTablesForRegistration(
new GeneratedConfigRegistrationOptions
{
IncludedTableNames = new[] { ItemConfigBindings.TableName }
});
var predicateOnlyRegistrationTables = GeneratedConfigCatalog.GetTablesForRegistration(
new GeneratedConfigRegistrationOptions
{
TableFilter = static metadata =>
string.Equals(metadata.TableName, MonsterConfigBindings.TableName, StringComparison.Ordinal)
});
var monsterOnlyOptions = new GeneratedConfigRegistrationOptions
{
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
};
var predicateOnlyOptions = new GeneratedConfigRegistrationOptions
{
TableFilter = static metadata =>
string.Equals(metadata.TableName, MonsterConfigBindings.TableName, StringComparison.Ordinal)
};
Assert.Multiple(() =>
{
Assert.That(monsterDomainTables.Select(static metadata => metadata.TableName),
Is.EqualTo(new[] { MonsterConfigBindings.TableName }));
Assert.That(missingDomainTables, Is.Empty);
Assert.That(itemOnlyRegistrationTables.Select(static metadata => metadata.TableName),
Is.EqualTo(new[] { ItemConfigBindings.TableName }));
Assert.That(predicateOnlyRegistrationTables.Select(static metadata => metadata.TableName),
Is.EqualTo(new[] { MonsterConfigBindings.TableName }));
Assert.That(GeneratedConfigCatalog.GetTablesForRegistration().Select(static metadata => metadata.TableName),
Is.SupersetOf(new[] { ItemConfigBindings.TableName, MonsterConfigBindings.TableName }));
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, monsterOnlyOptions), Is.True);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(itemMetadata, monsterOnlyOptions), Is.False);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, predicateOnlyOptions), Is.True);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(itemMetadata, predicateOnlyOptions), Is.False);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, options: null), Is.True);
});
}
/// <summary>
/// 验证聚合注册入口可以通过生成配置域、表名集合和自定义谓词收敛多表项目的启动粒度。
/// </summary>
[Test]
public async Task RegisterAllGeneratedConfigTables_Should_Support_Filtering_By_Domain_Table_Name_And_Predicate()
{
CreateMonsterFiles();
CreateItemFiles();
var domainRegistry = new ConfigRegistry();
var domainLoader = new YamlConfigLoader(_rootPath)
.RegisterAllGeneratedConfigTables(
new GeneratedConfigRegistrationOptions
{
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
});
await domainLoader.LoadAsync(domainRegistry);
var tableNameRegistry = new ConfigRegistry();
var tableNameLoader = new YamlConfigLoader(_rootPath)
.RegisterAllGeneratedConfigTables(
new GeneratedConfigRegistrationOptions
{
IncludedTableNames = new[] { ItemConfigBindings.TableName }
});
await tableNameLoader.LoadAsync(tableNameRegistry);
var emptyAllowListRegistry = new ConfigRegistry();
var emptyAllowListLoader = new YamlConfigLoader(_rootPath)
.RegisterAllGeneratedConfigTables(
new GeneratedConfigRegistrationOptions
{
IncludedConfigDomains = Array.Empty<string>(),
IncludedTableNames = Array.Empty<string>()
});
await emptyAllowListLoader.LoadAsync(emptyAllowListRegistry);
var monsterDomain = MonsterConfigBindings.ConfigDomain;
var predicateRegistry = new ConfigRegistry();
var predicateLoader = new YamlConfigLoader(_rootPath)
.RegisterAllGeneratedConfigTables(
new GeneratedConfigRegistrationOptions
{
TableFilter = metadata =>
string.Equals(metadata.ConfigDomain, monsterDomain, StringComparison.Ordinal)
});
await predicateLoader.LoadAsync(predicateRegistry);
Assert.Multiple(() =>
{
Assert.That(emptyAllowListRegistry.TryGetMonsterTable(out var emptyAllowListMonsterTable), Is.True);
Assert.That(emptyAllowListMonsterTable, Is.Not.Null);
Assert.That(emptyAllowListRegistry.TryGetItemTable(out var emptyAllowListItemTable), Is.True);
Assert.That(emptyAllowListItemTable, Is.Not.Null);
Assert.That(domainRegistry.TryGetMonsterTable(out var domainMonsterTable), Is.True);
Assert.That(domainMonsterTable, Is.Not.Null);
Assert.That(domainRegistry.TryGetItemTable(out _), Is.False);
Assert.That(tableNameRegistry.TryGetMonsterTable(out _), Is.False);
Assert.That(tableNameRegistry.TryGetItemTable(out var tableNameItemTable), Is.True);
Assert.That(tableNameItemTable, Is.Not.Null);
Assert.That(tableNameItemTable!.Get("potion").Name, Is.EqualTo("Potion"));
Assert.That(predicateRegistry.TryGetMonsterTable(out var predicateMonsterTable), Is.True);
Assert.That(predicateMonsterTable, Is.Not.Null);
Assert.That(predicateRegistry.TryGetItemTable(out _), Is.False);
});
}
/// <summary>
/// 在临时消费者根目录中创建测试文件。
/// </summary>
/// <param name="relativePath">相对根目录的文件路径。</param>
/// <param name="content">要写入的文件内容。</param>
private void CreateFile(
string relativePath,
string content)
{
var path = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
var directoryPath = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
File.WriteAllText(path, content.Replace("\n", Environment.NewLine, StringComparison.Ordinal));
}
/// <summary>
/// 在临时消费者目录中创建 monster schema 与 YAML 测试数据。
/// </summary>
private void CreateMonsterFiles()
{
CreateFile(
"schemas/monster.schema.json",
"""
{
"title": "Monster Config",
"description": "Defines one monster entry for the end-to-end consumer integration test.",
"type": "object",
"required": ["id", "name", "hp", "faction"],
"properties": {
"id": {
"type": "integer",
"description": "Monster identifier."
},
"name": {
"type": "string",
"description": "Monster display name.",
"x-gframework-index": true
},
"hp": {
"type": "integer",
"description": "Monster base health."
},
"faction": {
"type": "string",
"description": "Used by the integration test to validate generated non-unique queries.",
"x-gframework-index": true
}
}
}
""");
CreateFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
hp: 10
faction: dungeon
""");
CreateFile(
"monster/goblin.yaml",
"""
id: 2
name: Goblin
hp: 30
faction: dungeon
""");
}
/// <summary>
/// 在临时消费者目录中创建 item schema 与 YAML 测试数据,用于验证多表聚合注册和筛选行为。
/// </summary>
private void CreateItemFiles()
{
CreateFile(
"schemas/item.schema.json",
"""
{
"title": "Item Config",
"description": "Defines one item entry for aggregate registration filtering integration tests.",
"type": "object",
"required": ["id", "name", "category"],
"properties": {
"id": {
"type": "string",
"description": "Item identifier."
},
"name": {
"type": "string",
"description": "Item display name."
},
"category": {
"type": "string",
"description": "Used by integration tests to validate generated non-unique queries."
}
}
}
""");
CreateFile(
"item/potion.yaml",
"""
id: potion
name: Potion
category: consumable
""");
CreateFile(
"item/ether.yaml",
"""
id: ether
name: Ether
category: consumable
""");
}
}