using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Unity.Pipeline.Commands; using Unity.Pipeline.Editor.Testing; using Unity.Pipeline.Models; using UnityEditor.TestTools.TestRunner.Api; using UnityEngine; namespace Unity.Pipeline.Editor.Commands { /// /// Commands for running Unity tests programmatically /// public static class TestCommands { /// /// Execute Unity tests with filtering options. /// Synchronous by default (blocks until completion), async mode available with --async-tests flag. /// [CliCommand("run_tests", "Execute Unity tests with filtering options", MainThreadRequired = true)] public static async Task RunTests( [CliArg("mode", "Test mode: all, editor, playmode (default: all)")] string mode = "all", [CliArg("filter", "Test name filter pattern (case-insensitive partial match)")] string filter = "", [CliArg("filter_type", "Filter type: testName, assembly, category (default: testName)")] string filterType = "testName", [CliArg("include_explicit", "Include tests marked with [Explicit] attribute")] bool includeExplicit = false, [CliArg("async_tests", "Run asynchronously - return immediately, poll /test-status for results")] bool asyncTests = false, [CliArg("timeout", "Test execution timeout in seconds (default: 300)")] int timeout = 300) { // Return the full structured response (including failures and the Error field on a // failed run) rather than throwing: the server now awaits this Task and serializes the // unwrapped response, so the client receives complete, structured reporting. Throwing // would only surface an opaque message and discard the per-test results. return await PipelineTestRunner.ExecuteTestsAsync( mode, filter, filterType, includeExplicit, asyncTests, timeout); } /// /// List all available tests without executing any of them. /// Enumerates the test tree via TestRunnerApi.RetrieveTestList for the requested mode(s). /// [CliCommand("list_tests", "List all available tests (EditMode and/or PlayMode) without running them", MainThreadRequired = true)] public static async Task ListTests( [CliArg("mode", "Test mode: all, editor, playmode (default: all)")] string mode = "all") { try { if (!TryGetModes(mode, out var modes)) { return new TestListResponse { Success = false, Command = "list_tests", Error = $"Invalid mode '{mode}'. Use 'editor', 'playmode', or 'all'.", ExecutedAt = DateTime.UtcNow }; } var tests = new List(); foreach (var testMode in modes) { tests.AddRange(await RetrieveTestsAsync(testMode)); } return new TestListResponse { Success = true, Command = "list_tests", Mode = modes.Count == 2 ? "All" : modes[0].ToString(), Count = tests.Count, Tests = tests, Message = $"Found {tests.Count} test(s).", ExecutedAt = DateTime.UtcNow }; } catch (Exception ex) { Debug.LogError($"[TestCommands] list_tests failed: {ex.Message}"); return new TestListResponse { Success = false, Command = "list_tests", Error = "Failed to list tests", ErrorDetails = ex.ToString(), ExecutedAt = DateTime.UtcNow }; } } /// /// Get current test status for async test execution /// [CliCommand("test_status", "Get status of running async test execution", MainThreadRequired = false)] public static string GetTestStatus() { var status = PipelineTestRunner.GetTestStatus(); return status ?? "{\"status\":\"no_tests\",\"message\":\"No test run in progress\"}"; } /// /// Cancel running test execution /// [CliCommand("cancel_tests", "Cancel running test execution", MainThreadRequired = true)] public static object CancelTests() { return PipelineTestRunner.CancelTests(); } /// /// Resolve a mode string to the concrete TestMode(s) to enumerate. /// private static bool TryGetModes(string mode, out List modes) { switch (mode?.ToLowerInvariant()) { case null: case "": case "all": modes = new List { TestMode.EditMode, TestMode.PlayMode }; return true; case "editor": case "editmode": modes = new List { TestMode.EditMode }; return true; case "playmode": case "play": modes = new List { TestMode.PlayMode }; return true; default: modes = null; return false; } } /// /// Retrieve the test list for a single mode. RetrieveTestList enumerates the test tree (no /// execution) and invokes the callback on the main thread; we bridge it to a Task. /// private static Task> RetrieveTestsAsync(TestMode testMode) { var api = ScriptableObject.CreateInstance(); var tcs = new TaskCompletionSource>(); api.RetrieveTestList(testMode, root => { try { var items = new List(); CollectLeafTests(root, testMode, items); tcs.SetResult(items); } catch (Exception ex) { tcs.SetException(ex); } }); return tcs.Task; } /// /// Walk the test tree and collect leaf tests (actual test methods, not suites/fixtures). /// private static void CollectLeafTests(ITestAdaptor test, TestMode testMode, List items) { if (test == null) return; if (!test.HasChildren && !test.IsSuite) { items.Add(new TestListItem { FullName = test.FullName, Mode = testMode.ToString(), Assembly = test.TypeInfo?.Assembly?.GetName()?.Name, Categories = test.Categories?.ToList() ?? new List(), Explicit = test.RunState == RunState.Explicit }); return; } if (test.Children != null) { foreach (var child in test.Children) CollectLeafTests(child, testMode, items); } } } /// /// Response for the list_tests command: the available tests, without execution results. /// [Serializable] public class TestListResponse : CommandExecutionResponse { public string Mode { get; set; } // EditMode, PlayMode, or All public int Count { get; set; } public List Tests { get; set; } = new List(); } /// /// A single available test (no run state / outcome — this is a listing, not a result). /// [Serializable] public class TestListItem { public string FullName { get; set; } public string Mode { get; set; } public string Assembly { get; set; } public List Categories { get; set; } = new List(); public bool Explicit { get; set; } } }