using System.IO;
using System.Threading;
using GFramework.Core.Architectures;
using GFramework.Core.Extensions;
using GFramework.Core.Utility;
using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config;
using GFramework.Game.Config.Generated;
namespace GFramework.Game.Tests.Config;
///
/// 验证 场景下的官方配置模块接入链路。
/// 这些测试覆盖模块安装、utility 初始化顺序以及生成表访问,确保模块化入口能够替代手写 bootstrap 模板。
///
[TestFixture]
public class ArchitectureConfigIntegrationTests
{
///
/// 清理全局架构上下文,避免测试之间残留同类型架构绑定。
///
[SetUp]
[TearDown]
public void ResetGlobalArchitectureContext()
{
GameContext.Clear();
}
///
/// 架构初始化期间,通过 收敛生成表注册、加载与注册表暴露,
/// 并将 作为 utility 暴露给架构上下文读取。
///
[Test]
public async Task ConfigModuleCanRunDuringArchitectureInitialization()
{
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.Registry.TryGetItemTable(out _), Is.False);
Assert.That(architecture.Context.GetUtility(), Is.SameAs(architecture.Registry));
Assert.That(architecture.ConfigModule.IsInitialized, Is.True);
Assert.That(architecture.ConfigModule.IsHotReloadEnabled, Is.False);
});
}
finally
{
if (architecture is not null && initialized)
{
await architecture.DestroyAsync();
}
DeleteDirectoryIfExists(rootPath);
}
}
///
/// 验证配置模块会在其他 utility 初始化之前完成首次加载,
/// 这样依赖配置的 utility 无需再自行阻塞等待配置系统完成启动。
///
[Test]
public async Task ConfigModuleShouldLoadConfigBeforeDependentUtilityInitialization()
{
var rootPath = CreateTempConfigRoot();
ConsumerArchitecture? architecture = null;
var initialized = false;
try
{
architecture = new ConsumerArchitecture(rootPath);
await architecture.InitializeAsync();
initialized = true;
Assert.Multiple(() =>
{
Assert.That(architecture.ProbeUtility.InitializedWithLoadedConfig, Is.True);
Assert.That(architecture.ProbeUtility.ObservedMonsterName, Is.EqualTo("Slime"));
Assert.That(architecture.ProbeUtility.ObservedDungeonMonsterCount, Is.EqualTo(2));
});
}
finally
{
if (architecture is not null && initialized)
{
await architecture.DestroyAsync();
}
DeleteDirectoryIfExists(rootPath);
}
}
///
/// 验证同一个模块实例不会被重复安装到多个架构中,
/// 避免共享内部 bootstrap 状态导致跨架构生命周期混淆。
///
[Test]
public async Task GameConfigModuleShouldRejectReusingTheSameModuleInstance()
{
var rootPath = CreateTempConfigRoot();
ModuleOnlyArchitecture? firstArchitecture = null;
var firstDestroyed = false;
try
{
var module = CreateModule(rootPath);
firstArchitecture = new ModuleOnlyArchitecture(module);
await firstArchitecture.InitializeAsync();
var wasInitializedBeforeDestroy = module.IsInitialized;
await firstArchitecture.DestroyAsync();
firstDestroyed = true;
firstArchitecture = null;
GameContext.Clear();
var secondArchitecture = new ModuleOnlyArchitecture(module);
var exception =
Assert.ThrowsAsync(async () => await secondArchitecture.InitializeAsync());
Assert.Multiple(() =>
{
Assert.That(wasInitializedBeforeDestroy, Is.True);
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Message, Does.Contain("cannot be installed more than once"));
});
}
finally
{
if (firstArchitecture is not null && !firstDestroyed)
{
await firstArchitecture.DestroyAsync();
}
DeleteDirectoryIfExists(rootPath);
}
}
///
/// 验证配置模块在同步阻塞且存在 的线程上仍可完成架构初始化,
/// 直接覆盖 通过生命周期钩子执行同步桥接的真实路径。
///
[Test]
public void GameConfigModuleShouldSupportSynchronousBridgeOnBlockingSynchronizationContext()
{
var rootPath = CreateTempConfigRoot();
ConsumerArchitecture? architecture = null;
var initialized = false;
try
{
architecture = new ConsumerArchitecture(rootPath);
RunBlockingOnSynchronizationContext(
() => architecture.InitializeAsync(),
TimeSpan.FromSeconds(5));
initialized = true;
var monsterTable = architecture.Registry.GetMonsterTable();
Assert.Multiple(() =>
{
Assert.That(architecture.ConfigModule.IsInitialized, Is.True);
Assert.That(monsterTable.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(monsterTable.FindByFaction("dungeon").Count(), Is.EqualTo(2));
});
}
finally
{
if (architecture is not null && initialized)
{
architecture.DestroyAsync().GetAwaiter().GetResult();
}
DeleteDirectoryIfExists(rootPath);
}
}
///
/// 验证模块在架构已经越过安装窗口时会拒绝安装,
/// 并且失败不会消耗模块实例,便于后续在新的架构上重试安装。
///
[Test]
public async Task GameConfigModuleShouldRejectLateInstallationWithoutConsumingTheModuleInstance()
{
var rootPath = CreateTempConfigRoot();
ReadyOnlyArchitecture? readyArchitecture = null;
ModuleOnlyArchitecture? retryArchitecture = null;
var readyArchitectureInitialized = false;
var retryArchitectureInitialized = false;
try
{
var module = CreateModule(rootPath);
readyArchitecture = new ReadyOnlyArchitecture();
await readyArchitecture.InitializeAsync();
readyArchitectureInitialized = true;
var exception = Assert.Throws(() => readyArchitecture.InstallModule(module));
Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Message, Does.Contain("BeforeUtilityInit"));
Assert.That(readyArchitecture.Context.GetUtilities(), Is.Empty);
Assert.That(module.IsInitialized, Is.False);
});
await readyArchitecture.DestroyAsync();
readyArchitectureInitialized = false;
readyArchitecture = null;
GameContext.Clear();
retryArchitecture = new ModuleOnlyArchitecture(module);
await retryArchitecture.InitializeAsync();
retryArchitectureInitialized = true;
Assert.Multiple(() =>
{
Assert.That(module.IsInitialized, Is.True);
Assert.That(retryArchitecture.Registry.GetMonsterTable().Get(1).Name, Is.EqualTo("Slime"));
});
}
finally
{
if (retryArchitecture is not null && retryArchitectureInitialized)
{
await retryArchitecture.DestroyAsync();
}
if (readyArchitecture is not null && readyArchitectureInitialized)
{
await readyArchitecture.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;
}
///
/// 最佳努力尝试删除临时目录。
///
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.
}
catch (UnauthorizedAccessException)
{
// Ignored: cleanup is best effort and can transiently fail when files are still being released.
}
}
///
/// 在不处理消息队列的同步上下文线程上执行阻塞等待,
/// 用于回归验证初始化异步链不会依赖原上下文恢复 continuation。
///
/// 要同步阻塞执行的异步操作。
/// 等待线程结束的超时时间。
private static void RunBlockingOnSynchronizationContext(Func action, TimeSpan timeout)
{
ArgumentNullException.ThrowIfNull(action);
Exception? capturedException = null;
var workerThread = new Thread(() =>
{
SynchronizationContext.SetSynchronizationContext(new NonPumpingSynchronizationContext());
try
{
action().GetAwaiter().GetResult();
}
catch (Exception exception)
{
capturedException = exception;
}
})
{
IsBackground = true
};
workerThread.Start();
if (!workerThread.Join(timeout))
{
Assert.Fail("The blocking synchronization-context bridge did not complete within the expected timeout.");
}
if (capturedException != null)
{
throw new AssertionException(
$"The blocking synchronization-context bridge failed: {capturedException}");
}
}
///
/// 创建一个使用配置模块的模块实例。
///
/// 测试配置根目录。
/// 已配置的模块实例。
private static GameConfigModule CreateModule(string configRoot)
{
return new GameConfigModule(CreateBootstrapOptions(configRoot));
}
///
/// 创建供测试复用的配置启动选项。
///
/// 测试配置根目录。
/// 可用于模块或直接 bootstrap 的启动选项。
private static GameConfigBootstrapOptions CreateBootstrapOptions(string configRoot)
{
return new GameConfigBootstrapOptions
{
RootPath = configRoot,
ConfigureLoader = static loader =>
loader.RegisterAllGeneratedConfigTables(
new GeneratedConfigRegistrationOptions
{
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
})
};
}
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 GameConfigModule _configModule;
private readonly ConfigAwareProbeUtility _probeUtility = new();
///
/// 使用指定配置根目录创建一个消费者测试架构。
///
/// 测试配置根目录。
/// 当 为空时抛出。
public ConsumerArchitecture(string configRoot)
{
if (configRoot == null)
{
throw new ArgumentNullException(nameof(configRoot));
}
_configModule = CreateModule(configRoot);
}
///
/// 获取当前架构安装的配置模块。
///
///
/// 该模块会在 中安装,并在架构进入
/// 时完成首次加载。
/// 调用方可通过该属性观察模块初始化状态,但不应在架构初始化完成前假定加载已经成功。
///
public GameConfigModule ConfigModule => _configModule;
///
/// 获取由配置模块暴露到架构上下文中的注册表。
///
///
/// 该属性在模块安装后即可访问同一个注册表实例,但只有在模块首次加载完成后,
/// 其中的生成配置表读取才具备成功契约。
///
public IConfigRegistry Registry => _configModule.Registry;
///
/// 获取测试使用的怪物配置表。
///
/// 已经从 中解析出的怪物表包装。
/// 当模块首次加载尚未完成时抛出。
///
/// 该属性用于断言生成访问器在架构初始化完成后可直接读取;
/// 它依赖模块在 utility 初始化阶段之前已经完成首次加载。
///
public MonsterTable MonsterTable => Registry.GetMonsterTable();
///
/// 获取用于观测 utility 初始化阶段配置可见性的探针 utility。
///
///
/// 该 utility 会在 中注册,并在 utility 初始化阶段读取配置表,
/// 用于验证配置模块是否按约定在更早的生命周期阶段完成首载。
///
public ConfigAwareProbeUtility ProbeUtility => _probeUtility;
///
/// 在用户初始化阶段安装配置模块,并注册一个依赖配置的测试 utility,
/// 以验证模块会在 utility 初始化前完成首次加载。
///
protected override void OnInitialize()
{
InstallModule(_configModule);
RegisterUtility(_probeUtility);
}
}
///
/// 用于验证模块复用限制的最小架构。
///
private sealed class ModuleOnlyArchitecture(GameConfigModule configModule) : Architecture
{
///
/// 获取安装到当前测试架构中的配置模块。
///
///
/// 该属性直接暴露传入的模块实例,便于测试验证同一模块实例跨架构复用时的生命周期约束。
///
public GameConfigModule ConfigModule => configModule;
///
/// 获取当前模块共享的配置注册表。
///
///
/// 该注册表实例在模块安装后即与架构绑定,但只有在架构完成配置首载后,
/// 其中的强类型配置表访问才应被视为可用。
///
public IConfigRegistry Registry => configModule.Registry;
///
/// 安装外部传入的配置模块。
///
protected override void OnInitialize()
{
InstallModule(configModule);
}
}
///
/// 仅用于把架构推进到 Ready 阶段的空壳架构。
///
private sealed class ReadyOnlyArchitecture : Architecture
{
///
/// 该测试架构不注册任何组件,仅验证模块的安装窗口约束。
///
protected override void OnInitialize()
{
}
}
///
/// 在 utility 初始化阶段直接读取配置表的探针工具。
/// 如果模块没有在 utility 阶段开始前完成首次加载,这个探针会在初始化时失败。
///
private sealed class ConfigAwareProbeUtility : AbstractContextUtility
{
///
/// 获取一个值,指示初始化时是否已经读取到有效配置。
///
public bool InitializedWithLoadedConfig { get; private set; }
///
/// 获取初始化期间读取到的怪物名称。
///
public string? ObservedMonsterName { get; private set; }
///
/// 获取初始化期间读取到的 dungeon 阵营怪物数量。
///
public int ObservedDungeonMonsterCount { get; private set; }
///
/// 读取架构上下文中的配置注册表并验证目标表已经可用。
///
protected override void OnInit()
{
var registry = this.GetUtility();
var monsterTable = registry.GetMonsterTable();
ObservedMonsterName = monsterTable.Get(1).Name;
ObservedDungeonMonsterCount = monsterTable.FindByFaction("dungeon").Count();
InitializedWithLoadedConfig = true;
}
}
///
/// 模拟一个不会主动处理 回调的阻塞线程上下文。
/// 如果初始化链错误地捕获该上下文,continuation 会永久悬挂,从而暴露同步桥接死锁。
///
private sealed class NonPumpingSynchronizationContext : SynchronizationContext
{
///
/// 丢弃异步投递的 continuation,模拟被同步阻塞且未泵消息的宿主线程。
///
/// 要执行的回调。
/// 回调状态。
public override void Post(SendOrPostCallback d, object? state)
{
}
}
}