Files
PaulLubos 8d51737f97 Add configurable init_timeout for PlayMode test initialization
PlayMode tests require entering play mode which triggers a domain reload.
On large projects this can take >15s, causing the hardcoded 15s init
timeout to auto-fail the test job before tests actually start.

This adds an `init_timeout` parameter to `run_tests` that flows through
the Python server → C# RunTests handler → TestJobManager. When set, the
per-job timeout overrides the 15s default. The value is persisted across
domain reloads via SessionState.

Changes:
- Python: Add `init_timeout` param to `run_tests()` function signature
- C# RunTests: Read `initTimeout` param and pass to `StartJob()`
- C# TestJobManager: Per-job `InitTimeoutMs` field with fallback to
  `DefaultInitializationTimeoutMs` (15s), persisted in SessionState

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 08:29:34 +02:00

99 lines
3.8 KiB
C#

using System;
using System.Threading.Tasks;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Resources.Tests;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json.Linq;
using UnityEditor.TestTools.TestRunner.Api;
namespace MCPForUnity.Editor.Tools
{
/// <summary>
/// Starts a Unity Test Runner run asynchronously and returns a job id immediately.
/// Use get_test_job(job_id) to poll status/results.
/// </summary>
[McpForUnityTool("run_tests", AutoRegister = false, Group = "testing")]
public static class RunTests
{
public static Task<object> HandleCommand(JObject @params)
{
try
{
// Check for clear_stuck action first
if (ParamCoercion.CoerceBool(@params?["clear_stuck"], false))
{
bool wasCleared = TestJobManager.ClearStuckJob();
return Task.FromResult<object>(new SuccessResponse(
wasCleared ? "Stuck job cleared." : "No running job to clear.",
new { cleared = wasCleared }
));
}
string modeStr = @params?["mode"]?.ToString();
if (string.IsNullOrWhiteSpace(modeStr))
{
modeStr = "EditMode";
}
if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError))
{
return Task.FromResult<object>(new ErrorResponse(parseError));
}
var p = new ToolParams(@params);
bool includeDetails = p.GetBool("includeDetails");
bool includeFailedTests = p.GetBool("includeFailedTests");
var filterOptions = GetFilterOptions(@params);
long initTimeoutMs = p.GetInt("initTimeout") ?? 0;
string jobId = TestJobManager.StartJob(parsedMode.Value, filterOptions, initTimeoutMs);
return Task.FromResult<object>(new SuccessResponse("Test job started.", new
{
job_id = jobId,
status = "running",
mode = parsedMode.Value.ToString(),
include_details = includeDetails,
include_failed_tests = includeFailedTests
}));
}
catch (Exception ex)
{
// Normalize the already-running case to a stable error token.
if (ex.Message != null && ex.Message.IndexOf("already in progress", StringComparison.OrdinalIgnoreCase) >= 0)
{
return Task.FromResult<object>(new ErrorResponse("tests_running", new { reason = "tests_running", retry_after_ms = 5000 }));
}
return Task.FromResult<object>(new ErrorResponse($"Failed to start test job: {ex.Message}"));
}
}
private static TestFilterOptions GetFilterOptions(JObject @params)
{
if (@params == null)
{
return null;
}
var p = new ToolParams(@params);
var testNames = p.GetStringArray("testNames");
var groupNames = p.GetStringArray("groupNames");
var categoryNames = p.GetStringArray("categoryNames");
var assemblyNames = p.GetStringArray("assemblyNames");
if (testNames == null && groupNames == null && categoryNames == null && assemblyNames == null)
{
return null;
}
return new TestFilterOptions
{
TestNames = testNames,
GroupNames = groupNames,
CategoryNames = categoryNames,
AssemblyNames = assemblyNames
};
}
}
}