making sure the issue comes from GDUnit addon folder
All checks were successful
Create tag and build when new code gets to main / Export (push) Successful in 7m6s
All checks were successful
Create tag and build when new code gets to main / Export (push) Successful in 7m6s
This commit is contained in:
235
addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs
Normal file
235
addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// The GdUnit4 GDScript - C# API wrapper.
|
||||
/// </summary>
|
||||
public partial class GdUnit4CSharpApi : RefCounted
|
||||
{
|
||||
/// <summary>
|
||||
/// The signal to be emitted when the execution is completed.
|
||||
/// </summary>
|
||||
[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
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the API loaded.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if the API already loaded.</returns>
|
||||
public static bool IsApiLoaded()
|
||||
=> true;
|
||||
|
||||
/// <summary>
|
||||
/// Runs test discovery on the given script.
|
||||
/// </summary>
|
||||
/// <param name="sourceScript">The script to be scanned.</param>
|
||||
/// <returns>The list of tests discovered as dictionary.</returns>
|
||||
public static Array<Dictionary> 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<Dictionary>(),
|
||||
(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<Dictionary>();
|
||||
#pragma warning restore IDE0028 // Do not catch general exception types
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test suite based on the specified source path and line number.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">The path to the source file from which to create the test suite.</param>
|
||||
/// <param name="lineNumber">The line number in the source file where the method to test is defined.</param>
|
||||
/// <param name="testSuitePath">The path where the test suite should be created.</param>
|
||||
/// <returns>A dictionary containing information about the created test suite.</returns>
|
||||
public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath)
|
||||
=> GdUnit4NetApiGodotBridge.CreateTestSuite(sourcePath, lineNumber, testSuitePath);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version of the GdUnit4 assembly.
|
||||
/// </summary>
|
||||
/// <returns>The version string of the GdUnit4 assembly.</returns>
|
||||
public static string Version()
|
||||
=> GdUnit4NetApiGodotBridge.Version();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void _Notification(int what)
|
||||
{
|
||||
if (what != NotificationPredelete)
|
||||
return;
|
||||
executionCts?.Dispose();
|
||||
executionCts = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the tests and using the listener for reporting the results.
|
||||
/// </summary>
|
||||
/// <param name="tests">A list of tests to be executed.</param>
|
||||
/// <param name="listener">The listener to report the results.</param>
|
||||
public void ExecuteAsync(Array<Dictionary> 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<TestSuiteNode> { 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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will cancel the current test execution.
|
||||
/// </summary>
|
||||
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<Dictionary> 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<Dictionary> DiscoverTests(CSharpScript sourceScript) => new();
|
||||
|
||||
public void ExecuteAsync(Array<Dictionary> tests, Callable listener)
|
||||
{
|
||||
}
|
||||
|
||||
public static bool IsTestSuite(CSharpScript script)
|
||||
=> false;
|
||||
|
||||
public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath)
|
||||
=> new();
|
||||
}
|
||||
#endif
|
||||
0
addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs.uid
Normal file
0
addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs.uid
Normal file
114
addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd
Normal file
114
addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd
Normal file
@@ -0,0 +1,114 @@
|
||||
## GdUnit4CSharpApiLoader
|
||||
##
|
||||
## A bridge class that handles communication between GDScript and C# for the GdUnit4 testing framework.
|
||||
## This loader acts as a compatibility layer to safely access the .NET API and ensure that calls
|
||||
## only proceed when the .NET environment is properly configured and available.
|
||||
## [br]
|
||||
## The class handles:
|
||||
## - Verification of .NET runtime availability
|
||||
## - Loading the C# wrapper script
|
||||
## - Checking for the GdUnit4Api assembly
|
||||
## - Providing proxy methods to access GdUnit4 functionality in C#
|
||||
@static_unload
|
||||
class_name GdUnit4CSharpApiLoader
|
||||
extends RefCounted
|
||||
|
||||
## Cached reference to the loaded C# wrapper script
|
||||
static var _gdUnit4NetWrapper: Script
|
||||
|
||||
## Cached instance of the API (singleton pattern)
|
||||
static var _api_instance: RefCounted
|
||||
|
||||
|
||||
class TestEventListener extends RefCounted:
|
||||
|
||||
func publish_event(event: Dictionary) -> void:
|
||||
var test_event := GdUnitEvent.new().deserialize(event)
|
||||
GdUnitSignals.instance().gdunit_event.emit(test_event)
|
||||
|
||||
static var _test_event_listener := TestEventListener.new()
|
||||
|
||||
|
||||
## Returns an instance of the GdUnit4CSharpApi wrapper.[br]
|
||||
## @return Script: The loaded C# wrapper or null if .NET is not supported
|
||||
static func instance() -> Script:
|
||||
if not GdUnit4CSharpApiLoader.is_api_loaded():
|
||||
return null
|
||||
|
||||
return _gdUnit4NetWrapper
|
||||
|
||||
|
||||
## Returns or creates a single instance of the API [br]
|
||||
## This improves performance by reusing the same object
|
||||
static func api_instance() -> RefCounted:
|
||||
if _api_instance == null and is_api_loaded():
|
||||
@warning_ignore("unsafe_method_access")
|
||||
_api_instance = instance().new()
|
||||
return _api_instance
|
||||
|
||||
|
||||
static func is_engine_version_supported(engine_version: int = Engine.get_version_info().hex) -> bool:
|
||||
return engine_version >= 0x40200
|
||||
|
||||
|
||||
## Checks if the .NET environment is properly configured and available.[br]
|
||||
## @return bool: True if .NET is fully supported and the assembly is found
|
||||
static func is_api_loaded() -> bool:
|
||||
# If the wrapper is already loaded we don't need to check again
|
||||
if _gdUnit4NetWrapper != null:
|
||||
return true
|
||||
|
||||
# First we check if this is a Godot .NET runtime instance
|
||||
if not ClassDB.class_exists("CSharpScript") or not is_engine_version_supported():
|
||||
return false
|
||||
# Second we check the C# project file exists
|
||||
var assembly_name: String = ProjectSettings.get_setting("dotnet/project/assembly_name")
|
||||
if assembly_name.is_empty() or not FileAccess.file_exists("res://%s.csproj" % assembly_name):
|
||||
return false
|
||||
|
||||
# Finally load the wrapper and check if the GdUnit4 assembly can be found
|
||||
_gdUnit4NetWrapper = load("res://addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs")
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return _gdUnit4NetWrapper.call("IsApiLoaded")
|
||||
|
||||
|
||||
## Returns the version of the GdUnit4 .NET assembly.[br]
|
||||
## @return String: The version string or "unknown" if .NET is not supported
|
||||
static func version() -> String:
|
||||
if not GdUnit4CSharpApiLoader.is_api_loaded():
|
||||
return "unknown"
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return instance().Version()
|
||||
|
||||
|
||||
static func discover_tests(source_script: Script) -> Array[GdUnitTestCase]:
|
||||
var tests: Array = _gdUnit4NetWrapper.call("DiscoverTests", source_script)
|
||||
|
||||
return Array(tests.map(GdUnitTestCase.from_dict), TYPE_OBJECT, "RefCounted", GdUnitTestCase)
|
||||
|
||||
|
||||
static func execute(tests: Array[GdUnitTestCase]) -> void:
|
||||
var net_api := api_instance()
|
||||
if net_api == null:
|
||||
push_warning("Execute C# tests not supported!")
|
||||
return
|
||||
var tests_as_dict: Array[Dictionary] = Array(tests.map(GdUnitTestCase.to_dict), TYPE_DICTIONARY, "", null)
|
||||
|
||||
net_api.call("ExecuteAsync", tests_as_dict, _test_event_listener.publish_event)
|
||||
@warning_ignore("unsafe_property_access")
|
||||
await net_api.ExecutionCompleted
|
||||
|
||||
|
||||
static func create_test_suite(source_path: String, line_number: int, test_suite_path: String) -> GdUnitResult:
|
||||
if not GdUnit4CSharpApiLoader.is_api_loaded():
|
||||
return GdUnitResult.error("Can't create test suite. No .NET support found.")
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var result: Dictionary = instance().CreateTestSuite(source_path, line_number, test_suite_path)
|
||||
if result.has("error"):
|
||||
return GdUnitResult.error(str(result.get("error")))
|
||||
return GdUnitResult.success(result)
|
||||
|
||||
|
||||
static func is_csharp_file(resource_path: String) -> bool:
|
||||
var ext := resource_path.get_extension()
|
||||
return ext == "cs" and GdUnit4CSharpApiLoader.is_api_loaded()
|
||||
Reference in New Issue
Block a user