Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ All generated or modified code MUST include clear and meaningful comments where
- Every non-trivial feature, bug fix, or behavior change MUST include tests or an explicit justification for why a test
is not practical.
- Public API changes must be covered by unit or integration tests.
- When a public API defines multiple contract branches, tests MUST cover the meaningful variants, including null,
empty, default, and filtered inputs when those branches change behavior.
- Regression fixes should include a test that fails before the fix and passes after it.

### Test Organization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public async Task ConfigLoaderCanRunDuringArchitectureInitialization()
Assert.That(architecture.Registry.TryGetMonsterTable(out var retrieved), Is.True);
Assert.That(retrieved, Is.Not.Null);
Assert.That(retrieved!.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(architecture.Registry.TryGetItemTable(out _), Is.False);
Assert.That(architecture.Context.GetUtility<ConfigRegistry>(), Is.SameAs(architecture.Registry));
});
}
Expand Down Expand Up @@ -147,7 +148,11 @@ protected override void OnInitialize()
RegisterUtility(Registry);

var loader = new YamlConfigLoader(_configRoot)
.RegisterAllGeneratedConfigTables();
.RegisterAllGeneratedConfigTables(
new GeneratedConfigRegistrationOptions
{
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
});
loader.LoadAsync(Registry).GetAwaiter().GetResult();
MonsterTable = Registry.GetMonsterTable();
}
Expand Down
246 changes: 192 additions & 54 deletions GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,69 +44,34 @@ public void TearDown()
[Test]
public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Project()
{
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."
},
"hp": {
"type": "integer",
"description": "Monster base health."
},
"faction": {
"type": "string",
"description": "Used by the integration test to validate generated non-unique queries."
}
}
}
""");
CreateFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
hp: 10
faction: dungeon
""");
CreateFile(
"monster/goblin.yaml",
"""
id: 2
name: Goblin
hp: 30
faction: dungeon
""");
CreateMonsterFiles();
CreateItemFiles();

var registry = new ConfigRegistry();
var loader = new YamlConfigLoader(_rootPath)
.RegisterAllGeneratedConfigTables();

await loader.LoadAsync(registry);

var table = registry.GetMonsterTable();
var dungeonMonsters = table.FindByFaction("dungeon");
var monsterTable = registry.GetMonsterTable();
var dungeonMonsters = monsterTable.FindByFaction("dungeon");
var itemTable = registry.GetItemTable();

Assert.Multiple(() =>
{
Assert.That(
GeneratedConfigCatalog.Tables.Select(static metadata => metadata.TableName),
Does.Contain("monster"));
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"));
Expand All @@ -119,23 +84,100 @@ public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Projec
Is.EqualTo(MonsterConfigBindings.SchemaRelativePath));
Assert.That(MonsterConfigBindings.References.All, Is.Empty);
Assert.That(MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out _), Is.False);
Assert.That(table.Count, Is.EqualTo(2));
Assert.That(table.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(table.Get(2).Hp, Is.EqualTo(30));
Assert.That(table.FindByName("Slime").Select(static config => config.Id), Is.EqualTo(new[] { 1 }));
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(table.TryFindFirstByName("Goblin", out var goblin), Is.True);
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(table.TryFindFirstByFaction("dungeon", out var firstDungeonMonster), Is.True);
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(table.TryFindFirstByFaction("forest", out var missingMonster), Is.False);
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 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);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Expand All @@ -157,4 +199,100 @@ private void CreateFile(

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."
},
"hp": {
"type": "integer",
"description": "Monster base health."
},
"faction": {
"type": "string",
"description": "Used by the integration test to validate generated non-unique queries."
}
}
}
""");
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
""");
}
}
24 changes: 24 additions & 0 deletions GFramework.Game.Tests/schemas/item.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"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."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -452,15 +452,22 @@ public YamlConfigLoader RegisterTable<TKey, TValue>(
Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigCatalog"));
Assert.That(catalogSource, Does.Contain("public sealed class GeneratedConfigRegistrationOptions"));
Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigRegistrationExtensions"));
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedConfigDomains { get; init; }"));
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedTableNames { get; init; }"));
Assert.That(catalogSource, Does.Contain("public global::System.Predicate<GeneratedConfigCatalog.TableMetadata>? TableFilter { get; init; }"));
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IEqualityComparer<string>? ItemComparer { get; init; }"));
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }"));
Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);"));
Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options"));
Assert.That(catalogSource, Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[0], options))"));
Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(options.ItemComparer);"));
Assert.That(catalogSource, Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[1], options))"));
Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(options.MonsterComparer);"));
Assert.That(catalogSource, Does.Contain("ItemConfigBindings.Metadata.TableName"));
Assert.That(catalogSource, Does.Contain("MonsterConfigBindings.Metadata.TableName"));
Assert.That(catalogSource, Does.Contain("public static bool TryGetByTableName(string tableName, out TableMetadata metadata)"));
Assert.That(catalogSource, Does.Contain("private static bool ShouldRegisterTable("));
Assert.That(catalogSource, Does.Contain("private static bool MatchesOptionalAllowList("));
});
}
}
Loading
Loading