Skip to content

Commit 423ae56

Browse files
committed
refactor(TestRunnerService): improve GetAllTestsAsync readability with helper methods
1 parent 3547d2c commit 423ae56

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using McpUnity.Unity;
7+
using McpUnity.Utils;
8+
using UnityEngine;
9+
using UnityEditor;
10+
using UnityEditor.TestTools.TestRunner.Api;
11+
using Newtonsoft.Json.Linq;
12+
13+
namespace McpUnity.Services
14+
{
15+
/// <summary>
16+
/// Service for accessing Unity Test Runner functionality
17+
/// Implements ICallbacks for TestRunnerApi.
18+
/// </summary>
19+
public class TestRunnerService : ITestRunnerService, ICallbacks
20+
{
21+
private readonly TestRunnerApi _testRunnerApi;
22+
private TaskCompletionSource<JObject> _tcs;
23+
private bool _returnOnlyFailures;
24+
private List<ITestResultAdaptor> _results;
25+
26+
/// <summary>
27+
/// Constructor
28+
/// </summary>
29+
public TestRunnerService()
30+
{
31+
_testRunnerApi = ScriptableObject.CreateInstance<TestRunnerApi>();
32+
33+
_testRunnerApi.RegisterCallbacks(this);
34+
}
35+
36+
[MenuItem("Tools/MCP Unity/Debug call path")]
37+
public static async void DebugCallGetAllTests()
38+
{
39+
var service = new TestRunnerService();
40+
var tests = await service.GetAllTestsAsync();
41+
Debug.Log($"Retrieved {tests.Count} tests:");
42+
foreach (var t in tests)
43+
Debug.Log($"Test: {t.FullName} ({t.TestMode}) - State: {t.RunState}");
44+
}
45+
46+
/// <summary>
47+
/// Asynchronously retrieves all available tests using the TestRunnerApi.
48+
/// </summary>
49+
/// <param name="testModeFilter">Optional test mode filter (EditMode, PlayMode, or empty for all)</param>
50+
/// <returns>List of test items matching the specified test mode, or all tests if no mode specified</returns>
51+
public async Task<List<ITestAdaptor>> GetAllTestsAsync(string testModeFilter = "")
52+
{
53+
McpLogger.LogInfo($"Retrieving tests with filter: {testModeFilter ?? "All"}");
54+
55+
var tests = new List<ITestAdaptor>();
56+
var tcs = new TaskCompletionSource<bool>();
57+
var pendingOperations = 0;
58+
59+
// Determine which test modes to retrieve based on the filter
60+
var modesToRetrieve = GetTestModesToRetrieve(testModeFilter);
61+
62+
// Exit early if no modes match the filter
63+
if (modesToRetrieve.Count == 0)
64+
{
65+
McpLogger.LogInfo("No matching test modes found for filter");
66+
return tests;
67+
}
68+
69+
// Start retrieval for each test mode
70+
foreach (var mode in modesToRetrieve)
71+
{
72+
RetrieveTestsForMode(mode, tests, ref pendingOperations, tcs);
73+
}
74+
75+
// Wait for all retrievals to complete
76+
await tcs.Task;
77+
McpLogger.LogInfo($"Retrieved {tests.Count} tests matching filter: {testModeFilter ?? "All"}");
78+
79+
return tests;
80+
}
81+
82+
/// <summary>
83+
/// Determines which test modes to retrieve based on the provided filter.
84+
/// </summary>
85+
private List<TestMode> GetTestModesToRetrieve(string testModeFilter)
86+
{
87+
var modes = new List<TestMode>();
88+
89+
// If no filter or explicitly requesting EditMode tests
90+
if (string.IsNullOrEmpty(testModeFilter) ||
91+
testModeFilter.Equals("EditMode", StringComparison.OrdinalIgnoreCase))
92+
{
93+
modes.Add(TestMode.EditMode);
94+
}
95+
96+
// If no filter or explicitly requesting PlayMode tests
97+
if (string.IsNullOrEmpty(testModeFilter) ||
98+
testModeFilter.Equals("PlayMode", StringComparison.OrdinalIgnoreCase))
99+
{
100+
modes.Add(TestMode.PlayMode);
101+
}
102+
103+
return modes;
104+
}
105+
106+
/// <summary>
107+
/// Retrieves tests for a specific test mode and adds them to the results collection.
108+
/// </summary>
109+
private void RetrieveTestsForMode(
110+
TestMode mode,
111+
List<ITestAdaptor> tests,
112+
ref int pendingOperations,
113+
TaskCompletionSource<bool> tcs)
114+
{
115+
Interlocked.Increment(ref pendingOperations);
116+
117+
McpLogger.LogInfo($"Retrieving {mode} tests...");
118+
_testRunnerApi.RetrieveTestList(mode, adaptor =>
119+
{
120+
// Process the test results
121+
CollectTestItems(adaptor, tests);
122+
123+
// Check if this was the last pending operation
124+
if (Interlocked.Decrement(ref pendingOperations) == 0)
125+
{
126+
McpLogger.LogInfo($"All test retrievals completed");
127+
tcs.TrySetResult(true);
128+
}
129+
});
130+
}
131+
132+
/// <summary>
133+
/// Executes tests and returns a JSON summary.
134+
/// </summary>
135+
/// <param name="testMode">The test mode to run (EditMode or PlayMode).</param>
136+
/// <param name="returnOnlyFailures">If true, only failed test results are included in the output.</param>
137+
/// <param name="testFilter">A filter string to select specific tests to run.</param>
138+
/// <returns>Task that resolves with test results when tests are complete</returns>
139+
public async Task<JObject> ExecuteTestsAsync(TestMode testMode, bool returnOnlyFailures, string testFilter = "")
140+
{
141+
_tcs = new TaskCompletionSource<JObject>();
142+
_results = new List<ITestResultAdaptor>();
143+
_returnOnlyFailures = returnOnlyFailures;
144+
var filter = new Filter { testMode = testMode };
145+
146+
if (!string.IsNullOrEmpty(testFilter))
147+
{
148+
filter.testNames = new[] { testFilter };
149+
}
150+
151+
_testRunnerApi.Execute(new ExecutionSettings(filter));
152+
153+
return await WaitForCompletionAsync(
154+
McpUnitySettings.Instance.RequestTimeoutSeconds);
155+
}
156+
157+
/// <summary>
158+
/// Recursively collect test items from test adaptors
159+
/// </summary>
160+
private void CollectTestItems(ITestAdaptor testAdaptor, List<ITestAdaptor> tests)
161+
{
162+
if (testAdaptor.IsSuite)
163+
{
164+
// For suites (namespaces, classes), collect all children
165+
foreach (var child in testAdaptor.Children)
166+
{
167+
CollectTestItems(child, tests);
168+
}
169+
}
170+
else
171+
{
172+
tests.Add(testAdaptor);
173+
}
174+
}
175+
176+
#region ICallbacks Implementation
177+
178+
/// <summary>
179+
/// Called when the test run starts.
180+
/// </summary>
181+
public void RunStarted(ITestAdaptor testsToRun)
182+
{
183+
McpLogger.LogInfo($"Test run started: {testsToRun?.Name}");
184+
}
185+
186+
/// <summary>
187+
/// Called when an individual test starts.
188+
/// </summary>
189+
public void TestStarted(ITestAdaptor test)
190+
{
191+
// Optionally implement per-test start logic or logging.
192+
}
193+
194+
/// <summary>
195+
/// Called when an individual test finishes.
196+
/// </summary>
197+
public void TestFinished(ITestResultAdaptor result)
198+
{
199+
_results.Add(result);
200+
}
201+
202+
/// <summary>
203+
/// Called when the test run finishes.
204+
/// </summary>
205+
public void RunFinished(ITestResultAdaptor result)
206+
{
207+
var summary = BuildResultJson(_results, result);
208+
_tcs?.TrySetResult(summary);
209+
}
210+
211+
#endregion
212+
213+
#region Helpers
214+
215+
private async Task<JObject> WaitForCompletionAsync(int timeoutSeconds)
216+
{
217+
var delayTask = Task.Delay(TimeSpan.FromSeconds(timeoutSeconds));
218+
var winner = await Task.WhenAny(_tcs.Task, delayTask);
219+
220+
if (winner != _tcs.Task)
221+
{
222+
_tcs.TrySetResult(
223+
McpUnitySocketHandler.CreateErrorResponse(
224+
$"Test run timed out after {timeoutSeconds} seconds",
225+
"test_runner_timeout"));
226+
}
227+
return await _tcs.Task;
228+
}
229+
230+
private JObject BuildResultJson(List<ITestResultAdaptor> results, ITestResultAdaptor result)
231+
{
232+
int pass = results.Count(r => r.ResultState == "Passed");
233+
int fail = results.Count(r => r.ResultState == "Failed");
234+
int skip = results.Count(r => r.ResultState == "Skipped");
235+
236+
var arr = new JArray(results
237+
.Where(r => !_returnOnlyFailures || r.ResultState == "Failed")
238+
.Select(r => new JObject {
239+
["name"] = r.Name,
240+
["fullName"] = r.FullName,
241+
["state"] = r.ResultState,
242+
["message"] = r.Message,
243+
["duration"] = r.Duration
244+
}));
245+
246+
return new JObject {
247+
["success"] = true,
248+
["type"] = "text",
249+
["message"] = $"{result.Test.Name} test run completed: {pass}/{results.Count} passed - {fail}/{results.Count} failed - {skip}/{results.Count} skipped",
250+
["resultState"] = result.ResultState,
251+
["durationSeconds"] = result.Duration,
252+
["testCount"] = results.Count,
253+
["passCount"] = pass,
254+
["failCount"] = fail,
255+
["skipCount"] = skip,
256+
["results"] = arr
257+
};
258+
}
259+
260+
#endregion
261+
}
262+
}

0 commit comments

Comments
 (0)