fix(game-tests): 收敛配置与序列化测试告警

- 修复架构配置、启动流程与序列化测试中的异步等待和 invariant 解析告警

- 补充 AllOf 与 persistence 测试的残余状态校验与 ConfigureAwait 修正,继续压低 Game.Tests warning

- 更新 analyzer-warning-reduction 跟踪与 trace,纠正 RP-054 的 stop-condition 口径并记录 RP-055 指标
This commit is contained in:
gewuyou 2026-04-24 18:29:17 +08:00
parent 36507bbc52
commit 5aefd77ad0
7 changed files with 79 additions and 49 deletions

View File

@ -39,7 +39,7 @@ public class ArchitectureConfigIntegrationTests
try
{
architecture = new ConsumerArchitecture(rootPath);
await architecture.InitializeAsync();
await architecture.InitializeAsync().ConfigureAwait(false);
initialized = true;
var table = architecture.MonsterTable;
@ -63,7 +63,7 @@ public class ArchitectureConfigIntegrationTests
{
if (architecture is not null && initialized)
{
await architecture.DestroyAsync();
await architecture.DestroyAsync().ConfigureAwait(false);
}
DeleteDirectoryIfExists(rootPath);
@ -83,7 +83,7 @@ public class ArchitectureConfigIntegrationTests
try
{
architecture = new ConsumerArchitecture(rootPath);
await architecture.InitializeAsync();
await architecture.InitializeAsync().ConfigureAwait(false);
initialized = true;
Assert.Multiple(() =>
@ -97,7 +97,7 @@ public class ArchitectureConfigIntegrationTests
{
if (architecture is not null && initialized)
{
await architecture.DestroyAsync();
await architecture.DestroyAsync().ConfigureAwait(false);
}
DeleteDirectoryIfExists(rootPath);
@ -119,16 +119,16 @@ public class ArchitectureConfigIntegrationTests
var module = CreateModule(rootPath);
firstArchitecture = new ModuleOnlyArchitecture(module);
await firstArchitecture.InitializeAsync();
await firstArchitecture.InitializeAsync().ConfigureAwait(false);
var wasInitializedBeforeDestroy = module.IsInitialized;
await firstArchitecture.DestroyAsync();
await firstArchitecture.DestroyAsync().ConfigureAwait(false);
firstDestroyed = true;
firstArchitecture = null;
GameContext.Clear();
var secondArchitecture = new ModuleOnlyArchitecture(module);
var exception =
Assert.ThrowsAsync<InvalidOperationException>(async () => await secondArchitecture.InitializeAsync());
Assert.ThrowsAsync<InvalidOperationException>(async () => await secondArchitecture.InitializeAsync().ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -141,7 +141,7 @@ public class ArchitectureConfigIntegrationTests
{
if (firstArchitecture is not null && !firstDestroyed)
{
await firstArchitecture.DestroyAsync();
await firstArchitecture.DestroyAsync().ConfigureAwait(false);
}
DeleteDirectoryIfExists(rootPath);
@ -203,7 +203,7 @@ public class ArchitectureConfigIntegrationTests
var module = CreateModule(rootPath);
readyArchitecture = new ReadyOnlyArchitecture();
await readyArchitecture.InitializeAsync();
await readyArchitecture.InitializeAsync().ConfigureAwait(false);
readyArchitectureInitialized = true;
var exception = Assert.Throws<InvalidOperationException>(() => readyArchitecture.InstallModule(module));
@ -216,13 +216,13 @@ public class ArchitectureConfigIntegrationTests
Assert.That(module.IsInitialized, Is.False);
});
await readyArchitecture.DestroyAsync();
await readyArchitecture.DestroyAsync().ConfigureAwait(false);
readyArchitectureInitialized = false;
readyArchitecture = null;
GameContext.Clear();
retryArchitecture = new ModuleOnlyArchitecture(module);
await retryArchitecture.InitializeAsync();
await retryArchitecture.InitializeAsync().ConfigureAwait(false);
retryArchitectureInitialized = true;
Assert.Multiple(() =>
@ -235,12 +235,12 @@ public class ArchitectureConfigIntegrationTests
{
if (retryArchitecture is not null && retryArchitectureInitialized)
{
await retryArchitecture.DestroyAsync();
await retryArchitecture.DestroyAsync().ConfigureAwait(false);
}
if (readyArchitecture is not null && readyArchitectureInitialized)
{
await readyArchitecture.DestroyAsync();
await readyArchitecture.DestroyAsync().ConfigureAwait(false);
}
DeleteDirectoryIfExists(rootPath);

View File

@ -50,7 +50,7 @@ public class GameConfigBootstrapTests
var registry = new ConfigRegistry();
using var bootstrap = CreateBootstrap(registry);
await bootstrap.InitializeAsync();
await bootstrap.InitializeAsync().ConfigureAwait(false);
var monsterTable = registry.GetMonsterTable();
@ -74,7 +74,7 @@ public class GameConfigBootstrapTests
CreateMonsterFiles();
using var bootstrap = CreateBootstrap();
await bootstrap.InitializeAsync();
await bootstrap.InitializeAsync().ConfigureAwait(false);
var reloadTaskSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
bootstrap.StartHotReload(
@ -95,7 +95,7 @@ public class GameConfigBootstrapTests
faction: dungeon
""");
var tableName = await WaitForTaskWithinAsync(reloadTaskSource.Task, TimeSpan.FromSeconds(5));
var tableName = await WaitForTaskWithinAsync(reloadTaskSource.Task, TimeSpan.FromSeconds(5)).ConfigureAwait(false);
var monsterTable = bootstrap.Registry.GetMonsterTable();
Assert.Multiple(() =>
@ -163,11 +163,11 @@ public class GameConfigBootstrapTests
Is.True,
"The first initialization attempt did not reach the guarded lifecycle section.");
var secondCallerException = Assert.ThrowsAsync<InvalidOperationException>(async () => await bootstrap.InitializeAsync());
var secondCallerException = Assert.ThrowsAsync<InvalidOperationException>(async () => await bootstrap.InitializeAsync().ConfigureAwait(false));
continueInitialization.Set();
Assert.DoesNotThrowAsync(async () => await firstInitializeTask);
Assert.DoesNotThrowAsync(async () => await firstInitializeTask.ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -202,7 +202,7 @@ public class GameConfigBootstrapTests
})
});
var exception = Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await bootstrap.InitializeAsync());
var exception = Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await bootstrap.InitializeAsync().ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -311,12 +311,12 @@ public class GameConfigBootstrapTests
/// <returns>任务结果。</returns>
private static async Task<T> WaitForTaskWithinAsync<T>(Task<T> task, TimeSpan timeout)
{
var completedTask = await Task.WhenAny(task, Task.Delay(timeout));
var completedTask = await Task.WhenAny(task, Task.Delay(timeout)).ConfigureAwait(false);
if (!ReferenceEquals(completedTask, task))
{
Assert.Fail($"Timed out after {timeout} while waiting for file watcher notification.");
}
return await task;
return await task.ConfigureAwait(false);
}
}

View File

@ -522,7 +522,10 @@ public sealed class YamlConfigLoaderAllOfTests
/// <param name="content">要写入的 YAML 或 schema 内容。</param>
private void CreateConfigFile(string relativePath, string content)
{
ArgumentNullException.ThrowIfNull(_rootPath);
if (_rootPath is null)
{
throw new InvalidOperationException("Root path is not initialized.");
}
var filePath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
var directoryPath = Path.GetDirectoryName(filePath);
@ -609,7 +612,10 @@ public sealed class YamlConfigLoaderAllOfTests
/// <returns>已注册测试表与 schema 路径的加载器。</returns>
private YamlConfigLoader CreateMonsterRewardLoader()
{
ArgumentNullException.ThrowIfNull(_rootPath);
if (_rootPath is null)
{
throw new InvalidOperationException("Root path is not initialized.");
}
return new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterAllOfConfigStub>(

View File

@ -271,7 +271,7 @@ public class PersistenceTests
continueMigration.Set();
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loadTask.ConfigureAwait(false));
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save");
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save").ConfigureAwait(false);
Assert.Multiple(() =>
{
@ -593,7 +593,7 @@ public class PersistenceTests
throwingStorage.ThrowOnWrite = true;
Assert.ThrowsAsync<InvalidOperationException>(
async () => await repository.SaveAsync(primaryLocation, new TestSimpleData { Value = 99 }));
async () => await repository.SaveAsync(primaryLocation, new TestSimpleData { Value = 99 }).ConfigureAwait(false));
var cachedAfterFailure = await repository.LoadAsync<TestSimpleData>(primaryLocation);
Assert.That(cachedAfterFailure.Value, Is.EqualTo(1));

View File

@ -1,3 +1,4 @@
using System.Globalization;
using Newtonsoft.Json;
using GameJsonSerializer = GFramework.Game.Serializer.JsonSerializer;
@ -182,8 +183,8 @@ public sealed class JsonSerializerTests
var parts = raw.Split(':');
return new CoordinateStub
{
X = int.Parse(parts[0]),
Y = int.Parse(parts[1])
X = int.Parse(parts[0], CultureInfo.InvariantCulture),
Y = int.Parse(parts[1], CultureInfo.InvariantCulture)
};
}
}

View File

@ -6,35 +6,33 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-054`
- 当前阶段:`Phase 54`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-055`
- 当前阶段:`Phase 55`
- 当前焦点:
- `2026-04-24` 本轮继续按 `$gframework-batch-boot 75` 推进,切入 `GFramework.Game.Tests` 的低风险测试 warning而不进入 `YamlConfigLoaderTests.cs` 等高上下文热点
- 已完成 `PersistenceTestUtilities` 的单类型拆分,并在多组 YAML / persistence 测试中补齐 `.ConfigureAwait(false)` 与字段态显式状态检查
- `GFramework.Game.Tests` 当前 `Release` build 已从本轮入口观测值 `116 warning(s)` 收敛到 `71 warning(s)`,且本轮 touched files 已不再出现在 warning 输出里
- 当前工作树相对 `origin/main` 的累计 diff 已达到 `76` 个文件、`986` 行变更,超过 `$gframework-batch-boot 75` 的主停止阈值;本轮必须在提交后停止继续扩批
- `2026-04-24` 本轮先纠正了 batch stop-condition 的计算口径:应使用 `origin/main``HEAD` 的 merge-base 分支 diff而不是工作树 diff
- 在该正确口径下,`RP-054` 提交后的真实 branch 体积是 `23` 个文件、`603` 行;当前这批提交后的投影体积是 `26` 个文件、`691` 行,仍低于 `$gframework-batch-boot 75`
- 本轮已完成 `ArchitectureConfigIntegrationTests``GameConfigBootstrapTests``JsonSerializerTests` 的小热点清理,并顺手补齐 `YamlConfigLoaderAllOfTests` / `PersistenceTests` 的残余 warning
- 当前仍在 `GFramework.Game.Tests` 内推进,但剩余热点已经越来越集中到 `YamlConfigLoaderTests.cs``GeneratedConfigConsumerIntegrationTests.cs` 这类高上下文文件
## 当前活跃事实
- 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值
- 本轮直接执行仓库根目录 `dotnet clean` 仍在 `ValidateSolutionConfiguration` 阶段失败,输出未提供具体 error 文本
- 本轮直接执行仓库根目录 `dotnet build GFramework.sln -c Release` 成功,并给出 `116 warning(s)` 的当前整仓入口观测值;其中低风险热点主要落在 `GFramework.Game.Tests`
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release` 在本轮收尾验证中为 `71 Warning(s)``0 Error(s)`;剩余 warning 已集中在未触碰的 `YamlConfigLoaderTests.cs``GeneratedConfigConsumerIntegrationTests.cs``GameConfigBootstrapTests.cs``ArchitectureConfigIntegrationTests.cs``JsonSerializerTests.cs`
- 本轮已验证 `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderIfThenElseTests|FullyQualifiedName~YamlConfigLoaderDependentSchemasTests|FullyQualifiedName~YamlConfigLoaderDependentRequiredTests|FullyQualifiedName~YamlConfigLoaderNegationTests|FullyQualifiedName~YamlConfigLoaderAllOfTests|FullyQualifiedName~YamlConfigLoaderEnumTests|FullyQualifiedName~YamlConfigTextValidatorTests|FullyQualifiedName~YamlConfigSchemaValidatorTests|FullyQualifiedName~PersistenceTests"`,结果为 `Passed: 63`
- `PersistenceTestUtilities.cs` 已拆分为 `TestDataLocation.cs``TestSaveData.cs``TestVersionedSaveData.cs``TestSimpleData.cs``TestNamedData.cs`,与仓库“一文件一主类型”风格对齐
- 仓库根目录 `dotnet clean GFramework.sln -c Release` 仍在 `ValidateSolutionConfiguration` 阶段失败,项目级 `dotnet clean GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release` 也未能稳定提供 clean 基线
- 当前整仓最近一次直接观测值仍是 `dotnet build GFramework.sln -c Release``116 warning(s)`
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release` 已从上一批入口的 `116 warning(s)` 继续收敛到本轮收尾的 `63 warning(s)`
- 本轮已验证 `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureConfigIntegrationTests|FullyQualifiedName~GameConfigBootstrapTests|FullyQualifiedName~JsonSerializerTests"`,结果为 `Passed: 19`
- `GFramework.Game.Tests` 当前剩余 warning 主要集中在未触碰的 `YamlConfigLoaderTests.cs``GeneratedConfigConsumerIntegrationTests.cs`,以及少量未处理的 `GameConfigBootstrapTests` 之外热点
## 当前风险
- 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0
- 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
- 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线
- 缓解措施:若下一轮继续做整仓 warning reduction先定位 `dotnet clean` 的 solution-level / project-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 direct build 观测
- 仓库根目录`GFramework.Game.Tests``dotnet clean` 目前都无法给出新的 clean 基线
- 缓解措施:后续若继续整仓 warning reduction需要单独定位 clean 失败原因,或明确继续沿用 direct build 观测值作为临时真
- 当前 worktree 仍存在未跟踪的 `.codex` 目录
- 缓解措施:提交当前批次时只暂存 analyzer-warning-reduction 相关源码与 `ai-plan` 文件,避免把工作目录辅助文件混入提交
- 当前批次已触发 `$gframework-batch-boot 75` 的主停止条件
- 缓解措施:本轮提交后停止继续扩批;下一次继续前先评估是否需要基于更新后的 `origin/main` 重新选择基线,或切到新分支 / 新轮次处理剩余 `GFramework.Game.Tests` 热点
- `GFramework.Game.Tests` 的剩余 warning 主要集中在大文件与集成测试文件
- 缓解措施:后续若继续,优先把 `YamlConfigLoaderTests.cs` 单独作为一个高上下文切片处理,不要和其它 warning family 混批
- 下一轮若继续深入 `GFramework.Game.Tests`,很可能需要进入 `YamlConfigLoaderTests.cs` 这种高上下文大文件
- 缓解措施:把它单独作为一个明确的新批次处理,不与其它 warning family 混批
## 活跃文档
@ -57,11 +55,11 @@
- `dotnet clean GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 结果失败clean 阶段在 MSBuild 清理路径结束前返回 `0 Warning(s)``0 Error(s)`,未输出额外错误文本
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 结果:成功;`71 Warning(s)`、`0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderIfThenElseTests|FullyQualifiedName~YamlConfigLoaderDependentSchemasTests|FullyQualifiedName~YamlConfigLoaderDependentRequiredTests|FullyQualifiedName~YamlConfigLoaderNegationTests|FullyQualifiedName~YamlConfigLoaderAllOfTests|FullyQualifiedName~YamlConfigLoaderEnumTests|FullyQualifiedName~YamlConfigTextValidatorTests|FullyQualifiedName~YamlConfigSchemaValidatorTests|FullyQualifiedName~PersistenceTests"`
- 结果:成功;`Passed: 63`、`Failed: 0`
- 结果:成功;`63 Warning(s)`、`0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureConfigIntegrationTests|FullyQualifiedName~GameConfigBootstrapTests|FullyQualifiedName~JsonSerializerTests"`
- 结果:成功;`Passed: 19`、`Failed: 0`
## 下一步建议
1. 提交当前 `GFramework.Game.Tests` warning 清理批次与 `RP-054` tracking 更新,然后停止当前 batch loop因为 branch diff 已达 `76/75`
2. 下一轮若继续 warning reduction先决定是重新整理 `origin/main` 基线,还是单独开一个高上下文批次处理 `YamlConfigLoaderTests.cs`
1. 提交当前 `GFramework.Game.Tests` 小热点批次与 `RP-055` tracking 更新,继续保持只纳入本 topic 相关文件
2. 下一轮若继续 warning reduction优先决定是否接受进入 `YamlConfigLoaderTests.cs` 的高上下文批次

View File

@ -2,6 +2,31 @@
# Analyzer Warning Reduction 追踪
## 2026-04-24 — RP-055
### 阶段:修正 stop-condition 口径并继续 `GFramework.Game.Tests` 小热点
- 触发背景:
- `RP-054` 之后复核 batch stop-condition 时,发现之前一度把工作树 diff 错当成了 skill 要求的 branch diff
- 按正确口径 `merge-base(origin/main, HEAD)` 计算,`RP-054` 提交后的真实分支体积是 `23` 个文件、`603` 行,因此仍可继续下一批
- 当前剩余 warning 里,`ArchitectureConfigIntegrationTests``GameConfigBootstrapTests``JsonSerializerTests` 属于独立且低风险的小切片
- 主线程实施:
- 在 `ArchitectureConfigIntegrationTests.cs` 中补齐异步架构初始化 / 销毁和异常断言的 `.ConfigureAwait(false)`
- 在 `GameConfigBootstrapTests.cs` 中补齐启动流程、并发初始化断言与 `WaitForTaskWithinAsync``.ConfigureAwait(false)`
- 在 `JsonSerializerTests.cs` 中将坐标解析改为 `CultureInfo.InvariantCulture`
- 顺手清理 `YamlConfigLoaderAllOfTests.cs``PersistenceTests.cs` 中上一批遗漏的字段态状态检查和异步等待 warning
- 纠正 active tracking明确 stop-condition 必须使用 `origin/main...HEAD` 的 merge-base 分支 diff而不是工作树 diff
- 验证里程碑:
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 并行误用 build/test 时:出现 `MSB3026` / `CS2012` 文件占用噪声,不计入代码结论
- 串行复验:成功;`63 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureConfigIntegrationTests|FullyQualifiedName~GameConfigBootstrapTests|FullyQualifiedName~JsonSerializerTests"`
- 结果:成功;`Passed: 19``Failed: 0`
- 当前结论:
- `GFramework.Game.Tests` 已从上一批收尾时的 `71 warning(s)` 进一步降到 `63 warning(s)`
- 这次提交后的分支体积投影为 `26` 个文件、`691` 行,仍低于 `$gframework-batch-boot 75`
- 剩余热点越来越集中到 `YamlConfigLoaderTests.cs``GeneratedConfigConsumerIntegrationTests.cs`,后续继续时应把它们视为高上下文批次
## 2026-04-24 — RP-054
### 阶段:`GFramework.Game.Tests` 低风险测试 warning 批次(触发文件数停止阈值)