diff --git a/MCPForUnity/Editor/Services/TestRunnerService.cs b/MCPForUnity/Editor/Services/TestRunnerService.cs index 2ec90847f..ebb92a25a 100644 --- a/MCPForUnity/Editor/Services/TestRunnerService.cs +++ b/MCPForUnity/Editor/Services/TestRunnerService.cs @@ -356,13 +356,33 @@ internal TestRunResult(TestRunSummary summary, IReadOnlyList public int Failed => Summary.Failed; public int Skipped => Summary.Skipped; - public object ToSerializable(string mode) + public object ToSerializable(string mode, bool includeDetails = false, bool includeFailedTests = false) { + // Determine which results to include + IEnumerable resultsToSerialize; + if (includeDetails) + { + // Include all test results + resultsToSerialize = Results.Select(r => r.ToSerializable()); + } + else if (includeFailedTests) + { + // Include only failed and skipped tests + resultsToSerialize = Results + .Where(r => !string.Equals(r.State, "Passed", StringComparison.OrdinalIgnoreCase)) + .Select(r => r.ToSerializable()); + } + else + { + // No individual test results + resultsToSerialize = null; + } + return new { mode, summary = Summary.ToSerializable(), - results = Results.Select(r => r.ToSerializable()).ToList(), + results = resultsToSerialize?.ToList(), }; } diff --git a/MCPForUnity/Editor/Tools/RunTests.cs b/MCPForUnity/Editor/Tools/RunTests.cs index b40df2504..710b8b2dd 100644 --- a/MCPForUnity/Editor/Tools/RunTests.cs +++ b/MCPForUnity/Editor/Tools/RunTests.cs @@ -43,6 +43,27 @@ public static async Task HandleCommand(JObject @params) // Preserve default timeout if parsing fails } + bool includeDetails = false; + bool includeFailedTests = false; + try + { + var includeDetailsToken = @params?["includeDetails"]; + if (includeDetailsToken != null && bool.TryParse(includeDetailsToken.ToString(), out var parsedIncludeDetails)) + { + includeDetails = parsedIncludeDetails; + } + + var includeFailedTestsToken = @params?["includeFailedTests"]; + if (includeFailedTestsToken != null && bool.TryParse(includeFailedTestsToken.ToString(), out var parsedIncludeFailedTests)) + { + includeFailedTests = parsedIncludeFailedTests; + } + } + catch + { + // Preserve defaults if parsing fails + } + var filterOptions = ParseFilterOptions(@params); var testService = MCPServiceLocator.Tests; @@ -66,10 +87,9 @@ public static async Task HandleCommand(JObject @params) var result = await runTask.ConfigureAwait(true); - string message = - $"{parsedMode.Value} tests completed: {result.Passed}/{result.Total} passed, {result.Failed} failed, {result.Skipped} skipped"; + string message = FormatTestResultMessage(parsedMode.Value.ToString(), result); - var data = result.ToSerializable(parsedMode.Value.ToString()); + var data = result.ToSerializable(parsedMode.Value.ToString(), includeDetails, includeFailedTests); return new SuccessResponse(message, data); } @@ -100,6 +120,20 @@ private static TestFilterOptions ParseFilterOptions(JObject @params) }; } + internal static string FormatTestResultMessage(string mode, TestRunResult result) + { + string message = + $"{mode} tests completed: {result.Passed}/{result.Total} passed, {result.Failed} failed, {result.Skipped} skipped"; + + // Add warning when no tests matched the filter criteria + if (result.Total == 0) + { + message += " (No tests matched the specified filters)"; + } + + return message; + } + private static string[] ParseStringArray(JObject @params, string key) { var token = @params[key]; diff --git a/Server/src/services/tools/run_tests.py b/Server/src/services/tools/run_tests.py index 0529bb78a..4aecc4f37 100644 --- a/Server/src/services/tools/run_tests.py +++ b/Server/src/services/tools/run_tests.py @@ -34,7 +34,7 @@ class RunTestsTestResult(BaseModel): class RunTestsResult(BaseModel): mode: str summary: RunTestsSummary - results: list[RunTestsTestResult] + results: list[RunTestsTestResult] | None = None class RunTestsResponse(MCPResponse): @@ -52,6 +52,8 @@ async def run_tests( group_names: Annotated[list[str] | str, "Same as test_names, except it allows for Regex"] | None = None, category_names: Annotated[list[str] | str, "NUnit category names to filter by (tests marked with [Category] attribute)"] | None = None, assembly_names: Annotated[list[str] | str, "Assembly names to filter tests by"] | None = None, + include_failed_tests: Annotated[bool, "Include details for failed/skipped tests only (default: false)"] = False, + include_details: Annotated[bool, "Include details for all tests (default: false)"] = False, ) -> RunTestsResponse: unity_instance = get_unity_instance_from_context(ctx) @@ -88,6 +90,12 @@ def _coerce_string_list(value) -> list[str] | None: if assembly_names_list: params["assemblyNames"] = assembly_names_list + # Add verbosity parameters + if include_failed_tests: + params["includeFailedTests"] = True + if include_details: + params["includeDetails"] = True + response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "run_tests", params) await ctx.info(f'Response {response}') return RunTestsResponse(**response) if isinstance(response, dict) else response diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/Matrix4x4ConverterTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/Matrix4x4ConverterTests.cs index 221996fe5..696cc9e85 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/Matrix4x4ConverterTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/Matrix4x4ConverterTests.cs @@ -5,7 +5,7 @@ using NUnit.Framework; using UnityEngine; -namespace MCPForUnityTests.EditMode.Helpers +namespace MCPForUnityTests.Editor.Helpers { /// /// Tests for Matrix4x4Converter to ensure it safely serializes matrices diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs index 7fc0bc9ee..e7077f05a 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs @@ -7,11 +7,13 @@ using MCPForUnity.Editor.Tools; using Newtonsoft.Json.Linq; -namespace Tests.EditMode.Tools +namespace MCPForUnityTests.Editor.Tools { /// /// Tests for domain reload resilience - ensuring MCP requests succeed even during Unity domain reloads. /// + [Category("domain_reload")] + [Explicit("Intentionally triggers script compilation/domain reload; run explicitly to avoid slowing/flaking cold-start EditMode runs.")] public class DomainReloadResilienceTests { private const string TempDir = "Assets/Temp/DomainReloadTests"; diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MCPToolParameterTests.cs similarity index 99% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MCPToolParameterTests.cs index 079db4444..d58c5a7f3 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MCPToolParameterTests.cs @@ -8,7 +8,7 @@ using System.IO; using System.Text.RegularExpressions; -namespace Tests.EditMode +namespace MCPForUnityTests.Editor.Tools { /// /// Tests specifically for MCP tool parameter handling issues. diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MCPToolParameterTests.cs.meta similarity index 100% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs.meta rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MCPToolParameterTests.cs.meta diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs index 45f72222f..2b2dd8a80 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs @@ -1,9 +1,10 @@ using System; +using System.Collections; using Newtonsoft.Json.Linq; using NUnit.Framework; using UnityEditor; using UnityEngine; -using System.Threading; +using UnityEngine.TestTools; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Tools; using MCPForUnityTests.Editor.Tools.Fixtures; @@ -14,16 +15,17 @@ public class ManageScriptableObjectTests { private const string TempRoot = "Assets/Temp/ManageScriptableObjectTests"; private const string NestedFolder = TempRoot + "/Nested/Deeper"; + private const double UnityReadyTimeoutSeconds = 180.0; private string _createdAssetPath; private string _createdGuid; private string _matAPath; private string _matBPath; - [SetUp] - public void SetUp() + [UnitySetUp] + public IEnumerator SetUp() { - WaitForUnityReady(); + yield return WaitForUnityReady(UnityReadyTimeoutSeconds); EnsureFolder("Assets/Temp"); // Start from a clean slate every time (prevents intermittent setup failures). if (AssetDatabase.IsValidFolder(TempRoot)) @@ -47,7 +49,7 @@ public void SetUp() AssetDatabase.CreateAsset(new Material(shader), _matBPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); - WaitForUnityReady(); + yield return WaitForUnityReady(UnityReadyTimeoutSeconds); } [TearDown] @@ -308,7 +310,7 @@ private static JObject ToJObject(object result) return result as JObject ?? JObject.FromObject(result); } - private static void WaitForUnityReady(double timeoutSeconds = 30.0) + private static IEnumerator WaitForUnityReady(double timeoutSeconds = 30.0) { // Some EditMode tests trigger script compilation/domain reload. Tools like ManageScriptableObject // intentionally return "compiling_or_reloading" during these windows. Wait until Unity is stable @@ -320,7 +322,7 @@ private static void WaitForUnityReady(double timeoutSeconds = 30.0) { Assert.Fail($"Timed out waiting for Unity to finish compiling/updating (>{timeoutSeconds:0.0}s)."); } - Thread.Sleep(50); + yield return null; // yield to the editor loop so importing/compiling can actually progress } } } diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/RunTestsTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/RunTestsTests.cs new file mode 100644 index 000000000..6f2c036cd --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/RunTestsTests.cs @@ -0,0 +1,66 @@ +using System; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using MCPForUnity.Editor.Services; + +namespace MCPForUnityTests.Editor.Tools +{ + /// + /// Tests for RunTests tool functionality. + /// Note: We cannot easily test the full HandleCommand because it would create + /// recursive test runner calls. Instead, we test the message formatting logic. + /// + public class RunTestsTests + { + [Test] + public void FormatResultMessage_WithNoTests_IncludesWarning() + { + // Arrange + var summary = new TestRunSummary( + total: 0, + passed: 0, + failed: 0, + skipped: 0, + durationSeconds: 0.0, + resultState: "Passed" + ); + var result = new TestRunResult(summary, new TestRunTestResult[0]); + + // Act + string message = MCPForUnity.Editor.Tools.RunTests.FormatTestResultMessage("EditMode", result); + + // Assert - THIS IS THE NEW FEATURE + Assert.IsTrue( + message.Contains("No tests matched"), + $"Expected warning when total=0, but got: '{message}'" + ); + } + + [Test] + public void FormatResultMessage_WithTests_NoWarning() + { + // Arrange + var summary = new TestRunSummary( + total: 5, + passed: 4, + failed: 1, + skipped: 0, + durationSeconds: 1.5, + resultState: "Failed" + ); + var result = new TestRunResult(summary, new TestRunTestResult[0]); + + // Act + string message = MCPForUnity.Editor.Tools.RunTests.FormatTestResultMessage("EditMode", result); + + // Assert + Assert.IsFalse( + message.Contains("No tests matched"), + $"Should not have warning when tests exist, but got: '{message}'" + ); + Assert.IsTrue(message.Contains("4/5 passed"), "Should contain pass ratio"); + } + + // Use MCPForUnity.Editor.Tools.RunTests.FormatTestResultMessage directly. + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/RunTestsTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/RunTestsTests.cs.meta new file mode 100644 index 000000000..469e4ddcf --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/RunTestsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11f8926da5b67490ab04d70d442b1c19 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: