11using System ;
22using System . Collections . Generic ;
3+ using System . Linq ;
34using System . Threading ;
45using System . Threading . Tasks ;
56using McpUnity . Unity ;
7+ using McpUnity . Utils ;
68using UnityEngine ;
79using UnityEditor ;
810using UnityEditor . TestTools . TestRunner . Api ;
@@ -12,22 +14,23 @@ namespace McpUnity.Services
1214{
1315 /// <summary>
1416 /// Service for accessing Unity Test Runner functionality
17+ /// Implements ICallbacks for TestRunnerApi.
1518 /// </summary>
16- public class TestRunnerService : ITestRunnerService
19+ public class TestRunnerService : ITestRunnerService , ICallbacks
1720 {
1821 private readonly TestRunnerApi _testRunnerApi ;
19-
20- /// <summary>
21- /// Get the TestRunnerApi instance
22- /// </summary>
23- public TestRunnerApi TestRunnerApi => _testRunnerApi ;
24-
22+ private TaskCompletionSource < JObject > _tcs ;
23+ private bool _returnOnlyFailures ;
24+ private List < ITestResultAdaptor > _results ;
25+
2526 /// <summary>
2627 /// Constructor
2728 /// </summary>
2829 public TestRunnerService ( )
2930 {
3031 _testRunnerApi = ScriptableObject . CreateInstance < TestRunnerApi > ( ) ;
32+
33+ _testRunnerApi . RegisterCallbacks ( this ) ;
3134 }
3235
3336 [ MenuItem ( "Tools/MCP Unity/Debug call path" ) ]
@@ -43,131 +46,179 @@ public static async void DebugCallGetAllTests()
4346 /// <summary>
4447 /// Async retrieval of all tests using TestRunnerApi callbacks
4548 /// </summary>
46- /// <param name="testMode ">Optional test mode filter (EditMode, PlayMode, or empty for all)</param>
49+ /// <param name="testModeFilter ">Optional test mode filter (EditMode, PlayMode, or empty for all)</param>
4750 /// <returns>List of test items matching the specified test mode, or all tests if no mode specified</returns>
48- public async Task < List < TestItemInfo > > GetAllTestsAsync ( string testMode = "" )
51+ public async Task < List < ITestAdaptor > > GetAllTestsAsync ( string testModeFilter = "" )
4952 {
50- var tests = new List < TestItemInfo > ( ) ;
51- var tcs = new TaskCompletionSource < bool > ( ) ;
52- int pending = 0 ;
53+ var tests = new List < ITestAdaptor > ( ) ;
54+ var tasks = new List < Task < List < ITestAdaptor > > > ( ) ;
5355
54- if ( string . IsNullOrEmpty ( testMode ) || testMode . Equals ( "EditMode" , StringComparison . OrdinalIgnoreCase ) )
56+ if ( string . IsNullOrEmpty ( testModeFilter ) || testModeFilter . Equals ( "EditMode" , StringComparison . OrdinalIgnoreCase ) )
5557 {
56- Interlocked . Increment ( ref pending ) ;
57- _testRunnerApi . RetrieveTestList ( TestMode . EditMode , adaptor =>
58- {
59- CollectTestItems ( adaptor , tests ) ;
60- CheckDone ( ) ;
61- } ) ;
58+ tasks . Add ( RetrieveTestsAsync ( TestMode . EditMode ) ) ;
6259 }
63- if ( string . IsNullOrEmpty ( testMode ) || testMode . Equals ( "PlayMode" , StringComparison . OrdinalIgnoreCase ) )
60+ if ( string . IsNullOrEmpty ( testModeFilter ) || testModeFilter . Equals ( "PlayMode" , StringComparison . OrdinalIgnoreCase ) )
6461 {
65- Interlocked . Increment ( ref pending ) ;
66- _testRunnerApi . RetrieveTestList ( TestMode . PlayMode , adaptor =>
67- {
68- CollectTestItems ( adaptor , tests ) ;
69- CheckDone ( ) ;
70- } ) ;
62+ tasks . Add ( RetrieveTestsAsync ( TestMode . PlayMode ) ) ;
7163 }
7264
73- if ( pending == 0 )
74- tcs . SetResult ( true ) ;
75-
76- await tcs . Task ;
65+ var results = await Task . WhenAll ( tasks ) ;
7766
78- return tests ;
79-
80- void CheckDone ( )
67+ foreach ( var result in results )
8168 {
82- if ( Interlocked . Decrement ( ref pending ) == 0 )
83- tcs . TrySetResult ( true ) ;
69+ tests . AddRange ( result ) ;
8470 }
71+
72+ return tests ;
8573 }
8674
8775 /// <summary>
88- /// Execute tests with the provided parameters
76+ /// Executes tests and returns a JSON summary.
8977 /// </summary>
90- /// <param name="testMode">Test mode to run</param>
91- /// <param name="testFilter">Optional test filter </param>
92- /// <param name="completionSource">TaskCompletionSource to resolve when tests are complete </param>
78+ /// <param name="testMode">The test mode to run (EditMode or PlayMode). </param>
79+ /// <param name="returnOnlyFailures">If true, only failed test results are included in the output. </param>
80+ /// <param name="testFilter">A filter string to select specific tests to run. </param>
9381 /// <returns>Task that resolves with test results when tests are complete</returns>
94- public async void ExecuteTests (
95- TestMode testMode ,
96- string testFilter ,
97- TaskCompletionSource < JObject > completionSource )
82+ public async Task < JObject > ExecuteTestsAsync ( TestMode testMode , bool returnOnlyFailures , string testFilter = "" )
9883 {
99- // Create filter
100- var filter = new Filter
101- {
102- testMode = testMode
103- } ;
104-
105- // Apply name filter if provided
84+ _tcs = new TaskCompletionSource < JObject > ( ) ;
85+ _results = new List < ITestResultAdaptor > ( ) ;
86+ _returnOnlyFailures = returnOnlyFailures ;
87+ var filter = new Filter { testMode = testMode } ;
88+
10689 if ( ! string . IsNullOrEmpty ( testFilter ) )
10790 {
10891 filter . testNames = new [ ] { testFilter } ;
10992 }
110-
111- // Execute tests
93+
11294 _testRunnerApi . Execute ( new ExecutionSettings ( filter ) ) ;
11395
114- // Use timeout from settings if not specified
115- var timeoutSeconds = McpUnitySettings . Instance . RequestTimeoutSeconds ;
116-
117- Task completedTask = await Task . WhenAny (
118- completionSource . Task ,
119- Task . Delay ( TimeSpan . FromSeconds ( timeoutSeconds ) )
120- ) ;
96+ return await WaitForCompletionAsync (
97+ McpUnitySettings . Instance . RequestTimeoutSeconds ) ;
98+ }
99+
100+ /// <summary>
101+ /// Asynchronously retrieves all test adaptors for the specified test mode.
102+ /// </summary>
103+ /// <param name="mode">The test mode to retrieve tests for (EditMode or PlayMode).</param>
104+ /// <returns>A task that resolves to a list of ITestAdaptor representing all tests in the given mode.</returns>
105+ private Task < List < ITestAdaptor > > RetrieveTestsAsync ( TestMode mode )
106+ {
107+ var tcs = new TaskCompletionSource < List < ITestAdaptor > > ( ) ;
108+ var tests = new List < ITestAdaptor > ( ) ;
121109
122- if ( completedTask != completionSource . Task )
110+ _testRunnerApi . RetrieveTestList ( mode , adaptor =>
123111 {
124- completionSource . SetResult ( McpUnitySocketHandler . CreateErrorResponse (
125- $ "Test run timed out after { timeoutSeconds } seconds" ,
126- "test_runner_timeout"
127- ) ) ;
128- }
112+ CollectTestItems ( adaptor , tests ) ;
113+ tcs . SetResult ( tests ) ;
114+ } ) ;
115+
116+ return tcs . Task ;
129117 }
130118
131119 /// <summary>
132120 /// Recursively collect test items from test adaptors
133121 /// </summary>
134- private void CollectTestItems ( ITestAdaptor testAdaptor , List < TestItemInfo > tests , string parentPath = "" )
122+ private void CollectTestItems ( ITestAdaptor testAdaptor , List < ITestAdaptor > tests )
135123 {
136124 if ( testAdaptor . IsSuite )
137125 {
138126 // For suites (namespaces, classes), collect all children
139127 foreach ( var child in testAdaptor . Children )
140128 {
141- string currentPath = string . IsNullOrEmpty ( parentPath ) ? testAdaptor . Name : $ "{ parentPath } .{ testAdaptor . Name } ";
142- CollectTestItems ( child , tests , currentPath ) ;
129+ CollectTestItems ( child , tests ) ;
143130 }
144131 }
145132 else
146133 {
147- // For individual tests, add to the list
148- string fullPath = string . IsNullOrEmpty ( parentPath ) ? testAdaptor . Name : $ "{ parentPath } .{ testAdaptor . Name } ";
149-
150- tests . Add ( new TestItemInfo
151- {
152- Name = testAdaptor . Name ,
153- FullName = testAdaptor . FullName ,
154- Path = fullPath ,
155- TestMode = testAdaptor . TestMode . ToString ( ) ,
156- RunState = testAdaptor . RunState . ToString ( )
157- } ) ;
134+ tests . Add ( testAdaptor ) ;
158135 }
159136 }
160- }
161-
162- /// <summary>
163- /// Information about a test item
164- /// </summary>
165- public class TestItemInfo
166- {
167- public string Name { get ; set ; }
168- public string FullName { get ; set ; }
169- public string Path { get ; set ; }
170- public string TestMode { get ; set ; }
171- public string RunState { get ; set ; }
137+
138+ #region ICallbacks Implementation
139+
140+ /// <summary>
141+ /// Called when the test run starts.
142+ /// </summary>
143+ public void RunStarted ( ITestAdaptor testsToRun )
144+ {
145+ McpLogger . LogInfo ( $ "Test run started: { testsToRun ? . Name } ") ;
146+ }
147+
148+ /// <summary>
149+ /// Called when an individual test starts.
150+ /// </summary>
151+ public void TestStarted ( ITestAdaptor test )
152+ {
153+ // Optionally implement per-test start logic or logging.
154+ }
155+
156+ /// <summary>
157+ /// Called when an individual test finishes.
158+ /// </summary>
159+ public void TestFinished ( ITestResultAdaptor result )
160+ {
161+ _results . Add ( result ) ;
162+ }
163+
164+ /// <summary>
165+ /// Called when the test run finishes.
166+ /// </summary>
167+ public void RunFinished ( ITestResultAdaptor result )
168+ {
169+ var summary = BuildResultJson ( _results , result ) ;
170+ _tcs ? . TrySetResult ( summary ) ;
171+ }
172+
173+ #endregion
174+
175+ #region Helpers
176+
177+ private async Task < JObject > WaitForCompletionAsync ( int timeoutSeconds )
178+ {
179+ var delayTask = Task . Delay ( TimeSpan . FromSeconds ( timeoutSeconds ) ) ;
180+ var winner = await Task . WhenAny ( _tcs . Task , delayTask ) ;
181+
182+ if ( winner != _tcs . Task )
183+ {
184+ _tcs . TrySetResult (
185+ McpUnitySocketHandler . CreateErrorResponse (
186+ $ "Test run timed out after { timeoutSeconds } seconds",
187+ "test_runner_timeout" ) ) ;
188+ }
189+ return await _tcs . Task ;
190+ }
191+
192+ private JObject BuildResultJson ( List < ITestResultAdaptor > results , ITestResultAdaptor result )
193+ {
194+ int pass = results . Count ( r => r . ResultState == "Passed" ) ;
195+ int fail = results . Count ( r => r . ResultState == "Failed" ) ;
196+ int skip = results . Count ( r => r . ResultState == "Skipped" ) ;
197+
198+ var arr = new JArray ( results
199+ . Where ( r => ! _returnOnlyFailures || r . ResultState == "Failed" )
200+ . Select ( r => new JObject {
201+ [ "name" ] = r . Name ,
202+ [ "fullName" ] = r . FullName ,
203+ [ "state" ] = r . ResultState ,
204+ [ "message" ] = r . Message ,
205+ [ "duration" ] = r . Duration
206+ } ) ) ;
207+
208+ return new JObject {
209+ [ "success" ] = true ,
210+ [ "type" ] = "text" ,
211+ [ "message" ] = $ "{ result . Test . Name } test run completed: { pass } /{ results . Count } passed - { fail } /{ results . Count } failed - { skip } /{ results . Count } skipped",
212+ [ "resultState" ] = result . ResultState ,
213+ [ "durationSeconds" ] = result . Duration ,
214+ [ "testCount" ] = results . Count ,
215+ [ "passCount" ] = pass ,
216+ [ "failCount" ] = fail ,
217+ [ "skipCount" ] = skip ,
218+ [ "results" ] = arr
219+ } ;
220+ }
221+
222+ #endregion
172223 }
173224}
0 commit comments