Skip to content

Commit 61c72a8

Browse files
arcenoxclaude
andauthored
Add comprehensive unit tests for core TickerQ components (#582)
New test files covering must-have areas that were previously untested: - TickerExecutionTaskHandlerTests: success/failure/cancellation/terminate execution paths, parent-child execution with RunCondition logic, instrumentation logging, null delegate handling - TickerQDispatcherTests: constructor guards, dispatch delegation, empty/null context handling, priority routing - TerminateExecutionExceptionTests: all constructors, status defaults, inner exception preservation - SoftSchedulerNotifyDebounceTests: notify callback, duplicate suppression, flush, dispose idempotency, zero-value handling - RestartThrottleManagerTests: debounce coalescing, timer reset, dispose safety - WorkItemTests: constructor validation, null guard, token handling - PaginationResultTests: computed properties (TotalPages, HasPreviousPage, HasNextPage, FirstItemIndex, LastItemIndex), edge cases - TickerResultTests: all internal constructors via reflection - TickerHelperTests: serialization with/without GZip, round-trip, custom JSON options, complex objects https://claude.ai/code/session_01AmFBuKfp4VyfuRCviW7meu Co-authored-by: Claude <noreply@anthropic.com>
1 parent b2684f4 commit 61c72a8

9 files changed

+1506
-0
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using FluentAssertions;
2+
using TickerQ.Utilities.Models;
3+
4+
namespace TickerQ.Tests;
5+
6+
public class PaginationResultTests
7+
{
8+
[Fact]
9+
public void DefaultConstructor_InitializesEmptyItems()
10+
{
11+
var result = new PaginationResult<string>();
12+
13+
result.Items.Should().NotBeNull();
14+
result.Items.Should().BeEmpty();
15+
result.TotalCount.Should().Be(0);
16+
result.PageNumber.Should().Be(0);
17+
result.PageSize.Should().Be(0);
18+
}
19+
20+
[Fact]
21+
public void ParameterizedConstructor_SetsAllProperties()
22+
{
23+
var items = new[] { "a", "b", "c" };
24+
var result = new PaginationResult<string>(items, totalCount: 10, pageNumber: 2, pageSize: 3);
25+
26+
result.Items.Should().BeEquivalentTo(items);
27+
result.TotalCount.Should().Be(10);
28+
result.PageNumber.Should().Be(2);
29+
result.PageSize.Should().Be(3);
30+
}
31+
32+
[Fact]
33+
public void ParameterizedConstructor_HandlesNullItems()
34+
{
35+
var result = new PaginationResult<string>(null, totalCount: 5, pageNumber: 1, pageSize: 5);
36+
37+
result.Items.Should().NotBeNull();
38+
result.Items.Should().BeEmpty();
39+
}
40+
41+
[Fact]
42+
public void TotalPages_CalculatesCorrectly()
43+
{
44+
var result = new PaginationResult<string>
45+
{
46+
TotalCount = 25,
47+
PageSize = 10
48+
};
49+
50+
result.TotalPages.Should().Be(3); // ceil(25/10)
51+
}
52+
53+
[Fact]
54+
public void TotalPages_ReturnsOne_WhenItemsFitInOnePage()
55+
{
56+
var result = new PaginationResult<string>
57+
{
58+
TotalCount = 5,
59+
PageSize = 10
60+
};
61+
62+
result.TotalPages.Should().Be(1);
63+
}
64+
65+
[Fact]
66+
public void TotalPages_ReturnsExact_WhenEvenlySplit()
67+
{
68+
var result = new PaginationResult<string>
69+
{
70+
TotalCount = 20,
71+
PageSize = 10
72+
};
73+
74+
result.TotalPages.Should().Be(2);
75+
}
76+
77+
[Fact]
78+
public void HasPreviousPage_ReturnsFalse_OnFirstPage()
79+
{
80+
var result = new PaginationResult<string> { PageNumber = 1 };
81+
82+
result.HasPreviousPage.Should().BeFalse();
83+
}
84+
85+
[Fact]
86+
public void HasPreviousPage_ReturnsTrue_OnLaterPages()
87+
{
88+
var result = new PaginationResult<string> { PageNumber = 2 };
89+
90+
result.HasPreviousPage.Should().BeTrue();
91+
}
92+
93+
[Fact]
94+
public void HasNextPage_ReturnsTrue_WhenMorePagesExist()
95+
{
96+
var result = new PaginationResult<string>
97+
{
98+
PageNumber = 1,
99+
TotalCount = 20,
100+
PageSize = 10
101+
};
102+
103+
result.HasNextPage.Should().BeTrue();
104+
}
105+
106+
[Fact]
107+
public void HasNextPage_ReturnsFalse_OnLastPage()
108+
{
109+
var result = new PaginationResult<string>
110+
{
111+
PageNumber = 2,
112+
TotalCount = 20,
113+
PageSize = 10
114+
};
115+
116+
result.HasNextPage.Should().BeFalse();
117+
}
118+
119+
[Fact]
120+
public void FirstItemIndex_CalculatesCorrectly()
121+
{
122+
var result = new PaginationResult<string>
123+
{
124+
PageNumber = 3,
125+
PageSize = 10
126+
};
127+
128+
result.FirstItemIndex.Should().Be(21); // (3-1)*10 + 1
129+
}
130+
131+
[Fact]
132+
public void LastItemIndex_CappedByTotalCount()
133+
{
134+
var result = new PaginationResult<string>
135+
{
136+
PageNumber = 3,
137+
PageSize = 10,
138+
TotalCount = 25
139+
};
140+
141+
result.LastItemIndex.Should().Be(25); // Min(30, 25)
142+
}
143+
144+
[Fact]
145+
public void LastItemIndex_EqualsPageEnd_WhenFullPage()
146+
{
147+
var result = new PaginationResult<string>
148+
{
149+
PageNumber = 2,
150+
PageSize = 10,
151+
TotalCount = 30
152+
};
153+
154+
result.LastItemIndex.Should().Be(20);
155+
}
156+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using FluentAssertions;
2+
3+
namespace TickerQ.Tests;
4+
5+
public class RestartThrottleManagerTests
6+
{
7+
[Fact]
8+
public async Task RequestRestart_TriggersCallback_AfterDebounceWindow()
9+
{
10+
var triggered = false;
11+
using var manager = new RestartThrottleManager(() => triggered = true);
12+
13+
manager.RequestRestart();
14+
15+
// Debounce window is 50ms, give some extra time
16+
await Task.Delay(200);
17+
18+
triggered.Should().BeTrue();
19+
}
20+
21+
[Fact]
22+
public async Task MultipleRequests_CoalesceIntoSingleCallback()
23+
{
24+
var triggerCount = 0;
25+
using var manager = new RestartThrottleManager(() => Interlocked.Increment(ref triggerCount));
26+
27+
// Multiple rapid requests should coalesce
28+
manager.RequestRestart();
29+
manager.RequestRestart();
30+
manager.RequestRestart();
31+
32+
await Task.Delay(200);
33+
34+
triggerCount.Should().Be(1);
35+
}
36+
37+
[Fact]
38+
public async Task RequestRestart_ResetsTimer_OnSubsequentCalls()
39+
{
40+
var triggerCount = 0;
41+
using var manager = new RestartThrottleManager(() => Interlocked.Increment(ref triggerCount));
42+
43+
manager.RequestRestart();
44+
await Task.Delay(30); // Less than debounce window (50ms)
45+
manager.RequestRestart(); // Should reset the timer
46+
await Task.Delay(30); // Still less than full window from second request
47+
48+
// Should not have triggered yet since timer was reset
49+
// After full debounce from the last request it should trigger
50+
await Task.Delay(100);
51+
52+
triggerCount.Should().Be(1);
53+
}
54+
55+
[Fact]
56+
public void Dispose_CanBeCalledSafely()
57+
{
58+
var manager = new RestartThrottleManager(() => { });
59+
var act = () => manager.Dispose();
60+
61+
act.Should().NotThrow();
62+
}
63+
64+
[Fact]
65+
public void Dispose_BeforeAnyRequest_DoesNotThrow()
66+
{
67+
var manager = new RestartThrottleManager(() => { });
68+
69+
// Timer hasn't been created yet (lazy creation)
70+
var act = () => manager.Dispose();
71+
act.Should().NotThrow();
72+
}
73+
74+
[Fact]
75+
public void Dispose_AfterRequest_DoesNotThrow()
76+
{
77+
var manager = new RestartThrottleManager(() => { });
78+
manager.RequestRestart();
79+
80+
var act = () => manager.Dispose();
81+
act.Should().NotThrow();
82+
}
83+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using FluentAssertions;
2+
3+
namespace TickerQ.Tests;
4+
5+
public class SoftSchedulerNotifyDebounceTests
6+
{
7+
[Fact]
8+
public void NotifySafely_InvokesCallback_WithLatestValue()
9+
{
10+
var receivedValues = new List<string>();
11+
using var debounce = new SoftSchedulerNotifyDebounce(v => receivedValues.Add(v));
12+
13+
debounce.NotifySafely(5);
14+
15+
receivedValues.Should().Contain("5");
16+
}
17+
18+
[Fact]
19+
public void NotifySafely_SuppressesDuplicateValues()
20+
{
21+
var receivedValues = new List<string>();
22+
using var debounce = new SoftSchedulerNotifyDebounce(v => receivedValues.Add(v));
23+
24+
debounce.NotifySafely(3);
25+
var countAfterFirst = receivedValues.Count;
26+
27+
debounce.NotifySafely(3);
28+
var countAfterSecond = receivedValues.Count;
29+
30+
// Second call with same non-zero value should be suppressed
31+
countAfterSecond.Should().Be(countAfterFirst);
32+
}
33+
34+
[Fact]
35+
public void NotifySafely_AllowsDifferentValues()
36+
{
37+
var receivedValues = new List<string>();
38+
using var debounce = new SoftSchedulerNotifyDebounce(v => receivedValues.Add(v));
39+
40+
debounce.NotifySafely(1);
41+
debounce.NotifySafely(2);
42+
43+
receivedValues.Should().Contain("1");
44+
receivedValues.Should().Contain("2");
45+
}
46+
47+
[Fact]
48+
public void Flush_InvokesCallbackImmediately()
49+
{
50+
var receivedValues = new List<string>();
51+
using var debounce = new SoftSchedulerNotifyDebounce(v => receivedValues.Add(v));
52+
53+
debounce.NotifySafely(10);
54+
receivedValues.Clear();
55+
56+
debounce.NotifySafely(20);
57+
debounce.Flush();
58+
59+
// Flush should ensure the latest value is pushed
60+
receivedValues.Should().NotBeEmpty();
61+
}
62+
63+
[Fact]
64+
public void NotifySafely_DoesNotInvoke_AfterDispose()
65+
{
66+
var receivedValues = new List<string>();
67+
var debounce = new SoftSchedulerNotifyDebounce(v => receivedValues.Add(v));
68+
69+
debounce.Dispose();
70+
var countBeforeNotify = receivedValues.Count;
71+
72+
debounce.NotifySafely(100);
73+
74+
receivedValues.Count.Should().Be(countBeforeNotify);
75+
}
76+
77+
[Fact]
78+
public void Dispose_CanBeCalledMultipleTimes()
79+
{
80+
var debounce = new SoftSchedulerNotifyDebounce(_ => { });
81+
82+
var act = () =>
83+
{
84+
debounce.Dispose();
85+
debounce.Dispose();
86+
};
87+
88+
act.Should().NotThrow();
89+
}
90+
91+
[Fact]
92+
public void NotifySafely_AlwaysInvokes_ForZeroValue()
93+
{
94+
var callCount = 0;
95+
using var debounce = new SoftSchedulerNotifyDebounce(_ => callCount++);
96+
97+
// Zero is special: it always triggers the callback
98+
// (the code checks `latest != 0 && latest == last` for suppression)
99+
debounce.NotifySafely(0);
100+
debounce.NotifySafely(0);
101+
102+
callCount.Should().BeGreaterOrEqualTo(2);
103+
}
104+
105+
[Fact]
106+
public void Constructor_DoesNotInvoke_Callback()
107+
{
108+
var callCount = 0;
109+
using var debounce = new SoftSchedulerNotifyDebounce(_ => callCount++);
110+
111+
callCount.Should().Be(0);
112+
}
113+
}

0 commit comments

Comments
 (0)