Skip to content

Commit 6be82f2

Browse files
feat(util): add JAction parallel execution and improve test coverage (#607)
* feat(util): add parallel execution support to JAction - Add JActionExecutionContext for per-execution state isolation - Add JActionExecution result struct with per-execution Cancelled state - Add JActionExecutionHandle for per-execution cancellation control - Execute/ExecuteAsync now snapshot tasks enabling safe parallel execution - Switch from ValueTask to UniTask for better Unity integration - Add comprehensive tests for parallel execution, timeouts, and edge cases - Add MessageBox test hooks for pool state inspection - Update jaction skill documentation with new API and patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net> * fix(util): ensure proper disposal in tests with try-finally Address CodeQL warnings about potential missed Dispose() calls by wrapping test code in try-finally blocks. Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net> * fix: address code review feedback - Fix Task.CompletedTask -> UniTask.CompletedTask - Fix AsUniTask to delegate to awaiter (prevent double pool return) - Fix Reset to return execution contexts to pool before clearing - Fix MessageBox test hooks to use direct index access (avoid foreach) - Add comment explaining PlayerLoop single-threading in parallel test Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net> * refactor(util): use 'using' statements instead of try-finally Convert try-finally patterns to 'using' declarations for cleaner resource management as suggested by CodeQL static analysis. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net> * fix(ui): use LINQ First() instead of array indexing on HashSet HashSet does not support array indexing. Use LINQ First() method to get the first element from the collection. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net> --------- Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8d8ff35 commit 6be82f2

File tree

17 files changed

+2276
-633
lines changed

17 files changed

+2276
-633
lines changed

.claude-plugin/skills/jaction/SKILL.md

Lines changed: 203 additions & 131 deletions
Large diffs are not rendered by default.

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
using System;
2727
using System.Collections.Generic;
28+
using System.Linq;
2829
using System.Runtime.CompilerServices;
2930
using Cysharp.Threading.Tasks;
3031
using TMPro;
@@ -124,6 +125,64 @@ private static GameObject Prefab
124125
internal static bool SimulateNoPrefab;
125126
#endif
126127

128+
#if UNITY_INCLUDE_TESTS
129+
/// <summary>
130+
/// Test hook: Gets the pool state for verification in tests.
131+
/// Returns (activeCount, pooledCount).
132+
/// </summary>
133+
internal static (int activeCount, int pooledCount) TestGetPoolState()
134+
{
135+
return (ActiveMessageBoxes.Count, PooledMessageBoxes.Count);
136+
}
137+
138+
/// <summary>
139+
/// Test hook: Simulates clicking a button on the most recently shown message box.
140+
/// </summary>
141+
/// <param name="clickOk">If true, simulates clicking OK; otherwise simulates clicking Cancel.</param>
142+
/// <returns>True if a message box was found and the click was simulated.</returns>
143+
internal static bool TestSimulateButtonClick(bool clickOk)
144+
{
145+
if (ActiveMessageBoxes.Count == 0) return false;
146+
147+
// Get the first message box (any will do for testing)
148+
var target = ActiveMessageBoxes.First();
149+
target.HandleEvent(clickOk);
150+
return true;
151+
}
152+
153+
/// <summary>
154+
/// Test hook: Gets the button visibility state of the most recently shown message box.
155+
/// </summary>
156+
/// <returns>Tuple of (okButtonVisible, noButtonVisible), or null if no active boxes.</returns>
157+
internal static (bool okVisible, bool noVisible)? TestGetButtonVisibility()
158+
{
159+
if (ActiveMessageBoxes.Count == 0) return null;
160+
161+
var target = ActiveMessageBoxes.First();
162+
if (target._buttonOk == null || target._buttonNo == null)
163+
return null;
164+
165+
return (target._buttonOk.gameObject.activeSelf, target._buttonNo.gameObject.activeSelf);
166+
}
167+
168+
/// <summary>
169+
/// Test hook: Gets the text content of the most recently shown message box.
170+
/// </summary>
171+
/// <returns>Tuple of (title, content, okText, noText), or null if no active boxes.</returns>
172+
internal static (string title, string content, string okText, string noText)? TestGetContent()
173+
{
174+
if (ActiveMessageBoxes.Count == 0) return null;
175+
176+
var target = ActiveMessageBoxes.First();
177+
return (
178+
target._title?.text,
179+
target._content?.text,
180+
target._textOk?.text,
181+
target._textNo?.text
182+
);
183+
}
184+
#endif
185+
127186
private TextMeshProUGUI _content;
128187
private TextMeshProUGUI _textNo;
129188
private TextMeshProUGUI _textOk;

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/MessageBoxTests.cs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,5 +274,259 @@ public IEnumerator Show_MultipleCalls_AllReturnFalse_WhenNoPrefab() => UniTask.T
274274
});
275275

276276
#endregion
277+
278+
#region Pool State Tests (using test hooks)
279+
280+
[UnityTest]
281+
public IEnumerator Show_IncrementsActiveCount_WhenUsingTestHandler() => UniTask.ToCoroutine(async () =>
282+
{
283+
// Note: When TestHandler is set, the actual UI is bypassed,
284+
// so ActiveCount won't increase. This test verifies the expected behavior.
285+
MessageBox.TestHandler = (_, _, _, _) => UniTask.FromResult(true);
286+
287+
var (initialActive, _) = MessageBox.TestGetPoolState();
288+
await MessageBox.Show("Test", "Content");
289+
var (finalActive, _) = MessageBox.TestGetPoolState();
290+
291+
// With TestHandler, no actual MessageBox is created
292+
Assert.AreEqual(initialActive, finalActive);
293+
});
294+
295+
[Test]
296+
public void TestGetPoolState_ReturnsCorrectInitialState()
297+
{
298+
var (activeCount, pooledCount) = MessageBox.TestGetPoolState();
299+
300+
Assert.AreEqual(0, activeCount);
301+
Assert.AreEqual(0, pooledCount);
302+
}
303+
304+
[Test]
305+
public void TestGetPoolState_AfterDispose_ReturnsZero()
306+
{
307+
MessageBox.Dispose();
308+
309+
var (activeCount, pooledCount) = MessageBox.TestGetPoolState();
310+
311+
Assert.AreEqual(0, activeCount);
312+
Assert.AreEqual(0, pooledCount);
313+
}
314+
315+
[Test]
316+
public void TestSimulateButtonClick_ReturnsFalse_WhenNoActiveBoxes()
317+
{
318+
bool result = MessageBox.TestSimulateButtonClick(true);
319+
320+
Assert.IsFalse(result);
321+
}
322+
323+
[Test]
324+
public void TestGetButtonVisibility_ReturnsNull_WhenNoActiveBoxes()
325+
{
326+
var result = MessageBox.TestGetButtonVisibility();
327+
328+
Assert.IsNull(result);
329+
}
330+
331+
[Test]
332+
public void TestGetContent_ReturnsNull_WhenNoActiveBoxes()
333+
{
334+
var result = MessageBox.TestGetContent();
335+
336+
Assert.IsNull(result);
337+
}
338+
339+
#endregion
340+
341+
#region Button Visibility Tests
342+
343+
[UnityTest]
344+
public IEnumerator Show_EmptyOkText_PassesToHandler() => UniTask.ToCoroutine(async () =>
345+
{
346+
string receivedOk = "not-empty";
347+
348+
MessageBox.TestHandler = (_, _, ok, _) =>
349+
{
350+
receivedOk = ok;
351+
return UniTask.FromResult(true);
352+
};
353+
354+
await MessageBox.Show("Title", "Content", "", "Cancel");
355+
356+
// Empty string is passed through
357+
Assert.AreEqual("", receivedOk);
358+
});
359+
360+
[UnityTest]
361+
public IEnumerator Show_EmptyNoText_PassesToHandler() => UniTask.ToCoroutine(async () =>
362+
{
363+
string receivedNo = "not-empty";
364+
365+
MessageBox.TestHandler = (_, _, _, no) =>
366+
{
367+
receivedNo = no;
368+
return UniTask.FromResult(true);
369+
};
370+
371+
await MessageBox.Show("Title", "Content", "OK", "");
372+
373+
// Empty string is passed through
374+
Assert.AreEqual("", receivedNo);
375+
});
376+
377+
[UnityTest]
378+
public IEnumerator Show_NullOkText_PassesToHandler() => UniTask.ToCoroutine(async () =>
379+
{
380+
string receivedOk = "not-null";
381+
382+
MessageBox.TestHandler = (_, _, ok, _) =>
383+
{
384+
receivedOk = ok;
385+
return UniTask.FromResult(true);
386+
};
387+
388+
await MessageBox.Show("Title", "Content", null, "Cancel");
389+
390+
// Null is passed through
391+
Assert.IsNull(receivedOk);
392+
});
393+
394+
[UnityTest]
395+
public IEnumerator Show_NullNoText_PassesToHandler() => UniTask.ToCoroutine(async () =>
396+
{
397+
string receivedNo = "not-null";
398+
399+
MessageBox.TestHandler = (_, _, _, no) =>
400+
{
401+
receivedNo = no;
402+
return UniTask.FromResult(true);
403+
};
404+
405+
await MessageBox.Show("Title", "Content", "OK", null);
406+
407+
// Null is passed through
408+
Assert.IsNull(receivedNo);
409+
});
410+
411+
[UnityTest]
412+
public IEnumerator Show_BothButtonsNullOrEmpty_DefaultsToOkInHandler() => UniTask.ToCoroutine(async () =>
413+
{
414+
// Note: The safety check for both buttons being empty happens AFTER
415+
// TestHandler is checked, so TestHandler receives the original null values
416+
string receivedOk = "not-null";
417+
string receivedNo = "not-null";
418+
419+
MessageBox.TestHandler = (_, _, ok, no) =>
420+
{
421+
receivedOk = ok;
422+
receivedNo = no;
423+
return UniTask.FromResult(true);
424+
};
425+
426+
await MessageBox.Show("Title", "Content", null, null);
427+
428+
// TestHandler receives original null values
429+
Assert.IsNull(receivedOk);
430+
Assert.IsNull(receivedNo);
431+
});
432+
433+
#endregion
434+
435+
#region Null Content Handling Tests
436+
437+
[UnityTest]
438+
public IEnumerator Show_NullTitle_HandledGracefully() => UniTask.ToCoroutine(async () =>
439+
{
440+
string receivedTitle = "not-null";
441+
442+
MessageBox.TestHandler = (title, _, _, _) =>
443+
{
444+
receivedTitle = title;
445+
return UniTask.FromResult(true);
446+
};
447+
448+
bool result = await MessageBox.Show(null, "Content");
449+
450+
Assert.IsNull(receivedTitle);
451+
Assert.IsTrue(result);
452+
});
453+
454+
[UnityTest]
455+
public IEnumerator Show_NullContent_HandledGracefully() => UniTask.ToCoroutine(async () =>
456+
{
457+
string receivedContent = "not-null";
458+
459+
MessageBox.TestHandler = (_, content, _, _) =>
460+
{
461+
receivedContent = content;
462+
return UniTask.FromResult(true);
463+
};
464+
465+
bool result = await MessageBox.Show("Title", null);
466+
467+
Assert.IsNull(receivedContent);
468+
Assert.IsTrue(result);
469+
});
470+
471+
[UnityTest]
472+
public IEnumerator Show_EmptyStrings_HandledGracefully() => UniTask.ToCoroutine(async () =>
473+
{
474+
string receivedTitle = null;
475+
string receivedContent = null;
476+
477+
MessageBox.TestHandler = (title, content, _, _) =>
478+
{
479+
receivedTitle = title;
480+
receivedContent = content;
481+
return UniTask.FromResult(true);
482+
};
483+
484+
bool result = await MessageBox.Show("", "");
485+
486+
Assert.AreEqual("", receivedTitle);
487+
Assert.AreEqual("", receivedContent);
488+
Assert.IsTrue(result);
489+
});
490+
491+
#endregion
492+
493+
#region Concurrent Operations Tests
494+
495+
[UnityTest]
496+
public IEnumerator Show_MultipleConcurrent_AllComplete() => UniTask.ToCoroutine(async () =>
497+
{
498+
int completionCount = 0;
499+
500+
MessageBox.TestHandler = (_, _, _, _) =>
501+
{
502+
completionCount++;
503+
return UniTask.FromResult(true);
504+
};
505+
506+
// Show multiple message boxes concurrently
507+
var task1 = MessageBox.Show("Test1", "Content1");
508+
var task2 = MessageBox.Show("Test2", "Content2");
509+
var task3 = MessageBox.Show("Test3", "Content3");
510+
511+
await UniTask.WhenAll(task1, task2, task3);
512+
513+
Assert.AreEqual(3, completionCount);
514+
});
515+
516+
[UnityTest]
517+
public IEnumerator CloseAll_AfterMultipleShows_ClearsAll() => UniTask.ToCoroutine(async () =>
518+
{
519+
MessageBox.TestHandler = (_, _, _, _) => UniTask.FromResult(true);
520+
521+
await MessageBox.Show("Test1", "Content1");
522+
await MessageBox.Show("Test2", "Content2");
523+
524+
MessageBox.CloseAll();
525+
526+
var (activeCount, _) = MessageBox.TestGetPoolState();
527+
Assert.AreEqual(0, activeCount);
528+
});
529+
530+
#endregion
277531
}
278532
}

0 commit comments

Comments
 (0)