Merge pull request #286 from GeWuYou/fix/analyzer-warning-reduction-batch

Fix/analyzer warning reduction batch
This commit is contained in:
gewuyou 2026-04-24 23:05:53 +08:00 committed by GitHub
commit 9964962416
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 643 additions and 365 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

@ -50,65 +50,12 @@ public class GeneratedConfigConsumerIntegrationTests
var loader = new YamlConfigLoader(_rootPath)
.RegisterAllGeneratedConfigTables();
await loader.LoadAsync(registry);
await loader.LoadAsync(registry).ConfigureAwait(false);
var monsterTable = registry.GetMonsterTable();
var dungeonMonsters = monsterTable.FindByFaction("dungeon");
var itemTable = registry.GetItemTable();
Assert.Multiple(() =>
{
Assert.That(
GeneratedConfigCatalog.Tables.Select(static metadata => metadata.TableName),
Is.SupersetOf(new[] { "item", "monster" }));
Assert.That(GeneratedConfigCatalog.TryGetByTableName("item", out var itemCatalogEntry), Is.True);
Assert.That(itemCatalogEntry.ConfigDomain, Is.EqualTo("item"));
Assert.That(itemCatalogEntry.ConfigRelativePath, Is.EqualTo("item"));
Assert.That(itemCatalogEntry.SchemaRelativePath, Is.EqualTo("schemas/item.schema.json"));
Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var catalogEntry), Is.True);
Assert.That(catalogEntry.ConfigDomain, Is.EqualTo("monster"));
Assert.That(catalogEntry.ConfigRelativePath, Is.EqualTo("monster"));
Assert.That(catalogEntry.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
Assert.That(ItemConfigBindings.ConfigDomain, Is.EqualTo("item"));
Assert.That(ItemConfigBindings.Metadata.TableName, Is.EqualTo("item"));
Assert.That(MonsterConfigBindings.ConfigDomain, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.TableName, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.ConfigRelativePath, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
Assert.That(MonsterConfigBindings.Metadata.ConfigDomain, Is.EqualTo(MonsterConfigBindings.ConfigDomain));
Assert.That(MonsterConfigBindings.Metadata.TableName, Is.EqualTo(MonsterConfigBindings.TableName));
Assert.That(MonsterConfigBindings.Metadata.ConfigRelativePath,
Is.EqualTo(MonsterConfigBindings.ConfigRelativePath));
Assert.That(MonsterConfigBindings.Metadata.SchemaRelativePath,
Is.EqualTo(MonsterConfigBindings.SchemaRelativePath));
Assert.That(MonsterConfigBindings.References.All, Is.Empty);
Assert.That(MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out _), Is.False);
Assert.That(monsterTable.Count, Is.EqualTo(2));
Assert.That(monsterTable.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(monsterTable.Get(2).Hp, Is.EqualTo(30));
Assert.That(monsterTable.FindByName("Slime").Select(static config => config.Id), Is.EqualTo(new[] { 1 }));
Assert.That(dungeonMonsters.Select(static config => config.Name),
Is.EquivalentTo(new[] { "Slime", "Goblin" }));
Assert.That(monsterTable.TryFindFirstByName("Goblin", out var goblin), Is.True);
Assert.That(goblin, Is.Not.Null);
Assert.That(goblin!.Id, Is.EqualTo(2));
Assert.That(monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster), Is.True);
Assert.That(firstDungeonMonster, Is.Not.Null);
Assert.That(firstDungeonMonster!.Name, Is.AnyOf("Slime", "Goblin"));
Assert.That(monsterTable.TryFindFirstByFaction("forest", out var missingMonster), Is.False);
Assert.That(missingMonster, Is.Null);
Assert.That(registry.TryGetMonsterTable(out var generatedTable), Is.True);
Assert.That(generatedTable, Is.Not.Null);
Assert.That(generatedTable!.All().Select(static config => config.Name),
Is.EquivalentTo(new[] { "Slime", "Goblin" }));
Assert.That(itemTable.Count, Is.EqualTo(2));
Assert.That(itemTable.Get("potion").Name, Is.EqualTo("Potion"));
Assert.That(itemTable.FindByCategory("consumable").Select(static config => config.Id),
Is.EquivalentTo(new[] { "potion", "ether" }));
Assert.That(registry.TryGetItemTable(out var generatedItemTable), Is.True);
Assert.That(generatedItemTable, Is.Not.Null);
Assert.That(generatedItemTable!.Get("ether").Name, Is.EqualTo("Ether"));
});
AssertGeneratedBindingsLoadResults(registry, monsterTable, itemTable);
}
/// <summary>
@ -181,7 +128,7 @@ public class GeneratedConfigConsumerIntegrationTests
{
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
});
await domainLoader.LoadAsync(domainRegistry);
await domainLoader.LoadAsync(domainRegistry).ConfigureAwait(false);
var tableNameRegistry = new ConfigRegistry();
var tableNameLoader = new YamlConfigLoader(_rootPath)
@ -190,7 +137,7 @@ public class GeneratedConfigConsumerIntegrationTests
{
IncludedTableNames = new[] { ItemConfigBindings.TableName }
});
await tableNameLoader.LoadAsync(tableNameRegistry);
await tableNameLoader.LoadAsync(tableNameRegistry).ConfigureAwait(false);
var emptyAllowListRegistry = new ConfigRegistry();
var emptyAllowListLoader = new YamlConfigLoader(_rootPath)
@ -200,7 +147,7 @@ public class GeneratedConfigConsumerIntegrationTests
IncludedConfigDomains = Array.Empty<string>(),
IncludedTableNames = Array.Empty<string>()
});
await emptyAllowListLoader.LoadAsync(emptyAllowListRegistry);
await emptyAllowListLoader.LoadAsync(emptyAllowListRegistry).ConfigureAwait(false);
var monsterDomain = MonsterConfigBindings.ConfigDomain;
var predicateRegistry = new ConfigRegistry();
@ -211,28 +158,13 @@ public class GeneratedConfigConsumerIntegrationTests
TableFilter = metadata =>
string.Equals(metadata.ConfigDomain, monsterDomain, StringComparison.Ordinal)
});
await predicateLoader.LoadAsync(predicateRegistry);
await predicateLoader.LoadAsync(predicateRegistry).ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(emptyAllowListRegistry.TryGetMonsterTable(out var emptyAllowListMonsterTable), Is.True);
Assert.That(emptyAllowListMonsterTable, Is.Not.Null);
Assert.That(emptyAllowListRegistry.TryGetItemTable(out var emptyAllowListItemTable), Is.True);
Assert.That(emptyAllowListItemTable, Is.Not.Null);
Assert.That(domainRegistry.TryGetMonsterTable(out var domainMonsterTable), Is.True);
Assert.That(domainMonsterTable, Is.Not.Null);
Assert.That(domainRegistry.TryGetItemTable(out _), Is.False);
Assert.That(tableNameRegistry.TryGetMonsterTable(out _), Is.False);
Assert.That(tableNameRegistry.TryGetItemTable(out var tableNameItemTable), Is.True);
Assert.That(tableNameItemTable, Is.Not.Null);
Assert.That(tableNameItemTable!.Get("potion").Name, Is.EqualTo("Potion"));
Assert.That(predicateRegistry.TryGetMonsterTable(out var predicateMonsterTable), Is.True);
Assert.That(predicateMonsterTable, Is.Not.Null);
Assert.That(predicateRegistry.TryGetItemTable(out _), Is.False);
});
AssertGeneratedRegistrationFilteringResults(
domainRegistry,
tableNameRegistry,
emptyAllowListRegistry,
predicateRegistry);
}
/// <summary>
@ -270,7 +202,7 @@ public class GeneratedConfigConsumerIntegrationTests
MonsterConfigBindings.ValidateYaml(_rootPath, "monster/generated.yaml", yaml));
Assert.DoesNotThrowAsync(async () =>
await MonsterConfigBindings.ValidateYamlAsync(_rootPath, "monster/generated.yaml", yaml));
await MonsterConfigBindings.ValidateYamlAsync(_rootPath, "monster/generated.yaml", yaml).ConfigureAwait(false));
var invalidYaml = """
id: 3
@ -282,7 +214,7 @@ public class GeneratedConfigConsumerIntegrationTests
var exception = Assert.Throws<ConfigLoadException>(() =>
MonsterConfigBindings.ValidateYaml(_rootPath, "monster/generated.yaml", invalidYaml));
var asyncException = Assert.ThrowsAsync<ConfigLoadException>(async () =>
await MonsterConfigBindings.ValidateYamlAsync(_rootPath, "monster/generated.yaml", invalidYaml));
await MonsterConfigBindings.ValidateYamlAsync(_rootPath, "monster/generated.yaml", invalidYaml).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -367,6 +299,138 @@ public class GeneratedConfigConsumerIntegrationTests
""");
}
/// <summary>
/// 统一断言生成绑定加载后的目录元数据、查询入口与强类型表包装结果,
/// 以便缩短端到端测试主体并降低分析器对方法长度的告警。
/// </summary>
private static void AssertGeneratedBindingsLoadResults(
ConfigRegistry registry,
MonsterTable monsterTable,
ItemTable itemTable)
{
AssertGeneratedCatalogMetadata();
AssertGeneratedMonsterTableResults(registry, monsterTable);
AssertGeneratedItemTableResults(registry, itemTable);
}
/// <summary>
/// 断言消费者项目的生成目录元数据与静态绑定常量保持一致。
/// </summary>
private static void AssertGeneratedCatalogMetadata()
{
Assert.Multiple(() =>
{
Assert.That(
GeneratedConfigCatalog.Tables.Select(static metadata => metadata.TableName),
Is.SupersetOf(new[] { "item", "monster" }));
Assert.That(GeneratedConfigCatalog.TryGetByTableName("item", out var itemCatalogEntry), Is.True);
Assert.That(itemCatalogEntry.ConfigDomain, Is.EqualTo("item"));
Assert.That(itemCatalogEntry.ConfigRelativePath, Is.EqualTo("item"));
Assert.That(itemCatalogEntry.SchemaRelativePath, Is.EqualTo("schemas/item.schema.json"));
Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var catalogEntry), Is.True);
Assert.That(catalogEntry.ConfigDomain, Is.EqualTo("monster"));
Assert.That(catalogEntry.ConfigRelativePath, Is.EqualTo("monster"));
Assert.That(catalogEntry.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
Assert.That(ItemConfigBindings.ConfigDomain, Is.EqualTo("item"));
Assert.That(ItemConfigBindings.Metadata.TableName, Is.EqualTo("item"));
Assert.That(MonsterConfigBindings.ConfigDomain, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.TableName, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.ConfigRelativePath, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
Assert.That(MonsterConfigBindings.Metadata.ConfigDomain, Is.EqualTo(MonsterConfigBindings.ConfigDomain));
Assert.That(MonsterConfigBindings.Metadata.TableName, Is.EqualTo(MonsterConfigBindings.TableName));
Assert.That(MonsterConfigBindings.Metadata.ConfigRelativePath,
Is.EqualTo(MonsterConfigBindings.ConfigRelativePath));
Assert.That(MonsterConfigBindings.Metadata.SchemaRelativePath,
Is.EqualTo(MonsterConfigBindings.SchemaRelativePath));
Assert.That(MonsterConfigBindings.References.All, Is.Empty);
Assert.That(MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out _), Is.False);
});
}
/// <summary>
/// 断言 monster 绑定在注册表中的查询辅助、索引查询与强类型访问入口都可用。
/// </summary>
private static void AssertGeneratedMonsterTableResults(
ConfigRegistry registry,
MonsterTable monsterTable)
{
var dungeonMonsters = monsterTable.FindByFaction("dungeon");
Assert.Multiple(() =>
{
Assert.That(monsterTable.Count, Is.EqualTo(2));
Assert.That(monsterTable.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(monsterTable.Get(2).Hp, Is.EqualTo(30));
Assert.That(monsterTable.FindByName("Slime").Select(static config => config.Id), Is.EqualTo(new[] { 1 }));
Assert.That(dungeonMonsters.Select(static config => config.Name),
Is.EquivalentTo(new[] { "Slime", "Goblin" }));
Assert.That(monsterTable.TryFindFirstByName("Goblin", out var goblin), Is.True);
Assert.That(goblin, Is.Not.Null);
Assert.That(goblin!.Id, Is.EqualTo(2));
Assert.That(monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster), Is.True);
Assert.That(firstDungeonMonster, Is.Not.Null);
Assert.That(firstDungeonMonster!.Name, Is.AnyOf("Slime", "Goblin"));
Assert.That(monsterTable.TryFindFirstByFaction("forest", out var missingMonster), Is.False);
Assert.That(missingMonster, Is.Null);
Assert.That(registry.TryGetMonsterTable(out var generatedTable), Is.True);
Assert.That(generatedTable, Is.Not.Null);
Assert.That(generatedTable!.All().Select(static config => config.Name),
Is.EquivalentTo(new[] { "Slime", "Goblin" }));
});
}
/// <summary>
/// 断言 item 绑定的强类型表包装与按分类查询在聚合注册路径下可正常工作。
/// </summary>
private static void AssertGeneratedItemTableResults(
ConfigRegistry registry,
ItemTable itemTable)
{
Assert.Multiple(() =>
{
Assert.That(itemTable.Count, Is.EqualTo(2));
Assert.That(itemTable.Get("potion").Name, Is.EqualTo("Potion"));
Assert.That(itemTable.FindByCategory("consumable").Select(static config => config.Id),
Is.EquivalentTo(new[] { "potion", "ether" }));
Assert.That(registry.TryGetItemTable(out var generatedItemTable), Is.True);
Assert.That(generatedItemTable, Is.Not.Null);
Assert.That(generatedItemTable!.Get("ether").Name, Is.EqualTo("Ether"));
});
}
/// <summary>
/// 汇总断言不同聚合注册筛选条件下的装载结果,
/// 让测试主体聚焦于注册参数本身而不是展开大量重复断言。
/// </summary>
private static void AssertGeneratedRegistrationFilteringResults(
ConfigRegistry domainRegistry,
ConfigRegistry tableNameRegistry,
ConfigRegistry emptyAllowListRegistry,
ConfigRegistry predicateRegistry)
{
Assert.Multiple(() =>
{
Assert.That(emptyAllowListRegistry.TryGetMonsterTable(out var emptyAllowListMonsterTable), Is.True);
Assert.That(emptyAllowListMonsterTable, Is.Not.Null);
Assert.That(emptyAllowListRegistry.TryGetItemTable(out var emptyAllowListItemTable), Is.True);
Assert.That(emptyAllowListItemTable, Is.Not.Null);
Assert.That(domainRegistry.TryGetMonsterTable(out var domainMonsterTable), Is.True);
Assert.That(domainMonsterTable, Is.Not.Null);
Assert.That(domainRegistry.TryGetItemTable(out _), Is.False);
Assert.That(tableNameRegistry.TryGetMonsterTable(out _), Is.False);
Assert.That(tableNameRegistry.TryGetItemTable(out var tableNameItemTable), Is.True);
Assert.That(tableNameItemTable, Is.Not.Null);
Assert.That(tableNameItemTable!.Get("potion").Name, Is.EqualTo("Potion"));
Assert.That(predicateRegistry.TryGetMonsterTable(out var predicateMonsterTable), Is.True);
Assert.That(predicateMonsterTable, Is.Not.Null);
Assert.That(predicateRegistry.TryGetItemTable(out _), Is.False);
});
}
/// <summary>
/// 在临时消费者目录中创建 item schema 与 YAML 测试数据,用于验证多表聚合注册和筛选行为。
/// </summary>

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(() =>
{
@ -519,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);
@ -606,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

@ -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,8 +270,8 @@ public class PersistenceTests
repository.RegisterMigration(new TestSaveMigrationV2ToV3());
continueMigration.Set();
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loadTask);
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save");
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loadTask.ConfigureAwait(false));
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save").ConfigureAwait(false);
Assert.Multiple(() =>
{
@ -593,13 +593,13 @@ 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);
var cachedAfterFailure = await repository.LoadAsync<TestSimpleData>(primaryLocation).ConfigureAwait(false);
Assert.That(cachedAfterFailure.Value, Is.EqualTo(1));
throwingStorage.ThrowOnWrite = false;
await repository.SaveAsync(secondaryLocation, new TestSimpleData { Value = 7 });
await repository.SaveAsync(secondaryLocation, new TestSimpleData { Value = 7 }).ConfigureAwait(false);
using var verifyStorage = new FileStorage(root, new JsonSerializer(), ".json");
var verifyRepository = new UnifiedSettingsDataRepository(
@ -609,8 +609,8 @@ public class PersistenceTests
verifyRepository.RegisterDataType(primaryLocation, typeof(TestSimpleData));
verifyRepository.RegisterDataType(secondaryLocation, typeof(TestSimpleData));
var persistedPrimary = await verifyRepository.LoadAsync<TestSimpleData>(primaryLocation);
var persistedSecondary = await verifyRepository.LoadAsync<TestSimpleData>(secondaryLocation);
var persistedPrimary = await verifyRepository.LoadAsync<TestSimpleData>(primaryLocation).ConfigureAwait(false);
var persistedSecondary = await verifyRepository.LoadAsync<TestSimpleData>(secondaryLocation).ConfigureAwait(false);
Assert.Multiple(() =>
{
@ -655,12 +655,13 @@ public class PersistenceTests
repository.RegisterDataType(secondaryLocation, typeof(TestSimpleData));
throwingStorage.ThrowOnWrite = true;
Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.DeleteAsync(secondaryLocation));
Assert.ThrowsAsync<InvalidOperationException>(
async () => await repository.DeleteAsync(secondaryLocation).ConfigureAwait(false));
Assert.That(await repository.ExistsAsync(secondaryLocation), Is.True);
Assert.That(await repository.ExistsAsync(secondaryLocation).ConfigureAwait(false), Is.True);
throwingStorage.ThrowOnWrite = false;
await repository.SaveAsync(primaryLocation, new TestSimpleData { Value = 9 });
await repository.SaveAsync(primaryLocation, new TestSimpleData { Value = 9 }).ConfigureAwait(false);
using var verifyStorage = new FileStorage(root, new JsonSerializer(), ".json");
var verifyRepository = new UnifiedSettingsDataRepository(
@ -670,8 +671,8 @@ public class PersistenceTests
verifyRepository.RegisterDataType(primaryLocation, typeof(TestSimpleData));
verifyRepository.RegisterDataType(secondaryLocation, typeof(TestSimpleData));
var persistedPrimary = await verifyRepository.LoadAsync<TestSimpleData>(primaryLocation);
var persistedSecondary = await verifyRepository.LoadAsync<TestSimpleData>(secondaryLocation);
var persistedPrimary = await verifyRepository.LoadAsync<TestSimpleData>(primaryLocation).ConfigureAwait(false);
var persistedSecondary = await verifyRepository.LoadAsync<TestSimpleData>(secondaryLocation).ConfigureAwait(false);
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

@ -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

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

View File

@ -197,7 +197,7 @@ public sealed class GodotYamlConfigLoaderTests
var loader = CreateLoader(isEditor: false);
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () =>
await loader.LoadAsync(new ConfigRegistry()));
await loader.LoadAsync(new ConfigRegistry()).ConfigureAwait(false));
Assert.Multiple(() =>
{
@ -225,7 +225,7 @@ public sealed class GodotYamlConfigLoaderTests
configureLoader: static _ => { });
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"));
}
@ -254,7 +254,7 @@ public sealed class GodotYamlConfigLoaderTests
configureLoader: static _ => { });
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"));
}

View File

@ -1,3 +1,4 @@
using System;
using GFramework.Godot.Text;
namespace GFramework.Godot.Tests.Text;
@ -25,7 +26,7 @@ public sealed class RichTextMarkupTests
[Test]
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,
["speed"] = 4
@ -53,7 +54,7 @@ public sealed class RichTextMarkupTests
[Test]
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
};

View File

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

View File

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

View File

@ -144,7 +144,8 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
public virtual async ValueTask OnPauseAsync()
{
if (_scene != null)
await _scene.OnPauseAsync();
// 暂停后紧接着会修改 Owner 的处理开关,必须回到 Godot 主线程继续执行。
await _scene.OnPauseAsync().ConfigureAwait(true);
// 暂停处理
Owner.SetProcess(false);
@ -165,7 +166,8 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
return;
if (_scene != null)
await _scene.OnResumeAsync();
// 恢复完成后要立刻重新启用节点处理流程,因此显式保留当前同步上下文。
await _scene.OnResumeAsync().ConfigureAwait(true);
// 恢复处理
Owner.SetProcess(true);
@ -198,7 +200,8 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
public virtual async ValueTask OnUnloadAsync()
{
if (_scene != null)
await _scene.OnUnloadAsync();
// 卸载后的 QueueFreeX 必须在 Godot 节点线程上调用,不能切走同步上下文。
await _scene.OnUnloadAsync().ConfigureAwait(true);
// 释放节点
Owner.QueueFreeX();

View File

@ -6,36 +6,41 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-052`
- 当前阶段:`Phase 52`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-058`
- 当前阶段:`Phase 58`
- 当前焦点:
- `2026-04-24` 本轮从当前 PR review 的未解决线程回切到 `GFramework.Game` / `GFramework.Godot.SourceGenerators.Tests`
- `UnifiedSettingsFile.Sections``CloneFile` fallback 已对齐为“可保留原 comparer 时保留,否则显式回退到 `StringComparer.Ordinal`”的文档与实现契约
- `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 `await test.RunAsync();` 已统一补齐 `.ConfigureAwait(false)`,并同步让 `VerifyDiagnosticsAsync` 内部消费异步等待
- 当前批次仍需避免混入与 analyzer-warning-reduction 无关的既有工作树改动
- `2026-04-24` 使用 `$gframework-pr-review` 复核当前分支 PR #286 的 latest-head review threads、MegaLinter 与测试状态
- 已确认最新 head 上唯一未解决的实质代码线程指向 `GFramework.Godot/Scene/SceneBehaviorBase.cs``OnPauseAsync` 的缩进异常,并顺带对齐 `OnResumeAsync``OnUnloadAsync`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release` 通过,结果为 `565 Warning(s)``0 Error(s)`;当前跟进只处理 PR review 指向的格式问题,不扩散到既有 warning 基线
- `dotnet format GFramework.Godot/GFramework.Godot.csproj --verify-no-changes --no-restore --include GFramework.Godot/Scene/SceneBehaviorBase.cs` 已通过,当前文件不再残留格式差异
## 当前活跃事实
- 之前记录的 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 build` 成功,并给出 `1184 warning(s)` 的真实输出
- `GFramework.Godot.SourceGenerators.Tests` 已通过测试辅助模板抽取与 `ConfigureAwait(false)` 修正,当前 `Debug` / `Release` 构建均为 `0 Warning(s)`
- 本轮已验证 `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`,结果为 `Passed: 48`
- 本轮已把 PR #283 中仍打开的 `UnifiedSettingsDataRepository.cs` comparer 契约线程落到代码与 XML 注释,避免 fallback 语义继续依赖隐式默认 comparer
- 本轮已确认 `AutoRegisterExportedCollectionsGeneratorTests` 的 5 处裸 `await test.RunAsync();` 不是当前 Release build 告警来源,但仍作为 PR review 一致性项一并修正
- 仓库根目录 `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)`
- `RP-056` 已验证 `GeneratedConfigConsumerIntegrationTests.cs` 不再出现在项目 build warning 输出中
- `RP-057` 已验证 `PersistenceTests.cs` 不再出现在 `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --no-incremental` 的 warning 输出中
- 本轮已验证 `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~UnifiedSettingsDataRepository_SaveAsync_When_Persist_Fails_Should_Keep_Cache_Consistent|FullyQualifiedName~UnifiedSettingsDataRepository_DeleteAsync_When_Persist_Fails_Should_Keep_Cache_Consistent"`,结果为 `Passed: 2`
- `GFramework.Game.Tests` 当前剩余热点已经几乎完全集中到 `YamlConfigLoaderTests.cs` 这一高上下文文件
- PR #286 当前标题为 `Fix/analyzer warning reduction batch`;最新抓取时间点的 PR 状态仍为 `OPEN`
- 最新 reviewed commit 为 `2b707343577193fc9904517e6078149653e95698`CodeRabbit 于 `2026-04-24T12:44:12Z` 给出 `CHANGES_REQUESTED`
- latest-head review threads 中只有 `1` 个未解决线程,内容是 `SceneBehaviorBase.OnPauseAsync` 的缩进不一致;本地源码已修复并扩展到同段的 `OnResumeAsync` / `OnUnloadAsync`
- MegaLinter 的 `dotnet-format` 详细问题与上述格式异常一致;本地 `dotnet format --verify-no-changes` 已通过
- PR 上其余 nitpick 仅为可选建议或已明确留待后续批次处理,当前没有额外需要立即修复的 latest-head 代码线程
## 当前风险
- 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0
- 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
- 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线
- 缓解措施:若下一轮继续做整仓 warning reduction先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1184 warning(s)` direct build 观测值
- 当前 worktree 已存在与本批次无关的未提交改动
- 缓解措施:提交当前批次时只暂存 `GFramework.Godot.SourceGenerators.Tests` 与对应 `ai-plan` 文件,避免混入其他 topic 变更
- `GFramework.Game` 当前 `Release` build 仍带有既有 analyzer warning 基线
- 缓解措施:本轮仅验证改动未新增 `UnifiedSettingsDataRepository` / `UnifiedSettingsFile` 相关 warning若继续在该模块做 warning reduction需要另开切片处理现存基线
- 仓库根目录与 `GFramework.Game.Tests``dotnet clean` 目前都无法给出新的 clean 基线
- 缓解措施:后续若继续整仓 warning reduction需要单独定位 clean 失败原因,或明确继续沿用 direct build 观测值作为临时真值
- 当前 worktree 仍存在未跟踪的 `.codex` 目录
- 缓解措施:提交当前批次时只暂存 analyzer-warning-reduction 相关源码与 `ai-plan` 文件,避免把工作目录辅助文件混入提交
- 下一轮若继续深入 `GFramework.Game.Tests`,很可能需要进入 `YamlConfigLoaderTests.cs` 这种高上下文大文件
- 缓解措施:把它单独作为一个明确的新批次处理,不与其它 warning family 混批
- PR 标题检查当前仍显示 `Inconclusive`
- 缓解措施:如需让该检查转绿,需要单独更新 GitHub PR 标题;这不属于本地代码修改范围
## 活跃文档
@ -51,25 +56,31 @@
## 验证说明
- `dotnet clean`
- `dotnet clean GFramework.sln -c Release`
- 结果:失败;停在 solution `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`,未输出更具体的 error 文本
- `dotnet build`
- 结果:成功;`1184 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj`
- 初始结果:成功;`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)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 结果:成功;`Passed: 48``Failed: 0`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`533 Warning(s)``0 Error(s)`;模块仍存在既有 warning 基线,本轮 follow-up 仅处理 PR review 指向的 comparer 契约与测试异步等待一致性
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.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``Failed: 0`
- `dotnet build GFramework.sln -c Release`
- 结果:成功;`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`
- `RP-055` 收尾结果:成功;`63 Warning(s)``0 Error(s)`
- `RP-056` 当前结果:成功;`59 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --no-incremental`
- `RP-057` 热点重排前:成功;`253 Warning(s)``0 Error(s)`
- `RP-057` 当前结果:成功;`249 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`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~GeneratedConfigConsumerIntegrationTests"`
- 结果:成功;`Passed: 4``Failed: 0`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~UnifiedSettingsDataRepository_SaveAsync_When_Persist_Fails_Should_Keep_Cache_Consistent|FullyQualifiedName~UnifiedSettingsDataRepository_DeleteAsync_When_Persist_Fails_Should_Keep_Cache_Consistent"`
- 结果:成功;`Passed: 2``Failed: 0`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
- 结果:成功;`565 Warning(s)``0 Error(s)`
- `dotnet format GFramework.Godot/GFramework.Godot.csproj --verify-no-changes --no-restore --include GFramework.Godot/Scene/SceneBehaviorBase.cs`
- 首次运行失败restore 阶段异常退出,未进入格式验证
- 第二次运行(同命令追加 sandbox 提权成功workspace 仅提示加载 warning无格式差异
## 下一步建议
1. 提交当前 comparer 契约与 `ConfigureAwait(false)` PR follow-up并确认只纳入本 topic 相关文件
2. 视 PR review 反馈决定是否继续收敛 `GFramework.Game` 现有 warning 基线,或返回下一轮整仓 warning 热点筛选
1. 提交 `SceneBehaviorBase.cs``RP-058` tracking/trace 更新,清掉 PR #286 当前 latest-head 上的格式类 review thread
2. 若继续 warning reduction 主线,应回到 `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs`,把它作为独立高上下文批次处理

View File

@ -2,6 +2,163 @@
# Analyzer Warning Reduction 追踪
## 2026-04-24 — RP-058
### 阶段PR #286 latest-head review 格式跟进
- 触发背景:
- 用户要求执行 `$gframework-pr-review`,需要以当前分支 PR 页面而不是本地记忆为准,重新核对 CodeRabbit、MegaLinter 和测试状态
- 抓取脚本当前解析到的 PR 是 `#286`,最新 reviewed commit 为 `2b707343577193fc9904517e6078149653e95698`
- 最新 head 上真正未解决的代码线程只剩 `GFramework.Godot/Scene/SceneBehaviorBase.cs:148` 的缩进问题;其余 nitpick 为可选建议或已留待后续批次
- 主线程实施:
- 运行 `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`,确认 PR `OPEN`、测试 `2156/2156` 通过、MegaLinter 仅剩 `dotnet-format` 警告
- 复核 `SceneBehaviorBase.cs` 后确认 `OnPauseAsync` 的方法签名与方法体缩进异常仍存在于本地源码;同段的 `OnResumeAsync``OnUnloadAsync` 也有同类偏差
- 在不改变行为的前提下统一修正三个方法的缩进,保持现有 XML 注释、`ConfigureAwait(true)` 语义与 Godot 主线程说明不变
- 更新 active tracking / trace记录当前 PR review follow-up 已完成,本地剩余外部信号只剩 PR 标题检查
- 验证里程碑:
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
- 结果:成功;`565 Warning(s)``0 Error(s)`
- 结论:当前格式修复未引入编译错误;模块既有 warning 基线仍存在,但不属于本次 PR review 跟进范围
- `dotnet format GFramework.Godot/GFramework.Godot.csproj --verify-no-changes --no-restore --include GFramework.Godot/Scene/SceneBehaviorBase.cs`
- 首次运行失败sandbox 环境下在 build host / pipe 建立阶段报错,未进入真实格式比较
- 提权复验:成功;仅提示 workspace load warning无格式差异
- 当前结论:
- PR #286 当前 latest-head 上唯一未解决的实质代码 review thread 已在本地修复
- MegaLinter 暴露的 `dotnet-format` 问题已被本地 `verify-no-changes` 复验覆盖
- `Title check: Inconclusive` 仍然存在,但属于 GitHub PR 标题元数据问题,不能通过本地代码提交直接消除
## 2026-04-24 — RP-057
### 阶段:清理 `PersistenceTests.cs` 残余 `MA0004`
- 触发背景:
- `RP-056` 提交后重新做非增量热点排序时,`GFramework.Game.Tests` 的剩余测试项目 warning 已明显收敛,只剩 `PersistenceTests.cs` 少量 `MA0004``YamlConfigLoaderTests.cs` 大量 warning
- 为避免在同一轮直接进入 `YamlConfigLoaderTests.cs` 的大文件高上下文批次,先吃掉 `PersistenceTests.cs` 这个独立小切片
- 主线程实施:
- 在 `PersistenceTests.cs` 中为统一设置仓库失败缓存一致性相关测试补齐剩余 `.ConfigureAwait(false)`
- 覆盖保存失败与删除失败两个测试场景中的缓存读取、存在性检查、后续保存和最终验证读取
- 更新 active tracking / trace明确下一批若继续推进应单独进入 `YamlConfigLoaderTests.cs`
- 验证里程碑:
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --no-incremental`
- 热点重排前:成功;`253 Warning(s)``0 Error(s)`
- 修复后:成功;`249 Warning(s)``0 Error(s)`
- 结论:`PersistenceTests.cs` 不再出现在 warning 输出中
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~UnifiedSettingsDataRepository_SaveAsync_When_Persist_Fails_Should_Keep_Cache_Consistent|FullyQualifiedName~UnifiedSettingsDataRepository_DeleteAsync_When_Persist_Fails_Should_Keep_Cache_Consistent"`
- 结果:成功;`Passed: 2``Failed: 0`
- 当前结论:
- `PersistenceTests.cs` 的残余 warning 已清零,`GFramework.Game.Tests` 剩余热点几乎全部压缩到了 `YamlConfigLoaderTests.cs`
- 当前工作树投影下,分支体积为 `27` 个文件、`991` 行,仍低于 `$gframework-batch-boot 75`
- 按 batch skill 的低风险边界,这一轮应在提交后收口;下一轮再把 `YamlConfigLoaderTests.cs` 作为单独批次处理
## 2026-04-24 — RP-056
### 阶段:修复 `GeneratedConfigConsumerIntegrationTests` 编译错误并清零该文件 warning
- 触发背景:
- `RP-055` 继续推进时,`GeneratedConfigConsumerIntegrationTests.cs` 在 raw string `invalidYaml` 段落附近出现 `CS8999`,导致 `GFramework.Game.Tests` 暂时无法编译
- 该文件同时仍是项目内少数残留 warning 热点之一,因此适合作为同一批次中的单文件收尾
- 主线程实施:
- 修复 `GeneratedConfigConsumerIntegrationTests.cs` 中损坏的 `CreateMonsterFiles` raw string 与方法边界,恢复文件可编译状态
- 保留并整理上一轮已开始的 `.ConfigureAwait(false)` 与断言 helper 抽取
- 继续将 `AssertGeneratedBindingsLoadResults` 再拆分为 catalog / monster / item 三个辅助方法,清除该文件剩余 `MA0051`
- 更新 active tracking / trace沿用 `merge-base(origin/main, HEAD)` 作为 `$gframework-batch-boot 75` 的唯一 stop-condition 口径
- 验证里程碑:
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 结果:成功;`59 Warning(s)``0 Error(s)`
- 结论:`GeneratedConfigConsumerIntegrationTests.cs` 不再出现在 warning 输出中
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~GeneratedConfigConsumerIntegrationTests"`
- 结果:成功;`Passed: 4``Failed: 0`
- 当前结论:
- `GFramework.Game.Tests` 已从 `RP-055` 收尾时的 `63 warning(s)` 进一步收敛到 `59 warning(s)`
- 当前工作树投影下,分支体积为 `27` 个文件、`943` 行,仍低于 `$gframework-batch-boot 75`
- 后续若继续自动推进,最自然的下一批将进入 `YamlConfigLoaderTests.cs` 这类高上下文大文件
## 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 批次(触发文件数停止阈值)
- 触发背景:
- 用户要求“直接进入下一批”,继续沿 `$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 清理
- 触发背景:
- 用户以 `$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
### 阶段PR review follow-upcomparer 契约 + `ConfigureAwait(false)` 收尾)