Skip to content

Commit 3d4d19b

Browse files
perditavojoCopilot
andcommitted
test: add FormInputStateManager and AnnouncementService tests (+19 tests)
- FormInputStateManagerTests: 15 tests for TryBegin*/End* Interlocked state flags - AnnouncementServiceTests: 4 tests for Enqueue guard logic and Dispose idempotency Total: 150 -> 169 tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3787f37 commit 3d4d19b

2 files changed

Lines changed: 308 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using InputBox.Core.Services;
2+
using Xunit;
3+
4+
namespace InputBox.Tests;
5+
6+
/// <summary>
7+
/// AnnouncementService 的入隊守衛邏輯與生命週期行為測試
8+
/// <para>驗證空字串過濾、Dispose 後不再處理,以及有效訊息最終被播報的基本合約。</para>
9+
/// </summary>
10+
public class AnnouncementServiceTests : IDisposable
11+
{
12+
private AnnouncementService? _svc;
13+
14+
/// <summary>
15+
/// 在每個測試後確保服務被正確釋放。
16+
/// </summary>
17+
public void Dispose()
18+
{
19+
_svc?.Dispose();
20+
_svc = null;
21+
}
22+
23+
// ── Enqueue 守衛邏輯 ────────────────────────────────────────────────────
24+
25+
/// <summary>
26+
/// Enqueue 空字串時,委派不應被呼叫(守衛提前返回)。
27+
/// </summary>
28+
[Fact]
29+
public async Task Enqueue_EmptyString_DoesNotInvokeDelegate()
30+
{
31+
bool invoked = false;
32+
33+
_svc = new AnnouncementService(async (msg, interrupt, ct) =>
34+
{
35+
invoked = true;
36+
await Task.CompletedTask;
37+
});
38+
39+
_svc.Enqueue(string.Empty);
40+
41+
// 等待足夠時間讓背景工作有機會執行
42+
await Task.Delay(600);
43+
44+
Assert.False(invoked);
45+
}
46+
47+
/// <summary>
48+
/// Dispose 後 Enqueue 有效訊息,委派不應被呼叫(已設置 disposeSignaled)。
49+
/// </summary>
50+
[Fact]
51+
public async Task Enqueue_AfterDispose_DoesNotInvokeDelegate()
52+
{
53+
bool invoked = false;
54+
55+
_svc = new AnnouncementService(async (msg, interrupt, ct) =>
56+
{
57+
invoked = true;
58+
await Task.CompletedTask;
59+
});
60+
61+
_svc.Dispose();
62+
_svc.Enqueue("hello after dispose");
63+
64+
await Task.Delay(600);
65+
66+
Assert.False(invoked);
67+
}
68+
69+
/// <summary>
70+
/// 有效訊息應在背景延遲後被委派接收並播報。
71+
/// </summary>
72+
[Fact]
73+
public async Task Enqueue_ValidMessage_EventuallyInvokesDelegate()
74+
{
75+
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
76+
77+
_svc = new AnnouncementService(async (msg, interrupt, ct) =>
78+
{
79+
tcs.TrySetResult(msg);
80+
await Task.CompletedTask;
81+
});
82+
83+
_svc.Enqueue("測試廣播");
84+
85+
// 等待最多 2 秒讓背景工作完成(含 ducking 延遲與 jitter)
86+
string? received = await Task.WhenAny(tcs.Task, Task.Delay(2000)) == tcs.Task
87+
? tcs.Task.Result
88+
: null;
89+
90+
Assert.Equal("測試廣播", received);
91+
}
92+
93+
/// <summary>
94+
/// Dispose 應可安全呼叫多次而不拋出例外(冪等保護)。
95+
/// </summary>
96+
[Fact]
97+
public void Dispose_CalledMultipleTimes_DoesNotThrow()
98+
{
99+
_svc = new AnnouncementService(async (_, _, _) => await Task.CompletedTask);
100+
101+
var ex = Record.Exception(() =>
102+
{
103+
_svc.Dispose();
104+
_svc.Dispose();
105+
});
106+
107+
Assert.Null(ex);
108+
}
109+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
using InputBox.Core.Input;
2+
using Xunit;
3+
4+
namespace InputBox.Tests;
5+
6+
/// <summary>
7+
/// FormInputStateManager 的 Interlocked 狀態旗標管理邏輯測試
8+
/// <para>驗證所有「嘗試進入 / 結束」的互斥保護行為,確保並行狀態機正確防止重入。</para>
9+
/// </summary>
10+
public class FormInputStateManagerTests
11+
{
12+
// ── TryBeginReturning / EndReturning ───────────────────────────────────
13+
14+
/// <summary>
15+
/// 初始狀態下 TryBeginReturning 應成功(回傳 true)。
16+
/// </summary>
17+
[Fact]
18+
public void TryBeginReturning_WhenIdle_ReturnsTrue()
19+
{
20+
var mgr = new FormInputStateManager();
21+
Assert.True(mgr.TryBeginReturning());
22+
}
23+
24+
/// <summary>
25+
/// 已進入流程時,再次 TryBeginReturning 應失敗(回傳 false),防止重入。
26+
/// </summary>
27+
[Fact]
28+
public void TryBeginReturning_WhenAlreadyReturning_ReturnsFalse()
29+
{
30+
var mgr = new FormInputStateManager();
31+
mgr.TryBeginReturning();
32+
Assert.False(mgr.TryBeginReturning());
33+
}
34+
35+
/// <summary>
36+
/// EndReturning 後,TryBeginReturning 應再次成功。
37+
/// </summary>
38+
[Fact]
39+
public void EndReturning_ReleasesLock_AllowsNextTryBegin()
40+
{
41+
var mgr = new FormInputStateManager();
42+
mgr.TryBeginReturning();
43+
mgr.EndReturning();
44+
Assert.True(mgr.TryBeginReturning());
45+
}
46+
47+
// ── TryBeginTouchKeyboard / EndTouchKeyboard ───────────────────────────
48+
49+
/// <summary>
50+
/// 初始狀態下 TryBeginTouchKeyboard 應成功。
51+
/// </summary>
52+
[Fact]
53+
public void TryBeginTouchKeyboard_WhenIdle_ReturnsTrue()
54+
{
55+
var mgr = new FormInputStateManager();
56+
Assert.True(mgr.TryBeginTouchKeyboard());
57+
}
58+
59+
/// <summary>
60+
/// 已進入流程時,再次嘗試應失敗。
61+
/// </summary>
62+
[Fact]
63+
public void TryBeginTouchKeyboard_WhenBusy_ReturnsFalse()
64+
{
65+
var mgr = new FormInputStateManager();
66+
mgr.TryBeginTouchKeyboard();
67+
Assert.False(mgr.TryBeginTouchKeyboard());
68+
}
69+
70+
/// <summary>
71+
/// EndTouchKeyboard 後 IsShowingTouchKeyboard 屬性應為 false。
72+
/// </summary>
73+
[Fact]
74+
public void EndTouchKeyboard_ResetsIsShowingTouchKeyboard()
75+
{
76+
var mgr = new FormInputStateManager();
77+
mgr.TryBeginTouchKeyboard();
78+
mgr.EndTouchKeyboard();
79+
Assert.False(mgr.IsShowingTouchKeyboard);
80+
}
81+
82+
/// <summary>
83+
/// TryBeginTouchKeyboard 成功後,IsShowingTouchKeyboard 應為 true。
84+
/// </summary>
85+
[Fact]
86+
public void IsShowingTouchKeyboard_AfterTryBegin_IsTrue()
87+
{
88+
var mgr = new FormInputStateManager();
89+
mgr.TryBeginTouchKeyboard();
90+
Assert.True(mgr.IsShowingTouchKeyboard);
91+
}
92+
93+
// ── TryBeginFlashing / EndFlashing ─────────────────────────────────────
94+
95+
/// <summary>
96+
/// 閃爍流程的互斥保護:進入後再次嘗試應失敗。
97+
/// </summary>
98+
[Fact]
99+
public void TryBeginFlashing_WhenBusy_ReturnsFalse()
100+
{
101+
var mgr = new FormInputStateManager();
102+
mgr.TryBeginFlashing();
103+
Assert.False(mgr.TryBeginFlashing());
104+
}
105+
106+
/// <summary>
107+
/// EndFlashing 後應允許再次進入閃爍流程。
108+
/// </summary>
109+
[Fact]
110+
public void EndFlashing_AllowsNextTryBeginFlashing()
111+
{
112+
var mgr = new FormInputStateManager();
113+
mgr.TryBeginFlashing();
114+
mgr.EndFlashing();
115+
Assert.True(mgr.TryBeginFlashing());
116+
}
117+
118+
// ── TryBeginProcessingActivated / EndProcessingActivated ───────────────
119+
120+
/// <summary>
121+
/// Activated 事件處理流程的互斥保護:進入後再次嘗試應失敗。
122+
/// </summary>
123+
[Fact]
124+
public void TryBeginProcessingActivated_WhenBusy_ReturnsFalse()
125+
{
126+
var mgr = new FormInputStateManager();
127+
mgr.TryBeginProcessingActivated();
128+
Assert.False(mgr.TryBeginProcessingActivated());
129+
}
130+
131+
/// <summary>
132+
/// EndProcessingActivated 後應允許再次進入。
133+
/// </summary>
134+
[Fact]
135+
public void EndProcessingActivated_AllowsNextTryBegin()
136+
{
137+
var mgr = new FormInputStateManager();
138+
mgr.TryBeginProcessingActivated();
139+
mgr.EndProcessingActivated();
140+
Assert.True(mgr.TryBeginProcessingActivated());
141+
}
142+
143+
// ── BeginHotkeyCapture / EndHotkeyCapture / IsHotkeyCaptureActive ──────
144+
145+
/// <summary>
146+
/// 初始狀態下 IsHotkeyCaptureActive 應為 false。
147+
/// </summary>
148+
[Fact]
149+
public void IsHotkeyCaptureActive_Initially_IsFalse()
150+
{
151+
var mgr = new FormInputStateManager();
152+
Assert.False(mgr.IsHotkeyCaptureActive);
153+
}
154+
155+
/// <summary>
156+
/// BeginHotkeyCapture 後 IsHotkeyCaptureActive 應為 true。
157+
/// </summary>
158+
[Fact]
159+
public void BeginHotkeyCapture_SetsIsHotkeyCaptureActiveTrue()
160+
{
161+
var mgr = new FormInputStateManager();
162+
mgr.BeginHotkeyCapture();
163+
Assert.True(mgr.IsHotkeyCaptureActive);
164+
}
165+
166+
/// <summary>
167+
/// EndHotkeyCapture 後 IsHotkeyCaptureActive 應為 false。
168+
/// </summary>
169+
[Fact]
170+
public void EndHotkeyCapture_ClearsIsHotkeyCaptureActive()
171+
{
172+
var mgr = new FormInputStateManager();
173+
mgr.BeginHotkeyCapture();
174+
mgr.EndHotkeyCapture();
175+
Assert.False(mgr.IsHotkeyCaptureActive);
176+
}
177+
178+
// ── 各流程互不干擾 ──────────────────────────────────────────────────────
179+
180+
/// <summary>
181+
/// 各個狀態旗標應彼此獨立:進入 Returning 不影響 Flashing 的 TryBegin。
182+
/// </summary>
183+
[Fact]
184+
public void MultipleLocks_AreIndependent()
185+
{
186+
var mgr = new FormInputStateManager();
187+
mgr.TryBeginReturning();
188+
mgr.TryBeginFlashing();
189+
190+
// 各自被鎖定
191+
Assert.False(mgr.TryBeginReturning());
192+
Assert.False(mgr.TryBeginFlashing());
193+
194+
// 釋放其中一個不影響另一個
195+
mgr.EndReturning();
196+
Assert.True(mgr.TryBeginReturning());
197+
Assert.False(mgr.TryBeginFlashing());
198+
}
199+
}

0 commit comments

Comments
 (0)