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; }
}
}