// Copyright (c) 2025 Mike Schulze // MIT License - See LICENSE file in the repository root for full license text #pragma warning disable IDE1006 namespace gdUnit4.addons.gdUnit4.src.dotnet; #pragma warning restore IDE1006 #if GDUNIT4NET_API_V5 using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using GdUnit4; using GdUnit4.Api; using Godot; using Godot.Collections; /// /// The GdUnit4 GDScript - C# API wrapper. /// public partial class GdUnit4CSharpApi : RefCounted { /// /// The signal to be emitted when the execution is completed. /// [Signal] #pragma warning disable CA1711 public delegate void ExecutionCompletedEventHandler(); #pragma warning restore CA1711 #pragma warning disable CA2213, SA1201 private CancellationTokenSource? executionCts; #pragma warning restore CA2213, SA1201 /// /// Indicates if the API loaded. /// /// Returns true if the API already loaded. public static bool IsApiLoaded() => true; /// /// Runs test discovery on the given script. /// /// The script to be scanned. /// The list of tests discovered as dictionary. public static Array DiscoverTests(CSharpScript sourceScript) { try { // Get the list of test case descriptors from the API var testCaseDescriptors = GdUnit4NetApiGodotBridge.DiscoverTestsFromScript(sourceScript); // Convert each TestCaseDescriptor to a Dictionary return testCaseDescriptors .Select(descriptor => new Dictionary { ["guid"] = descriptor.Id.ToString(), ["managed_type"] = descriptor.ManagedType, ["test_name"] = descriptor.ManagedMethod, ["source_file"] = sourceScript.ResourcePath, ["line_number"] = descriptor.LineNumber, ["attribute_index"] = descriptor.AttributeIndex, ["require_godot_runtime"] = descriptor.RequireRunningGodotEngine, ["code_file_path"] = descriptor.CodeFilePath ?? string.Empty, ["simple_name"] = descriptor.SimpleName, ["fully_qualified_name"] = descriptor.FullyQualifiedName, ["assembly_location"] = descriptor.AssemblyPath }) .Aggregate( new Array(), (array, dict) => { array.Add(dict); return array; }); } #pragma warning disable CA1031 catch (Exception e) #pragma warning restore CA1031 { GD.PrintErr($"Error discovering tests: {e.Message}\n{e.StackTrace}"); #pragma warning disable IDE0028 // Do not catch general exception types return new Array(); #pragma warning restore IDE0028 // Do not catch general exception types } } /// /// Creates a test suite based on the specified source path and line number. /// /// The path to the source file from which to create the test suite. /// The line number in the source file where the method to test is defined. /// The path where the test suite should be created. /// A dictionary containing information about the created test suite. public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) => GdUnit4NetApiGodotBridge.CreateTestSuite(sourcePath, lineNumber, testSuitePath); /// /// Gets the version of the GdUnit4 assembly. /// /// The version string of the GdUnit4 assembly. public static string Version() => GdUnit4NetApiGodotBridge.Version(); /// public override void _Notification(int what) { if (what != NotificationPredelete) return; executionCts?.Dispose(); executionCts = null; } /// /// Executes the tests and using the listener for reporting the results. /// /// A list of tests to be executed. /// The listener to report the results. public void ExecuteAsync(Array tests, Callable listener) { try { // Cancel any ongoing execution executionCts?.Cancel(); executionCts?.Dispose(); // Create new cancellation token source executionCts = new CancellationTokenSource(); Debug.Assert(tests != null, nameof(tests) + " != null"); var testSuiteNodes = new List { BuildTestSuiteNodeFrom(tests) }; GdUnit4NetApiGodotBridge.ExecuteAsync(testSuiteNodes, listener, executionCts.Token) .GetAwaiter() .OnCompleted(() => EmitSignal(SignalName.ExecutionCompleted)); } #pragma warning disable CA1031 catch (Exception e) #pragma warning restore CA1031 { GD.PrintErr($"Error executing tests: {e.Message}\n{e.StackTrace}"); Task.Run(() => { }).GetAwaiter().OnCompleted(() => EmitSignal(SignalName.ExecutionCompleted)); } } /// /// Will cancel the current test execution. /// public void CancelExecution() { try { executionCts?.Cancel(); } #pragma warning disable CA1031 catch (Exception e) #pragma warning restore CA1031 { GD.PrintErr($"Error cancelling execution: {e.Message}"); } } // Convert a set of Tests stored as Dictionaries to TestSuiteNode // all tests are assigned to a single test suit internal static TestSuiteNode BuildTestSuiteNodeFrom(Array tests) { if (tests.Count == 0) throw new InvalidOperationException("Cant build 'TestSuiteNode' from an empty test set."); // Create a suite ID var suiteId = Guid.NewGuid(); var firstTest = tests[0]; var managedType = firstTest["managed_type"].AsString(); var assemblyLocation = firstTest["assembly_location"].AsString(); var sourceFile = firstTest["source_file"].AsString(); // Create TestCaseNodes for each test in the suite var testCaseNodes = tests .Select(test => new TestCaseNode { Id = Guid.Parse(test["guid"].AsString()), ParentId = suiteId, ManagedMethod = test["test_name"].AsString(), LineNumber = test["line_number"].AsInt32(), AttributeIndex = test["attribute_index"].AsInt32(), RequireRunningGodotEngine = test["require_godot_runtime"].AsBool() }) .ToList(); return new TestSuiteNode { Id = suiteId, ParentId = Guid.Empty, ManagedType = managedType, AssemblyPath = assemblyLocation, SourceFile = sourceFile, Tests = testCaseNodes }; } } #else using Godot; using Godot.Collections; public partial class GdUnit4CSharpApi : RefCounted { [Signal] public delegate void ExecutionCompletedEventHandler(); public static bool IsApiLoaded() { GD.PushWarning("No `gdunit4.api` dependency found, check your project dependencies."); return false; } public static string Version() => "Unknown"; public static Array DiscoverTests(CSharpScript sourceScript) => new(); public void ExecuteAsync(Array tests, Callable listener) { } public static bool IsTestSuite(CSharpScript script) => false; public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) => new(); } #endif