baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
229
tests/MUnique.OpenMU.Web.Tests/DebouncerTest.cs
Normal file
229
tests/MUnique.OpenMU.Web.Tests/DebouncerTest.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
// <copyright file="DebouncerTests.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Web.Tests;
|
||||
|
||||
using System.Threading;
|
||||
using MUnique.OpenMU.Web.Shared.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="Debouncer"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class DebouncerTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests that the constructor throws <see cref="ArgumentOutOfRangeException"/> when delay is zero.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Constructor_WithZeroDelay_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Debouncer(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the constructor throws <see cref="ArgumentOutOfRangeException"/> when delay is negative.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Constructor_WithNegativeDelay_ThrowsArgumentOutOfRangeException()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Debouncer(-1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a single debounced action executes successfully.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DebounceAsync_SingleCall_ExecutesAction()
|
||||
{
|
||||
using var debouncer = new Debouncer(50);
|
||||
var executed = false;
|
||||
var executionCompleted = new TaskCompletionSource<bool>();
|
||||
|
||||
await debouncer.DebounceAsync(() =>
|
||||
{
|
||||
executed = true;
|
||||
executionCompleted.TrySetResult(true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
await executionCompleted.Task;
|
||||
|
||||
Assert.That(executed, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that rapid consecutive calls only execute the last action.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DebounceAsync_RapidCalls_OnlyLastExecutes()
|
||||
{
|
||||
using var debouncer = new Debouncer(100);
|
||||
var callCount = 0;
|
||||
var lastValue = string.Empty;
|
||||
var executionCompleted = new TaskCompletionSource<bool>();
|
||||
|
||||
// Fire 5 rapid calls
|
||||
var tasks = new List<Task>();
|
||||
for (var i = 1; i <= 5; i++)
|
||||
{
|
||||
var captured = i;
|
||||
tasks.Add(debouncer.DebounceAsync(() =>
|
||||
{
|
||||
Interlocked.Increment(ref callCount);
|
||||
lastValue = $"call-{captured}";
|
||||
executionCompleted.TrySetResult(true);
|
||||
return Task.CompletedTask;
|
||||
}));
|
||||
|
||||
if (i < 5)
|
||||
{
|
||||
await Task.Delay(20); // rapid but not instant
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
await executionCompleted.Task;
|
||||
|
||||
Assert.That(callCount, Is.EqualTo(1));
|
||||
Assert.That(lastValue, Is.EqualTo("call-5"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the cancellation token overload passes the token to the action.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DebounceAsync_WithCancellationToken_PassesTokenToAction()
|
||||
{
|
||||
using var debouncer = new Debouncer(50);
|
||||
CancellationToken receivedToken = default;
|
||||
|
||||
await debouncer.DebounceAsync(token =>
|
||||
{
|
||||
receivedToken = token;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
Assert.That(receivedToken.IsCancellationRequested, Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that calling DebounceAsync after disposal returns without executing the action.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DebounceAsync_AfterDispose_ReturnsWithoutExecuting()
|
||||
{
|
||||
var debouncer = new Debouncer(50);
|
||||
debouncer.Dispose();
|
||||
var executed = false;
|
||||
|
||||
await debouncer.DebounceAsync(() =>
|
||||
{
|
||||
executed = true;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
Assert.That(executed, Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that calling Cancel while an action is pending prevents its execution.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Cancel_WhilePending_PreventsExecution()
|
||||
{
|
||||
using var debouncer = new Debouncer(200);
|
||||
var executed = false;
|
||||
var executionCompleted = new TaskCompletionSource<bool>();
|
||||
|
||||
var task = debouncer.DebounceAsync(() =>
|
||||
{
|
||||
executed = true;
|
||||
executionCompleted.TrySetResult(true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
await Task.Delay(50);
|
||||
debouncer.Cancel();
|
||||
await task;
|
||||
|
||||
Assert.That(executed, Is.False);
|
||||
Assert.That(executionCompleted.Task.IsCompleted, Is.False, "Execution should not have completed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that passing a null action throws ArgumentNullException.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void DebounceAsync_WithNullAction_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
using var debouncer = new Debouncer(50);
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(async () => await debouncer.DebounceAsync((Func<Task>)null!), Throws.TypeOf<ArgumentNullException>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that passing a null cancellable action throws ArgumentNullException.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void DebounceAsync_WithNullCancellableAction_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
using var debouncer = new Debouncer(50);
|
||||
|
||||
// Act & Assert
|
||||
Assert.That(async () => await debouncer.DebounceAsync((Func<CancellationToken, Task>)null!), Throws.TypeOf<ArgumentNullException>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that actions spaced beyond the debounced window each execute independently.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task DebounceAsync_SpacedCalls_EachExecutes()
|
||||
{
|
||||
using var debouncer = new Debouncer(50);
|
||||
var callCount = 0;
|
||||
var executionCompleted = new TaskCompletionSource<bool>();
|
||||
|
||||
// First call
|
||||
await debouncer.DebounceAsync(() =>
|
||||
{
|
||||
Interlocked.Increment(ref callCount);
|
||||
executionCompleted.TrySetResult(true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
await executionCompleted.Task;
|
||||
executionCompleted = new TaskCompletionSource<bool>();
|
||||
|
||||
// Second call - no fixed delay, just let the first complete
|
||||
await debouncer.DebounceAsync(() =>
|
||||
{
|
||||
Interlocked.Increment(ref callCount);
|
||||
executionCompleted.TrySetResult(true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
await executionCompleted.Task;
|
||||
|
||||
Assert.That(callCount, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that disposing multiple times does not throw an exception.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Dispose_MultipleTimes_DoesNotThrow()
|
||||
{
|
||||
var debouncer = new Debouncer(50);
|
||||
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
debouncer.Dispose();
|
||||
debouncer.Dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DocumentationFile>bin\Debug\MUnique.OpenMU.Web.Tests.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.Web.Tests.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\src\SharedAssemblyInfo.cs" Link="Properties\SharedAssemblyInfo.cs" />
|
||||
<Compile Include="..\..\src\SharedGlobalUsings.cs" Link="SharedGlobalUsings.cs" />
|
||||
<Compile Include="..\SharedTestUsings.cs" Link="SharedTestUsings.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\src\stylecop.json" Link="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\src\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="NUnit" />
|
||||
<PackageReference Include="NUnit3TestAdapter" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Web\Shared\MUnique.OpenMU.Web.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\src\Web\AdminPanel\MUnique.OpenMU.Web.AdminPanel.csproj" />
|
||||
<ProjectReference Include="..\..\src\Web\ItemEditor\MUnique.OpenMU.Web.ItemEditor.csproj" />
|
||||
<ProjectReference Include="..\..\src\Web\Map\MUnique.OpenMU.Web.Map.csproj" />
|
||||
<ProjectReference Include="..\..\src\Persistence\MUnique.OpenMU.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\src\Persistence\InMemory\MUnique.OpenMU.Persistence.InMemory.csproj" />
|
||||
<ProjectReference Include="..\..\src\DataModel\MUnique.OpenMU.DataModel.csproj" />
|
||||
<ProjectReference Include="..\..\src\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
|
||||
<ProjectReference Include="..\..\src\GameLogic\MUnique.OpenMU.GameLogic.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user