fix(game-tests): 收敛 Game.Tests 测试告警

- 修复多组 YAML 与 persistence 测试中的 ConfigureAwait 使用与状态校验,清理低风险 analyzer 告警

- 重构 PersistenceTestUtilities 为单类型文件,消除测试辅助模型的文件命名告警

- 更新 analyzer-warning-reduction 跟踪与 trace,记录 RP-054 批次结果与 75 文件阈值停止点
This commit is contained in:
gewuyou 2026-04-24 18:17:21 +08:00
parent 6ff07ad3d9
commit 36507bbc52
17 changed files with 256 additions and 193 deletions

View File

@ -90,7 +90,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -124,7 +124,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterAllOfConfigStub>("monster");
var reward = table.Get(1).Reward;
@ -165,7 +165,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -210,7 +210,10 @@ public sealed class YamlConfigLoaderAllOfTests
}
""");
ArgumentNullException.ThrowIfNull(_rootPath);
if (_rootPath is null)
{
throw new InvalidOperationException("Root path is not initialized.");
}
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterTagConfigStub>(
@ -220,7 +223,7 @@ public sealed class YamlConfigLoaderAllOfTests
static config => config.Id);
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -255,7 +258,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -295,7 +298,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -335,7 +338,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -375,7 +378,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -415,7 +418,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -455,7 +458,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -499,7 +502,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{

View File

@ -75,7 +75,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -124,7 +124,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterRewardConfigStub>("monster");
Assert.That(table.Count, Is.EqualTo(1));
@ -169,7 +169,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterRewardConfigStub>("monster");
var reward = table.Get(1).Reward;
@ -217,7 +217,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -267,7 +267,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateCaseSensitiveRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -317,7 +317,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{

View File

@ -74,7 +74,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -106,7 +106,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterDependentSchemasConfigStub>("monster");
Assert.That(table.Count, Is.EqualTo(1));
@ -133,7 +133,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterDependentSchemasConfigStub>("monster");
var reward = table.Get(1).Reward;
@ -174,7 +174,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -220,7 +220,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -264,7 +264,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -283,7 +283,10 @@ public sealed class YamlConfigLoaderDependentSchemasTests
/// <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);
@ -353,7 +356,10 @@ public sealed class YamlConfigLoaderDependentSchemasTests
/// <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, MonsterDependentSchemasConfigStub>(

View File

@ -75,7 +75,7 @@ public class YamlConfigLoaderEnumTests
var loader = CreateLoader<MonsterRewardConfigStub>();
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterRewardConfigStub>("monster");
Assert.Multiple(() =>
@ -127,7 +127,7 @@ public class YamlConfigLoaderEnumTests
var loader = CreateLoader<MonsterRewardConfigStub>();
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -176,7 +176,7 @@ public class YamlConfigLoaderEnumTests
var loader = CreateLoader<MonsterDropItemIdsConfigStub>();
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{

View File

@ -105,7 +105,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -137,7 +137,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterConditionalConfigStub>("monster");
var reward = table.Get(1).Reward;
@ -170,7 +170,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -202,7 +202,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterConditionalConfigStub>("monster");
var reward = table.Get(1).Reward;
@ -250,7 +250,10 @@ public sealed class YamlConfigLoaderIfThenElseTests
}
""");
ArgumentNullException.ThrowIfNull(_rootPath);
if (_rootPath is null)
{
throw new InvalidOperationException("Root path is not initialized.");
}
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterTagConfigStub>(
@ -260,7 +263,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
static config => config.Id);
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -301,7 +304,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -342,7 +345,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -390,7 +393,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -409,7 +412,10 @@ public sealed class YamlConfigLoaderIfThenElseTests
/// <param name="content">配置文件内容。</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);
@ -496,7 +502,10 @@ public sealed class YamlConfigLoaderIfThenElseTests
/// <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, MonsterConditionalConfigStub>(

View File

@ -70,7 +70,7 @@ public sealed class YamlConfigLoaderNegationTests
var loader = CreateMonsterLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -118,7 +118,7 @@ public sealed class YamlConfigLoaderNegationTests
var loader = CreateMonsterLoader();
var registry = CreateRegistry();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterConfigStub>("monster");
@ -172,7 +172,7 @@ public sealed class YamlConfigLoaderNegationTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -227,7 +227,7 @@ public sealed class YamlConfigLoaderNegationTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var table = registry.GetTable<int, MonsterRewardConfigStub>("monster");
@ -272,7 +272,7 @@ public sealed class YamlConfigLoaderNegationTests
var loader = CreateMonsterLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
Assert.Multiple(() =>
{

View File

@ -191,7 +191,10 @@ public sealed class YamlConfigSchemaValidatorTests
string relativePath,
string content)
{
ArgumentNullException.ThrowIfNull(_rootPath);
if (_rootPath is null)
{
throw new InvalidOperationException("Root path is not initialized.");
}
var fullPath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
var directoryPath = Path.GetDirectoryName(fullPath);

View File

@ -132,7 +132,7 @@ public sealed class YamlConfigTextValidatorTests
"monster/generated.yaml",
"""
id: 1
"""));
""").ConfigureAwait(false));
Assert.Multiple(() =>
{

View File

@ -1,114 +0,0 @@
using System;
using GFramework.Game.Abstractions.Data;
using GFramework.Game.Abstractions.Enums;
namespace GFramework.Game.Tests.Data;
/// <summary>
/// 为持久化测试提供稳定的测试数据位置实现。
/// </summary>
internal sealed class TestDataLocation : IDataLocation
{
/// <summary>
/// 初始化测试数据位置。
/// </summary>
/// <param name="key">测试使用的存储键。</param>
/// <param name="kinds">测试使用的存储类型。</param>
/// <param name="namespaceValue">测试使用的命名空间。</param>
/// <param name="metadata">附加测试元数据。</param>
public TestDataLocation(
string key,
StorageKinds kinds = StorageKinds.Local,
string? namespaceValue = null,
IReadOnlyDictionary<string, string>? metadata = null)
{
Key = key;
Kinds = kinds;
Namespace = namespaceValue;
Metadata = metadata;
}
/// <summary>
/// 获取测试数据对应的存储键。
/// </summary>
public string Key { get; }
/// <summary>
/// 获取测试数据使用的存储类型。
/// </summary>
public StorageKinds Kinds { get; }
/// <summary>
/// 获取测试数据使用的命名空间。
/// </summary>
public string? Namespace { get; }
/// <summary>
/// 获取附加到测试位置上的元数据。
/// </summary>
public IReadOnlyDictionary<string, string>? Metadata { get; }
}
/// <summary>
/// 为基础存档仓库测试提供的简单存档模型。
/// </summary>
internal sealed class TestSaveData : IData
{
/// <summary>
/// 获取或设置测试存档中的名称字段。
/// </summary>
public string Name { get; set; } = string.Empty;
}
/// <summary>
/// 为存档迁移测试提供的版本化存档模型。
/// </summary>
internal sealed class TestVersionedSaveData : IVersionedData
{
/// <summary>
/// 获取或设置测试存档中的名称字段。
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 获取或设置测试存档中的等级字段。
/// </summary>
public int Level { get; set; }
/// <summary>
/// 获取或设置测试存档中的经验字段。
/// </summary>
public int Experience { get; set; }
/// <summary>
/// 获取或设置当前测试存档的版本号。
/// </summary>
public int Version { get; set; } = 3;
/// <summary>
/// 获取或设置测试存档的最后修改时间。
/// </summary>
public DateTime LastModified { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// 为通用持久化测试提供的简单数据模型。
/// </summary>
internal sealed class TestSimpleData : IData
{
/// <summary>
/// 获取或设置测试数据中的整数值。
/// </summary>
public int Value { get; set; }
}
/// <summary>
/// 为批量持久化测试提供的另一种数据模型,用于验证运行时类型不会在接口路径上退化。
/// </summary>
internal sealed class TestNamedData : IData
{
/// <summary>
/// 获取或设置测试数据中的名称值。
/// </summary>
public string Name { get; set; } = string.Empty;
}

View File

@ -38,12 +38,12 @@ public class PersistenceTests
using var storage = new FileStorage(root, new JsonSerializer(), ".json");
var saved = new TestSimpleData { Value = 5 };
await storage.WriteAsync("folder/item", saved);
await storage.WriteAsync("folder/item", saved).ConfigureAwait(false);
var loaded = await storage.ReadAsync<TestSimpleData>("folder/item");
var loaded = await storage.ReadAsync<TestSimpleData>("folder/item").ConfigureAwait(false);
Assert.That(loaded.Value, Is.EqualTo(saved.Value));
Assert.ThrowsAsync<ArgumentException>(async () => await storage.WriteAsync("../escape", new TestSimpleData()));
Assert.ThrowsAsync<ArgumentException>(async () => await storage.WriteAsync("../escape", new TestSimpleData()).ConfigureAwait(false));
}
/// <summary>
@ -108,7 +108,7 @@ public class PersistenceTests
.RegisterMigration(new TestSaveMigrationV2ToV3());
var loaded = await repository.LoadAsync(1);
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save");
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save").ConfigureAwait(false);
Assert.Multiple(() =>
{
@ -185,7 +185,7 @@ public class PersistenceTests
var repository = new SaveRepository<TestVersionedSaveData>(storage, config)
.RegisterMigration(new TestSaveMigrationV1ToV2());
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.LoadAsync(1));
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.LoadAsync(1).ConfigureAwait(false));
Assert.That(exception!.Message, Does.Contain("from version 2"));
}
@ -218,8 +218,8 @@ public class PersistenceTests
var repository = new SaveRepository<TestVersionedSaveData>(storage, config)
.RegisterMigration(new TestSaveMigrationV1ToV2ReturningV3());
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.LoadAsync(1));
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save");
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.LoadAsync(1).ConfigureAwait(false));
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save").ConfigureAwait(false);
Assert.Multiple(() =>
{
@ -270,7 +270,7 @@ public class PersistenceTests
repository.RegisterMigration(new TestSaveMigrationV2ToV3());
continueMigration.Set();
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loadTask);
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loadTask.ConfigureAwait(false));
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save");
Assert.Multiple(() =>

View File

@ -0,0 +1,49 @@
using GFramework.Game.Abstractions.Data;
using GFramework.Game.Abstractions.Enums;
namespace GFramework.Game.Tests.Data;
/// <summary>
/// 为持久化测试提供稳定的测试数据位置实现。
/// </summary>
internal sealed class TestDataLocation : IDataLocation
{
/// <summary>
/// 初始化测试数据位置。
/// </summary>
/// <param name="key">测试使用的存储键。</param>
/// <param name="kinds">测试使用的存储类型。</param>
/// <param name="namespaceValue">测试使用的命名空间。</param>
/// <param name="metadata">附加测试元数据。</param>
public TestDataLocation(
string key,
StorageKinds kinds = StorageKinds.Local,
string? namespaceValue = null,
IReadOnlyDictionary<string, string>? metadata = null)
{
Key = key;
Kinds = kinds;
Namespace = namespaceValue;
Metadata = metadata;
}
/// <summary>
/// 获取测试数据对应的存储键。
/// </summary>
public string Key { get; }
/// <summary>
/// 获取测试数据使用的存储类型。
/// </summary>
public StorageKinds Kinds { get; }
/// <summary>
/// 获取测试数据使用的命名空间。
/// </summary>
public string? Namespace { get; }
/// <summary>
/// 获取附加到测试位置上的元数据。
/// </summary>
public IReadOnlyDictionary<string, string>? Metadata { get; }
}

View File

@ -0,0 +1,14 @@
using GFramework.Game.Abstractions.Data;
namespace GFramework.Game.Tests.Data;
/// <summary>
/// 为批量持久化测试提供的另一种数据模型,用于验证运行时类型不会在接口路径上退化。
/// </summary>
internal sealed class TestNamedData : IData
{
/// <summary>
/// 获取或设置测试数据中的名称值。
/// </summary>
public string Name { get; set; } = string.Empty;
}

View File

@ -0,0 +1,14 @@
using GFramework.Game.Abstractions.Data;
namespace GFramework.Game.Tests.Data;
/// <summary>
/// 为基础存档仓库测试提供的简单存档模型。
/// </summary>
internal sealed class TestSaveData : IData
{
/// <summary>
/// 获取或设置测试存档中的名称字段。
/// </summary>
public string Name { get; set; } = string.Empty;
}

View File

@ -0,0 +1,14 @@
using GFramework.Game.Abstractions.Data;
namespace GFramework.Game.Tests.Data;
/// <summary>
/// 为通用持久化测试提供的简单数据模型。
/// </summary>
internal sealed class TestSimpleData : IData
{
/// <summary>
/// 获取或设置测试数据中的整数值。
/// </summary>
public int Value { get; set; }
}

View File

@ -0,0 +1,35 @@
using System;
using GFramework.Game.Abstractions.Data;
namespace GFramework.Game.Tests.Data;
/// <summary>
/// 为存档迁移测试提供的版本化存档模型。
/// </summary>
internal sealed class TestVersionedSaveData : IVersionedData
{
/// <summary>
/// 获取或设置测试存档中的名称字段。
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 获取或设置测试存档中的等级字段。
/// </summary>
public int Level { get; set; }
/// <summary>
/// 获取或设置测试存档中的经验字段。
/// </summary>
public int Experience { get; set; }
/// <summary>
/// 获取或设置当前测试存档的版本号。
/// </summary>
public int Version { get; set; } = 3;
/// <summary>
/// 获取或设置测试存档的最后修改时间。
/// </summary>
public DateTime LastModified { get; set; } = DateTime.UtcNow;
}

View File

@ -6,34 +6,35 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-053`
- 当前阶段:`Phase 53`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-054`
- 当前阶段:`Phase 54`
- 当前焦点:
- `2026-04-24` 本轮`$gframework-batch-boot 75` 重新建立当前分支相对 `origin/main` 的 batch 基线,并从整仓 `Release` build 里挑选低风险热点
- `GFramework.Godot` 已完成一轮独立 warning 清理:`GodotYamlConfigEnvironment` 的目录枚举逻辑已拆分 helper`AbstractArchitecture` / `SceneBehaviorBase` 中需要保留 Godot 同步上下文的 await 已显式改为 `.ConfigureAwait(true)`
- `GFramework.Godot.Tests` 已同步清理对应测试 warning异步断言显式使用 `.ConfigureAwait(false)``RichTextMarkupTests` 中测试字典显式指定 `StringComparer.Ordinal`
- 当前代码批次相对 `origin/main` 的待提交 diff 为 `6` 个文件、`107` 行变更,远低于 `$gframework-batch-boot 75` 的主停止阈值;本轮在这里收口,是因为下一批候选将进入 `GFramework.Game` 等高基线模块,而不再是同等级低风险切片
- `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` 的主停止阈值;本轮必须在提交后停止继续扩批
## 当前活跃事实
- 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值
- 本轮直接执行仓库根目录 `dotnet clean` 仍在 `ValidateSolutionConfiguration` 阶段失败,输出未提供具体 error 文本
- 本轮直接执行仓库根目录 `dotnet build GFramework.sln -c Release` 成功,并给出 `1122 warning(s)` 的当前整仓观测值
- `GFramework.Godot/GFramework.Godot.csproj -c Release` 在本轮收尾验证中已达到 `0 Warning(s)``0 Error(s)`
- `GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release` 在串行复验中已达到 `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`
- `GFramework.Godot` 原先暴露的 `MA0051``MA0004` 热点都已清理完成;当前同域低风险切片基本耗尽
- 本轮直接执行仓库根目录 `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 build`,容易再次把 warning 数量误判为 0
- 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
- 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线
- 缓解措施:若下一轮继续做整仓 warning reduction先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1122 warning(s)` direct build 观测值
- 缓解措施:若下一轮继续做整仓 warning reduction先定位 `dotnet clean` 的 solution-level / project-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 direct build 观测值
- 当前 worktree 仍存在未跟踪的 `.codex` 目录
- 缓解措施:提交当前批次时只暂存 analyzer-warning-reduction 相关源码与 `ai-plan` 文件,避免把工作目录辅助文件混入提交
- 下一轮最明显的剩余热点将转入 `GFramework.Game` 等高 warning 基线模块
- 缓解措施:恢复时先重新跑整仓 build 热点筛选,再决定是否接受更高上下文成本的 `GFramework.Game` 切片,或先排查 solution-level clean 失败原因
- 当前批次已触发 `$gframework-batch-boot 75` 的主停止条件
- 缓解措施:本轮提交后停止继续扩批;下一次继续前先评估是否需要基于更新后的 `origin/main` 重新选择基线,或切到新分支 / 新轮次处理剩余 `GFramework.Game.Tests` 热点
- `GFramework.Game.Tests` 的剩余 warning 主要集中在大文件与集成测试文件
- 缓解措施:后续若继续,优先把 `YamlConfigLoaderTests.cs` 单独作为一个高上下文切片处理,不要和其它 warning family 混批
## 活跃文档
@ -52,16 +53,15 @@
- `dotnet clean GFramework.sln -c Release`
- 结果:失败;停在 solution `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`,未输出更具体的 error 文本
- `dotnet build GFramework.sln -c Release`
- 结果:成功;`1122 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
- 结果:成功;`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)`
- 结果:成功;`116 Warning(s)``0 Error(s)`
- `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`
## 下一步建议
1. 提交当前 `GFramework.Godot` / `GFramework.Godot.Tests` warning 清理批次,并继续保持只纳入本 topic 相关文件
2. 下一轮若继续使用 `$gframework-batch-boot 75`,先决定是优先排查 solution-level `dotnet clean` 失败,还是接受更高上下文成本进入 `GFramework.Game` warning 热点
1. 提交当前 `GFramework.Game.Tests` warning 清理批次与 `RP-054` tracking 更新,然后停止当前 batch loop因为 branch diff 已达 `76/75`
2. 下一轮若继续 warning reduction应先决定是重新整理 `origin/main` 基线,还是单独开一个高上下文批次处理 `YamlConfigLoaderTests.cs`

View File

@ -2,6 +2,36 @@
# Analyzer Warning Reduction 追踪
## 2026-04-24 — RP-054
### 阶段:`GFramework.Game.Tests` 低风险测试 warning 批次(触发文件数停止阈值)
- 触发背景:
- 用户要求“直接进入下一批”,继续沿 `$gframework-batch-boot 75` 自动推进 warning reduction
- 以 `origin/main` 为基线时,上一批提交后分支累计 diff 仍只有 `8` 个文件,足够再落一个独立批次
- 重新执行 `dotnet clean GFramework.sln -c Release` 仍停在 `ValidateSolutionConfiguration`,因此继续以直接 `dotnet build GFramework.sln -c Release` 的输出挑选低风险热点
- 主线程实施:
- 从整仓 `Release build``116 warning(s)` 入口观测值中,选择 `GFramework.Game.Tests` 的小型测试文件和 `PersistenceTestUtilities.cs` 作为当前批次,刻意避开 `YamlConfigLoaderTests.cs` 这类高上下文大文件
- 在 `YamlConfigLoaderIfThenElseTests.cs``YamlConfigLoaderDependentSchemasTests.cs``YamlConfigLoaderDependentRequiredTests.cs``YamlConfigLoaderNegationTests.cs``YamlConfigLoaderAllOfTests.cs``YamlConfigLoaderEnumTests.cs``YamlConfigTextValidatorTests.cs``PersistenceTests.cs` 中补齐 `.ConfigureAwait(false)`,并把字段态 `_rootPath``ThrowIfNull` 改为显式 `InvalidOperationException`
- 将 `PersistenceTestUtilities.cs` 拆分为 `TestDataLocation.cs``TestSaveData.cs``TestVersionedSaveData.cs``TestSimpleData.cs``TestNamedData.cs`,消除 `MA0048` 并对齐仓库的一文件一主类型风格
- 在 `YamlConfigSchemaValidatorTests.cs` 中把字段态 `_rootPath` 的校验改成显式状态异常,避免继续触发 `MA0015`
- 验证里程碑:
- `dotnet clean GFramework.sln -c Release`
- 结果:失败;停在 `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.sln -c Release`
- 结果:成功;`116 Warning(s)``0 Error(s)`
- `dotnet clean GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 结果失败clean 阶段提前结束,`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 第一轮批次后:成功;`80 Warning(s)``0 Error(s)`
- 收尾修正后:成功;`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`
- 当前结论:
- `GFramework.Game.Tests` 本轮入口热点已从 `116 warning(s)` 收敛到 `71 warning(s)`,且本轮 touched files 不再出现在 warning 输出中
- 当前工作树相对 `origin/main` 的累计 diff 已达到 `76` 个文件、`986` 行,超过 `$gframework-batch-boot 75` 的主停止阈值
- 按批处理技能规则,本轮必须在提交当前批次后停止;剩余候选应在新一轮里单独评估,尤其是 `YamlConfigLoaderTests.cs`
## 2026-04-24 — RP-053
### 阶段:`GFramework.Godot` / `GFramework.Godot.Tests` 小批次 warning 清理