using System.IO; using GFramework.Game.Config; namespace GFramework.Game.Tests.Config; /// /// 验证 YAML 配置加载器的目录扫描与注册行为。 /// [TestFixture] public class YamlConfigLoaderTests { private string _rootPath = null!; /// /// 为每个测试创建独立临时目录,避免文件系统状态互相污染。 /// [SetUp] public void SetUp() { _rootPath = Path.Combine(Path.GetTempPath(), "GFramework.ConfigTests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_rootPath); } /// /// 清理测试期间创建的临时目录。 /// [TearDown] public void TearDown() { if (Directory.Exists(_rootPath)) { Directory.Delete(_rootPath, true); } } /// /// 验证加载器能够扫描 YAML 文件并将结果写入注册表。 /// [Test] public async Task LoadAsync_Should_Register_Table_From_Yaml_Files() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime hp: 10 """); CreateConfigFile( "monster/goblin.yml", """ id: 2 name: Goblin hp: 30 """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", static config => config.Id); var registry = new ConfigRegistry(); await loader.LoadAsync(registry); var table = registry.GetTable("monster"); Assert.Multiple(() => { 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)); }); } /// /// 验证注册的配置目录不存在时会抛出清晰错误。 /// [Test] public void LoadAsync_Should_Throw_When_Config_Directory_Does_Not_Exist() { var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("monster")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证某个配置表加载失败时,注册表不会留下部分成功的中间状态。 /// [Test] public void LoadAsync_Should_Not_Mutate_Registry_When_A_Later_Table_Fails() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime hp: 10 """); var registry = new ConfigRegistry(); registry.RegisterTable( "existing", new InMemoryConfigTable( new[] { new ExistingConfigStub(100, "Original") }, static config => config.Id)); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", static config => config.Id) .RegisterTable("broken", "broken", static config => config.Id); Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(registry.Count, Is.EqualTo(1)); Assert.That(registry.HasTable("monster"), Is.False); Assert.That(registry.GetTable("existing").Get(100).Name, Is.EqualTo("Original")); }); } /// /// 验证非法 YAML 会被包装成带文件路径的反序列化错误。 /// [Test] public void LoadAsync_Should_Throw_With_File_Path_When_Yaml_Is_Invalid() { CreateConfigFile( "monster/slime.yaml", """ id: [1 name: Slime """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("slime.yaml")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证启用 schema 校验后,缺失必填字段会在反序列化前被拒绝。 /// [Test] public void LoadAsync_Should_Throw_When_Required_Property_Is_Missing_According_To_Schema() { CreateConfigFile( "monster/slime.yaml", """ id: 1 hp: 10 """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "hp": { "type": "integer" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("name")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证启用 schema 校验后,类型不匹配的标量字段会被拒绝。 /// [Test] public void LoadAsync_Should_Throw_When_Property_Type_Does_Not_Match_Schema() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime hp: high """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "hp": { "type": "integer" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("hp")); Assert.That(exception!.Message, Does.Contain("integer")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证启用 schema 校验后,未知字段不会再被静默忽略。 /// [Test] public void LoadAsync_Should_Throw_When_Unknown_Property_Is_Present_In_Schema_Bound_Mode() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime attackPower: 2 """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "hp": { "type": "integer" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("attackPower")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证数组字段的元素类型会按 schema 校验。 /// [Test] public void LoadAsync_Should_Throw_When_Array_Item_Type_Does_Not_Match_Schema() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime dropRates: - 1 - potion """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "dropRates": { "type": "array", "items": { "type": "integer" } } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable( "monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("dropRates")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 创建测试用配置文件。 /// /// 相对根目录的文件路径。 /// 文件内容。 private void CreateConfigFile(string relativePath, string content) { var fullPath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar)); var directory = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); } File.WriteAllText(fullPath, content); } /// /// 创建测试用 schema 文件。 /// /// 相对根目录的文件路径。 /// 文件内容。 private void CreateSchemaFile(string relativePath, string content) { CreateConfigFile(relativePath, content); } /// /// 用于 YAML 加载测试的最小怪物配置类型。 /// private sealed class MonsterConfigStub { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置名称。 /// public string Name { get; set; } = string.Empty; /// /// 获取或设置生命值。 /// public int Hp { get; set; } } /// /// 用于数组 schema 校验测试的最小怪物配置类型。 /// private sealed class MonsterConfigIntegerArrayStub { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置名称。 /// public string Name { get; set; } = string.Empty; /// /// 获取或设置掉落率列表。 /// public IReadOnlyList DropRates { get; set; } = Array.Empty(); } /// /// 用于验证注册表一致性的现有配置类型。 /// /// 配置主键。 /// 配置名称。 private sealed record ExistingConfigStub(int Id, string Name); }