fix(godot): 清理 Godot 模块与测试项目告警

- 优化 GodotYamlConfigEnvironment 目录枚举逻辑,拆分 helper 以消除 MA0051

- 修复 Godot 生命周期 await 的上下文声明,显式保留主线程同步上下文

- 更新 Godot.Tests 异步断言与字符串 comparer,用例项目构建收敛到 0 warning(s)

- 补充 analyzer-warning-reduction 跟踪与 trace,记录 RP-053 的批次结果与验证
This commit is contained in:
gewuyou 2026-04-24 17:04:53 +08:00
parent 63f563cd49
commit 6ff07ad3d9
8 changed files with 115 additions and 87 deletions

View File

@ -20,7 +20,7 @@ public sealed class AbstractArchitectureModuleInstallationTests
var module = new RecordingGodotModule(); var module = new RecordingGodotModule();
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => var exception = Assert.ThrowsAsync<InvalidOperationException>(async () =>
await architecture.InstallGodotModuleForTestAsync(module)); await architecture.InstallGodotModuleForTestAsync(module).ConfigureAwait(false));
Assert.Multiple(() => Assert.Multiple(() =>
{ {

View File

@ -197,7 +197,7 @@ public sealed class GodotYamlConfigLoaderTests
var loader = CreateLoader(isEditor: false); var loader = CreateLoader(isEditor: false);
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => var exception = Assert.ThrowsAsync<ConfigLoadException>(async () =>
await loader.LoadAsync(new ConfigRegistry())); await loader.LoadAsync(new ConfigRegistry()).ConfigureAwait(false));
Assert.Multiple(() => Assert.Multiple(() =>
{ {
@ -225,7 +225,7 @@ public sealed class GodotYamlConfigLoaderTests
configureLoader: static _ => { }); configureLoader: static _ => { });
var exception = Assert.ThrowsAsync<ArgumentException>(async () => var exception = Assert.ThrowsAsync<ArgumentException>(async () =>
await loader.LoadAsync(new ConfigRegistry())); await loader.LoadAsync(new ConfigRegistry()).ConfigureAwait(false));
Assert.That(exception!.ParamName, Is.EqualTo("relativePath")); Assert.That(exception!.ParamName, Is.EqualTo("relativePath"));
} }
@ -254,7 +254,7 @@ public sealed class GodotYamlConfigLoaderTests
configureLoader: static _ => { }); configureLoader: static _ => { });
var exception = Assert.ThrowsAsync<ArgumentException>(async () => var exception = Assert.ThrowsAsync<ArgumentException>(async () =>
await loader.LoadAsync(new ConfigRegistry())); await loader.LoadAsync(new ConfigRegistry()).ConfigureAwait(false));
Assert.That(exception!.ParamName, Is.EqualTo("relativePath")); Assert.That(exception!.ParamName, Is.EqualTo("relativePath"));
} }

View File

@ -1,3 +1,4 @@
using System;
using GFramework.Godot.Text; using GFramework.Godot.Text;
namespace GFramework.Godot.Tests.Text; namespace GFramework.Godot.Tests.Text;
@ -25,7 +26,7 @@ public sealed class RichTextMarkupTests
[Test] [Test]
public void Effect_Should_Sort_Environment_Parameters_By_Key() public void Effect_Should_Sort_Environment_Parameters_By_Key()
{ {
var env = new Dictionary<string, object?> var env = new Dictionary<string, object?>(StringComparer.Ordinal)
{ {
["tick"] = 0.1f, ["tick"] = 0.1f,
["speed"] = 4 ["speed"] = 4
@ -53,7 +54,7 @@ public sealed class RichTextMarkupTests
[Test] [Test]
public void Effect_Should_Reject_Invalid_Environment_Key_Tokens() public void Effect_Should_Reject_Invalid_Environment_Key_Tokens()
{ {
var env = new Dictionary<string, object?> var env = new Dictionary<string, object?>(StringComparer.Ordinal)
{ {
["bad key"] = 1 ["bad key"] = 1
}; };

View File

@ -112,8 +112,8 @@ public abstract class AbstractArchitecture(
// 在附加流程完成前先登记模块,保证后续任一步失败时仍能参与架构销毁阶段的清理。 // 在附加流程完成前先登记模块,保证后续任一步失败时仍能参与架构销毁阶段的清理。
_extensions.Add(module); _extensions.Add(module);
// 等待锚点准备就绪,并保持 Godot 同步上下文,以便后续附加逻辑安全访问节点 API // 显式保留 Godot 同步上下文,确保后续 AddChild 和 OnAttach 仍在节点可访问的主线程执行
await anchor.WaitUntilReadyAsync(); await anchor.WaitUntilReadyAsync().ConfigureAwait(true);
// 延迟调用将扩展节点添加为锚点的子节点 // 延迟调用将扩展节点添加为锚点的子节点
anchor.CallDeferred(Node.MethodName.AddChild, module.Node); anchor.CallDeferred(Node.MethodName.AddChild, module.Node);

View File

@ -104,41 +104,36 @@ internal sealed class GodotYamlConfigEnvironment
private static IReadOnlyList<GodotYamlConfigDirectoryEntry>? EnumerateDirectoryCore(string path) private static IReadOnlyList<GodotYamlConfigDirectoryEntry>? EnumerateDirectoryCore(string path)
{ {
if (!path.IsGodotPath()) return path.IsGodotPath()
? EnumerateGodotDirectory(path)
: EnumerateFileSystemDirectory(path);
}
private static IReadOnlyList<GodotYamlConfigDirectoryEntry>? EnumerateFileSystemDirectory(string path)
{
try
{ {
try if (!Directory.Exists(path))
{ {
if (!Directory.Exists(path)) return null;
{ }
return null;
}
return Directory return Directory
.EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly) .EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly)
.Select(static entryPath => new GodotYamlConfigDirectoryEntry( .Select(static entryPath => new GodotYamlConfigDirectoryEntry(
Path.GetFileName(entryPath), Path.GetFileName(entryPath),
Directory.Exists(entryPath))) Directory.Exists(entryPath)))
.ToArray(); .ToArray();
}
catch (IOException)
{
// 非 Godot 路径分支与公开契约保持一致:宿主无法访问目录时返回 null而不是泄漏底层异常。
return null;
}
catch (UnauthorizedAccessException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
catch (NotSupportedException)
{
return null;
}
} }
catch (Exception ex) when (IsExpectedDirectoryEnumerationException(ex))
{
// 非 Godot 路径分支与公开契约保持一致:宿主无法访问目录时返回 null而不是泄漏底层异常。
return null;
}
}
private static IReadOnlyList<GodotYamlConfigDirectoryEntry>? EnumerateGodotDirectory(string path)
{
using var directory = DirAccess.Open(path); using var directory = DirAccess.Open(path);
if (directory == null) if (directory == null)
{ {
@ -170,9 +165,15 @@ internal sealed class GodotYamlConfigEnvironment
// 目录枚举句柄必须成对结束,避免未来循环体扩展后在异常路径上遗留引擎状态。 // 目录枚举句柄必须成对结束,避免未来循环体扩展后在异常路径上遗留引擎状态。
directory.ListDirEnd(); directory.ListDirEnd();
} }
return entries; return entries;
} }
private static bool IsExpectedDirectoryEnumerationException(Exception exception)
{
return exception is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException;
}
private static bool FileExistsCore(string path) private static bool FileExistsCore(string path)
{ {
return path.IsGodotPath() return path.IsGodotPath()

View File

@ -141,10 +141,11 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
/// 当场景被其他场景覆盖或失去焦点时调用。 /// 当场景被其他场景覆盖或失去焦点时调用。
/// </summary> /// </summary>
/// <returns>表示暂停操作完成的ValueTask。</returns> /// <returns>表示暂停操作完成的ValueTask。</returns>
public virtual async ValueTask OnPauseAsync() public virtual async ValueTask OnPauseAsync()
{ {
if (_scene != null) if (_scene != null)
await _scene.OnPauseAsync(); // 暂停后紧接着会修改 Owner 的处理开关,必须回到 Godot 主线程继续执行。
await _scene.OnPauseAsync().ConfigureAwait(true);
// 暂停处理 // 暂停处理
Owner.SetProcess(false); Owner.SetProcess(false);
@ -159,13 +160,14 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
/// 当场景重新获得焦点或从暂停状态恢复时调用。 /// 当场景重新获得焦点或从暂停状态恢复时调用。
/// </summary> /// </summary>
/// <returns>表示恢复操作完成的ValueTask。</returns> /// <returns>表示恢复操作完成的ValueTask。</returns>
public virtual async ValueTask OnResumeAsync() public virtual async ValueTask OnResumeAsync()
{ {
if (Owner.IsInvalidNode()) if (Owner.IsInvalidNode())
return; return;
if (_scene != null) if (_scene != null)
await _scene.OnResumeAsync(); // 恢复完成后要立刻重新启用节点处理流程,因此显式保留当前同步上下文。
await _scene.OnResumeAsync().ConfigureAwait(true);
// 恢复处理 // 恢复处理
Owner.SetProcess(true); Owner.SetProcess(true);
@ -195,10 +197,11 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
/// 在场景完全退出后调用,释放占用的内存和资源。 /// 在场景完全退出后调用,释放占用的内存和资源。
/// </summary> /// </summary>
/// <returns>表示卸载操作完成的ValueTask。</returns> /// <returns>表示卸载操作完成的ValueTask。</returns>
public virtual async ValueTask OnUnloadAsync() public virtual async ValueTask OnUnloadAsync()
{ {
if (_scene != null) if (_scene != null)
await _scene.OnUnloadAsync(); // 卸载后的 QueueFreeX 必须在 Godot 节点线程上调用,不能切走同步上下文。
await _scene.OnUnloadAsync().ConfigureAwait(true);
// 释放节点 // 释放节点
Owner.QueueFreeX(); Owner.QueueFreeX();

View File

@ -6,36 +6,34 @@
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-052` - 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-053`
- 当前阶段:`Phase 52` - 当前阶段:`Phase 53`
- 当前焦点: - 当前焦点:
- `2026-04-24` 本轮从当前 PR review 的未解决线程回切到 `GFramework.Game` / `GFramework.Godot.SourceGenerators.Tests` - `2026-04-24` 本轮`$gframework-batch-boot 75` 重新建立当前分支相对 `origin/main` 的 batch 基线,并从整仓 `Release` build 里挑选低风险热点
- `UnifiedSettingsFile.Sections` 与 `CloneFile` fallback 已对齐为“可保留原 comparer 时保留,否则显式回退到 `StringComparer.Ordinal`”的文档与实现契约 - `GFramework.Godot` 已完成一轮独立 warning 清理:`GodotYamlConfigEnvironment` 的目录枚举逻辑已拆分 helper`AbstractArchitecture` / `SceneBehaviorBase` 中需要保留 Godot 同步上下文的 await 已显式改为 `.ConfigureAwait(true)`
- `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 `await test.RunAsync();` 已统一补齐 `.ConfigureAwait(false)`,并同步让 `VerifyDiagnosticsAsync` 内部消费异步等待 - `GFramework.Godot.Tests` 已同步清理对应测试 warning异步断言显式使用 `.ConfigureAwait(false)``RichTextMarkupTests` 中测试字典显式指定 `StringComparer.Ordinal`
- 当前批次仍需避免混入与 analyzer-warning-reduction 无关的既有工作树改动 - 当前代码批次相对 `origin/main` 的待提交 diff 为 `6` 个文件、`107` 行变更,远低于 `$gframework-batch-boot 75` 的主停止阈值;本轮在这里收口,是因为下一批候选将进入 `GFramework.Game` 等高基线模块,而不再是同等级低风险切片
## 当前活跃事实 ## 当前活跃事实
- 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值 - 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值
- 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理clean `Release` build 从 9 个 warning 降至 0 个 warning
- 当前已确认解决的文件包括 `BindNodeSignalGenerator.cs``GetNodeGenerator.cs``GodotProjectMetadataGenerator.cs``Registration/AutoRegisterExportedCollectionsGenerator.cs`
- 本轮直接执行仓库根目录 `dotnet clean` 仍在 `ValidateSolutionConfiguration` 阶段失败,输出未提供具体 error 文本 - 本轮直接执行仓库根目录 `dotnet clean` 仍在 `ValidateSolutionConfiguration` 阶段失败,输出未提供具体 error 文本
- 本轮直接执行仓库根目录 `dotnet build` 成功,并给出 `1184 warning(s)` 的真实输出 - 本轮直接执行仓库根目录 `dotnet build GFramework.sln -c Release` 成功,并给出 `1122 warning(s)` 的当前整仓观测值
- `GFramework.Godot.SourceGenerators.Tests` 已通过测试辅助模板抽取与 `ConfigureAwait(false)` 修正,当前 `Debug` / `Release` 构建均为 `0 Warning(s)` - `GFramework.Godot/GFramework.Godot.csproj -c Release` 在本轮收尾验证中已达到 `0 Warning(s)``0 Error(s)`
- 本轮已验证 `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`,结果为 `Passed: 48` - `GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release` 在串行复验中已达到 `0 Warning(s)``0 Error(s)`
- 本轮已把 PR #283 中仍打开的 `UnifiedSettingsDataRepository.cs` comparer 契约线程落到代码与 XML 注释,避免 fallback 语义继续依赖隐式默认 comparer - 本轮已验证 `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~AbstractArchitectureModuleInstallationTests|FullyQualifiedName~GodotYamlConfigLoaderTests|FullyQualifiedName~RichTextMarkupTests"`,结果为 `Passed: 15`
- 本轮已确认 `AutoRegisterExportedCollectionsGeneratorTests` 的 5 处裸 `await test.RunAsync();` 不是当前 Release build 告警来源,但仍作为 PR review 一致性项一并修正 - `GFramework.Godot` 原先暴露的 `MA0051``MA0004` 热点都已清理完成;当前同域低风险切片基本耗尽
## 当前风险 ## 当前风险
- 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0 - 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0
- 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build` - 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
- 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线 - 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线
- 缓解措施:若下一轮继续做整仓 warning reduction先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1184 warning(s)` direct build 观测值 - 缓解措施:若下一轮继续做整仓 warning reduction先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1122 warning(s)` direct build 观测值
- 当前 worktree 已存在与本批次无关的未提交改动 - 当前 worktree 仍存在未跟踪的 `.codex` 目录
- 缓解措施:提交当前批次时只暂存 `GFramework.Godot.SourceGenerators.Tests` 与对应 `ai-plan` 文件,避免混入其他 topic 变更 - 缓解措施:提交当前批次时只暂存 analyzer-warning-reduction 相关源码与 `ai-plan` 文件,避免把工作目录辅助文件混入提交
- `GFramework.Game` 当前 `Release` build 仍带有既有 analyzer warning 基线 - 下一轮最明显的剩余热点将转入 `GFramework.Game` 等高 warning 基线模块
- 缓解措施:本轮仅验证改动未新增 `UnifiedSettingsDataRepository` / `UnifiedSettingsFile` 相关 warning若继续在该模块做 warning reduction需要另开切片处理现存基线 - 缓解措施:恢复时先重新跑整仓 build 热点筛选,再决定是否接受更高上下文成本的 `GFramework.Game` 切片,或先排查 solution-level clean 失败原因
## 活跃文档 ## 活跃文档
@ -51,25 +49,19 @@
## 验证说明 ## 验证说明
- `dotnet clean` - `dotnet clean GFramework.sln -c Release`
- 结果:失败;停在 solution `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`,未输出更具体的 error 文本 - 结果:失败;停在 solution `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`,未输出更具体的 error 文本
- `dotnet build` - `dotnet build GFramework.sln -c Release`
- 结果:成功;`1184 Warning(s)``0 Error(s)` - 结果:成功;`1122 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj` - `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
- 初始结果:成功;`24 Warning(s)``0 Error(s)`
- 本轮收尾结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)` - 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build` - `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~AbstractArchitectureModuleInstallationTests|FullyQualifiedName~GodotYamlConfigLoaderTests|FullyQualifiedName~RichTextMarkupTests"`
- 结果:成功;`Passed: 48``Failed: 0` - 结果:成功;`Passed: 15``Failed: 0`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` - `dotnet build GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release`
- 结果:成功;`533 Warning(s)``0 Error(s)`;模块仍存在既有 warning 基线,本轮 follow-up 仅处理 PR review 指向的 comparer 契约与测试异步等待一致性 - 首次并行验证:成功;`1 Warning(s)``0 Error(s)``MSB3026` 来自与并行 `dotnet test` 竞争同一输出 DLL
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release` - 串行复验:成功;`0 Warning(s)``0 Error(s)`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 结果:成功;`Passed: 48``Failed: 0`
## 下一步建议 ## 下一步建议
1. 提交当前 comparer 契约与 `ConfigureAwait(false)` PR follow-up并确认只纳入本 topic 相关文件 1. 提交当前 `GFramework.Godot` / `GFramework.Godot.Tests` warning 清理批次,并继续保持只纳入本 topic 相关文件
2. 视 PR review 反馈决定是否继续收敛 `GFramework.Game` 现有 warning 基线,或返回下一轮整仓 warning 热点筛选 2. 下一轮若继续使用 `$gframework-batch-boot 75`,先决定是优先排查 solution-level `dotnet clean` 失败,还是接受更高上下文成本进入 `GFramework.Game` warning 热点

View File

@ -2,6 +2,37 @@
# Analyzer Warning Reduction 追踪 # Analyzer Warning Reduction 追踪
## 2026-04-24 — RP-053
### 阶段:`GFramework.Godot` / `GFramework.Godot.Tests` 小批次 warning 清理
- 触发背景:
- 用户以 `$gframework-batch-boot 75` 要求继续按批次推进 analyzer warning reduction并以 `origin/main` 作为累计分支 diff 基线
- 当前 worktree `fix/analyzer-warning-reduction-batch` 相对 `origin/main` 的已提交分支 diff 为 `0` 个文件,具备继续落一个低风险 warning batch 的空间
- solution-level `dotnet clean GFramework.sln -c Release` 仍在 `ValidateSolutionConfiguration` 阶段失败,因此本轮继续用直接 `dotnet build GFramework.sln -c Release` 建立热点观察值
- 主线程实施:
- 运行 `dotnet build GFramework.sln -c Release`,确认当前整仓观测值为 `1122 warning(s)`,并从输出中挑选 `GFramework.Godot` 的小范围热点作为本轮批次
- 在 `GodotYamlConfigEnvironment.cs` 中按“普通文件系统 / Godot 路径”拆分目录枚举 helper消除 `MA0051`
- 在 `AbstractArchitecture.cs``SceneBehaviorBase.cs` 中将必须保留 Godot 主线程上下文的 await 显式改为 `.ConfigureAwait(true)`,清理 `MA0004` 并把线程意图写入注释
- 在 `GFramework.Godot.Tests` 中补齐异步断言的 `.ConfigureAwait(false)`,并让 `RichTextMarkupTests` 的测试字典显式指定 `StringComparer.Ordinal`
- 验证里程碑:
- `dotnet clean GFramework.sln -c Release`
- 结果:失败;停在 `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.sln -c Release`
- 结果:成功;`1122 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
- 第一轮修复后:成功;`12 Warning(s)``0 Error(s)`,仅剩 `MA0004`
- 第二轮修复后:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~AbstractArchitectureModuleInstallationTests|FullyQualifiedName~GodotYamlConfigLoaderTests|FullyQualifiedName~RichTextMarkupTests"`
- 结果:成功;`Passed: 15``Failed: 0`
- `dotnet build GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release`
- 并行验证时:成功;`1 Warning(s)``0 Error(s)``MSB3026` 为与并行 `dotnet test` 竞争输出 DLL 的文件占用
- 串行复验:成功;`0 Warning(s)``0 Error(s)`
- 当前结论:
- `GFramework.Godot``GFramework.Godot.Tests` 本轮直接涉及的 warning 已全部清零
- 当前待提交代码批次相对 `origin/main` 的源码 diff 为 `6` 个文件、`107` 行,距离 `$gframework-batch-boot 75` 主停止阈值仍有充足余量
- 继续推进的下一批候选将主要落在 `GFramework.Game` 等高 warning 基线模块,已不再属于当前同等级低风险切片,因此本轮在这里收口并进入提交
## 2026-04-24 — RP-052 ## 2026-04-24 — RP-052
### 阶段PR review follow-upcomparer 契约 + `ConfigureAwait(false)` 收尾) ### 阶段PR review follow-upcomparer 契约 + `ConfigureAwait(false)` 收尾)