Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageIcon>icon.jpg</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>9.1.1</Version>
<DotNetVersion>[9.0.0,10.0.0)</DotNetVersion>
<DotNetVersion>[10.0.0,11.0.0)</DotNetVersion>
<LangVersion>default</LangVersion>
</PropertyGroup>

Expand Down
156 changes: 156 additions & 0 deletions tests/TickerQ.Tests/PaginationResultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using FluentAssertions;
using TickerQ.Utilities.Models;

namespace TickerQ.Tests;

public class PaginationResultTests
{
[Fact]
public void DefaultConstructor_InitializesEmptyItems()
{
var result = new PaginationResult<string>();

result.Items.Should().NotBeNull();
result.Items.Should().BeEmpty();
result.TotalCount.Should().Be(0);
result.PageNumber.Should().Be(0);
result.PageSize.Should().Be(0);
}

[Fact]
public void ParameterizedConstructor_SetsAllProperties()
{
var items = new[] { "a", "b", "c" };
var result = new PaginationResult<string>(items, totalCount: 10, pageNumber: 2, pageSize: 3);

result.Items.Should().BeEquivalentTo(items);
result.TotalCount.Should().Be(10);
result.PageNumber.Should().Be(2);
result.PageSize.Should().Be(3);
}

[Fact]
public void ParameterizedConstructor_HandlesNullItems()
{
var result = new PaginationResult<string>(null, totalCount: 5, pageNumber: 1, pageSize: 5);

result.Items.Should().NotBeNull();
result.Items.Should().BeEmpty();
}

[Fact]
public void TotalPages_CalculatesCorrectly()
{
var result = new PaginationResult<string>
{
TotalCount = 25,
PageSize = 10
};

result.TotalPages.Should().Be(3); // ceil(25/10)
}

[Fact]
public void TotalPages_ReturnsOne_WhenItemsFitInOnePage()
{
var result = new PaginationResult<string>
{
TotalCount = 5,
PageSize = 10
};

result.TotalPages.Should().Be(1);
}

[Fact]
public void TotalPages_ReturnsExact_WhenEvenlySplit()
{
var result = new PaginationResult<string>
{
TotalCount = 20,
PageSize = 10
};

result.TotalPages.Should().Be(2);
}

[Fact]
public void HasPreviousPage_ReturnsFalse_OnFirstPage()
{
var result = new PaginationResult<string> { PageNumber = 1 };

result.HasPreviousPage.Should().BeFalse();
}

[Fact]
public void HasPreviousPage_ReturnsTrue_OnLaterPages()
{
var result = new PaginationResult<string> { PageNumber = 2 };

result.HasPreviousPage.Should().BeTrue();
}

[Fact]
public void HasNextPage_ReturnsTrue_WhenMorePagesExist()
{
var result = new PaginationResult<string>
{
PageNumber = 1,
TotalCount = 20,
PageSize = 10
};

result.HasNextPage.Should().BeTrue();
}

[Fact]
public void HasNextPage_ReturnsFalse_OnLastPage()
{
var result = new PaginationResult<string>
{
PageNumber = 2,
TotalCount = 20,
PageSize = 10
};

result.HasNextPage.Should().BeFalse();
}

[Fact]
public void FirstItemIndex_CalculatesCorrectly()
{
var result = new PaginationResult<string>
{
PageNumber = 3,
PageSize = 10
};

result.FirstItemIndex.Should().Be(21); // (3-1)*10 + 1
}

[Fact]
public void LastItemIndex_CappedByTotalCount()
{
var result = new PaginationResult<string>
{
PageNumber = 3,
PageSize = 10,
TotalCount = 25
};

result.LastItemIndex.Should().Be(25); // Min(30, 25)
}

[Fact]
public void LastItemIndex_EqualsPageEnd_WhenFullPage()
{
var result = new PaginationResult<string>
{
PageNumber = 2,
PageSize = 10,
TotalCount = 30
};

result.LastItemIndex.Should().Be(20);
}
}
83 changes: 83 additions & 0 deletions tests/TickerQ.Tests/RestartThrottleManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using FluentAssertions;

namespace TickerQ.Tests;

public class RestartThrottleManagerTests
{
[Fact]
public async Task RequestRestart_TriggersCallback_AfterDebounceWindow()
{
var triggered = false;
using var manager = new RestartThrottleManager(() => triggered = true);

manager.RequestRestart();

// Debounce window is 50ms, give some extra time
await Task.Delay(200);

triggered.Should().BeTrue();
}

[Fact]
public async Task MultipleRequests_CoalesceIntoSingleCallback()
{
var triggerCount = 0;
using var manager = new RestartThrottleManager(() => Interlocked.Increment(ref triggerCount));

// Multiple rapid requests should coalesce
manager.RequestRestart();
manager.RequestRestart();
manager.RequestRestart();

await Task.Delay(200);

triggerCount.Should().Be(1);
}

[Fact]
public async Task RequestRestart_ResetsTimer_OnSubsequentCalls()
{
var triggerCount = 0;
using var manager = new RestartThrottleManager(() => Interlocked.Increment(ref triggerCount));

manager.RequestRestart();
await Task.Delay(30); // Less than debounce window (50ms)
manager.RequestRestart(); // Should reset the timer
await Task.Delay(30); // Still less than full window from second request

// Should not have triggered yet since timer was reset
// After full debounce from the last request it should trigger
await Task.Delay(100);

triggerCount.Should().Be(1);
}

[Fact]
public void Dispose_CanBeCalledSafely()
{
var manager = new RestartThrottleManager(() => { });
var act = () => manager.Dispose();

act.Should().NotThrow();
}

[Fact]
public void Dispose_BeforeAnyRequest_DoesNotThrow()
{
var manager = new RestartThrottleManager(() => { });

// Timer hasn't been created yet (lazy creation)
var act = () => manager.Dispose();
act.Should().NotThrow();
}

[Fact]
public void Dispose_AfterRequest_DoesNotThrow()
{
var manager = new RestartThrottleManager(() => { });
manager.RequestRestart();

var act = () => manager.Dispose();
act.Should().NotThrow();
}
}
113 changes: 113 additions & 0 deletions tests/TickerQ.Tests/SoftSchedulerNotifyDebounceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using FluentAssertions;

namespace TickerQ.Tests;

public class SoftSchedulerNotifyDebounceTests
{
[Fact]
public void NotifySafely_InvokesCallback_WithLatestValue()
{
var receivedValues = new List<string>();
using var debounce = new SoftSchedulerNotifyDebounce(v => receivedValues.Add(v));

debounce.NotifySafely(5);

receivedValues.Should().Contain("5");
}

[Fact]
public void NotifySafely_SuppressesDuplicateValues()
{
var receivedValues = new List<string>();
using var debounce = new SoftSchedulerNotifyDebounce(v => receivedValues.Add(v));

debounce.NotifySafely(3);
var countAfterFirst = receivedValues.Count;

debounce.NotifySafely(3);
var countAfterSecond = receivedValues.Count;

// Second call with same non-zero value should be suppressed
countAfterSecond.Should().Be(countAfterFirst);
}

[Fact]
public void NotifySafely_AllowsDifferentValues()
{
var receivedValues = new List<string>();
using var debounce = new SoftSchedulerNotifyDebounce(v => receivedValues.Add(v));

debounce.NotifySafely(1);
debounce.NotifySafely(2);

receivedValues.Should().Contain("1");
receivedValues.Should().Contain("2");
}

[Fact]
public void Flush_InvokesCallbackImmediately()
{
var receivedValues = new List<string>();
using var debounce = new SoftSchedulerNotifyDebounce(v => receivedValues.Add(v));

debounce.NotifySafely(10);
receivedValues.Clear();

debounce.NotifySafely(20);
debounce.Flush();

// Flush should ensure the latest value is pushed
receivedValues.Should().NotBeEmpty();
}

[Fact]
public void NotifySafely_DoesNotInvoke_AfterDispose()
{
var receivedValues = new List<string>();
var debounce = new SoftSchedulerNotifyDebounce(v => receivedValues.Add(v));

debounce.Dispose();
var countBeforeNotify = receivedValues.Count;

debounce.NotifySafely(100);

receivedValues.Count.Should().Be(countBeforeNotify);
}

[Fact]
public void Dispose_CanBeCalledMultipleTimes()
{
var debounce = new SoftSchedulerNotifyDebounce(_ => { });

var act = () =>
{
debounce.Dispose();
debounce.Dispose();
};

act.Should().NotThrow();
}

[Fact]
public void NotifySafely_AlwaysInvokes_ForZeroValue()
{
var callCount = 0;
using var debounce = new SoftSchedulerNotifyDebounce(_ => callCount++);

// Zero is special: it always triggers the callback
// (the code checks `latest != 0 && latest == last` for suppression)
debounce.NotifySafely(0);
debounce.NotifySafely(0);

callCount.Should().BeGreaterOrEqualTo(2);
}

[Fact]
public void Constructor_DoesNotInvoke_Callback()
{
var callCount = 0;
using var debounce = new SoftSchedulerNotifyDebounce(_ => callCount++);

callCount.Should().Be(0);
}
}
Loading
Loading