fix(source-generators-tests): 收敛 schema 快照测试超长方法

- 重构 SchemaConfigGeneratorSnapshotTests 的 monster 场景输入与快照辅助逻辑,消除单个超长测试方法

- 更新 analyzer warning reduction 的 tracking 与 trace,记录 RP-033 基线和验证结果
This commit is contained in:
gewuyou 2026-04-23 13:01:06 +08:00
parent 9d251ab1f8
commit 3203239726
3 changed files with 218 additions and 159 deletions

View File

@ -8,171 +8,189 @@ namespace GFramework.SourceGenerators.Tests.Config;
[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<TKey, TValue> : IConfigTable
where TKey : notnull
{
TValue Get(TKey key);
bool TryGet(TKey key, out TValue? value);
bool ContainsKey(TKey key);
IReadOnlyCollection<TValue> All();
}
public interface IConfigRegistry
{
IConfigTable<TKey, TValue> GetTable<TKey, TValue>(string name)
where TKey : notnull;
bool TryGetTable<TKey, TValue>(string name, out IConfigTable<TKey, TValue>? table)
where TKey : notnull;
}
}
namespace GFramework.Game.Config
{
public sealed class YamlConfigLoader
{
public YamlConfigLoader RegisterTable<TKey, TValue>(
string tableName,
string relativePath,
string schemaRelativePath,
Func<TValue, TKey> keySelector,
IEqualityComparer<TKey>? 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"]
}
}
},
"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"
}
}
}
}
}
}
""";
/// <summary>
/// 验证一个最小 monster schema 能生成配置类型、表包装和注册辅助。
/// </summary>
/// 验证一个最小 monster schema 能生成配置类型、表包装和注册辅助。
/// </summary>
[Test]
public Task Snapshot_SchemaConfigGenerator()
{
const string source = """
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<TKey, TValue> : IConfigTable
where TKey : notnull
{
TValue Get(TKey key);
bool TryGet(TKey key, out TValue? value);
bool ContainsKey(TKey key);
IReadOnlyCollection<TValue> All();
}
public interface IConfigRegistry
{
IConfigTable<TKey, TValue> GetTable<TKey, TValue>(string name)
where TKey : notnull;
bool TryGetTable<TKey, TValue>(string name, out IConfigTable<TKey, TValue>? table)
where TKey : notnull;
}
}
namespace GFramework.Game.Config
{
public sealed class YamlConfigLoader
{
public YamlConfigLoader RegisterTable<TKey, TValue>(
string tableName,
string relativePath,
string schemaRelativePath,
Func<TValue, TKey> keySelector,
IEqualityComparer<TKey>? comparer = null)
where TKey : notnull
{
return this;
}
}
}
""";
const string schema = """
{
"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"]
}
}
},
"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"
}
}
}
}
}
}
""";
var generatedSources = GenerateSourcesForMonsterSchema();
var snapshotFolder = GetSchemaSnapshotFolder();
return AssertAllSnapshotsAsync(generatedSources, snapshotFolder);
}
/// <summary>
/// 运行 monster schema 场景,并把生成结果转换为按 hint name 索引的字典。
/// </summary>
/// <returns>当前快照场景的全部生成文件内容。</returns>
private static IReadOnlyDictionary<string, string> GenerateSourcesForMonsterSchema()
{
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
RuntimeContractsSource,
("monster.schema.json", MonsterSchema));
var generatedSources = result.Results
return result.Results
.Single()
.GeneratedSources
.ToDictionary(
static sourceResult => sourceResult.HintName,
static sourceResult => sourceResult.SourceText.ToString(),
StringComparer.Ordinal);
}
/// <summary>
/// 解析 schema 生成器快照目录,确保断言始终落在仓库内已提交的 snapshot 资产上。
/// </summary>
/// <returns>schema 生成器快照目录的绝对路径。</returns>
private static string GetSchemaSnapshotFolder()
{
var snapshotFolder = Path.Combine(
TestContext.CurrentContext.TestDirectory,
"..",
@ -181,9 +199,7 @@ public class SchemaConfigGeneratorSnapshotTests
"Config",
"snapshots",
"SchemaConfigGenerator");
snapshotFolder = Path.GetFullPath(snapshotFolder);
return AssertAllSnapshotsAsync(generatedSources, snapshotFolder);
return Path.GetFullPath(snapshotFolder);
}
/// <summary>

View File

@ -7,8 +7,8 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-032`
- 当前阶段:`Phase 32`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-033`
- 当前阶段:`Phase 33`
- 当前焦点:
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
- 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock
@ -61,10 +61,15 @@
将内联测试源码与期望快照抽到类级常量、补齐测试类 XML 文档,并将仅作转发的异步测试改为直接返回 `Task`
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `43` 条降到 `40` 条;
`AutoRegisterModuleGeneratorTests.cs` 已不再出现在 `MA0051` 列表中
- 已完成 `GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs``MA0051` 收口:
将 monster 场景的运行时契约与 schema 输入提取为类级常量,并把生成结果与快照目录解析拆成小 helper保持
生成文件名、快照目录和断言语义不变
- 当前 `GFramework.SourceGenerators.Tests` Release build 基线已从 `40` 条降到 `39` 条;
`SchemaConfigGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
- `GFramework.Godot``Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
- 下一轮默认继续拆分 `GFramework.SourceGenerators.Tests``MA0051` 热点,优先处理
`GeneratorSnapshotTest``ContextGetGeneratorTests`
`GeneratorSnapshotTest``ContextRegistrationAnalyzerTests`
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
@ -104,6 +109,8 @@
`43` 条,并通过 focused snapshot tests 保持行为不变
- 已完成 `AutoRegisterModuleGeneratorTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests` Release build 基线已降到
`40` 条,并通过 focused generator tests 保持输出契约不变
- 已完成 `SchemaConfigGeneratorSnapshotTests` 的单文件 `MA0051` 收口;当前 `GFramework.SourceGenerators.Tests`
Release build 基线已降到 `39` 条,并通过 focused snapshot test 保持生成输出契约不变
## 当前活跃事实
@ -146,6 +153,8 @@
并用 focused schema generator tests 验证 50 个用例通过
- `RP-032` 已完成 `AutoRegisterModuleGeneratorTests` 的 3 个 `MA0051` 收口:通过提取类级常量承载测试源码与快照,保持
生成文件名、断言路径与源生成输出不变;`GFramework.SourceGenerators.Tests` warnings-only 基线由 `43` 降至 `40`
- `RP-033` 已完成 `SchemaConfigGeneratorSnapshotTests``MA0051` 收口monster schema 运行时契约与 schema 输入已提取为
类级常量,生成结果映射与快照目录解析已拆为小 helper`GFramework.SourceGenerators.Tests` warnings-only 基线由 `40` 降至 `39`
- `RP-021` 使用 `$gframework-pr-review` 复核当前分支 PR #269 后,修复仍在本地成立的 4 个项:将
`CqrsHandlerRegistryGenerator` 拆分为职责清晰的 partial 文件、为 `ContextAwareGenerator` 生成字段增加稳定前缀并补上
`SetContextProvider` 的运行时 null 校验、为 `Option<T>` 补齐 `<remarks>`,并新增字段重名场景的生成器快照测试
@ -362,13 +371,18 @@
- 结果:`43 Warning(s)``0 Error(s)``LoggerGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~LoggerGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`6 Passed``0 Failed`
- `RP-033` 的验证结果:
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`39 Warning(s)``0 Error(s)``SchemaConfigGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~SchemaConfigGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`1 Passed``0 Failed`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 下一轮优先继续 `GFramework.SourceGenerators.Tests``MA0051` 收口,先在 `AutoRegisterModuleGeneratorTests`、
`GeneratorSnapshotTest` 或 `ContextGetGeneratorTests` 中选择一个单写集推进,不再把已清零的 `MA0004` / `MA0048` 混回写集
2. 下一轮优先继续 `GFramework.SourceGenerators.Tests``MA0051` 收口,先在 `GeneratorSnapshotTest`、
`ContextRegistrationAnalyzerTests` 或 `ContextGetGeneratorTests` 中选择一个单写集推进,不再把已清零的 `MA0004` / `MA0048` 混回写集
3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的
`object` lock
4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build

View File

@ -1,5 +1,34 @@
# Analyzer Warning Reduction 追踪
## 2026-04-23 — RP-033
### 阶段:`SchemaConfigGeneratorSnapshotTests.cs` `MA0051` 收口RP-033
- 启动复核:
- 按 `gframework-boot` 流程恢复当前 worktree读取 `AGENTS.md``.ai/environment/tools.ai.yaml`
`ai-plan/public/README.md` 与 active topic 跟踪文件,确认当前分支 `fix/analyzer-warning-reduction-batch`
仍映射到 `analyzer-warning-reduction`
- 用
`dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
复核当前 `MA0051` 热点,确认 `SchemaConfigGeneratorSnapshotTests.cs` 仍保留 1 个超长方法,适合作为单文件低风险写集
- 决策:
- 保持 monster schema 场景的输入源码、schema 文本、生成文件名与快照目录不变,只收敛测试方法长度
- 沿用前几轮 snapshot test 的收口策略:提取类级常量承载大段 fixture 输入,再用小 helper 封装生成结果映射与快照目录解析
- 同一测试项目的 build/test 继续采用串行验证;并行执行会在 WSL worktree 上制造瞬时输出缺失,导致 `MSB3030` / `CS0006`
- 实施调整:
- 为 `SchemaConfigGeneratorSnapshotTests` 新增 `RuntimeContractsSource``MonsterSchema` 类级常量,保留既有 monster 场景内容
- 把生成结果字典构造拆到 `GenerateSourcesForMonsterSchema()`,把快照目录解析拆到 `GetSchemaSnapshotFolder()`
- 保持 `AssertAllSnapshotsAsync(...)`、快照文件名与断言流程不变,不改生成器逻辑和 snapshot 资产
- 验证结果:
- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`39 Warning(s)``0 Error(s)``SchemaConfigGeneratorSnapshotTests.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-build --disable-build-servers --filter FullyQualifiedName~SchemaConfigGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`1 Passed``0 Failed`
- 下一步建议:
- 若继续压缩 `GFramework.SourceGenerators.Tests``MA0051`,优先处理只剩单个超长方法的 `GeneratorSnapshotTest`
`ContextRegistrationAnalyzerTests`
- 若希望继续按 warning 数量收敛,则回到 `ContextGetGeneratorTests.cs`,但需要接受更大的单文件写集
## 2026-04-23 — RP-032
### 阶段:`AutoRegisterModuleGeneratorTests.cs` `MA0051` 收口RP-032