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
38 changes: 19 additions & 19 deletions GFramework.Core.Abstractions/Internals/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// IsExternalInit.cs
// This type is required to support init-only setters and record types
// when targeting netstandard2.0 or older frameworks.
#if !NET5_0_OR_GREATER
using System.ComponentModel;
// ReSharper disable CheckNamespace
namespace System.Runtime.CompilerServices;
/// <summary>
/// 提供一个占位符类型,用于支持 C# 9.0 的 init 访问器功能。
/// 该类型在 .NET 5.0 及更高版本中已内置,因此仅在较低版本的 .NET 中定义。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
// IsExternalInit.cs
// This type is required to support init-only setters and record types
// when targeting netstandard2.0 or older frameworks.

#if !NET5_0_OR_GREATER
using System.ComponentModel;

// ReSharper disable CheckNamespace

namespace System.Runtime.CompilerServices;

/// <summary>
/// 提供一个占位符类型,用于支持 C# 9.0 的 init 访问器功能。
/// 该类型在 .NET 5.0 及更高版本中已内置,因此仅在较低版本的 .NET 中定义。
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
#endif
155 changes: 155 additions & 0 deletions GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using GFramework.Core.Architectures;
using GFramework.Game.Config;
using GFramework.Game.Config.Generated;
using NUnit.Framework;

namespace GFramework.Game.Tests.Config;

/// <summary>
/// 验证在 <see cref="Architecture" /> 初始化流程中可以注册配置注册表、执行加载并通过生成的表访问器读取数据。
/// </summary>
[TestFixture]
public class ArchitectureConfigIntegrationTests
{
/// <summary>
/// 架构初始化期间,通过 <see cref="YamlConfigLoader" /> 注册生成表,
/// 并将 <see cref="ConfigRegistry" /> 作为 utility 暴露给架构上下文读取。
/// </summary>
[Test]
public async Task ConfigLoaderCanRunDuringArchitectureInitialization()
{
var rootPath = CreateTempConfigRoot();
ConsumerArchitecture? architecture = null;
var initialized = false;
try
{
architecture = new ConsumerArchitecture(rootPath);
await architecture.InitializeAsync();
initialized = true;

var table = architecture.MonsterTable;

Assert.Multiple(() =>
{
Assert.That(table.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(table.Get(2).Hp, Is.EqualTo(30));
Assert.That(table.FindByFaction("dungeon").Select(static config => config.Name),
Is.EquivalentTo(new[] { "Slime", "Goblin" }));
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.Context.GetUtility<ConfigRegistry>(), Is.SameAs(architecture.Registry));
});
}
finally
{
if (architecture is not null && initialized)
{
await architecture.DestroyAsync();
}

DeleteDirectoryIfExists(rootPath);
}
}

private static string CreateTempConfigRoot()
{
var rootPath = Path.Combine(Path.GetTempPath(), "GFramework.ConfigArchitecture", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(rootPath);
Directory.CreateDirectory(Path.Combine(rootPath, "schemas"));
Directory.CreateDirectory(Path.Combine(rootPath, "monster"));
File.WriteAllText(Path.Combine(rootPath, "schemas", "monster.schema.json"), MonsterSchemaJson);
File.WriteAllText(Path.Combine(rootPath, "monster", "slime.yaml"), MonsterSlimeYaml);
File.WriteAllText(Path.Combine(rootPath, "monster", "goblin.yaml"), MonsterGoblinYaml);
return rootPath;
}

/// <summary>
/// 最佳努力尝试删除临时目录。
/// </summary>
private static void DeleteDirectoryIfExists(string path)
{
if (!Directory.Exists(path))
{
return;
}

try
{
Directory.Delete(path, true);
}
catch (IOException)
{
// Ignored: cleanup is best effort and should not fail the test.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
catch (UnauthorizedAccessException)
{
// Ignored: cleanup is best effort and can transiently fail when files are still being released.
}
}

private const string MonsterSchemaJson = @"{
""title"": ""Monster Config"",
""description"": ""Defines one monster entry for the generated 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.""
}
}
}";

private const string MonsterSlimeYaml =
"id: 1\nname: Slime\nhp: 10\nfaction: dungeon\n";

private const string MonsterGoblinYaml =
"id: 2\nname: Goblin\nhp: 30\nfaction: dungeon\n";

private sealed class ConsumerArchitecture : Architecture
{
private readonly string _configRoot;

public ConfigRegistry Registry { get; }

public MonsterTable MonsterTable { get; private set; } = null!;

public ConsumerArchitecture(string configRoot)
{
_configRoot = configRoot ?? throw new ArgumentNullException(nameof(configRoot));
Registry = new ConfigRegistry();
}

protected override void OnInitialize()
{
RegisterUtility(Registry);

var loader = new YamlConfigLoader(_configRoot)
.RegisterMonsterTable();
loader.LoadAsync(Registry).GetAwaiter().GetResult();
MonsterTable = Registry.GetMonsterTable();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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
Expand Down Expand Up @@ -37,7 +39,7 @@ public void TearDown()

/// <summary>
/// 验证生成器自动拾取消费者项目的 schema 后,
/// 可以用生成的注册辅助完成加载,并通过强类型表包装访问运行时数据
/// 可以用生成的注册辅助完成加载,并通过强类型表包装访问运行时数据与查询辅助
/// </summary>
[Test]
public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Project()
Expand All @@ -49,7 +51,7 @@ public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Projec
"title": "Monster Config",
"description": "Defines one monster entry for the end-to-end consumer integration test.",
"type": "object",
"required": ["id", "name", "hp"],
"required": ["id", "name", "hp", "faction"],
"properties": {
"id": {
"type": "integer",
Expand All @@ -62,6 +64,10 @@ public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Projec
"hp": {
"type": "integer",
"description": "Monster base health."
},
"faction": {
"type": "string",
"description": "Used by the integration test to validate generated non-unique queries."
}
}
}
Expand All @@ -72,13 +78,15 @@ public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Projec
id: 1
name: Slime
hp: 10
faction: dungeon
""");
CreateFile(
"monster/goblin.yaml",
"""
id: 2
name: Goblin
hp: 30
faction: dungeon
""");

var registry = new ConfigRegistry();
Expand All @@ -88,6 +96,7 @@ public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Projec
await loader.LoadAsync(registry);

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

Assert.Multiple(() =>
{
Expand All @@ -106,6 +115,16 @@ public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Projec
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(dungeonMonsters.Select(static config => config.Name), Is.EquivalentTo(new[] { "Slime", "Goblin" }));
Assert.That(table.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(firstDungeonMonster, Is.Not.Null);
Assert.That(firstDungeonMonster!.Name, Is.AnyOf("Slime", "Goblin"));
Assert.That(table.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),
Expand All @@ -131,4 +150,4 @@ private void CreateFile(

File.WriteAllText(path, content.Replace("\n", Environment.NewLine, StringComparison.Ordinal));
}
}
}
7 changes: 6 additions & 1 deletion GFramework.Game.Tests/schemas/monster.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"required": [
"id",
"name",
"hp"
"hp",
"faction"
],
"properties": {
"id": {
Expand All @@ -19,6 +20,10 @@
"hp": {
"type": "integer",
"description": "Monster base health."
},
"faction": {
"type": "string",
"description": "Used by integration tests to validate generated non-unique queries."
}
}
}
Loading
Loading