Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.IO;
using System.Linq;
using GFramework.Game.Config;
using GFramework.Game.Config.Generated;

Expand All @@ -13,6 +11,8 @@ namespace GFramework.Game.Tests.Config;
[TestFixture]
public class GeneratedConfigConsumerIntegrationTests
{
private string _rootPath = null!;

/// <summary>
/// 为每个端到端测试准备独立的配置根目录,避免编译期 schema 资产与运行时写入互相污染。
/// </summary>
Expand All @@ -35,8 +35,6 @@ public void TearDown()
}
}

private string _rootPath = null!;

/// <summary>
/// 验证生成器自动拾取消费者项目的 schema 后,
/// 可以用生成的聚合注册辅助完成加载,并通过强类型表包装访问运行时数据与查询辅助。
Expand Down Expand Up @@ -88,7 +86,8 @@ public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Projec
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(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));
Expand Down Expand Up @@ -154,10 +153,13 @@ public void GeneratedConfigCatalog_Should_Expose_Domain_And_Registration_Diagnos
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(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, predicateOnlyOptions),
Is.True);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(itemMetadata, predicateOnlyOptions),
Is.False);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, options: null), Is.True);
});
}
Expand Down Expand Up @@ -232,6 +234,61 @@ public async Task RegisterAllGeneratedConfigTables_Should_Support_Filtering_By_D
});
}

/// <summary>
/// 验证生成绑定会同时暴露 YAML 序列化、schema 路径解析与文本校验入口。
/// </summary>
[Test]
public async Task GeneratedBindings_Should_Expose_Serializer_And_Validator_Helpers()
{
CreateMonsterFiles();

var config = new MonsterConfig
{
Id = 3,
Name = "Bat",
Hp = 12,
Faction = "cave"
};

var yaml = MonsterConfigBindings.SerializeToYaml(config);
var schemaPath = MonsterConfigBindings.GetSchemaPath(_rootPath);
var configDirectoryPath = MonsterConfigBindings.GetConfigDirectoryPath(_rootPath);

Assert.Multiple(() =>
{
Assert.That(schemaPath, Is.EqualTo(Path.Combine(_rootPath, "schemas", "monster.schema.json")));
Assert.That(configDirectoryPath, Is.EqualTo(Path.Combine(_rootPath, "monster")));
Assert.That(yaml, Does.Contain("id: 3"));
Assert.That(yaml, Does.Contain("name: Bat"));
Assert.That(yaml, Does.Contain("hp: 12"));
Assert.That(yaml, Does.Contain("faction: cave"));
Assert.That(yaml.EndsWith(Environment.NewLine, StringComparison.Ordinal), Is.True);
});

Assert.DoesNotThrow(() =>
MonsterConfigBindings.ValidateYaml(_rootPath, "monster/generated.yaml", yaml));

Assert.DoesNotThrowAsync(async () =>
await MonsterConfigBindings.ValidateYamlAsync(_rootPath, "monster/generated.yaml", yaml));
Comment thread
GeWuYou marked this conversation as resolved.

var invalidYaml = """
id: 3
name: Bat
hp: 12
unknownField: true
""";

var exception = Assert.Throws<ConfigLoadException>(() =>
MonsterConfigBindings.ValidateYaml(_rootPath, "monster/generated.yaml", invalidYaml));

Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Diagnostic.SchemaPath, Is.EqualTo(schemaPath));
Assert.That(exception.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.UnknownProperty));
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// <summary>
/// 在临时消费者根目录中创建测试文件。
/// </summary>
Expand Down
165 changes: 165 additions & 0 deletions GFramework.Game.Tests/Config/YamlConfigTextValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
using System.IO;
using GFramework.Game.Config;

namespace GFramework.Game.Tests.Config;

/// <summary>
/// 验证公开的 YAML 文本校验入口可以在保存前复用运行时同一套 schema 规则。
/// </summary>
[TestFixture]
public sealed class YamlConfigTextValidatorTests
{
private string _rootPath = null!;

/// <summary>
/// 为每个测试准备独立临时目录。
/// </summary>
[SetUp]
public void SetUp()
{
_rootPath = Path.Combine(Path.GetTempPath(), "GFramework.TextValidatorTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_rootPath);
}

/// <summary>
/// 清理测试临时目录。
/// </summary>
[TearDown]
public void TearDown()
{
if (Directory.Exists(_rootPath))
{
Directory.Delete(_rootPath, true);
}
}

/// <summary>
/// 验证合法 YAML 文本会通过公开校验入口。
/// </summary>
[Test]
public void Validate_Should_Succeed_When_Yaml_Matches_Schema()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "hp"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" }
}
}
""");

Assert.DoesNotThrow(() =>
YamlConfigTextValidator.Validate(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
name: Slime
hp: 10
"""));
}

/// <summary>
/// 验证结构错误会继续通过稳定的配置异常类型暴露给宿主。
/// </summary>
[Test]
public void Validate_Should_Throw_ConfigLoadException_When_Yaml_Contains_Unknown_Field()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
}
""");

var exception = Assert.Throws<ConfigLoadException>(() =>
YamlConfigTextValidator.Validate(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
name: Slime
hp: 10
"""));

Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Diagnostic.TableName, Is.EqualTo("monster"));
Assert.That(exception.Diagnostic.SchemaPath, Is.EqualTo(schemaPath));
Assert.That(exception.Diagnostic.YamlPath, Is.EqualTo("monster/generated.yaml"));
Assert.That(exception.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.UnknownProperty));
});
}

/// <summary>
/// 验证异步入口与同步入口共享相同校验语义。
/// </summary>
[Test]
public async Task ValidateAsync_Should_Throw_ConfigLoadException_When_Required_Field_Is_Missing()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
}
""");

var exception = Assert.ThrowsAsync<ConfigLoadException>(async () =>
await YamlConfigTextValidator.ValidateAsync(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
"""));

Assert.Multiple(() =>
{
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.MissingRequiredProperty));
Assert.That(exception.Diagnostic.SchemaPath, Is.EqualTo(schemaPath));
Assert.That(exception.Diagnostic.YamlPath, Is.EqualTo("monster/generated.yaml"));
});
}

/// <summary>
/// 在临时目录中创建 schema 文件。
/// </summary>
/// <param name="relativePath">相对根目录的路径。</param>
/// <param name="content">文件内容。</param>
/// <returns>写入后的绝对路径。</returns>
private string CreateSchemaFile(
string relativePath,
string content)
{
var fullPath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
var directoryPath = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrWhiteSpace(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}

File.WriteAllText(fullPath, content.Replace("\n", Environment.NewLine, StringComparison.Ordinal));
return fullPath;
}
}
Loading
Loading