using System.IO; namespace GFramework.SourceGenerators.Tests.Config; /// /// 验证 schema 配置生成器的生成快照。 /// [TestFixture] public class SchemaConfigGeneratorSnapshotTests { private const string RuntimeContractsSource = """ using System; using System.Collections.Generic; namespace GFramework.Game.Abstractions.Config { public interface IConfigTable { Type KeyType { get; } Type ValueType { get; } int Count { get; } } public interface IConfigTable : IConfigTable where TKey : notnull { TValue Get(TKey key); bool TryGet(TKey key, out TValue? value); bool ContainsKey(TKey key); IReadOnlyCollection All(); } public interface IConfigRegistry { IConfigTable GetTable(string name) where TKey : notnull; bool TryGetTable(string name, out IConfigTable? table) where TKey : notnull; } } namespace GFramework.Game.Config { public sealed class YamlConfigLoader { public YamlConfigLoader RegisterTable( string tableName, string relativePath, string schemaRelativePath, Func keySelector, IEqualityComparer? comparer = null) where TKey : notnull { return this; } } } """; private const string MonsterSchema = """ { "title": "Monster Config", "description": "Represents one monster entry generated from schema metadata.", "type": "object", "minProperties": 4, "maxProperties": 8, "required": ["id", "name", "reward", "phases"], "properties": { "id": { "type": "integer", "description": "Unique monster identifier." }, "name": { "type": "string", "title": "Monster Name", "description": "Localized monster display name.", "x-gframework-index": true, "minLength": 3, "maxLength": 16, "pattern": "^[A-Z][a-z]+$", "default": "Slime", "enum": ["Slime", "Goblin"] }, "hp": { "type": "integer", "const": 10, "minimum": 1, "maximum": 999, "exclusiveMinimum": 0, "exclusiveMaximum": 1000, "multipleOf": 5, "default": 10 }, "dropItems": { "description": "Referenced drop ids.", "type": "array", "minItems": 1, "maxItems": 3, "minContains": 1, "maxContains": 2, "uniqueItems": true, "contains": { "type": "string", "const": "potion" }, "items": { "type": "string", "minLength": 3, "maxLength": 12, "enum": ["potion", "slime_gel"] }, "default": ["potion"], "x-gframework-ref-table": "item" }, "reward": { "type": "object", "description": "Reward payload.", "minProperties": 2, "maxProperties": 2, "required": ["gold", "currency"], "properties": { "gold": { "type": "integer", "minimum": 0, "default": 10 }, "currency": { "type": "string", "enum": ["coin", "gem"] } }, "dependentRequired": { "currency": ["gold"] }, "dependentSchemas": { "currency": { "type": "object", "required": ["gold"], "properties": { "gold": { "type": "integer" } } } }, "allOf": [ { "type": "object", "required": ["gold"], "properties": { "gold": { "type": "integer" } } } ], "if": { "type": "object", "properties": { "currency": { "type": "string", "const": "gem" } } }, "then": { "type": "object", "required": ["gold"], "properties": { "gold": { "type": "integer" } } }, "else": { "type": "object", "required": ["currency"], "properties": { "currency": { "type": "string" } } } }, "phases": { "type": "array", "description": "Encounter phases.", "items": { "type": "object", "required": ["wave", "monsterId"], "properties": { "wave": { "type": "integer" }, "monsterId": { "type": "string", "description": "Monster reference id.", "minLength": 2, "maxLength": 32, "x-gframework-ref-table": "monster" } } } } } } """; /// /// 验证一个最小 monster schema 能生成配置类型、表包装和注册辅助。 /// [Test] public Task Snapshot_SchemaConfigGenerator() { var generatedSources = GenerateSourcesForMonsterSchema(); var snapshotFolder = GetSchemaSnapshotFolder(); return AssertAllSnapshotsAsync(generatedSources, snapshotFolder); } /// /// 运行 monster schema 场景,并把生成结果转换为按 hint name 索引的字典。 /// /// 当前快照场景的全部生成文件内容。 private static IReadOnlyDictionary GenerateSourcesForMonsterSchema() { var result = SchemaGeneratorTestDriver.Run( RuntimeContractsSource, ("monster.schema.json", MonsterSchema)); return result.Results .Single() .GeneratedSources .ToDictionary( static sourceResult => sourceResult.HintName, static sourceResult => sourceResult.SourceText.ToString(), StringComparer.Ordinal); } /// /// 解析 schema 生成器快照目录,确保断言始终落在仓库内已提交的 snapshot 资产上。 /// /// schema 生成器快照目录的绝对路径。 private static string GetSchemaSnapshotFolder() { var snapshotFolder = Path.Combine( TestContext.CurrentContext.TestDirectory, "..", "..", "..", "Config", "snapshots", "SchemaConfigGenerator"); return Path.GetFullPath(snapshotFolder); } /// /// 对单个生成文件执行快照断言。 /// /// 生成结果字典。 /// 快照目录。 /// 快照文件名。 private static async Task AssertSnapshotAsync( IReadOnlyDictionary generatedSources, string snapshotFolder, string generatedFileName, string snapshotFileName) { if (!generatedSources.TryGetValue(generatedFileName, out var actual)) { Assert.Fail($"Generated source '{generatedFileName}' was not found."); return; } var path = Path.Combine(snapshotFolder, snapshotFileName); if (!File.Exists(path)) { Directory.CreateDirectory(snapshotFolder); await File.WriteAllTextAsync(path, actual).ConfigureAwait(false); Assert.Fail($"Snapshot not found. Generated new snapshot at:\n{path}"); } var expected = await File.ReadAllTextAsync(path).ConfigureAwait(false); Assert.That( Normalize(expected), Is.EqualTo(Normalize(actual)), $"Snapshot mismatch: {generatedFileName}"); } /// /// 依次验证 schema 生成器产出的全部核心快照文件。 /// /// 生成结果字典。 /// 快照目录。 /// 全部快照断言完成后的异步任务。 private static async Task AssertAllSnapshotsAsync( IReadOnlyDictionary generatedSources, string snapshotFolder) { await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfig.g.cs", "MonsterConfig.g.txt") .ConfigureAwait(false); await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterTable.g.cs", "MonsterTable.g.txt") .ConfigureAwait(false); await AssertSnapshotAsync( generatedSources, snapshotFolder, "MonsterConfigBindings.g.cs", "MonsterConfigBindings.g.txt") .ConfigureAwait(false); await AssertSnapshotAsync( generatedSources, snapshotFolder, "GeneratedConfigCatalog.g.cs", "GeneratedConfigCatalog.g.txt") .ConfigureAwait(false); } /// /// 标准化快照文本以避免平台换行差异。 /// /// 原始文本。 /// 标准化后的文本。 private static string Normalize(string text) { return text.Replace("\r\n", "\n", StringComparison.Ordinal).Trim(); } }