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

Fix/analyzer warning reduction batch
This commit is contained in:
gewuyou 2026-04-25 14:35:38 +08:00 committed by GitHub
commit 4ad880c1e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 1078 additions and 983 deletions

View File

@ -174,8 +174,15 @@ internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandl
/// </exception>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}
if (logger is null)
{
throw new ArgumentNullException(nameof(logger));
}
services.AddTransient<INotificationHandler<AdditionalAssemblyNotification>>(_ => CreateHandler());
logger.Debug(

View File

@ -339,7 +339,7 @@ public class ArchitectureContextTests
{
workersReady.Signal();
startGate.Wait();
return await context.SendRequestAsync(new TestCqrsRequest());
return await context.SendRequestAsync(new TestCqrsRequest()).ConfigureAwait(false);
}))
.ToArray();

View File

@ -95,14 +95,14 @@ public class ArchitectureLifecycleBehaviorTests
/// 验证用户初始化失败时,等待 Ready 的任务会失败并进入 FailedInitialization 阶段。
/// </summary>
[Test]
public async Task InitializeAsync_When_OnInitialize_Throws_Should_Mark_FailedInitialization()
public void InitializeAsync_When_OnInitialize_Throws_Should_Mark_FailedInitialization()
{
var architecture = new PhaseTrackingArchitecture(() => throw new InvalidOperationException("boom"));
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await architecture.InitializeAsync());
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => architecture.InitializeAsync());
Assert.That(exception, Is.Not.Null);
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.FailedInitialization));
Assert.ThrowsAsync<InvalidOperationException>(async () => await architecture.WaitUntilReadyAsync());
Assert.ThrowsAsync<InvalidOperationException>(() => architecture.WaitUntilReadyAsync());
}
/// <summary>
@ -139,7 +139,7 @@ public class ArchitectureLifecycleBehaviorTests
var destroyOrder = new List<string>();
var architecture = new FailingInitializationArchitecture(destroyOrder);
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await architecture.InitializeAsync());
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => architecture.InitializeAsync());
Assert.That(exception, Is.Not.Null);
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.FailedInitialization));

View File

@ -351,12 +351,12 @@ public class TestArchitectureContextV3 : IArchitectureContext
public ValueTask<TResponse> SendRequestAsync<TResponse>(IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -366,12 +366,12 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -380,10 +380,10 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <returns>命令响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -393,12 +393,12 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <param name="query">要发送的查询。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>查询结果任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendQueryAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -407,35 +407,35 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <typeparam name="TResponse">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param>
/// <returns>查询结果。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public ValueTask PublishAsync<TNotification>(TNotification notification,
CancellationToken cancellationToken = default) where TNotification : INotification
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public ValueTask SendAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
where TCommand : IRequest<Unit>
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void SendCommand(ICommand command)

View File

@ -401,11 +401,11 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="request">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendRequestAsync<TResponse>(IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -414,10 +414,10 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="request">要发送的请求。</param>
/// <returns>请求响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -427,12 +427,12 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -441,10 +441,10 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <returns>命令响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -454,12 +454,12 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="query">要发送的查询。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>查询结果任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendQueryAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -468,10 +468,10 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param>
/// <returns>查询结果。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -481,11 +481,11 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="notification">要发布的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>通知发布任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask PublishAsync<TNotification>(TNotification notification,
CancellationToken cancellationToken = default) where TNotification : INotification
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -495,12 +495,12 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="request">流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步响应流。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -510,11 +510,11 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令发送任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask SendAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
where TCommand : IRequest<Unit>
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -524,11 +524,11 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="command">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>

View File

@ -168,52 +168,52 @@ public class TestArchitectureWithRegistry : IArchitecture
T IArchitecture.RegisterSystem<T>(T system)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
T IArchitecture.RegisterModel<T>(T model)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
T IArchitecture.RegisterUtility<T>(T utility)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
/// <exception cref="NotSupportedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
/// <exception cref="NotSupportedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public IArchitectureModule InstallModule(IArchitectureModule module)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
IArchitectureLifecycleHook IArchitecture.RegisterLifecycleHook(IArchitectureLifecycleHook hook)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
Task IArchitecture.WaitUntilReadyAsync()
@ -223,17 +223,17 @@ public class TestArchitectureWithRegistry : IArchitecture
public void RegisterUtility<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, IUtility
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterModel<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, IModel
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterSystem<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, ISystem
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void Initialize()
@ -242,7 +242,7 @@ public class TestArchitectureWithRegistry : IArchitecture
public void Destroy()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
Task IAsyncInitializable.InitializeAsync()
@ -257,7 +257,7 @@ public class TestArchitectureWithRegistry : IArchitecture
public Task WaitUntilReadyAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterLifecycleHook(IArchitectureLifecycleHook hook)
@ -266,12 +266,12 @@ public class TestArchitectureWithRegistry : IArchitecture
public Task InitializeAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public ValueTask DestroyAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
}
@ -287,7 +287,7 @@ public class TestArchitectureContextWithRegistry : TestArchitectureContext
_registry = registry;
}
public override TUtility GetUtility<TUtility>()
public override TUtility? GetUtility<TUtility>() where TUtility : class
{
if (typeof(TUtility) == typeof(TestRegistry))
{
@ -313,72 +313,72 @@ public class TestArchitectureWithoutRegistry : IArchitecture
T IArchitecture.RegisterSystem<T>(T system)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
T IArchitecture.RegisterModel<T>(T model)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
T IArchitecture.RegisterUtility<T>(T utility)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
/// <exception cref="NotSupportedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
/// <exception cref="NotSupportedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public IArchitectureModule InstallModule(IArchitectureModule module)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
IArchitectureLifecycleHook IArchitecture.RegisterLifecycleHook(IArchitectureLifecycleHook hook)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public Task WaitUntilReadyAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterUtility<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, IUtility
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterModel<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, IModel
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterSystem<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, ISystem
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void Initialize()
@ -387,17 +387,17 @@ public class TestArchitectureWithoutRegistry : IArchitecture
public Task InitializeAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public ValueTask DestroyAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void Destroy()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterLifecycleHook(IArchitectureLifecycleHook hook)

View File

@ -110,7 +110,7 @@ public class AbstractAsyncCommandTests
var command = new TestAsyncCommandWithExceptionV3(input);
var asyncCommand = (IAsyncCommand)command;
Assert.ThrowsAsync<InvalidOperationException>(async () => await asyncCommand.ExecuteAsync());
Assert.ThrowsAsync<InvalidOperationException>(() => asyncCommand.ExecuteAsync());
}
/// <summary>

View File

@ -94,7 +94,7 @@ public class CommandExecutorTests
[Test]
public void SendAsync_WithNullCommand_Should_ThrowArgumentNullException()
{
Assert.ThrowsAsync<ArgumentNullException>(async () => await _commandExecutor.SendAsync(null!));
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync(null!));
}
/// <summary>
@ -118,7 +118,7 @@ public class CommandExecutorTests
[Test]
public void SendAsync_WithResult_AndNullCommand_Should_ThrowArgumentNullException()
{
Assert.ThrowsAsync<ArgumentNullException>(async () => await _commandExecutor.SendAsync<int>(null!));
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync<int>(null!));
}
}

View File

@ -25,7 +25,7 @@ public sealed class AsyncKeyLockManagerTests
using var manager = new AsyncKeyLockManager();
// Act
await using var handle = await manager.AcquireLockAsync("test-key");
await using var handle = await manager.AcquireLockAsync("test-key").ConfigureAwait(false);
// Assert
Assert.That(handle, Is.Not.Null);
@ -47,13 +47,13 @@ public sealed class AsyncKeyLockManagerTests
var index = i;
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync("same-key");
await using var handle = await manager.AcquireLockAsync("same-key").ConfigureAwait(false);
executionOrder.Add(index);
await Task.Delay(10);
await Task.Delay(10).ConfigureAwait(false);
}));
}
await Task.WhenAll(tasks);
await Task.WhenAll(tasks).ConfigureAwait(false);
// Assert
Assert.That(executionOrder.Count, Is.EqualTo(5));
@ -75,15 +75,15 @@ public sealed class AsyncKeyLockManagerTests
var key = $"key-{i}";
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync(key);
await using var handle = await manager.AcquireLockAsync(key).ConfigureAwait(false);
var current = Interlocked.Increment(ref concurrentCount);
maxConcurrent = Math.Max(maxConcurrent, current);
await Task.Delay(50);
await Task.Delay(50).ConfigureAwait(false);
Interlocked.Decrement(ref concurrentCount);
}));
}
await Task.WhenAll(tasks);
await Task.WhenAll(tasks).ConfigureAwait(false);
// Assert
Assert.That(maxConcurrent, Is.GreaterThan(1));
@ -94,13 +94,13 @@ public sealed class AsyncKeyLockManagerTests
{
// Arrange
using var manager = new AsyncKeyLockManager();
var handle = await manager.AcquireLockAsync("test-key");
var handle = await manager.AcquireLockAsync("test-key").ConfigureAwait(false);
// Act
await handle.DisposeAsync();
await handle.DisposeAsync().ConfigureAwait(false);
// Assert - 应该能再次获取锁
await using var handle2 = await manager.AcquireLockAsync("test-key");
await using var handle2 = await manager.AcquireLockAsync("test-key").ConfigureAwait(false);
Assert.That(handle2, Is.Not.Null);
}
@ -117,13 +117,13 @@ public sealed class AsyncKeyLockManagerTests
var key = $"key-{i % 10}";
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync(key);
await Task.Delay(1);
await using var handle = await manager.AcquireLockAsync(key).ConfigureAwait(false);
await Task.Delay(1).ConfigureAwait(false);
}));
}
// Assert
Assert.DoesNotThrowAsync(async () => await Task.WhenAll(tasks));
await Task.WhenAll(tasks).ConfigureAwait(false);
}
[Test]
@ -139,14 +139,14 @@ public sealed class AsyncKeyLockManagerTests
{
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync("same-key");
await using var handle = await manager.AcquireLockAsync("same-key").ConfigureAwait(false);
var temp = counter;
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
counter = temp + 1;
}));
}
await Task.WhenAll(tasks);
await Task.WhenAll(tasks).ConfigureAwait(false);
// Assert
Assert.That(counter, Is.EqualTo(100));
@ -161,13 +161,13 @@ public sealed class AsyncKeyLockManagerTests
lockTimeout: TimeSpan.FromMilliseconds(200));
// Act
await using (var handle = await manager.AcquireLockAsync("temp-key"))
await using (var handle = await manager.AcquireLockAsync("temp-key").ConfigureAwait(false))
{
// 持有锁
}
// 等待清理
await Task.Delay(400);
await Task.Delay(400).ConfigureAwait(false);
var stats = manager.GetStatistics();
@ -184,10 +184,10 @@ public sealed class AsyncKeyLockManagerTests
lockTimeout: TimeSpan.FromMilliseconds(200));
// Act
await using var handle = await manager.AcquireLockAsync("active-key");
await using var handle = await manager.AcquireLockAsync("active-key").ConfigureAwait(false);
// 等待清理尝试
await Task.Delay(400);
await Task.Delay(400).ConfigureAwait(false);
var activeLocks = manager.GetActiveLocks();
@ -202,9 +202,9 @@ public sealed class AsyncKeyLockManagerTests
using var manager = new AsyncKeyLockManager();
// Act
await using (await manager.AcquireLockAsync("key1"))
await using (await manager.AcquireLockAsync("key1").ConfigureAwait(false))
{
await using var handle2 = await manager.AcquireLockAsync("key2");
await using var handle2 = await manager.AcquireLockAsync("key2").ConfigureAwait(false);
var stats = manager.GetStatistics();
// Assert
@ -223,8 +223,8 @@ public sealed class AsyncKeyLockManagerTests
using var manager = new AsyncKeyLockManager();
// Act
await using var handle1 = await manager.AcquireLockAsync("key1");
await using var handle2 = await manager.AcquireLockAsync("key2");
await using var handle1 = await manager.AcquireLockAsync("key1").ConfigureAwait(false);
await using var handle2 = await manager.AcquireLockAsync("key2").ConfigureAwait(false);
var activeLocks = manager.GetActiveLocks();
@ -243,7 +243,7 @@ public sealed class AsyncKeyLockManagerTests
manager.Dispose();
// Act & Assert
Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.AcquireLockAsync("test-key"));
Assert.ThrowsAsync<ObjectDisposedException>(() => manager.AcquireLockAsync("test-key").AsTask());
}
[Test]
@ -254,14 +254,14 @@ public sealed class AsyncKeyLockManagerTests
using var cts = new CancellationTokenSource();
// 先获取锁
await using var handle = await manager.AcquireLockAsync("test-key", cts.Token);
await using var handle = await manager.AcquireLockAsync("test-key", cts.Token).ConfigureAwait(false);
// Act
await cts.CancelAsync();
await cts.CancelAsync().ConfigureAwait(false);
// Assert
Assert.CatchAsync<OperationCanceledException>(async () =>
await manager.AcquireLockAsync("test-key", cts.Token));
Assert.CatchAsync<OperationCanceledException>(() =>
manager.AcquireLockAsync("test-key", cts.Token).AsTask());
}
[Test]
@ -295,14 +295,14 @@ public sealed class AsyncKeyLockManagerTests
{
for (var j = 0; j < 10; j++)
{
await using var handle = await manager.AcquireLockAsync($"key-{j % 5}");
await Task.Delay(10);
await using var handle = await manager.AcquireLockAsync($"key-{j % 5}").ConfigureAwait(false);
await Task.Delay(10).ConfigureAwait(false);
}
}));
}
// Assert
Assert.DoesNotThrowAsync(async () => await Task.WhenAll(tasks));
await Task.WhenAll(tasks).ConfigureAwait(false);
}
[Test]
@ -324,14 +324,14 @@ public sealed class AsyncKeyLockManagerTests
{
// Arrange
using var manager = new AsyncKeyLockManager();
var handle = await manager.AcquireLockAsync("test-key");
var handle = await manager.AcquireLockAsync("test-key").ConfigureAwait(false);
// Act
await handle.DisposeAsync();
await handle.DisposeAsync();
await handle.DisposeAsync().ConfigureAwait(false);
await handle.DisposeAsync().ConfigureAwait(false);
handle.Dispose();
// Assert - 不应该抛出异常
Assert.Pass();
}
}
}

View File

@ -104,7 +104,7 @@ public class AsyncOperationTests
op.SetException(expectedException);
Assert.That(async () => await op.Task, Throws.InstanceOf<InvalidOperationException>());
Assert.That(() => op.Task, Throws.InstanceOf<InvalidOperationException>());
}
/// <summary>
@ -339,4 +339,4 @@ public class AsyncOperationTests
Assert.DoesNotThrow(() => op.SetCompleted());
}
}
}

View File

@ -6,6 +6,7 @@ using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Coroutine.Extensions;
using GFramework.Core.Coroutine.Instructions;
using Moq;
using NUnit.Framework;
namespace GFramework.Core.Tests.Coroutine;
@ -18,6 +19,8 @@ namespace GFramework.Core.Tests.Coroutine;
[TestFixture]
public class CommandCoroutineExtensionsTests
{
private static readonly TimeSpan WaitForTaskTimeout = TimeSpan.FromSeconds(1);
/// <summary>
/// 测试用的简单命令类
/// </summary>
@ -83,15 +86,10 @@ public class CommandCoroutineExtensionsTests
var coroutine = contextAware.SendCommandCoroutineWithErrorHandler(command, ex => capturedException = ex);
// 迭代协程直到完成
while (coroutine.MoveNext())
{
if (coroutine.Current is WaitForTask waitForTask)
{
// 等待任务完成
await Task.Delay(10);
}
}
Assert.That(coroutine.MoveNext(), Is.True);
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
await WaitForTaskAsync((WaitForTask)coroutine.Current);
Assert.That(coroutine.MoveNext(), Is.False);
Assert.That(capturedException, Is.Null);
}
@ -114,15 +112,10 @@ public class CommandCoroutineExtensionsTests
var coroutine = contextAware.SendCommandCoroutineWithErrorHandler(command, ex => capturedException = ex);
// 迭代协程直到完成
while (coroutine.MoveNext())
{
if (coroutine.Current is WaitForTask waitForTask)
{
// 等待任务完成
await Task.Delay(10);
}
}
Assert.That(coroutine.MoveNext(), Is.True);
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
await WaitForTaskAsync((WaitForTask)coroutine.Current);
Assert.That(coroutine.MoveNext(), Is.False);
Assert.That(capturedException, Is.Not.Null);
// 异常被包装为 AggregateException
@ -148,17 +141,12 @@ public class CommandCoroutineExtensionsTests
var coroutine = contextAware.SendCommandCoroutineWithErrorHandler(command);
// 迭代协程应该抛出异常
Assert.Throws<InvalidOperationException>(() =>
{
while (coroutine.MoveNext())
{
if (coroutine.Current is WaitForTask waitForTask)
{
Task.Delay(10).Wait();
}
}
});
Assert.That(coroutine.MoveNext(), Is.True);
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
Assert.That(
SpinWait.SpinUntil(() => ((WaitForTask)coroutine.Current).IsDone, WaitForTaskTimeout),
Is.True);
Assert.Throws<InvalidOperationException>(() => coroutine.MoveNext());
}
/// <summary>
@ -201,8 +189,9 @@ public class CommandCoroutineExtensionsTests
});
// 启动协程并等待命令执行完成
coroutine.MoveNext(); // 进入命令发送阶段
if (coroutine.Current is WaitForTask waitForTask) await Task.Delay(10); // 等待命令任务完成
Assert.That(coroutine.MoveNext(), Is.True); // 进入命令发送阶段
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
await WaitForTaskAsync((WaitForTask)coroutine.Current);
// 此时协程应该在等待事件
Assert.That(coroutine.MoveNext(), Is.True); // 等待事件阶段
@ -296,15 +285,16 @@ public class CommandCoroutineExtensionsTests
command); // null回调
// 启动协程
coroutine.MoveNext(); // 进入命令发送阶段
if (coroutine.Current is WaitForTask waitForTask) await Task.Delay(10); // 等待命令任务完成
Assert.That(coroutine.MoveNext(), Is.True); // 进入命令发送阶段
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
await WaitForTaskAsync((WaitForTask)coroutine.Current);
// 触发事件
var testEvent = new TestEvent { Data = "TestData" };
eventCallback?.Invoke(testEvent);
// 协程应该能正常完成
Assert.That(() => coroutine.MoveNext(), Throws.Nothing);
Assert.DoesNotThrow(() => coroutine.MoveNext());
}
/// <summary>
@ -340,8 +330,9 @@ public class CommandCoroutineExtensionsTests
_ => { });
// 启动协程 - 命令失败时协程仍然继续
coroutine.MoveNext(); // 进入命令发送阶段
if (coroutine.Current is WaitForTask waitForTask) await Task.Delay(10); // 等待命令任务完成
Assert.That(coroutine.MoveNext(), Is.True); // 进入命令发送阶段
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
await WaitForTaskAsync((WaitForTask)coroutine.Current);
// 命令执行失败后,协程继续执行
Assert.Pass();
@ -441,4 +432,18 @@ public class CommandCoroutineExtensionsTests
// 调用 MoveNext 时应该抛出 InvalidOperationException
Assert.Throws<InvalidOperationException>(() => coroutine.MoveNext());
}
}
private static async Task WaitForTaskAsync(WaitForTask waitForTask)
{
var timeoutAt = DateTime.UtcNow + WaitForTaskTimeout;
// 协程通过轮询 IsDone 观察异步命令完成,这里保持相同语义但避免固定延时。
while (!waitForTask.IsDone)
{
if (DateTime.UtcNow >= timeoutAt)
Assert.Fail("WaitForTask did not complete within the expected time.");
await Task.Yield();
}
}
}

View File

@ -254,7 +254,7 @@ public class QueryCoroutineExtensionsTests
{
Name = "ComplexName",
Values = new List<int> { 1, 2, 3 },
Metadata = new Dictionary<string, object> { { "key", "value" } }
Metadata = new Dictionary<string, object>(StringComparer.Ordinal) { { "key", "value" } }
};
ComplexResult? receivedResult = null;
@ -362,7 +362,7 @@ internal class ComplexQuery : IQuery<ComplexResult>
private IArchitectureContext? _context;
public string Name { get; set; } = string.Empty;
public List<int> Values { get; set; } = new();
public Dictionary<string, object> Metadata { get; set; } = new();
public Dictionary<string, object> Metadata { get; set; } = new(StringComparer.Ordinal);
public void SetContext(IArchitectureContext context)
{
@ -388,4 +388,4 @@ internal class ComplexResult
public string ProcessedName { get; set; } = string.Empty;
public int Sum { get; set; }
public int Count { get; set; }
}
}

View File

@ -66,8 +66,6 @@ public class TaskCoroutineExtensionsTests
var task = Task.FromResult(42);
var instruction = task.AsCoroutineInstruction();
task.Wait();
Assert.That(instruction.Result, Is.EqualTo(42));
}
@ -165,7 +163,7 @@ public class TaskCoroutineExtensionsTests
Assert.That(completed, Is.False);
tcs.SetResult(null);
Task.Delay(50).Wait();
Task.Delay(50).ConfigureAwait(false).GetAwaiter().GetResult();
scheduler.Update();
scheduler.Update();
@ -189,7 +187,7 @@ public class TaskCoroutineExtensionsTests
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1));
tcs.SetResult(42);
Task.Delay(50).Wait();
Task.Delay(50).ConfigureAwait(false).GetAwaiter().GetResult();
scheduler.Update();
scheduler.Update();
@ -265,7 +263,7 @@ public class TaskCoroutineExtensionsTests
tcs.SetResult(null);
tcs2.SetResult(42);
Task.Delay(50).Wait();
Task.Delay(50).ConfigureAwait(false).GetAwaiter().GetResult();
scheduler.Update();
scheduler.Update();
@ -286,4 +284,4 @@ public class TaskCoroutineExtensionsTests
CurrentTime += DeltaTime;
}
}
}
}

View File

@ -14,7 +14,7 @@ namespace GFramework.Core.Tests.Coroutine
}
[Test]
public async Task Constructor_WithCompletedTask_IsDoneImmediately()
public void Constructor_WithCompletedTask_IsDoneImmediately()
{
// Arrange
var completedTask = Task.FromResult("test");
@ -28,7 +28,7 @@ namespace GFramework.Core.Tests.Coroutine
}
[Test]
public async Task Constructor_WithIncompleteTask_IsNotDoneInitially()
public void Constructor_WithIncompleteTask_IsNotDoneInitially()
{
// Arrange
var tcs = new TaskCompletionSource<string>();
@ -54,7 +54,7 @@ namespace GFramework.Core.Tests.Coroutine
// Act
tcs.SetResult("completed");
await Task.Delay(10); // Allow time for continuation
await Task.Delay(10).ConfigureAwait(false); // Allow time for continuation
// Assert final state
Assert.That(waitForTask.IsDone, Is.True);
@ -62,7 +62,7 @@ namespace GFramework.Core.Tests.Coroutine
}
[Test]
public async Task Update_DoesNotChangeState()
public void Update_DoesNotChangeState()
{
// Arrange
var completedTask = Task.FromResult("test");
@ -85,7 +85,7 @@ namespace GFramework.Core.Tests.Coroutine
// Act
tcs.SetException(new InvalidOperationException("Test exception"));
await Task.Delay(10); // Allow time for continuation
await Task.Delay(10).ConfigureAwait(false); // Allow time for continuation
// Assert
Assert.That(waitForTask.IsDone, Is.True);
@ -93,4 +93,4 @@ namespace GFramework.Core.Tests.Coroutine
Assert.That(waitForTask.Exception?.InnerException, Is.TypeOf<InvalidOperationException>());
}
}
}
}

View File

@ -284,15 +284,14 @@ public class WaitForTaskTests
var expectedValue = 123;
var task = Task.Run(async () =>
{
await Task.Delay(50);
await Task.Delay(50).ConfigureAwait(false);
return expectedValue;
});
var wait = new WaitForTask<int>(task);
await task;
Task.Delay(100).Wait();
await task.ConfigureAwait(false);
await Task.Delay(100).ConfigureAwait(false);
Assert.That(wait.IsDone, Is.True);
Assert.That(wait.Result, Is.EqualTo(expectedValue));
@ -313,4 +312,4 @@ public class WaitForTaskTests
Assert.That(wait.IsDone, Is.True);
}
}
}

View File

@ -33,11 +33,11 @@ public class AsyncExtensionsTests
public void WithTimeout_Should_Throw_TimeoutException_When_Task_Exceeds_Timeout()
{
// Act & Assert
Assert.ThrowsAsync<TimeoutException>(async () =>
await AsyncExtensions.WithTimeoutAsync(
Assert.ThrowsAsync<TimeoutException>(() =>
AsyncExtensions.WithTimeoutAsync(
async ct =>
{
await Task.Delay(TimeSpan.FromSeconds(2), ct);
await Task.Delay(TimeSpan.FromSeconds(2), ct).ConfigureAwait(false);
return 42;
},
TimeSpan.FromMilliseconds(100)));
@ -53,11 +53,11 @@ public class AsyncExtensionsTests
using var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
Assert.ThrowsAsync<TaskCanceledException>(async () =>
await AsyncExtensions.WithTimeoutAsync(
Assert.ThrowsAsync<TaskCanceledException>(() =>
AsyncExtensions.WithTimeoutAsync(
async ct =>
{
await Task.Delay(TimeSpan.FromSeconds(2), ct);
await Task.Delay(TimeSpan.FromSeconds(2), ct).ConfigureAwait(false);
return 42;
},
TimeSpan.FromSeconds(1),
@ -74,13 +74,13 @@ public class AsyncExtensionsTests
var innerTaskCanceled = false;
// Act & Assert
Assert.ThrowsAsync<TimeoutException>(async () =>
await AsyncExtensions.WithTimeoutAsync(
Assert.ThrowsAsync<TimeoutException>(() =>
AsyncExtensions.WithTimeoutAsync(
async ct =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(5), ct);
await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false);
return 0;
}
catch (OperationCanceledException)
@ -121,8 +121,8 @@ public class AsyncExtensionsTests
public void WithTimeout_NoResult_Should_Throw_TimeoutException_When_Task_Exceeds_Timeout()
{
// Act & Assert
Assert.ThrowsAsync<TimeoutException>(async () =>
await AsyncExtensions.WithTimeoutAsync(
Assert.ThrowsAsync<TimeoutException>(() =>
AsyncExtensions.WithTimeoutAsync(
ct => Task.Delay(TimeSpan.FromSeconds(2), ct),
TimeSpan.FromMilliseconds(100)));
}
@ -137,13 +137,13 @@ public class AsyncExtensionsTests
var innerTaskCanceled = false;
// Act & Assert
Assert.ThrowsAsync<TimeoutException>(async () =>
await AsyncExtensions.WithTimeoutAsync(
Assert.ThrowsAsync<TimeoutException>(() =>
AsyncExtensions.WithTimeoutAsync(
async ct =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(5), ct);
await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@ -217,8 +217,8 @@ public class AsyncExtensionsTests
};
// Act & Assert
Assert.ThrowsAsync<AggregateException>(async () =>
await taskFactory.WithRetryAsync(2, TimeSpan.FromMilliseconds(10)));
Assert.ThrowsAsync<AggregateException>(() =>
taskFactory.WithRetryAsync(2, TimeSpan.FromMilliseconds(10)));
}
/// <summary>
@ -236,11 +236,11 @@ public class AsyncExtensionsTests
};
// Act & Assert
Assert.ThrowsAsync<AggregateException>(async () =>
await taskFactory.WithRetryAsync(3, TimeSpan.FromMilliseconds(10),
Assert.ThrowsAsync<AggregateException>(() =>
taskFactory.WithRetryAsync(3, TimeSpan.FromMilliseconds(10),
ex => ex is not ArgumentException));
await Task.Delay(50); // 等待任务完成
await Task.Delay(50).ConfigureAwait(false); // 等待任务完成
Assert.That(attemptCount, Is.EqualTo(1)); // 不应该重试
}
@ -330,4 +330,4 @@ public class AsyncExtensionsTests
// Assert
Assert.That(capturedEx, Is.SameAs(expectedException));
}
}
}

View File

@ -173,7 +173,7 @@ public class CollectionExtensionsTests
{
var method = typeof(GFramework.Core.Extensions.CollectionExtensions)
.GetMethods()
.Single(static method => method.Name == nameof(GFramework.Core.Extensions.CollectionExtensions.ToDictionarySafe));
.Single(static method => string.Equals(method.Name, nameof(GFramework.Core.Extensions.CollectionExtensions.ToDictionarySafe), StringComparison.Ordinal));
var methodGenericArguments = method.GetGenericArguments();
var returnTypeGenericArguments = method.ReturnType.GetGenericArguments();

View File

@ -11,6 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Globalization;
using GFramework.Core.Functional;
namespace GFramework.Core.Tests.Extensions;
@ -122,7 +123,7 @@ public class ResultExtensionsTests
public void Map_Should_Transform_Success_Value()
{
var result = Result<int>.Succeed(42);
var mapped = result.Map(x => x.ToString());
var mapped = result.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsSuccess, Is.True);
Assert.That(mapped.Match(succ: v => v, fail: _ => ""), Is.EqualTo("42"));
}
@ -135,7 +136,7 @@ public class ResultExtensionsTests
{
var exception = new Exception("Error");
var result = Result<int>.Fail(exception);
var mapped = result.Map(x => x.ToString());
var mapped = result.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsFaulted, Is.True);
Assert.That(mapped.Exception, Is.SameAs(exception));
}
@ -157,7 +158,7 @@ public class ResultExtensionsTests
public void Bind_Should_Chain_Success_Results()
{
var result = Result<int>.Succeed(42);
var bound = result.Bind(x => Result<string>.Succeed(x.ToString()));
var bound = result.Bind(x => Result<string>.Succeed(x.ToString(CultureInfo.InvariantCulture)));
Assert.That(bound.IsSuccess, Is.True);
Assert.That(bound.Match(succ: v => v, fail: _ => ""), Is.EqualTo("42"));
}
@ -170,7 +171,7 @@ public class ResultExtensionsTests
{
var exception = new Exception("Error");
var result = Result<int>.Fail(exception);
var bound = result.Bind(x => Result<string>.Succeed(x.ToString()));
var bound = result.Bind(x => Result<string>.Succeed(x.ToString(CultureInfo.InvariantCulture)));
Assert.That(bound.IsFaulted, Is.True);
Assert.That(bound.Exception, Is.SameAs(exception));
}
@ -449,9 +450,9 @@ public class ResultExtensionsTests
{
var result = await ResultExtensions.TryAsync(async () =>
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
return 42;
});
}).ConfigureAwait(false);
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Match(succ: v => v, fail: _ => 0), Is.EqualTo(42));
}
@ -464,9 +465,9 @@ public class ResultExtensionsTests
{
var result = await ResultExtensions.TryAsync<int>(async () =>
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
throw new InvalidOperationException("Error");
});
}).ConfigureAwait(false);
Assert.That(result.IsFaulted, Is.True);
Assert.That(result.Exception, Is.TypeOf<InvalidOperationException>());
}
@ -477,7 +478,8 @@ public class ResultExtensionsTests
[Test]
public async Task TryAsync_Should_Handle_Synchronous_Exceptions()
{
var result = await ResultExtensions.TryAsync<int>(() => throw new InvalidOperationException("Sync error"));
var result = await ResultExtensions.TryAsync<int>(() => throw new InvalidOperationException("Sync error"))
.ConfigureAwait(false);
Assert.That(result.IsFaulted, Is.True);
}
@ -487,7 +489,7 @@ public class ResultExtensionsTests
[Test]
public void TryAsync_Should_Throw_ArgumentNullException_When_Function_Is_Null()
{
Assert.ThrowsAsync<ArgumentNullException>(async () => await ResultExtensions.TryAsync<int>(null!));
Assert.ThrowsAsync<ArgumentNullException>(() => ResultExtensions.TryAsync<int>(null!));
}
/// <summary>

View File

@ -11,6 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Globalization;
using GFramework.Core.Functional;
using NUnit.Framework;
@ -122,7 +123,7 @@ public class OptionTests
public void Map_WithSome_Should_Map_Value()
{
var option = Option<int>.Some(42);
var mapped = option.Map(x => x.ToString());
var mapped = option.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsSome, Is.True);
Assert.That(mapped.GetOrElse(""), Is.EqualTo("42"));
}
@ -134,7 +135,7 @@ public class OptionTests
public void Map_WithNone_Should_Return_None()
{
var option = Option<int>.None;
var mapped = option.Map(x => x.ToString());
var mapped = option.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsNone, Is.True);
}
@ -155,7 +156,7 @@ public class OptionTests
public void Bind_WithSome_Should_Bind_Value()
{
var option = Option<string>.Some("42");
var bound = option.Bind(s => int.TryParse(s, out var i)
var bound = option.Bind(s => int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)
? Option<int>.Some(i)
: Option<int>.None);
@ -170,7 +171,7 @@ public class OptionTests
public void Bind_WithSome_Can_Return_None()
{
var option = Option<string>.Some("invalid");
var bound = option.Bind(s => int.TryParse(s, out var i)
var bound = option.Bind(s => int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)
? Option<int>.Some(i)
: Option<int>.None);
@ -505,4 +506,4 @@ public class OptionTests
var result = option.ToString();
Assert.That(result, Is.EqualTo("None"));
}
}
}

View File

@ -1,3 +1,4 @@
using System.Globalization;
using GFramework.Core.Functional.Pipe;
using NUnit.Framework;
@ -123,7 +124,7 @@ public class PipeExtensionsTests
var result = value
.Pipe(x => x * 2)
.Pipe(x => x + 10)
.Pipe(x => x.ToString());
.Pipe(x => x.ToString(CultureInfo.InvariantCulture));
// Assert
Assert.That(result, Is.EqualTo("20"));
@ -139,7 +140,7 @@ public class PipeExtensionsTests
var value = 42;
// Act
var result = value.Let(x => x.ToString());
var result = value.Let(x => x.ToString(CultureInfo.InvariantCulture));
// Assert
Assert.That(result, Is.EqualTo("42"));
@ -171,7 +172,7 @@ public class PipeExtensionsTests
var result = value.Let(s => new
{
Original = s,
Upper = s.ToUpper(),
Upper = s.ToUpperInvariant(),
Length = s.Length
});
@ -280,4 +281,4 @@ public class PipeExtensionsTests
// Assert
Assert.That(result, Is.EqualTo("Large: 20"));
}
}
}

View File

@ -11,6 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Globalization;
using GFramework.Core.Functional;
using NUnit.Framework;
@ -314,7 +315,7 @@ public class ResultTTests
public void Map_Should_Transform_Value_When_Success()
{
var result = Result<int>.Succeed(42);
var mapped = result.Map(x => x.ToString());
var mapped = result.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsSuccess, Is.True);
Assert.That(mapped.Match(succ: v => v, fail: _ => ""), Is.EqualTo("42"));
}
@ -327,7 +328,7 @@ public class ResultTTests
{
var exception = new Exception("Error");
var result = Result<int>.Fail(exception);
var mapped = result.Map(x => x.ToString());
var mapped = result.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsFaulted, Is.True);
Assert.That(mapped.Exception, Is.SameAs(exception));
}
@ -360,7 +361,7 @@ public class ResultTTests
public void Bind_Should_Chain_Success_Results()
{
var result = Result<int>.Succeed(42);
var bound = result.Bind(x => Result<string>.Succeed(x.ToString()));
var bound = result.Bind(x => Result<string>.Succeed(x.ToString(CultureInfo.InvariantCulture)));
Assert.That(bound.IsSuccess, Is.True);
Assert.That(bound.Match(succ: v => v, fail: _ => ""), Is.EqualTo("42"));
}
@ -373,7 +374,7 @@ public class ResultTTests
{
var exception = new Exception("Error");
var result = Result<int>.Fail(exception);
var bound = result.Bind(x => Result<string>.Succeed(x.ToString()));
var bound = result.Bind(x => Result<string>.Succeed(x.ToString(CultureInfo.InvariantCulture)));
Assert.That(bound.IsFaulted, Is.True);
Assert.That(bound.Exception, Is.SameAs(exception));
}
@ -413,9 +414,9 @@ public class ResultTTests
var result = Result<int>.Succeed(42);
var mapped = await result.MapAsync(async x =>
{
await Task.Delay(1);
return x.ToString();
});
await Task.Delay(1).ConfigureAwait(false);
return x.ToString(CultureInfo.InvariantCulture);
}).ConfigureAwait(false);
Assert.That(mapped.IsSuccess, Is.True);
Assert.That(mapped.Match(succ: v => v, fail: _ => ""), Is.EqualTo("42"));
}
@ -430,9 +431,9 @@ public class ResultTTests
var result = Result<int>.Fail(exception);
var mapped = await result.MapAsync(async x =>
{
await Task.Delay(1);
return x.ToString();
});
await Task.Delay(1).ConfigureAwait(false);
return x.ToString(CultureInfo.InvariantCulture);
}).ConfigureAwait(false);
Assert.That(mapped.IsFaulted, Is.True);
Assert.That(mapped.Exception, Is.SameAs(exception));
}
@ -446,9 +447,9 @@ public class ResultTTests
var result = Result<int>.Succeed(42);
var mapped = await result.MapAsync<string>(async _ =>
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
throw new InvalidOperationException("Async error");
});
}).ConfigureAwait(false);
Assert.That(mapped.IsFaulted, Is.True);
Assert.That(mapped.Exception, Is.TypeOf<InvalidOperationException>());
}
@ -550,7 +551,7 @@ public class ResultTTests
public void Equals_Should_Return_False_When_Exception_Types_Differ()
{
var result1 = Result<int>.Fail(new InvalidOperationException("Error"));
var result2 = Result<int>.Fail(new ArgumentException("Error"));
var result2 = Result<int>.Fail(new InvalidCastException("Error"));
Assert.That(result1.Equals(result2), Is.False);
}
@ -804,4 +805,4 @@ public class ResultTTests
var bottom2 = Result<int>.Bottom;
Assert.That(bottom1.Equals(bottom2), Is.True);
}
}
}

View File

@ -329,7 +329,7 @@ public class ResultTests
{
// Arrange
var result1 = Result.Failure(new InvalidOperationException("Error"));
var result2 = Result.Failure(new ArgumentException("Error"));
var result2 = Result.Failure(new InvalidCastException("Error"));
// Act & Assert
Assert.That(result1.Equals(result2), Is.False);

View File

@ -9,7 +9,7 @@ public class LocalizationTableTests
public void GetRawText_ShouldReturnCorrectText()
{
// Arrange
var data = new Dictionary<string, string>
var data = new Dictionary<string, string>(StringComparer.Ordinal)
{
["test.key"] = "Test Value"
};
@ -26,13 +26,13 @@ public class LocalizationTableTests
public void GetRawText_WithFallback_ShouldReturnFallbackValue()
{
// Arrange
var fallbackData = new Dictionary<string, string>
var fallbackData = new Dictionary<string, string>(StringComparer.Ordinal)
{
["test.key"] = "Fallback Value"
};
var fallbackTable = new LocalizationTable("test", "eng", fallbackData);
var data = new Dictionary<string, string>();
var data = new Dictionary<string, string>(StringComparer.Ordinal);
var table = new LocalizationTable("test", "zhs", data, fallbackTable);
// Act
@ -46,7 +46,7 @@ public class LocalizationTableTests
public void ContainsKey_ShouldReturnTrue_WhenKeyExists()
{
// Arrange
var data = new Dictionary<string, string>
var data = new Dictionary<string, string>(StringComparer.Ordinal)
{
["test.key"] = "Test Value"
};
@ -63,13 +63,13 @@ public class LocalizationTableTests
public void Merge_ShouldOverrideExistingValues()
{
// Arrange
var data = new Dictionary<string, string>
var data = new Dictionary<string, string>(StringComparer.Ordinal)
{
["test.key"] = "Original Value"
};
var table = new LocalizationTable("test", "eng", data);
var overrides = new Dictionary<string, string>
var overrides = new Dictionary<string, string>(StringComparer.Ordinal)
{
["test.key"] = "Override Value"
};
@ -81,4 +81,4 @@ public class LocalizationTableTests
// Assert
Assert.That(result, Is.EqualTo("Override Value"));
}
}
}

View File

@ -48,7 +48,7 @@ public class DefaultLogFormatterTests
[Test]
public void Format_WithProperties_ShouldIncludeProperties()
{
var properties = new Dictionary<string, object?>
var properties = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["UserId"] = 12345,
["UserName"] = "TestUser"
@ -66,7 +66,7 @@ public class DefaultLogFormatterTests
[Test]
public void Format_WithNullProperty_ShouldHandleNull()
{
var properties = new Dictionary<string, object?>
var properties = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["Key1"] = null
};
@ -114,4 +114,4 @@ public class DefaultLogFormatterTests
Assert.That(result, Does.Contain(message));
}
}
}

View File

@ -54,7 +54,7 @@ public class JsonLogFormatterTests
[Test]
public void Format_WithProperties_ShouldIncludePropertiesObject()
{
var properties = new Dictionary<string, object?>
var properties = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["UserId"] = 12345,
["UserName"] = "TestUser",
@ -93,7 +93,7 @@ public class JsonLogFormatterTests
[Test]
public void Format_WithNullProperty_ShouldHandleNull()
{
var properties = new Dictionary<string, object?>
var properties = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["Key1"] = null,
["Key2"] = "value"
@ -170,7 +170,7 @@ public class JsonLogFormatterTests
[Test]
public void Format_WithComplexProperties_ShouldSerializeCorrectly()
{
var properties = new Dictionary<string, object?>
var properties = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["Number"] = 123,
["String"] = "test",
@ -184,4 +184,4 @@ public class JsonLogFormatterTests
Assert.That(() => JsonDocument.Parse(result), Throws.Nothing);
}
}
}

View File

@ -152,22 +152,22 @@ public class LogContextTests
var task1Values = new List<object?>();
var task2Values = new List<object?>();
var task1 = Task.Run(() =>
var task1 = Task.Run(async () =>
{
using (LogContext.Push("TaskId", "Task1"))
{
task1Values.Add(LogContext.Current["TaskId"]);
Task.Delay(50).Wait();
await Task.Delay(50);
task1Values.Add(LogContext.Current["TaskId"]);
}
});
var task2 = Task.Run(() =>
var task2 = Task.Run(async () =>
{
using (LogContext.Push("TaskId", "Task2"))
{
task2Values.Add(LogContext.Current["TaskId"]);
Task.Delay(50).Wait();
await Task.Delay(50);
task2Values.Add(LogContext.Current["TaskId"]);
}
});
@ -201,4 +201,4 @@ public class LogContextTests
Assert.That(LogContext.Current.Count, Is.EqualTo(0));
}
}
}

View File

@ -13,7 +13,7 @@ public class LogEntryTests
public void Constructor_WithAllParameters_ShouldCreateEntry()
{
var timestamp = DateTime.UtcNow;
var properties = new Dictionary<string, object?> { ["Key1"] = "Value1" };
var properties = new Dictionary<string, object?>(StringComparer.Ordinal) { ["Key1"] = "Value1" };
var exception = new InvalidOperationException("Test");
var entry = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", exception, properties);
@ -63,7 +63,7 @@ public class LogEntryTests
public void GetAllProperties_WithProperties_ShouldReturnOnlyProperties()
{
LogContext.Clear();
var properties = new Dictionary<string, object?> { ["PropKey"] = "PropValue" };
var properties = new Dictionary<string, object?>(StringComparer.Ordinal) { ["PropKey"] = "PropValue" };
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Test message", null, properties);
var allProps = entry.GetAllProperties();
@ -78,7 +78,7 @@ public class LogEntryTests
LogContext.Clear();
using (LogContext.Push("ContextKey", "ContextValue"))
{
var properties = new Dictionary<string, object?> { ["PropKey"] = "PropValue" };
var properties = new Dictionary<string, object?>(StringComparer.Ordinal) { ["PropKey"] = "PropValue" };
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Test message", null, properties);
var allProps = entry.GetAllProperties();
@ -97,7 +97,7 @@ public class LogEntryTests
LogContext.Clear();
using (LogContext.Push("Key1", "ContextValue"))
{
var properties = new Dictionary<string, object?> { ["Key1"] = "PropValue" };
var properties = new Dictionary<string, object?>(StringComparer.Ordinal) { ["Key1"] = "PropValue" };
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Test message", null, properties);
var allProps = entry.GetAllProperties();
@ -124,7 +124,7 @@ public class LogEntryTests
public void RecordEquality_WithSameValues_ShouldBeEqual()
{
var timestamp = DateTime.UtcNow;
var properties = new Dictionary<string, object?> { ["Key1"] = "Value1" };
var properties = new Dictionary<string, object?>(StringComparer.Ordinal) { ["Key1"] = "Value1" };
var entry1 = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", null, properties);
var entry2 = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", null, properties);
@ -142,4 +142,4 @@ public class LogEntryTests
Assert.That(entry1, Is.Not.EqualTo(entry2));
}
}
}

View File

@ -68,7 +68,7 @@ public class RollingFileAppenderTests
}
// 检查是否生成了多个文件
var files = Directory.GetFiles(_testDir, "*.log").OrderBy(f => f).ToArray();
var files = Directory.GetFiles(_testDir, "*.log");
Assert.That(files.Length, Is.GreaterThan(1));
}
@ -108,13 +108,21 @@ public class RollingFileAppenderTests
appender.Flush();
}
var files = Directory.GetFiles(_testDir, "*.log").Select(Path.GetFileName).OrderBy(f => f).ToArray();
var files = Directory.GetFiles(_testDir, "*.log")
.Select(static path => Path.GetFileName(path) ?? string.Empty)
.OrderBy(f => f, System.StringComparer.Ordinal)
.ToArray();
// 应该有 app.log, app.1.log, app.2.log 等
Assert.That(files, Does.Contain("app.log"));
if (files.Length > 1)
{
Assert.That(files.Any(f => f.StartsWith("app.") && f.EndsWith(".log") && f != "app.log"), Is.True);
Assert.That(
files.Any(f =>
f.StartsWith("app.", System.StringComparison.Ordinal) &&
f.EndsWith(".log", System.StringComparison.Ordinal) &&
!string.Equals(f, "app.log", System.StringComparison.Ordinal)),
Is.True);
}
}
@ -163,4 +171,4 @@ public class RollingFileAppenderTests
var content = File.ReadAllText(_testFilePath);
Assert.That(content, Does.Contain("Test message"));
}
}
}

View File

@ -20,7 +20,7 @@ public sealed class AsyncTestModel : AbstractModel, IAsyncInitializable
/// <returns>表示异步操作的Task</returns>
public async Task InitializeAsync()
{
await Task.Delay(10);
await Task.Delay(10).ConfigureAwait(false);
Initialized = true;
}
@ -45,4 +45,4 @@ public sealed class AsyncTestModel : AbstractModel, IAsyncInitializable
protected override void OnInit()
{
}
}
}

View File

@ -22,9 +22,9 @@ public class PauseStackManagerTests
/// 在每个测试方法执行后清理资源
/// </summary>
[TearDown]
public void TearDown()
public async Task TearDown()
{
_manager.DestroyAsync();
await _manager.DestroyAsync().ConfigureAwait(false);
}
private PauseStackManager _manager = null!;
@ -416,7 +416,7 @@ public class PauseStackManagerTests
_manager.Push("Gameplay", PauseGroup.Gameplay);
mockHandler.Reset();
await _manager.DestroyAsync();
await _manager.DestroyAsync().ConfigureAwait(false);
Assert.That(mockHandler.CallCount, Is.EqualTo(2));
Assert.That(mockHandler.LastIsPaused, Is.False);

View File

@ -80,7 +80,7 @@ public class AbstractAsyncQueryTests
var query = new TestAsyncQueryWithExceptionV4(input);
var asyncQuery = (IAsyncQuery<int>)query;
Assert.ThrowsAsync<InvalidOperationException>(async () => await asyncQuery.DoAsync());
Assert.ThrowsAsync<InvalidOperationException>(() => asyncQuery.DoAsync());
}
/// <summary>

View File

@ -44,7 +44,7 @@ public class AsyncQueryExecutorTests
[Test]
public void SendAsync_WithNullQuery_Should_ThrowArgumentNullException()
{
Assert.ThrowsAsync<ArgumentNullException>(async () => await _asyncQueryExecutor.SendAsync<int>(null!));
Assert.ThrowsAsync<ArgumentNullException>(() => _asyncQueryExecutor.SendAsync<int>(null!));
}
/// <summary>
@ -100,7 +100,7 @@ public class AsyncQueryExecutorTests
var input = new TestAsyncQueryInput { Value = 0 };
var query = new TestAsyncQueryWithException(input);
Assert.ThrowsAsync<InvalidOperationException>(async () => await _asyncQueryExecutor.SendAsync(query));
Assert.ThrowsAsync<InvalidOperationException>(() => _asyncQueryExecutor.SendAsync(query));
}
/// <summary>

View File

@ -33,7 +33,7 @@ public class TestResourceLoader : IResourceLoader<TestResource>
public async Task<TestResource> LoadAsync(string path)
{
await Task.Delay(10); // 模拟异步加载
await Task.Delay(10).ConfigureAwait(false); // 模拟异步加载
return Load(path);
}
@ -99,7 +99,7 @@ public class ResourceManagerTests
[Test]
public async Task LoadAsync_Should_Load_Resource()
{
var resource = await _resourceManager.LoadAsync<TestResource>("test/resource1.txt");
var resource = await _resourceManager.LoadAsync<TestResource>("test/resource1.txt").ConfigureAwait(false);
Assert.That(resource, Is.Not.Null);
Assert.That(resource!.Content, Is.EqualTo("Content 1"));
@ -404,4 +404,4 @@ public class ResourceManagerTests
handle2!.Dispose();
Assert.That(_resourceManager.IsLoaded("test/resource2.txt"), Is.False);
}
}
}

View File

@ -93,7 +93,7 @@ public class ContextAwareEnvironmentExtensionsTests
return false;
}
public T GetRequired<T>(string key) where T : class => throw new NotImplementedException();
public T GetRequired<T>(string key) where T : class => throw new NotSupportedException();
public void Register(string key, object value)
{
@ -115,7 +115,7 @@ public class ContextAwareEnvironmentExtensionsTests
return false;
}
public T GetRequired<T>(string key) where T : class => throw new NotImplementedException();
public T GetRequired<T>(string key) where T : class => throw new NotSupportedException();
public void Register(string key, object value)
{
@ -129,4 +129,4 @@ public class ContextAwareEnvironmentExtensionsTests
private class TestContextAware : ContextAwareBase
{
}
}
}

View File

@ -181,8 +181,8 @@ public class ContextAwareServiceExtensionsTests
// Assert
Assert.That(results, Has.Count.GreaterThanOrEqualTo(2));
Assert.That(results.Any(s => s is TestSystem ts && ts.Name == "System1"), Is.True);
Assert.That(results.Any(s => s is TestSystem ts && ts.Name == "System2"), Is.True);
Assert.That(results.Any(s => s is TestSystem ts && string.Equals(ts.Name, "System1", System.StringComparison.Ordinal)), Is.True);
Assert.That(results.Any(s => s is TestSystem ts && string.Equals(ts.Name, "System2", System.StringComparison.Ordinal)), Is.True);
}
[Test]
@ -200,8 +200,8 @@ public class ContextAwareServiceExtensionsTests
// Assert
Assert.That(results, Has.Count.EqualTo(2));
Assert.That(results.Any(m => m.Name == "Model1"), Is.True);
Assert.That(results.Any(m => m.Name == "Model2"), Is.True);
Assert.That(results.Any(m => string.Equals(m.Name, "Model1", System.StringComparison.Ordinal)), Is.True);
Assert.That(results.Any(m => string.Equals(m.Name, "Model2", System.StringComparison.Ordinal)), Is.True);
}
[Test]
@ -219,8 +219,8 @@ public class ContextAwareServiceExtensionsTests
// Assert
Assert.That(results, Has.Count.EqualTo(2));
Assert.That(results.Any(u => u.Name == "Utility1"), Is.True);
Assert.That(results.Any(u => u.Name == "Utility2"), Is.True);
Assert.That(results.Any(u => string.Equals(u.Name, "Utility1", System.StringComparison.Ordinal)), Is.True);
Assert.That(results.Any(u => string.Equals(u.Name, "Utility2", System.StringComparison.Ordinal)), Is.True);
}
[Test]
@ -304,4 +304,4 @@ public class ContextAwareServiceExtensionsTests
private class TestContextAware : ContextAwareBase
{
}
}
}

View File

@ -136,9 +136,9 @@ public class StateMachineSystemTests
/// 测试DestroyAsync方法不抛出异常
/// </summary>
[Test]
public async Task DestroyAsync_Should_Not_Throw_Exception()
public void DestroyAsync_Should_Not_Throw_Exception()
{
Assert.That(async () => await _stateMachine!.DestroyAsync(), Throws.Nothing);
Assert.That(() => _stateMachine!.DestroyAsync(), Throws.Nothing);
}
/// <summary>

View File

@ -216,7 +216,7 @@ public class StateMachineTests
[Test]
public void ChangeToAsync_ToUnregisteredState_Should_ThrowInvalidOperationException()
{
Assert.ThrowsAsync<InvalidOperationException>(async () => await _stateMachine.ChangeToAsync<TestStateV2>());
Assert.ThrowsAsync<InvalidOperationException>(() => _stateMachine.ChangeToAsync<TestStateV2>());
}
/// <summary>
@ -525,7 +525,7 @@ public sealed class TestAsyncState : IState, IAsyncState
/// <param name="from">从哪个状态进入</param>
public async Task OnEnterAsync(IState? from)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
EnterCalled = true;
EnterCallCount++;
EnterFrom = from;
@ -537,7 +537,7 @@ public sealed class TestAsyncState : IState, IAsyncState
/// <param name="to">离开到哪个状态</param>
public async Task OnExitAsync(IState? to)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
ExitCalled = true;
ExitCallCount++;
ExitTo = to;
@ -550,7 +550,7 @@ public sealed class TestAsyncState : IState, IAsyncState
/// <returns>是否允许转换</returns>
public async Task<bool> CanTransitionToAsync(IState target)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
CanTransitionToCallCount++;
return AllowTransition;
}
@ -601,4 +601,4 @@ public static class StateMachineExtensions
.GetValue(stateMachine) is Dictionary<Type, IState> states &&
states.ContainsKey(typeof(T));
}
}
}

View File

@ -674,7 +674,7 @@ public sealed class ConcreteAsyncStateV2 : IState, IAsyncState
/// <param name="from">从哪个状态进入</param>
public async Task OnEnterAsync(IState? from)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
EnterCalled = true;
EnterCallCount++;
EnterFrom = from;
@ -686,7 +686,7 @@ public sealed class ConcreteAsyncStateV2 : IState, IAsyncState
/// <param name="to">退出到哪个状态</param>
public async Task OnExitAsync(IState? to)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
ExitCalled = true;
ExitCallCount++;
ExitTo = to;
@ -699,7 +699,7 @@ public sealed class ConcreteAsyncStateV2 : IState, IAsyncState
/// <returns>如果可以转换则返回true否则返回false</returns>
public async Task<bool> CanTransitionToAsync(IState target)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
CanTransitionToAsyncAction?.Invoke(target);
return AllowTransitions;
}
@ -731,4 +731,4 @@ public sealed class ConcreteAsyncStateV2 : IState, IAsyncState
{
throw new InvalidOperationException("Sync CanTransitionTo should not be called for async state");
}
}
}

View File

@ -16,7 +16,7 @@ public sealed class AsyncTestSystem : ISystem, IAsyncInitializable
public async Task InitializeAsync()
{
await Task.Delay(10);
await Task.Delay(10).ConfigureAwait(false);
Initialized = true;
}
@ -44,4 +44,4 @@ public sealed class AsyncTestSystem : ISystem, IAsyncInitializable
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
}
}

View File

@ -89,11 +89,11 @@ public class AsyncArchitectureTests : ArchitectureTestsBase<AsyncTestArchitectur
/// </summary>
/// <returns>异步任务</returns>
[Test]
public async Task Architecture_Should_Stop_Initialization_When_Model_Init_Fails()
public void Architecture_Should_Stop_Initialization_When_Model_Init_Fails()
{
Architecture!.AddPostRegistrationHook(a => { a.RegisterModel(new FailingModel()); });
Assert.ThrowsAsync<InvalidOperationException>(async () => await Architecture.InitializeAsync());
Assert.ThrowsAsync<InvalidOperationException>(() => Architecture.InitializeAsync());
Assert.That(
Architecture.CurrentPhase,
@ -134,14 +134,14 @@ public class AsyncArchitectureTests : ArchitectureTestsBase<AsyncTestArchitectur
/// </summary>
/// <returns>异步任务</returns>
[Test]
public async Task InitializeAsync_Should_Handle_Exception_Correctly()
public void InitializeAsync_Should_Handle_Exception_Correctly()
{
Architecture!.AddPostRegistrationHook(a =>
a.RegisterModel(new FailingModel())
);
Assert.ThrowsAsync<InvalidOperationException>(async () => await Architecture.InitializeAsync());
Assert.ThrowsAsync<InvalidOperationException>(() => Architecture.InitializeAsync());
AssertInitializationFailed();
}
}
}

View File

@ -20,8 +20,15 @@ public static class CollectionExtensions
/// </example>
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(action);
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
foreach (var item in source) action(item);
}
@ -58,7 +65,10 @@ public static class CollectionExtensions
/// </example>
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class
{
ArgumentNullException.ThrowIfNull(source);
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
return source.Where(item => item is not null)!;
}
@ -88,9 +98,20 @@ public static class CollectionExtensions
Func<T, TValue> valueSelector) where TKey : notnull
#pragma warning restore MA0016
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(keySelector);
ArgumentNullException.ThrowIfNull(valueSelector);
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (keySelector is null)
{
throw new ArgumentNullException(nameof(keySelector));
}
if (valueSelector is null)
{
throw new ArgumentNullException(nameof(valueSelector));
}
var dictionary = new Dictionary<TKey, TValue>();

View File

@ -16,7 +16,11 @@ public static class ContextAwareEnvironmentExtensions
/// <returns>指定类型的环境对象,如果无法转换则返回null</returns>
public static T? GetEnvironment<T>(this IContextAware contextAware) where T : class
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetEnvironment() as T;
}
@ -28,8 +32,12 @@ public static class ContextAwareEnvironmentExtensions
/// <returns>环境对象</returns>
public static IEnvironment GetEnvironment(this IContextAware contextAware)
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetEnvironment();
}
}
}

View File

@ -17,6 +17,7 @@ public static class ContextAwareEventExtensions
public static void SendEvent<TEvent>(this IContextAware contextAware) where TEvent : new()
{
ArgumentNullException.ThrowIfNull(contextAware);
var context = contextAware.GetContext();
context.SendEvent<TEvent>();
}
@ -67,4 +68,4 @@ public static class ContextAwareEventExtensions
var context = contextAware.GetContext();
context.UnRegisterEvent(onEvent);
}
}
}

View File

@ -24,7 +24,11 @@ public static class ContextAwareServiceExtensions
/// <exception cref="InvalidOperationException">当指定服务未注册时抛出</exception>
public static TService GetService<TService>(this IContextAware contextAware) where TService : class
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return GetRequiredComponent(context, static architectureContext => architectureContext.GetService<TService>(),
"Service");
@ -40,7 +44,11 @@ public static class ContextAwareServiceExtensions
/// <exception cref="InvalidOperationException">当指定系统未注册时抛出</exception>
public static TSystem GetSystem<TSystem>(this IContextAware contextAware) where TSystem : class, ISystem
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return GetRequiredComponent(context, static architectureContext => architectureContext.GetSystem<TSystem>(),
"System");
@ -56,7 +64,11 @@ public static class ContextAwareServiceExtensions
/// <exception cref="InvalidOperationException">当指定模型未注册时抛出</exception>
public static TModel GetModel<TModel>(this IContextAware contextAware) where TModel : class, IModel
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return GetRequiredComponent(context, static architectureContext => architectureContext.GetModel<TModel>(),
"Model");
@ -72,7 +84,11 @@ public static class ContextAwareServiceExtensions
/// <exception cref="InvalidOperationException">当指定工具未注册时抛出</exception>
public static TUtility GetUtility<TUtility>(this IContextAware contextAware) where TUtility : class, IUtility
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return GetRequiredComponent(context, static architectureContext => architectureContext.GetUtility<TUtility>(),
"Utility");
@ -92,7 +108,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TService> GetServices<TService>(this IContextAware contextAware)
where TService : class
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetServices<TService>();
}
@ -107,7 +127,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TSystem> GetSystems<TSystem>(this IContextAware contextAware)
where TSystem : class, ISystem
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetSystems<TSystem>();
}
@ -122,7 +146,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TModel> GetModels<TModel>(this IContextAware contextAware)
where TModel : class, IModel
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetModels<TModel>();
}
@ -137,7 +165,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TUtility> GetUtilities<TUtility>(this IContextAware contextAware)
where TUtility : class, IUtility
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetUtilities<TUtility>();
}
@ -152,7 +184,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TService> GetServicesByPriority<TService>(this IContextAware contextAware)
where TService : class
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetServicesByPriority<TService>();
}
@ -167,7 +203,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TSystem> GetSystemsByPriority<TSystem>(this IContextAware contextAware)
where TSystem : class, ISystem
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetSystemsByPriority<TSystem>();
}
@ -182,7 +222,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TModel> GetModelsByPriority<TModel>(this IContextAware contextAware)
where TModel : class, IModel
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetModelsByPriority<TModel>();
}
@ -197,7 +241,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TUtility> GetUtilitiesByPriority<TUtility>(this IContextAware contextAware)
where TUtility : class, IUtility
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetUtilitiesByPriority<TUtility>();
}
@ -206,11 +254,14 @@ public static class ContextAwareServiceExtensions
Func<IArchitectureContext, TComponent> resolver, string componentKind)
where TComponent : class
{
ArgumentNullException.ThrowIfNull(context);
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var component = resolver(context);
return component ?? throw new InvalidOperationException($"{componentKind} {typeof(TComponent)} not registered");
}
#endregion
}
}

View File

@ -27,7 +27,9 @@ public static class GuardExtensions
this T? value,
[CallerArgumentExpression(nameof(value))] string? paramName = null) where T : class
{
ArgumentNullException.ThrowIfNull(value, paramName);
if (value is null)
throw new ArgumentNullException(paramName);
return value;
}
@ -51,7 +53,8 @@ public static class GuardExtensions
this string? value,
[CallerArgumentExpression(nameof(value))] string? paramName = null)
{
ArgumentNullException.ThrowIfNull(value, paramName);
if (value is null)
throw new ArgumentNullException(paramName);
if (value.Length == 0)
throw new ArgumentException("字符串不能为空", paramName);
@ -79,7 +82,8 @@ public static class GuardExtensions
this string? value,
[CallerArgumentExpression(nameof(value))] string? paramName = null)
{
ArgumentNullException.ThrowIfNull(value, paramName);
if (value is null)
throw new ArgumentNullException(paramName);
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("字符串不能为空或仅包含空白字符", paramName);
@ -108,7 +112,8 @@ public static class GuardExtensions
this IEnumerable<T>? source,
[CallerArgumentExpression(nameof(source))] string? paramName = null)
{
ArgumentNullException.ThrowIfNull(source, paramName);
if (source is null)
throw new ArgumentNullException(paramName);
if (!source.Any())
throw new ArgumentException("集合不能为空", paramName);

View File

@ -30,10 +30,14 @@ public static class NumericExtensions
ArgumentNullException.ThrowIfNull(max);
if (min.CompareTo(max) > 0)
{
throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})", nameof(min));
}
if (inclusive)
{
return value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0;
}
return value.CompareTo(min) > 0 && value.CompareTo(max) < 0;
}
@ -71,7 +75,9 @@ public static class NumericExtensions
public static float InverseLerp(this float value, float from, float to)
{
if (Math.Abs(to - from) < float.Epsilon)
{
throw new DivideByZeroException("起始值和目标值不能相等");
}
return (value - from) / (to - from);
}

View File

@ -27,8 +27,15 @@ public static class StoreEventBusExtensions
bool publishDispatches = true,
bool publishStateChanges = true)
{
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(eventBus);
if (store is null)
{
throw new ArgumentNullException(nameof(store));
}
if (eventBus is null)
{
throw new ArgumentNullException(nameof(eventBus));
}
IUnRegister? dispatchBridge = null;
IUnRegister? stateBridge = null;
@ -60,8 +67,15 @@ public static class StoreEventBusExtensions
/// <returns>用于移除 dispatch 桥接中间件的句柄。</returns>
public static IUnRegister BridgeDispatchesToEventBus<TState>(this Store<TState> store, IEventBus eventBus)
{
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(eventBus);
if (store is null)
{
throw new ArgumentNullException(nameof(store));
}
if (eventBus is null)
{
throw new ArgumentNullException(nameof(eventBus));
}
return store.RegisterMiddleware(new DispatchEventBusMiddleware<TState>(eventBus));
}
@ -77,8 +91,15 @@ public static class StoreEventBusExtensions
public static IUnRegister BridgeStateChangesToEventBus<TState>(this IReadonlyStore<TState> store,
IEventBus eventBus)
{
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(eventBus);
if (store is null)
{
throw new ArgumentNullException(nameof(store));
}
if (eventBus is null)
{
throw new ArgumentNullException(nameof(eventBus));
}
return store.Subscribe(state =>
eventBus.Send(new StoreStateChangedEvent<TState>(state, DateTimeOffset.UtcNow)));
@ -115,4 +136,4 @@ public static class StoreEventBusExtensions
_eventBus.Send(new StoreDispatchedEvent<TState>(dispatchRecord));
}
}
}
}

View File

@ -42,11 +42,15 @@ public static class StringExtensions
ArgumentNullException.ThrowIfNull(suffix);
if (maxLength < suffix.Length)
{
throw new ArgumentOutOfRangeException(nameof(maxLength),
$"最大长度必须至少为后缀长度 ({suffix.Length})");
}
if (str.Length <= maxLength)
{
return str;
}
return string.Concat(str.AsSpan(0, maxLength - suffix.Length), suffix);
}
@ -71,4 +75,4 @@ public static class StringExtensions
return string.Join(separator, values);
}
}
}

View File

@ -40,6 +40,7 @@ public sealed class StoreBuilder<TState> : IStoreBuilder<TState>
public IStoreBuilder<TState> UseMiddleware(IStoreMiddleware<TState> middleware)
{
ArgumentNullException.ThrowIfNull(middleware);
_configurators.Add(store => store.UseMiddleware(middleware));
return this;
}
@ -109,6 +110,7 @@ public sealed class StoreBuilder<TState> : IStoreBuilder<TState>
public IStoreBuilder<TState> AddReducer<TAction>(Func<TState, TAction, TState> reducer)
{
ArgumentNullException.ThrowIfNull(reducer);
_configurators.Add(store => store.RegisterReducer(reducer));
return this;
}
@ -122,7 +124,8 @@ public sealed class StoreBuilder<TState> : IStoreBuilder<TState>
public IStoreBuilder<TState> AddReducer<TAction>(IReducer<TState, TAction> reducer)
{
ArgumentNullException.ThrowIfNull(reducer);
_configurators.Add(store => store.RegisterReducer(reducer));
return this;
}
}
}

View File

@ -24,10 +24,13 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// </summary>
private readonly List<SelectionListenerSubscription> _listeners = [];
/// <summary>
/// 保护监听器集合和底层 Store 订阅句柄的同步锁。
/// </summary>
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
/// <summary>
/// 负责从完整状态中投影出局部状态的选择器。
@ -82,7 +85,9 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// <returns>用于取消订阅的句柄。</returns>
IUnRegister IEvent.Register(Action onEvent)
{
ArgumentNullException.ThrowIfNull(onEvent);
if (onEvent is null)
throw new ArgumentNullException(nameof(onEvent));
return Register(_ => onEvent());
}
@ -94,7 +99,8 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// <exception cref="ArgumentNullException">当 <paramref name="onValueChanged"/> 为 <see langword="null"/> 时抛出。</exception>
public IUnRegister Register(Action<TSelected> onValueChanged)
{
ArgumentNullException.ThrowIfNull(onValueChanged);
if (onValueChanged is null)
throw new ArgumentNullException(nameof(onValueChanged));
var subscription = new SelectionListenerSubscription(onValueChanged);
var shouldAttach = false;
@ -126,7 +132,8 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
public IUnRegister RegisterWithInitValue(Action<TSelected> action)
{
ArgumentNullException.ThrowIfNull(action);
if (action is null)
throw new ArgumentNullException(nameof(action));
var subscription = new SelectionListenerSubscription(action)
{
@ -189,7 +196,8 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// <exception cref="ArgumentNullException">当 <paramref name="onValueChanged"/> 为 <see langword="null"/> 时抛出。</exception>
public void UnRegister(Action<TSelected> onValueChanged)
{
ArgumentNullException.ThrowIfNull(onValueChanged);
if (onValueChanged is null)
throw new ArgumentNullException(nameof(onValueChanged));
SelectionListenerSubscription? subscriptionToRemove = null;
@ -391,4 +399,4 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// </summary>
public TSelected PendingValue { get; set; } = default!;
}
}
}

View File

@ -128,7 +128,7 @@ public class ArchitectureConfigIntegrationTests
var secondArchitecture = new ModuleOnlyArchitecture(module);
var exception =
Assert.ThrowsAsync<InvalidOperationException>(async () => await secondArchitecture.InitializeAsync().ConfigureAwait(false));
Assert.ThrowsAsync<InvalidOperationException>(() => secondArchitecture.InitializeAsync());
Assert.Multiple(() =>
{

View File

@ -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().ConfigureAwait(false));
var secondCallerException = Assert.ThrowsAsync<InvalidOperationException>(() => bootstrap.InitializeAsync());
continueInitialization.Set();
Assert.DoesNotThrowAsync(async () => await firstInitializeTask.ConfigureAwait(false));
Assert.DoesNotThrowAsync(() => firstInitializeTask);
Assert.Multiple(() =>
{
@ -202,7 +202,7 @@ public class GameConfigBootstrapTests
})
});
var exception = Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await bootstrap.InitializeAsync().ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => bootstrap.InitializeAsync());
Assert.Multiple(() =>
{

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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -165,7 +165,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -223,7 +223,7 @@ public sealed class YamlConfigLoaderAllOfTests
static config => config.Id);
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -258,7 +258,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -298,7 +298,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -338,7 +338,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -378,7 +378,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -418,7 +418,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -458,7 +458,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -502,7 +502,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -217,7 +217,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -174,7 +174,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{

View File

@ -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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -170,7 +170,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -263,7 +263,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
static config => config.Id);
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -304,7 +304,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -345,7 +345,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -393,7 +393,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{

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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -172,7 +172,7 @@ public sealed class YamlConfigLoaderNegationTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -272,7 +272,7 @@ public sealed class YamlConfigLoaderNegationTests
var loader = CreateMonsterLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{

View File

@ -143,7 +143,7 @@ public class YamlConfigLoaderTests
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -185,7 +185,7 @@ public class YamlConfigLoaderTests
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id)
.RegisterTable<int, MonsterConfigStub>("broken", "broken", static config => config.Id);
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -216,7 +216,7 @@ public class YamlConfigLoaderTests
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -257,7 +257,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -306,7 +306,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -352,7 +352,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -398,7 +398,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -445,7 +445,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -494,7 +494,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -543,7 +543,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -591,7 +591,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -681,7 +681,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -769,7 +769,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -817,7 +817,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -931,7 +931,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -980,7 +980,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1028,7 +1028,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1074,7 +1074,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1174,7 +1174,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1226,7 +1226,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1280,7 +1280,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1336,7 +1336,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1394,7 +1394,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1452,7 +1452,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1561,7 +1561,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1617,7 +1617,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1635,57 +1635,7 @@ public class YamlConfigLoaderTests
[Test]
public async Task LoadAsync_Should_Accept_Object_Array_When_Contains_Matches_Declared_Subset_Properties()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
entries:
-
id: 1
weight: 2
-
id: 2
weight: 3
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "entries"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"entries": {
"type": "array",
"minContains": 1,
"contains": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"type": "integer",
"const": 1
}
}
},
"items": {
"type": "object",
"required": ["id", "weight"],
"properties": {
"id": { "type": "integer" },
"weight": { "type": "integer" }
}
}
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterWeightedEntryArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var loader = CreateLoaderForContainsSubsetObjectArrayScenario();
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
@ -1740,7 +1690,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1797,7 +1747,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1904,7 +1854,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1954,7 +1904,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2004,7 +1954,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2055,7 +2005,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2107,7 +2057,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2161,7 +2111,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2261,7 +2211,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2317,7 +2267,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2369,7 +2319,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2423,7 +2373,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2478,7 +2428,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2537,7 +2487,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2553,14 +2503,7 @@ public class YamlConfigLoaderTests
[Test]
public void LoadAsync_Should_Throw_When_Nested_Object_Array_Reference_Target_Is_Missing()
{
CreateConfigFile(
"item/potion.yaml",
"""
id: potion
name: Potion
""");
CreateConfigFile(
"monster/slime.yaml",
var loader = CreateItemBackedMonsterLoader<MonsterPhaseDropConfigStub>(
"""
id: 1
name: Slime
@ -2571,21 +2514,7 @@ public class YamlConfigLoaderTests
-
wave: 2
dropItemId: bomb
""");
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
""",
"""
{
"type": "object",
@ -2609,16 +2538,12 @@ public class YamlConfigLoaderTests
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, MonsterPhaseDropConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
""",
static config => config.Id,
("item/potion.yaml", "potion", "Potion"));
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2749,7 +2674,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2766,41 +2691,14 @@ public class YamlConfigLoaderTests
[Test]
public void LoadAsync_Should_Throw_When_Array_Reference_Item_Is_Missing()
{
CreateConfigFile(
"item/potion.yaml",
"""
id: potion
name: Potion
""");
CreateConfigFile(
"item/slime-gel.yaml",
"""
id: slime_gel
name: Slime Gel
""");
CreateConfigFile(
"monster/slime.yaml",
var loader = CreateItemBackedMonsterLoader<MonsterDropArrayConfigStub>(
"""
id: 1
name: Slime
dropItemIds:
- potion
- missing_item
""");
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
""",
"""
{
"type": "object",
@ -2815,16 +2713,13 @@ public class YamlConfigLoaderTests
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
""",
static config => config.Id,
("item/potion.yaml", "potion", "Potion"),
("item/slime-gel.yaml", "slime_gel", "Slime Gel"));
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2841,35 +2736,14 @@ public class YamlConfigLoaderTests
[Test]
public void LoadAsync_Should_Throw_When_Contains_Matched_Reference_Target_Is_Missing()
{
CreateConfigFile(
"item/potion.yaml",
"""
id: potion
name: Potion
""");
CreateConfigFile(
"monster/slime.yaml",
var loader = CreateItemBackedMonsterLoader<MonsterDropArrayConfigStub>(
"""
id: 1
name: Slime
dropItemIds:
- potion
- missing_item
""");
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
""",
"""
{
"type": "object",
@ -2890,16 +2764,12 @@ public class YamlConfigLoaderTests
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
""",
static config => config.Id,
("item/potion.yaml", "potion", "Potion"));
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -3353,6 +3223,111 @@ public class YamlConfigLoaderTests
}
}
/// <summary>
/// 为对象数组 <c>contains</c> 子集匹配场景创建加载器,避免测试方法体被大段固定 schema 稀释。
/// </summary>
/// <returns>已注册目标表的加载器。</returns>
private YamlConfigLoader CreateLoaderForContainsSubsetObjectArrayScenario()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
entries:
-
id: 1
weight: 2
-
id: 2
weight: 3
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "entries"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"entries": {
"type": "array",
"minContains": 1,
"contains": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"type": "integer",
"const": 1
}
}
},
"items": {
"type": "object",
"required": ["id", "weight"],
"properties": {
"id": { "type": "integer" },
"weight": { "type": "integer" }
}
}
}
}
}
""");
return new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterWeightedEntryArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
}
/// <summary>
/// 为跨表引用加载测试创建标准 item 表夹具,并按既有顺序注册 <c>item</c> 与 <c>monster</c>。
/// </summary>
/// <typeparam name="TMonsterConfig">monster 表的配置类型。</typeparam>
/// <param name="monsterConfigContent">monster 配置文件内容。</param>
/// <param name="monsterSchemaContent">monster schema 内容。</param>
/// <param name="keySelector">monster 表主键选择器。</param>
/// <param name="items">要写入的 item 配置文件集合。</param>
/// <returns>已完成 schema 与表注册的加载器。</returns>
private YamlConfigLoader CreateItemBackedMonsterLoader<TMonsterConfig>(
string monsterConfigContent,
string monsterSchemaContent,
Func<TMonsterConfig, int> keySelector,
params (string RelativePath, string ItemId, string Name)[] items)
{
foreach (var (relativePath, itemId, name) in items)
{
CreateConfigFile(
relativePath,
$"""
id: {itemId}
name: {name}
""");
}
CreateConfigFile("monster/slime.yaml", monsterConfigContent);
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile("schemas/monster.schema.json", monsterSchemaContent);
return new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, TMonsterConfig>("monster", "monster", "schemas/monster.schema.json", keySelector);
}
/// <summary>
/// 创建测试用配置文件。
/// </summary>
@ -3389,13 +3364,13 @@ public class YamlConfigLoaderTests
/// <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);
}
/// <summary>

View File

@ -110,7 +110,7 @@ public sealed class YamlConfigTextValidatorTests
/// 验证异步入口与同步入口共享相同校验语义。
/// </summary>
[Test]
public async Task ValidateAsync_Should_Throw_ConfigLoadException_When_Required_Field_Is_Missing()
public void ValidateAsync_Should_Throw_ConfigLoadException_When_Required_Field_Is_Missing()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
@ -125,14 +125,14 @@ public sealed class YamlConfigTextValidatorTests
}
""");
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () =>
await YamlConfigTextValidator.ValidateAsync(
var exception = Assert.ThrowsAsync<ConfigLoadException>(() =>
YamlConfigTextValidator.ValidateAsync(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
""").ConfigureAwait(false));
"""));
Assert.Multiple(() =>
{

View File

@ -43,7 +43,7 @@ public class PersistenceTests
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()).ConfigureAwait(false));
Assert.ThrowsAsync<ArgumentException>(() => storage.WriteAsync("../escape", new TestSimpleData()));
}
/// <summary>
@ -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).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => repository.LoadAsync(1));
Assert.That(exception!.Message, Does.Contain("from version 2"));
}
@ -218,7 +218,7 @@ public class PersistenceTests
var repository = new SaveRepository<TestVersionedSaveData>(storage, config)
.RegisterMigration(new TestSaveMigrationV1ToV2ReturningV3());
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.LoadAsync(1).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => repository.LoadAsync(1));
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.ConfigureAwait(false));
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => loadTask);
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save").ConfigureAwait(false);
Assert.Multiple(() =>
@ -593,7 +593,7 @@ public class PersistenceTests
throwingStorage.ThrowOnWrite = true;
Assert.ThrowsAsync<InvalidOperationException>(
async () => await repository.SaveAsync(primaryLocation, new TestSimpleData { Value = 99 }).ConfigureAwait(false));
() => repository.SaveAsync(primaryLocation, new TestSimpleData { Value = 99 }));
var cachedAfterFailure = await repository.LoadAsync<TestSimpleData>(primaryLocation).ConfigureAwait(false);
Assert.That(cachedAfterFailure.Value, Is.EqualTo(1));
@ -656,7 +656,7 @@ public class PersistenceTests
throwingStorage.ThrowOnWrite = true;
Assert.ThrowsAsync<InvalidOperationException>(
async () => await repository.DeleteAsync(secondaryLocation).ConfigureAwait(false));
() => repository.DeleteAsync(secondaryLocation));
Assert.That(await repository.ExistsAsync(secondaryLocation).ConfigureAwait(false), Is.True);

View File

@ -140,7 +140,7 @@ public abstract class RouterBase<TRoute, TContext> : AbstractSystem
try
{
Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, routeKey);
var canEnter = await guard.CanEnterAsync(routeKey, context);
var canEnter = await guard.CanEnterAsync(routeKey, context).ConfigureAwait(false);
if (!canEnter)
{
@ -182,7 +182,7 @@ public abstract class RouterBase<TRoute, TContext> : AbstractSystem
try
{
Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, routeKey);
var canLeave = await guard.CanLeaveAsync(routeKey);
var canLeave = await guard.CanLeaveAsync(routeKey).ConfigureAwait(false);
if (!canLeave)
{
@ -241,4 +241,4 @@ public abstract class RouterBase<TRoute, TContext> : AbstractSystem
}
#endregion
}
}

View File

@ -89,13 +89,9 @@ public abstract class SceneRouterBase
var @event = CreateEvent(sceneKey, SceneTransitionType.Replace, param);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await ClearInternalAsync();
await PushInternalAsync(sceneKey, param);
await AfterChangeAsync(@event);
});
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecuteReplaceCoreAsync(@event, sceneKey, param)).ConfigureAwait(true);
}
finally
{
@ -195,12 +191,9 @@ public abstract class SceneRouterBase
var @event = CreateEvent(sceneKey, SceneTransitionType.Push, param);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await PushInternalAsync(sceneKey, param);
await AfterChangeAsync(@event);
});
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecutePushCoreAsync(@event, sceneKey, param)).ConfigureAwait(true);
}
finally
{
@ -276,12 +269,9 @@ public abstract class SceneRouterBase
var @event = CreateEvent(null, SceneTransitionType.Pop);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await PopInternalAsync();
await AfterChangeAsync(@event);
});
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecutePopCoreAsync(@event)).ConfigureAwait(true);
}
finally
{
@ -347,12 +337,9 @@ public abstract class SceneRouterBase
var @event = CreateEvent(null, SceneTransitionType.Clear);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await ClearInternalAsync();
await AfterChangeAsync(@event);
});
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecuteClearCoreAsync(@event)).ConfigureAwait(true);
}
finally
{
@ -370,7 +357,7 @@ public abstract class SceneRouterBase
{
while (Stack.Count > 0)
{
await PopInternalAsync();
await PopInternalAsync().ConfigureAwait(true);
}
}
@ -378,6 +365,67 @@ public abstract class SceneRouterBase
#region Helper Methods
// Scene 生命周期回调和 pipeline handlers 可能依赖引擎线程,因此这些核心切换顺序统一显式保留上下文。
/// <summary>
/// 执行 Replace 的核心切换顺序。
/// </summary>
/// <param name="event">场景转换事件。</param>
/// <param name="sceneKey">目标场景键名。</param>
/// <param name="param">场景进入参数。</param>
/// <returns>异步任务。</returns>
private async Task ExecuteReplaceCoreAsync(
SceneTransitionEvent @event,
string sceneKey,
ISceneEnterParam? param)
{
await BeforeChangeAsync(@event).ConfigureAwait(true);
await ClearInternalAsync().ConfigureAwait(true);
await PushInternalAsync(sceneKey, param).ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
/// 执行 Push 的核心切换顺序。
/// </summary>
/// <param name="event">场景转换事件。</param>
/// <param name="sceneKey">目标场景键名。</param>
/// <param name="param">场景进入参数。</param>
/// <returns>异步任务。</returns>
private async Task ExecutePushCoreAsync(
SceneTransitionEvent @event,
string sceneKey,
ISceneEnterParam? param)
{
await BeforeChangeAsync(@event).ConfigureAwait(true);
await PushInternalAsync(sceneKey, param).ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
/// 执行 Pop 的核心切换顺序。
/// </summary>
/// <param name="event">场景转换事件。</param>
/// <returns>异步任务。</returns>
private async Task ExecutePopCoreAsync(SceneTransitionEvent @event)
{
await BeforeChangeAsync(@event).ConfigureAwait(true);
await PopInternalAsync().ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
/// 执行 Clear 的核心切换顺序。
/// </summary>
/// <param name="event">场景转换事件。</param>
/// <returns>异步任务。</returns>
private async Task ExecuteClearCoreAsync(SceneTransitionEvent @event)
{
await BeforeChangeAsync(@event).ConfigureAwait(true);
await ClearInternalAsync().ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
/// 创建场景转换事件对象。
/// </summary>
@ -407,7 +455,7 @@ public abstract class SceneRouterBase
private async Task BeforeChangeAsync(SceneTransitionEvent @event)
{
Log.Debug("BeforeChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.BeforeChange);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.BeforeChange).ConfigureAwait(true);
Log.Debug("BeforeChange phases completed: {0}", @event.TransitionType);
}
@ -418,9 +466,9 @@ public abstract class SceneRouterBase
private async Task AfterChangeAsync(SceneTransitionEvent @event)
{
Log.Debug("AfterChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.AfterChange);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.AfterChange).ConfigureAwait(true);
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
}
#endregion
}
}

View File

@ -148,7 +148,7 @@ public class SceneTransitionPipeline
foreach (var handler in sortedHandlers)
{
var options = _options[handler];
await ExecuteSingleHandlerAsync(handler, options, @event, cancellationToken);
await ExecuteSingleHandlerAsync(handler, options, @event, cancellationToken).ConfigureAwait(false);
}
Log.Debug("Pipeline execution completed for phases: {0}", phases);
@ -173,7 +173,7 @@ public class SceneTransitionPipeline
if (handlers.Count == 0)
{
await coreAction();
await coreAction().ConfigureAwait(false);
return;
}
@ -191,11 +191,11 @@ public class SceneTransitionPipeline
var options = _aroundOptions[handler];
var next = pipeline;
pipeline = async () => await ExecuteSingleAroundHandlerAsync(
handler, options, @event, next, cancellationToken);
pipeline = () => ExecuteSingleAroundHandlerAsync(
handler, options, @event, next, cancellationToken);
}
await pipeline();
await pipeline().ConfigureAwait(false);
}
private List<ISceneTransitionHandler> FilterAndSortHandlers(
@ -283,7 +283,7 @@ public class SceneTransitionPipeline
? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)
: null;
await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken);
await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken).ConfigureAwait(false);
Log.Debug("Around handler completed: {0}", handler.GetType().Name);
}
@ -296,4 +296,4 @@ public class SceneTransitionPipeline
throw;
}
}
}
}

View File

@ -19,7 +19,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
public async Task ApplyAll()
{
// 遍历所有设置应用器并尝试应用
foreach (var applicator in _model.AllApplicators()) await TryApplyAsync(applicator);
foreach (var applicator in _model.AllApplicators()) await TryApplyAsync(applicator).ConfigureAwait(false);
}
/// <summary>
@ -41,7 +41,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
/// <returns>完成的任务</returns>
public async Task SaveAll()
{
await _model.SaveAllAsync();
await _model.SaveAllAsync().ConfigureAwait(false);
}
/// <summary>
@ -51,7 +51,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
public async Task ResetAll()
{
_model.ResetAll();
await ApplyAll();
await ApplyAll().ConfigureAwait(false);
}
/// <summary>
@ -62,7 +62,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
public async Task Reset<T>() where T : class, ISettingsData, IResetApplyAbleSettings, new()
{
_model.Reset<T>();
await Apply<T>();
await Apply<T>().ConfigureAwait(false);
}
@ -87,7 +87,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
try
{
await applyAbleSettings.ApplyAsync();
await applyAbleSettings.ApplyAsync().ConfigureAwait(false);
// 发送设置应用成功事件
this.SendEvent(new SettingsAppliedEvent<ISettingsSection>(section, true));
}

View File

@ -143,11 +143,11 @@ public sealed class FileStorage : IFileStorage, IDisposable
ObjectDisposedException.ThrowIf(_disposed, this);
var path = ToPath(key);
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
if (File.Exists(path))
File.Delete(path);
}
var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false);
await using var configuredPathLock = pathLock.ConfigureAwait(false);
if (File.Exists(path))
File.Delete(path);
}
#endregion
@ -178,10 +178,10 @@ public sealed class FileStorage : IFileStorage, IDisposable
ObjectDisposedException.ThrowIf(_disposed, this);
var path = ToPath(key);
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
return File.Exists(path);
}
var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false);
await using var configuredPathLock = pathLock.ConfigureAwait(false);
return File.Exists(path);
}
#endregion
@ -236,23 +236,24 @@ public sealed class FileStorage : IFileStorage, IDisposable
ObjectDisposedException.ThrowIf(_disposed, this);
var path = ToPath(key);
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
if (!File.Exists(path))
throw new FileNotFoundException($"Storage key not found: {key}", path);
var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false);
await using var configuredPathLock = pathLock.ConfigureAwait(false);
await using var fs = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
_bufferSize,
useAsync: true);
if (!File.Exists(path))
throw new FileNotFoundException($"Storage key not found: {key}", path);
using var sr = new StreamReader(fs, Encoding.UTF8);
var content = await sr.ReadToEndAsync().ConfigureAwait(false);
return _serializer.Deserialize<T>(content);
}
var fs = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
_bufferSize,
useAsync: true);
await using var configuredFileStream = fs.ConfigureAwait(false);
using var sr = new StreamReader(fs, Encoding.UTF8, true, -1, leaveOpen: true);
var content = await sr.ReadToEndAsync().ConfigureAwait(false);
return _serializer.Deserialize<T>(content);
}
#endregion
@ -354,38 +355,42 @@ public sealed class FileStorage : IFileStorage, IDisposable
var path = ToPath(key);
var tempPath = path + ".tmp";
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false);
await using var configuredPathLock = pathLock.ConfigureAwait(false);
try
{
try
{
var content = _serializer.Serialize(value);
var content = _serializer.Serialize(value);
// 先写入临时文件
await using (var fs = new FileStream(
tempPath,
FileMode.Create,
FileAccess.Write,
FileShare.None,
_bufferSize,
useAsync: true))
{
await using var sw = new StreamWriter(fs, Encoding.UTF8);
await sw.WriteAsync(content).ConfigureAwait(false);
await sw.FlushAsync().ConfigureAwait(false);
}
// 原子性替换目标文件
File.Move(tempPath, path, overwrite: true);
}
catch
// 先写入临时文件
{
// 清理临时文件
if (File.Exists(tempPath))
File.Delete(tempPath);
throw;
var fs = new FileStream(
tempPath,
FileMode.Create,
FileAccess.Write,
FileShare.None,
_bufferSize,
useAsync: true);
await using var configuredFileStream = fs.ConfigureAwait(false);
var sw = new StreamWriter(fs, Encoding.UTF8, leaveOpen: true);
await using var configuredStreamWriter = sw.ConfigureAwait(false);
await sw.WriteAsync(content).ConfigureAwait(false);
await sw.FlushAsync().ConfigureAwait(false);
}
// 原子性替换目标文件
File.Move(tempPath, path, overwrite: true);
}
catch
{
// 清理临时文件
if (File.Exists(tempPath))
File.Delete(tempPath);
throw;
}
}
#endregion
}
}

View File

@ -100,9 +100,9 @@ public sealed class ScopedStorage(IStorage inner, string prefix) : IScopedStorag
/// </summary>
/// <param name="key">要删除的键</param>
/// <returns>异步操作任务</returns>
public async Task DeleteAsync(string key)
public Task DeleteAsync(string key)
{
await inner.DeleteAsync(Key(key));
return inner.DeleteAsync(Key(key));
}
/// <summary>
@ -166,4 +166,4 @@ public sealed class ScopedStorage(IStorage inner, string prefix) : IScopedStorag
{
return new ScopedStorage(inner, Key(scope));
}
}
}

View File

@ -101,10 +101,10 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await DoPushPageInternalAsync(uiKey, param, policy);
await AfterChangeAsync(@event);
});
await BeforeChangeAsync(@event).ConfigureAwait(true);
await DoPushPageInternalAsync(uiKey, param, policy).ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -129,10 +129,10 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
DoPushPageInternal(page, param, policy);
await AfterChangeAsync(@event);
});
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -149,7 +149,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
var leavingUiKey = Stack.Peek().Key;
if (!await ExecuteLeaveGuardsAsync(leavingUiKey))
if (!await ExecuteLeaveGuardsAsync(leavingUiKey).ConfigureAwait(true))
{
Log.Warn("Pop blocked by guard: {0}", leavingUiKey);
return;
@ -160,10 +160,10 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
DoPopInternal(policy);
await AfterChangeAsync(@event);
});
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -182,15 +182,15 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
DoClearInternal(popPolicy);
var page = _factory.Create(uiKey);
Log.Debug("Get/Create UI Page instance for Replace: {0}", page.GetType().Name);
DoPushPageInternal(page, param, pushPolicy);
await AfterChangeAsync(@event);
});
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -211,12 +211,12 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
DoClearInternal(popPolicy);
Log.Debug("Use existing UI Page instance for Replace: {0}", page.GetType().Name);
DoPushPageInternal(page, param, pushPolicy);
await AfterChangeAsync(@event);
});
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -229,10 +229,10 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
DoClearInternal(UiPopPolicy.Destroy);
await AfterChangeAsync(@event);
});
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -650,7 +650,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
private async Task BeforeChangeAsync(UiTransitionEvent @event)
{
Log.Debug("BeforeChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, UiTransitionPhases.BeforeChange);
await _pipeline.ExecuteAsync(@event, UiTransitionPhases.BeforeChange).ConfigureAwait(true);
Log.Debug("BeforeChange phases completed: {0}", @event.TransitionType);
}
@ -661,7 +661,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
private async Task AfterChangeAsync(UiTransitionEvent @event)
{
Log.Debug("AfterChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, UiTransitionPhases.AfterChange);
await _pipeline.ExecuteAsync(@event, UiTransitionPhases.AfterChange).ConfigureAwait(true);
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
}
@ -673,7 +673,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
/// <param name="policy">过渡策略</param>
private async Task DoPushPageInternalAsync(string uiKey, IUiPageEnterParam? param, UiTransitionPolicy policy)
{
if (!await ExecuteEnterGuardsAsync(uiKey, param))
if (!await ExecuteEnterGuardsAsync(uiKey, param).ConfigureAwait(true))
{
Log.Warn("Push blocked by guard: {0}", uiKey);
return;

View File

@ -133,7 +133,7 @@ public class UiTransitionPipeline
foreach (var handler in sortedHandlers)
{
var options = _options[handler];
await ExecuteSingleHandlerAsync(handler, options, @event, cancellationToken);
await ExecuteSingleHandlerAsync(handler, options, @event, cancellationToken).ConfigureAwait(false);
}
Log.Debug("Pipeline execution completed for phases: {0}", phases);
@ -158,7 +158,7 @@ public class UiTransitionPipeline
if (handlers.Count == 0)
{
await coreAction();
await coreAction().ConfigureAwait(false);
return;
}
@ -176,11 +176,11 @@ public class UiTransitionPipeline
var options = _aroundOptions[handler];
var next = pipeline;
pipeline = async () => await ExecuteSingleAroundHandlerAsync(
pipeline = () => ExecuteSingleAroundHandlerAsync(
handler, options, @event, next, cancellationToken);
}
await pipeline();
await pipeline().ConfigureAwait(false);
}
private List<IUiTransitionHandler> FilterAndSortHandlers(
@ -268,7 +268,7 @@ public class UiTransitionPipeline
? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)
: null;
await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken);
await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken).ConfigureAwait(false);
Log.Debug("Around handler completed: {0}", handler.GetType().Name);
}
@ -281,4 +281,4 @@ public class UiTransitionPipeline
throw;
}
}
}
}

View File

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

View File

@ -6,41 +6,85 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-058`
- 当前阶段:`Phase 58`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-064`
- 当前阶段:`Phase 64`
- 当前焦点:
- `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` 已通过,当前文件不再残留格式差异
- `2026-04-25` 当前 turn 先执行 `$gframework-pr-review`,复核 PR #288 的 latest-head unresolved 线程与折叠评论
- 已收敛一批经本地复核后仍成立的 review 建议,包括 `ThrowIfNull` 回退、测试桩 XML 注释修正、`FileStorage` 资源所有权、`SceneRouterBase` 线程亲和语义与若干测试噪音
- 已确认用户在 WSL 下直接执行的标准 `dotnet build -c Release` 路径可用;前一轮失败主要来自主线程附加的 workaround 参数而非仓库本身不可构建
- 基线 `origin/main` 仍为 `9964962``2026-04-24T23:05:53+08:00`
- 当前累计 branch diff 相对 `origin/main``75` 个文件、`2098` 行,已触达本轮 `75 files` 阈值
- `RP-061` 之后已接受 2 个批次提交:`03c73a8``9ce1fa6`
- 当前默认恢复入口不再继续扩写集;若要继续 analyzer reduction优先重新抓取 PR #288 的 unresolved 线程并按最新 head 再做一轮收口
## 当前活跃事实
- 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值
- 仓库根目录 `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 代码线程
- 当前 `origin/main` 基线提交为 `9964962``2026-04-24T23:05:53+08:00`)。
- 本轮 `Core.Tests` 低风险机械型清理已落地到:
- `ArchitectureAdditionalCqrsHandlersTests.cs`
- `RegistryInitializationHookBaseTests.cs`
- `CommandCoroutineExtensionsTests.cs`
- `TaskCoroutineExtensionsTests.cs`
- `WaitForTaskTTests.cs`
- `AsyncExtensionsTests.cs`
- `LogContextTests.cs`
- `PauseStackManagerTests.cs`
- 本 turn 结合 PR #288 latest-head review 额外收敛了以下仍然成立的问题:
- `AsyncExtensionsTests.cs`:修复 `WithTimeoutAsync` 无返回值测试中错误返回 `ConfiguredTaskAwaitable` 导致的 `CS0029` / `CS1662`
- `ContextAwareCommandExtensions.cs`
- `ContextAwareQueryExtensions.cs`
- `ContextAwareEventExtensions.cs`
- `AsyncExtensions.cs`
- `AsyncKeyLockManagerTests.cs`:去掉两处不会产生额外价值的 `Assert.DoesNotThrowAsync(() => Task.WhenAll(...))` 包装,并把取消断言改为直接消费 `ValueTask.AsTask()`
- `AsyncArchitectureTests.cs`
- `ArchitectureLifecycleBehaviorTests.cs`
- `StateMachineSystemTests.cs`
- `RegistryInitializationHookBaseTests.cs`
- `NumericExtensions.cs`
- `StringExtensions.cs`
- `StoreBuilder.cs`
- `StoreSelection.cs`
- `ArchitectureServicesTests.cs`
- `GameContextTests.cs`
- `RollingFileAppenderTests.cs`
- `TaskCoroutineExtensionsTests.cs`
- `WaitForTaskTests.cs`
- `ScopedStorage.cs`
- `FileStorage.cs`
- `SceneRouterBase.cs`
- 当前 PR review 观察:
- PR`#288`
- latest reviewed commit`70c42b579f70c90ab5461a02e611c0fbd8d8a6f2`
- 抓取时 `coderabbitai[bot]``6` 个 open threads`greptile-apps[bot]``2` 个 open threads
- `Actionable comments posted: 7``outside diff + nitpick = 19` 并不等于必须全收;本 turn 仅接受经本地复核后仍成立且不与仓库约束冲突的建议
- 本轮 `Core` runtime 低风险机械型清理已落地到:
- `AsyncExtensions.cs`
- `CollectionExtensions.cs`
- `ContextAwareCommandExtensions.cs`
- `ContextAwareEnvironmentExtensions.cs`
- `ContextAwareEventExtensions.cs`
- `ContextAwareQueryExtensions.cs`
- `ContextAwareServiceExtensions.cs`
- `GuardExtensions.cs`
- `NumericExtensions.cs`
- `StoreEventBusExtensions.cs`
- `StringExtensions.cs`
- `StoreBuilder.cs`
- `StoreSelection.cs`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal` 当前结果为 `0 Warning(s)``0 Error(s)`,可作为本轮 runtime 变更的最终最小 Release build 验证。
- `GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-incremental``03c73a8` 提交前的最近一次可信主线程结果为 `198 Warning(s)``0 Error(s)`;该观测值覆盖了 `ArchitectureContextTests``ArchitectureServicesTests``GameContextTests``ResultTests``AsyncTestModel``AsyncTestSystem``ContextAwareEnvironmentExtensionsTests` 的 7 文件批次。
- 当前累计 branch diff 相对 `origin/main``75` 个文件、`2098` 行;本轮主停止条件已经达到。
## 当前风险
- 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0
- 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
- 仓库根目录与 `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 标题;这不属于本地代码修改范围
- `dotnet clean GFramework.sln -c Release``dotnet clean GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release` 仍无法稳定提供新的 clean 基线。
- 缓解措施:后续若继续整仓 warning reduction需要单独定位 clean 失败原因,或明确继续沿用 direct build 观测值作为临时真值。
- 当前 worktree 仍存在未跟踪的 `.codex` 目录。
- 缓解措施:提交当前批次时只暂存 analyzer-warning-reduction 相关源码与 `ai-plan` 文件,避免把工作目录辅助文件混入提交。
- 将分支继续推过 `75 files` 会明显降低本轮 reviewability。
- 缓解措施:当前恢复点默认停止;如需继续,建议在新 turn 明确新的文件阈值或先 rebase / refresh baseline。
- `GFramework.Core``GFramework.Game``GFramework.Core.Tests` 当前都仍存在模块级历史 warning 基线。
- 缓解措施:本 turn 已确保本次 touched files 不再引入新的编译错误,并消化了当前 PR review 中仍成立的高信号问题;若要继续 warning reduction应开新批次按模块系统化收敛。
## 活跃文档
@ -56,31 +100,35 @@
## 验证说明
- `dotnet clean GFramework.sln -c Release`
- 结果:失败;停在 solution `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`,未输出更具体的 error 文本
- `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无格式差异
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 历史结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-incremental --no-restore -p:RestoreFallbackFolders= -v:diag`
- 历史结果:失败;`MSB4276`,默认 SDK resolver 无法解析 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator`,属于当前 WSL / dotnet 10 环境阻塞
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018``ResolvePackageAssets` 命中失效 Windows fallback package folder `D:\Tool\Development Tools\Microsoft Visual Studio\Shared\NuGetPackages`
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net9.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018`,原因同上
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018`,原因同上
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果:成功;定位到 PR `#288`,提取 latest-head unresolved AI review threads、MegaLinter 与 Docstring Coverage 信号
- `dotnet restore GFramework.sln -p:RestoreFallbackFolders="" -v minimal`
- 结果:成功;已刷新 WSL 原生 restore 元数据,清除先前的 stale fallback package folder 阻塞
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:成功;`28 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`329 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`137 Warning(s)``0 Error(s)`
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:TestTargetFrameworks=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`NU1201``GFramework.Tests.Common` 仅支持 `net10.0`,因此不能用 `net8.0` 旁路验证 `Core.Tests`
- `git diff --name-only origin/main...HEAD | wc -l`
- 当前结果:`75`
- `git diff --numstat origin/main...HEAD`
- 当前结果:累计 `1083` added、`1015` deleted`2098` changed lines
## 下一步建议
1. 提交 `SceneBehaviorBase.cs``RP-058` tracking/trace 更新,清掉 PR #286 当前 latest-head 上的格式类 review thread
2. 若继续 warning reduction 主线,应回到 `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs`,把它作为独立高上下文批次处理
1. 当前 turn 已按标准 WSL `dotnet build` 路径完成 `Core` / `Game` / `Core.Tests` Release build 验证;后续若继续 PR #288 收尾,优先重新抓取 unresolved threads确认哪些线程已可直接 resolve。
2. 若后续要继续 `Core` / `Core.Tests` / `Game` warning reduction应以当前标准 build 输出为新真值,而不是继续沿用上一轮带 workaround 参数的失败命令。
3. 若要开启下一轮批处理,优先选择新的 stop-condition例如新的 file 阈值、warning 目标或限定到单模块)后再继续。

View File

@ -1,251 +1,120 @@
# Analyzer Warning Reduction 追踪
# Analyzer Warning Reduction 追踪
## 2026-04-25 — RP-064
## 2026-04-24 — RP-058
### 阶段PR #286 latest-head review 格式跟进
### 阶段:按标准 WSL build 路径复核 PR #288 建议并完成本轮收口
- 触发背景:
- 用户要求执行 `$gframework-pr-review`,需要以当前分支 PR 页面而不是本地记忆为准,重新核对 CodeRabbit、MegaLinter 和测试状态
- 抓取脚本当前解析到的 PR 是 `#286`,最新 reviewed commit 为 `2b707343577193fc9904517e6078149653e95698`
- 最新 head 上真正未解决的代码线程只剩 `GFramework.Godot/Scene/SceneBehaviorBase.cs:148` 的缩进问题;其余 nitpick 为可选建议或已留待后续批次
- 用户指出“在 WSL 里直接执行 `dotnet build` 可以成功”,要求主线程按普通路径重新验证,而不是继续使用带 `MSBuildEnableWorkloadResolver=false``--no-restore`、手工 `TargetFramework` 的 workaround 命令
- 当前任务仍属于 PR #288 review follow-up因此本轮重点改为“区分哪些 AI 建议值得采纳”以及“用真实 WSL build 结果验证”
- 主线程实施:
- 运行 `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)` 收尾)
- 触发背景:
- 当前分支 PR #283 的最新 review 中,`greptile-apps[bot]` 仍有一个未解决线程,指出 `UnifiedSettingsDataRepository.CloneFile` fallback 会静默丢失原 comparer
- CodeRabbit 另指出 `AutoRegisterExportedCollectionsGeneratorTests.cs` 中还残留 5 处 `await test.RunAsync();`,与同项目其他测试文件的 `.ConfigureAwait(false)` 风格不一致
- 主线程实施:
- 复核 PR review JSON、`UnifiedSettingsDataRepository.cs``UnifiedSettingsFile.cs``AutoRegisterExportedCollectionsGeneratorTests.cs` 的当前代码,确认只有 comparer 契约线程仍属最新 head 上的实质问题
- 将 `UnifiedSettingsFile.Sections` 的 XML 注释补充为显式 comparer 契约,并把默认字典初始化改为 `StringComparer.Ordinal`
- 将 `CloneFile` fallback 从隐式默认 comparer 改为显式 `StringComparer.Ordinal`,并同步修正文档注释,避免继续暗含“保留原语义”的错误表述
- 把 `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 5 处 `await test.RunAsync();` 统一为 `.ConfigureAwait(false)`,同时让 `VerifyDiagnosticsAsync` 内部也消费 `ConfigureAwait(false)`
- 重新抓取 PR #288 review确认 latest-head open threads 为 `CodeRabbit 6 + Greptile 2`
- 复核 `outside diff + nitpick` 的 19 条建议,只采纳本地仍成立的建议;拒绝把“评论总数”机械等同于“必须全改”
- 完成以下高信号修复:
- `ContextAware*` / `AsyncExtensions` / `NumericExtensions` / `StringExtensions` / `StoreBuilder`:回退为 `ArgumentNullException.ThrowIfNull(...)`
- `ArchitectureServicesTests` / `GameContextTests`:同步 XML `<exception>``NotSupportedException`
- `RegistryInitializationHookBaseTests`:修复 override 可空签名实现,避免再次引入编译错误
- `RollingFileAppenderTests` / `TaskCoroutineExtensionsTests` / `WaitForTaskTests` / `ScopedStorage`:移除无收益噪音代码
- `FileStorage`:通过 `leaveOpen: true` 修正 `FileStream` 的双重释放语义
- `SceneRouterBase`:统一显式 `ConfigureAwait(true)` 并补齐引擎线程亲和说明
- `StoreSelection`:保留 `net9.0+``System.Threading.Lock`,同时修正条件编译旁的注释写法,避免 `CS1587`
- 验证里程碑:
- `dotnet restore GFramework.sln -p:RestoreFallbackFolders="" -v minimal`
- 结果:成功;证明先前 `MSB4018` 来自 stale restore 元数据,而不是当前 WSL 默认 build 路径本身不可用
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:成功;`28 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`533 Warning(s)``0 Error(s)``GFramework.Game` 仍有既有 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`
- 首次并行复验:失败;`FileNotFoundException`,原因是 `--no-build` 测试在 Release DLL 落盘前启动
- 串行复验:成功;`Passed: 48``Failed: 0`
- 结果:成功;`329 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`137 Warning(s)``0 Error(s)`
- 当前结论:
- PR #283 当前仍打开的 comparer review thread 已在本地代码与 XML 注释层面得到对应修复
- `AutoRegisterExportedCollectionsGeneratorTests` 的异步等待风格已与同项目其他测试保持一致
- 当前改动已通过直接受影响测试项目的 Release build 与串行 Release test 复验,可进入提交阶段
- 用户关于“WSL 里直接 `dotnet build` 可行”的判断正确
- 前一轮失败的核心原因不是仓库不可构建,而是主线程附加的 workaround 参数改变了 MSBuild 行为
- 本轮已完成 PR #288 中一组仍成立的建议修复,并重新拿到标准 WSL 路径下的 Release build 验证
- 剩余 review 线程需要在新 head 上重新抓取后再决定是否逐条 resolve
## 2026-04-24 — RP-051
## 2026-04-25 — RP-063
### 阶段:`GFramework.Godot.SourceGenerators.Tests` warning 清零
### 阶段:先收口 PR #288 latest-head 编译错误,再暂停在环境阻塞点并准备提交
- 触发背景:
- 用户要求直接运行 `dotnet clean`,不再添加额外 shell 包装solution-level `dotnet clean` 仍然在 `ValidateSolutionConfiguration` 阶段失败
- 直接执行仓库根目录 `dotnet build` 成功,并输出 `1184 warning(s)`,说明当前真实热点已从 `GFramework.Godot.SourceGenerators` 转移到对应测试项目
- 用户显式要求先执行 `$gframework-pr-review`,并指出 `AsyncExtensionsTests.cs(126,23)` 当前存在 `CS0029` / `CS1662` 构建错误
- 当前 worktree 仍是 `fix/analyzer-warning-reduction-batch`,因此本 turn 继续沿用 `analyzer-warning-reduction` 的 active recovery 文档
- 主线程实施:
- 以 `GFramework.Godot.SourceGenerators.Tests` 为独立批次,先确认该项目本地基线为 `24 warning(s)`
- 在 `BindNodeSignalGeneratorTests.cs``AutoSceneGeneratorTests.cs``AutoUiPageGeneratorTests.cs``GetNodeGeneratorTests.cs``AutoRegisterExportedCollectionsGeneratorTests.cs``GodotProjectMetadataGeneratorTests.cs` 中抽取共享 source / diagnostic helper压缩重复长方法
- 在 `Core/GeneratorTest.cs` 中补充 `ConfigureAwait(false)`,清除项目内唯一 `MA0004`
- 把 `GFramework.Godot.SourceGenerators.Tests` 项目 warning 从 `24` 降到 `0`
- 运行 PR review 抓取脚本,确认当前分支对应 PR `#288`
- 核对 latest-head unresolved review threads 后,优先修复 `AsyncExtensionsTests.cs``ct => Task.Delay(...).ConfigureAwait(false)` 错误返回 `ConfiguredTaskAwaitable` 的问题
- 顺手收敛多处已被 latest review 点名且本地仍成立的低风险残留:
- 测试中的 `async``await`
- `ValueTask` 断言包装
- `RegistryInitializationHookBaseTests.cs` 的可空返回签名
- `NumericExtensions.cs``StringExtensions.cs``StoreBuilder.cs` 的 Allman 花括号残留
- `StoreSelection.cs``net9.0+` 下切到 `System.Threading.Lock`,同时保留 `net8.0` 兼容分支
- 验证里程碑:
- `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)`
- 第一批(`BindNodeSignal` + `GeneratorTest`)后:`16 Warning(s)`
- 第二批(`AutoScene` / `AutoUiPage` / `GetNode`)后:`8 Warning(s)`
- 第三批(`Registration` / `Project`)后:`1 Warning(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`
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果:成功;确认 PR `#288` 的 latest-head unresolved AI review threads 共 `9` 个,其中 `AsyncExtensionsTests.cs:126` 为 critical 编译错误
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018``ResolvePackageAssets` 仍读取失效 Windows fallback package folder `D:\Tool\Development Tools\Microsoft Visual Studio\Shared\NuGetPackages`
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net9.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;原因同上
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;原因同上
- 当前结论:
- `GFramework.Godot.SourceGenerators.Tests` 已在 `Debug` / `Release` 构建下达到 `0 warning(s)`
- 按 `origin/main` merge-base 计算并只纳入当前暂存批次时,累计分支 diff 为 `23` 个文件,低于 `$gframework-batch-boot 75` 的主停止阈值
- 仓库根目录 `dotnet clean` 仍无法稳定产出新的 clean 基线,需要在下一轮单独排查
- 当前 worktree 已有与本批次无关的既有改动;提交时必须只暂存 analyzer warning reduction 相关文件
- 用户点名的 `AsyncExtensionsTests.cs` 编译错误已在源码层修复
- 本 turn 未能拿到新的可通过 Release build阻塞点已从先前记录的 `MSB4276` 收敛为当前 `obj/*.csproj.nuget.g.props` 中 stale Windows fallback package folder 导致的 `MSB4018`
- 用户随后要求“先不管这个了,先提交吧”,因此本 turn 在记录环境阻塞后先执行提交收口
## 2026-04-24 — RP-050
## 2026-04-25 — RP-062
### 阶段:clean-build 基线修正与 `GFramework.Godot.SourceGenerators` 切片清零
### 阶段:触达 `$gframework-batch-boot 75` 停止阈值并收口到 `75 files / 2098 lines`
- 触发背景:
- 用户确认之前的 `0 Warning(s)` 来自增量构建假阴性;只有先 `dotnet clean``dotnet build`warning 才会重新出现
- 用户给出 clean solution build 的真实结果:`Build succeeded with 1193 warning(s)`
- `RP-061` 收尾时分支相对 `origin/main` 仍只有 `48` 个已提交文件,距离本轮 `75 files` 停止条件还有明显空间
- 用户明确允许继续委派 subagent因此主线程继续把低风险机械型写集拆成互不重叠的 test / runtime 小批次
- 本轮主目标不是继续深挖单个高上下文热点,而是用新的低风险文件精确把 branch diff 推到阈值后停止
- 主线程实施:
- 纠正当前 topic 的 active todo / trace把 clean build 作为新的 warning 检查真值
- 在 `BindNodeSignalGenerator.cs``GetNodeGenerator.cs``GodotProjectMetadataGenerator.cs` 中完成分阶段方法抽取与字符串比较修正
- 在 `Registration/AutoRegisterExportedCollectionsGenerator.cs` 中拆分 `TryCreateRegistration`,清除最后一个 `MA0051`
- 更新 `AGENTS.md`,明确 warning 检查必须先 `dotnet clean``dotnet build`
- 先接受并提交 7 文件 `Core.Tests` 收尾批次为 `03c73a8` `test(core-tests): 收敛测试桩与辅助类型 warning`
- 随后主线程与多个 worker 并行收口以下新增文件:
- `ArchitectureAdditionalCqrsHandlersTests.cs`
- `RegistryInitializationHookBaseTests.cs`
- `CommandCoroutineExtensionsTests.cs`
- `TaskCoroutineExtensionsTests.cs`
- `WaitForTaskTTests.cs`
- `AsyncExtensionsTests.cs`
- `LogContextTests.cs`
- `PauseStackManagerTests.cs`
- `AsyncExtensions.cs`
- `CollectionExtensions.cs`
- `ContextAwareCommandExtensions.cs`
- `ContextAwareEnvironmentExtensions.cs`
- `ContextAwareEventExtensions.cs`
- `ContextAwareQueryExtensions.cs`
- `ContextAwareServiceExtensions.cs`
- `GuardExtensions.cs`
- `NumericExtensions.cs`
- `StoreEventBusExtensions.cs`
- `StringExtensions.cs`
- `StoreBuilder.cs`
- `StoreSelection.cs`
- 将上述 22 文件批次收口为 `9ce1fa6` `refactor(core): 收敛 Core 扩展与测试的机械 warning`
- 验证里程碑:
- `dotnet clean GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release`
- 首次验证:成功;`1 Warning(s)`,剩余 `Registration/AutoRegisterExportedCollectionsGenerator.cs(182,25)` `MA0051`
- 修复后复验:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-incremental --no-restore -p:RestoreFallbackFolders= -v:diag`
- 结果:失败;`MSB4276`,默认 SDK resolver 缺少 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator`
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:TestTargetFrameworks=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`NU1201``GFramework.Tests.Common` 仅支持 `net10.0`,不能作为 `Core.Tests` 的 net8 旁路验证
- `git diff --name-only origin/main...HEAD | wc -l`
- 结果:`75`
- `git diff --numstat origin/main...HEAD`
- 结果:累计 `1083` added、`1015` deleted`2098` changed lines
- 当前结论:
- `GFramework.Godot.SourceGenerators` 已在 clean `Release` build 下从 9 个 warning 降到 0 个 warning
- 整仓库 warning 基线仍以用户确认的 clean solution build `1193 warning(s)` 为准
- 下一轮应继续从 clean solution build 输出中选择新的低风险热点
- 本轮 `$gframework-batch-boot 75` 已精确达到主停止条件,默认恢复点应停止在 `9ce1fa6`
- `Core` runtime 的本轮机械型改动已有可通过的最小 Release build 验证
- `Core.Tests` 的继续推进当前首先受 `MSB4276` 环境阻塞影响;下一轮若要继续,应先修复构建环境,再重新建立 warning 基线
## Archive Context
## 历史归档指针
- 当前轮次归档:
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
- 历史跟踪归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
- 历史 trace 归档:
- 早期 trace 归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)