├── Analyzers ├── AnalyzerReleases.Unshipped.md ├── nupkg │ └── .gitignore ├── src │ └── AssemblyInfo.cs ├── ReleaseNotes.txt ├── AnalyzerReleases.Shipped.md └── documentation │ ├── index.md │ ├── GdUnit0201.md │ ├── GdUnit0501.md │ └── GdUnit0500.md ├── Api ├── nupkg │ └── .gitignore ├── buildTransitive │ └── gdUnit4.api.props ├── build │ └── gdUnit4.api.props ├── src │ ├── asserts │ │ ├── IBoolAssert.cs │ │ ├── ISignalAssert.cs │ │ ├── CompareExtensions.cs │ │ ├── IObjectAssert.cs │ │ ├── IEnumerableAssert.cs │ │ ├── IDictionaryAssert.cs │ │ ├── IVectorAssert.cs │ │ ├── BoolAssert.cs │ │ ├── INumberAssert.cs │ │ ├── ITuple.cs │ │ ├── IValueExtractor.cs │ │ ├── IStringAssert.cs │ │ ├── Comparable.cs │ │ ├── Tuple.cs │ │ ├── ObjectAssert.cs │ │ └── IExceptionAssert.cs │ ├── AssemblyInfo.cs │ ├── core │ │ ├── runners │ │ │ ├── GdUnit4TestRunnerSceneTemplate.cs │ │ │ ├── GodotLogger.cs │ │ │ ├── DefaultTestRunner.cs │ │ │ └── GdUnit4TestRunnerSceneCore.cs │ │ ├── hooks │ │ │ ├── StdOutHookFactory.cs │ │ │ └── StdOutConsoleHook.cs │ │ ├── commands │ │ │ ├── Response.cs │ │ │ ├── IsAlive.cs │ │ │ ├── BaseCommand.cs │ │ │ └── TerminateGodotInstanceCommand.cs │ │ ├── execution │ │ │ ├── BeforeExecutionStage.cs │ │ │ ├── monitoring │ │ │ │ ├── OrphanNodesMonitor.cs │ │ │ │ ├── GodotExceptionPattern.cs │ │ │ │ ├── GodotPushErrorPattern.cs │ │ │ │ └── MemoryPool.cs │ │ │ ├── BeforeTestExecutionStage.cs │ │ │ ├── DirectCommandExecutor.cs │ │ │ ├── exceptions │ │ │ │ └── ExecutionTimeoutException.cs │ │ │ └── TestCaseExecutionStage.cs │ │ ├── reporting │ │ │ └── TestReportCollector.cs │ │ ├── DebuggerUtils.cs │ │ ├── attributes │ │ │ ├── TestCategoryAttribute.cs │ │ │ ├── RequireGodotRuntimeAttribute.cs │ │ │ ├── TestSuiteAttribute.cs │ │ │ ├── AfterAttribute.cs │ │ │ ├── BeforeAttribute.cs │ │ │ ├── BeforeTestAttribute.cs │ │ │ ├── AfterTestAttribute.cs │ │ │ ├── TraitAttribute.cs │ │ │ ├── GodotExceptionMonitorAttribute.cs │ │ │ ├── TestStageAttribute.cs │ │ │ └── FuzzerAttribute.cs │ │ ├── extensions │ │ │ └── SystemVectorExtension.cs │ │ ├── discovery │ │ │ └── CodeNavigation.cs │ │ ├── data │ │ │ └── IValueProvider.cs │ │ └── MouseMoveTask.cs │ ├── api │ │ ├── LogLevel.cs │ │ ├── TestNode.cs │ │ ├── ITestRunner.cs │ │ ├── ITestEventListener.cs │ │ ├── TestAssemblyNode.cs │ │ ├── IGdUnitAwaitable.cs │ │ ├── TestSuiteNode.cs │ │ ├── IDebuggerFramework.cs │ │ ├── TestCaseNode.cs │ │ ├── ITestEngineLogger.cs │ │ ├── ReportType.cs │ │ ├── ICommandExecutor.cs │ │ ├── ITestReport.cs │ │ ├── TestRunnerConfig.cs │ │ └── ITestEvent.cs │ └── constraints │ │ ├── IBoolConstraint.cs │ │ └── IObjectConstraint.cs └── nuget.config ├── TestAdapter ├── nupkg │ └── .gitignore ├── src │ ├── AssemblyInfo.cs │ ├── extensions │ │ ├── TestEventExtensions.cs │ │ └── StringExtensions.cs │ ├── discovery │ │ └── TestCaseDiscoverySink.cs │ ├── DefaultDebuggerFramework.cs │ ├── RiderDebuggerFramework.cs │ ├── utilities │ │ ├── IdeDetector.cs │ │ ├── Utils.cs │ │ └── Logger.cs │ ├── settings │ │ └── GdUnit4SettingsProvider.cs │ └── execution │ │ └── TestCaseFilter.cs └── ReleaseNotes.txt ├── Api.Test ├── .gitignore ├── .gitattributes ├── src │ ├── core │ │ ├── resources │ │ │ ├── testsuites │ │ │ │ └── mono │ │ │ │ │ ├── spaceA │ │ │ │ │ └── TestSuite.cs │ │ │ │ │ ├── spaceB │ │ │ │ │ └── TestSuite.cs │ │ │ │ │ ├── noSpace │ │ │ │ │ └── TestSuiteWithoutNamespace.cs │ │ │ │ │ ├── NotATestSuite.cs │ │ │ │ │ ├── ExampleTestSuiteA.cs │ │ │ │ │ ├── TestSuiteAllStagesSuccess.cs │ │ │ │ │ ├── TestSuiteWithFileScopedNamespace.cs │ │ │ │ │ ├── TestSuiteFailOnTestCase1.cs │ │ │ │ │ ├── TestSuiteFailOnStageBeforeTest.cs │ │ │ │ │ ├── TestSuiteFailOnStageAfter.cs │ │ │ │ │ ├── TestSuiteFailOnStageBefore.cs │ │ │ │ │ ├── TestSuiteFailOnStageAfterTest.cs │ │ │ │ │ ├── TestSuiteFailOnMultiStages.cs │ │ │ │ │ ├── TestSuiteParameterizedTests.cs │ │ │ │ │ ├── TestSuiteAllTestsFailWithExceptions.cs │ │ │ │ │ ├── TestSuiteFailAndOrphansDetected.cs │ │ │ │ │ └── TestSuiteAbortOnTestTimeout.cs │ │ │ ├── scenes │ │ │ │ ├── SimpleScene.scn │ │ │ │ ├── DragAndDrop │ │ │ │ │ ├── icon.png │ │ │ │ │ ├── DragAndDropControl.tscn │ │ │ │ │ ├── DragAndDropTestScene.gd │ │ │ │ │ ├── DragAndDropTestScene.tscn │ │ │ │ │ └── DragAndDropControl.gd │ │ │ │ ├── TestSceneWithInitialization.tscn │ │ │ │ ├── SimpleScene.gd │ │ │ │ ├── TestSceneWithExceptionTest.tscn │ │ │ │ ├── SimpleScene.tscn │ │ │ │ ├── TestSceneWithInitialization.cs │ │ │ │ ├── Spell.gd │ │ │ │ ├── TestSceneWithButton.tscn │ │ │ │ ├── TestSceneWithExceptionTest.cs │ │ │ │ └── Spell.cs │ │ │ └── sources │ │ │ │ ├── TestPerson.cs │ │ │ │ └── TestPerson2.cs │ │ ├── UtilsX2.cs │ │ ├── execution │ │ │ └── monitoring │ │ │ │ ├── ExampleEventBus.cs │ │ │ │ └── ExampleWithWithEventBus.cs │ │ ├── ExternalDataPoints.cs │ │ ├── hooks │ │ │ └── StdOutConsoleHookTest.cs │ │ ├── GodotRuntimeAnalyzerExampleTestSuite.cs │ │ ├── discovery │ │ │ ├── DiscoverTestUtils.cs │ │ │ └── TestCaseDescriptorTest.cs │ │ └── TestSuiteWithExpectedExceptions.cs │ ├── GdUnit4NetApiGodotBridgeTest.cs │ ├── asserts │ │ ├── ValueFormatter.cs │ │ ├── CSharpTypes │ │ │ └── Player.cs │ │ └── Example.cs │ ├── extensions │ │ ├── GodotVariantExtensionsTest.cs │ │ └── GodotObjectExtensionsTest.cs │ └── UtilsTest.cs ├── .editorconfig ├── run_tests.sh ├── nuget.config ├── Directory.Build.props ├── project.godot ├── icon.svg └── GdUnit4ApiTest.csproj ├── Example ├── .gitignore ├── assets │ └── TestExplorerRun.png ├── .editorconfig ├── Directory.Build.props ├── src │ └── Menu.tscn ├── nuget.config ├── project.godot ├── test │ ├── api │ │ ├── CSharpTypes │ │ │ └── Player.cs │ │ └── AssertionsTest.cs │ └── ExampleTest.cs ├── icon.svg ├── .runsettings ├── ExampleProject.csproj └── .runsettings-ci ├── global.json ├── Analyzers.Test ├── src │ ├── UtilsX1.cs │ ├── UtilsX2.cs │ ├── ModuleInitializer.cs │ ├── TestSourceBuilder.cs │ └── ExampleTest.cs ├── GdUnit4Analyzers.Tests.csproj ├── .runsettings └── test.ruleset ├── TestAdapter.Test ├── test │ ├── NoOpLogger.cs │ ├── TestUtils.cs │ ├── resources │ │ ├── project.godot │ │ └── project.godot2 │ └── utilities │ │ └── UtilsTest.cs ├── GdUnit4TestAdapter.Tests.csproj ├── .runsettings └── test.ruleset ├── .vscode ├── extensions.json ├── tasks.json └── settings.json ├── GdUnit4Net.sln.DotSettings ├── stylecop.json ├── clean.sh ├── ProjectVersions.props └── LICENSE /Analyzers/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Api/nupkg/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /Analyzers/nupkg/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /TestAdapter/nupkg/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /Api.Test/.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | -------------------------------------------------------------------------------- /Example/.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | gdunit4_testadapter/ 4 | 5 | -------------------------------------------------------------------------------- /Api.Test/.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /Example/assets/TestExplorerRun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godot-gdunit-labs/gdUnit4Net/HEAD/Example/assets/TestExplorerRun.png -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/spaceA/TestSuite.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.SpaceA; 2 | 3 | public class TestSuite 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/spaceB/TestSuite.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.SpaceB; 2 | 3 | public class TestSuite 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/SimpleScene.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godot-gdunit-labs/gdUnit4Net/HEAD/Api.Test/src/core/resources/scenes/SimpleScene.scn -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/DragAndDrop/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godot-gdunit-labs/gdUnit4Net/HEAD/Api.Test/src/core/resources/scenes/DragAndDrop/icon.png -------------------------------------------------------------------------------- /Api.Test/.editorconfig: -------------------------------------------------------------------------------- 1 | # Temporary ignore the global editor config for this sub project 2 | root = true 3 | 4 | # Apply to all files in this directory and subdirectories 5 | [*] 6 | -------------------------------------------------------------------------------- /Example/.editorconfig: -------------------------------------------------------------------------------- 1 | # Temporary ignore the global editor config for this sub project 2 | root = true 3 | 4 | # Apply to all files in this directory and subdirectories 5 | [*] 6 | -------------------------------------------------------------------------------- /Api/buildTransitive/gdUnit4.api.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(DefineConstants);GDUNIT4NET_API_V5 4 | 5 | 6 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.308", 4 | "rollForward": "latestPatch", 5 | "allowPrerelease": false 6 | }, 7 | "msbuild-sdks": { 8 | "Godot.NET.Sdk": "4.4" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Api/build/gdUnit4.api.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(DefineConstants);GDUNIT4NET_API_V5 5 | 6 | 7 | -------------------------------------------------------------------------------- /Analyzers.Test/src/UtilsX1.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Analyzers.Test; 2 | 3 | public static class UtilsX1 4 | { 5 | // [MethodImpl(MethodImplOptions.AggressiveInlining)] 6 | public static string Foo() => string.Empty; 7 | } 8 | -------------------------------------------------------------------------------- /Analyzers/src/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("gdUnit4Analyzers.Tests")] 7 | -------------------------------------------------------------------------------- /TestAdapter.Test/test/NoOpLogger.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.TestAdapter.Test; 2 | 3 | using Api; 4 | 5 | internal sealed class NoOpLogger : ITestEngineLogger 6 | { 7 | public void SendMessage(LogLevel logLevel, string message) 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/noSpace/TestSuiteWithoutNamespace.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CA1050 // Declare types in namespaces 2 | public class TestSuiteWithoutNamespace 3 | #pragma warning restore CA1050 // Declare types in namespaces 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /TestAdapter/src/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("gdUnit4.TestAdapter.Test")] 7 | -------------------------------------------------------------------------------- /Api.Test/src/core/UtilsX2.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Core; 2 | 3 | using Godot; 4 | 5 | public static class UtilsX2 6 | { 7 | public static string Foo() => ""; 8 | 9 | public static string Bar() => ProjectSettings.GlobalizePath("res://src/core/resources/sources/TestPerson.cs"); 10 | } 11 | -------------------------------------------------------------------------------- /Api.Test/run_tests.sh: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env bash 2 | 3 | rm -rf ./reports 4 | $GODOT_BIN . --headless --build-solutions --quit-after 100 --verbose 5 | echo "Compile exit: $?" 6 | 7 | 8 | echo "Run tests" 9 | 10 | dotnet test --settings .runsettings --results-directory ./reports 11 | echo "Exit Code: $?" 12 | -------------------------------------------------------------------------------- /Analyzers.Test/src/UtilsX2.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Analyzers.Test; 2 | 3 | using Godot; 4 | 5 | public static class UtilsX2 6 | { 7 | public static string Foo() => string.Empty; 8 | 9 | public static string Bar() => ProjectSettings.GlobalizePath("res://src/core/resources/sources/TestPerson.cs"); 10 | } 11 | -------------------------------------------------------------------------------- /Api.Test/src/GdUnit4NetApiGodotBridgeTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests; 2 | 3 | using static Assertions; 4 | 5 | [TestSuite] 6 | public class GdUnit4NetApiGodotBridgeTest 7 | { 8 | [TestCase] 9 | public void Version() 10 | => AssertThat(GdUnit4NetApiGodotBridge.Version()).StartsWith("5.1."); 11 | } 12 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/TestSceneWithInitialization.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://b4d0favbx8ckg"] 2 | 3 | [ext_resource type="Script" path="res://src/core/resources/scenes/TestSceneWithInitialization.cs" id="1"] 4 | 5 | [node name="SceneWithInitialization" type="Node2D"] 6 | script = ExtResource("1") 7 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/SimpleScene.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | class Player extends Node: 4 | var position :Vector3 = Vector3.ZERO 5 | 6 | 7 | func _init(): 8 | set_name("Player") 9 | 10 | 11 | func is_on_floor() -> bool: 12 | return true 13 | 14 | 15 | func _ready(): 16 | add_child(Player.new(), true) 17 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-dotnettools.csharp", 4 | "EditorConfig.EditorConfig", 5 | "selcukermaya.se-csproj-extensions", 6 | "josefpihrt-vscode.roslynator", 7 | "streetsidesoftware.code-spell-checker", 8 | "DavidAnson.vscode-markdownlint", 9 | "Gruntfuggly.todo-tree" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/NotATestSuite.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using static GdUnit4.Assertions; 4 | 5 | // will be ignored because of missing `[TestSuite]` annotation 6 | public partial class NotATestSuite 7 | { 8 | [TestCase] 9 | public void TestFoo() 10 | => AssertBool(true).IsEqual(false); 11 | } 12 | -------------------------------------------------------------------------------- /Api.Test/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | -------------------------------------------------------------------------------- /Example/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | false 7 | false 8 | 9 | 10 | -------------------------------------------------------------------------------- /Api/src/asserts/IBoolAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Asserts; 4 | 5 | using Constraints; 6 | 7 | /// 8 | /// An Assertion to verify boolean values. 9 | /// 10 | public interface IBoolAssert : IBoolConstraint, IAssertMessage 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /Api/src/asserts/ISignalAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Asserts; 4 | 5 | using Constraints; 6 | 7 | /// 8 | /// An Assertion Tool to verify Godot signals. 9 | /// 10 | public interface ISignalAssert : ISignalConstraint, IAssertMessage 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /Api.Test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | false 7 | false 8 | 9 | 10 | -------------------------------------------------------------------------------- /Api/src/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("GdUnit4ApiTest")] 7 | [assembly: InternalsVisibleTo("GdUnit4.TestAdapter")] 8 | [assembly: InternalsVisibleTo("GdUnit4.Analyzers")] 9 | [assembly: InternalsVisibleTo("GdUnit4Analyzers.Tests")] 10 | -------------------------------------------------------------------------------- /GdUnit4Net.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | False -------------------------------------------------------------------------------- /Api.Test/src/core/execution/monitoring/ExampleEventBus.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Core.Execution.Monitoring; 2 | 3 | using Godot; 4 | 5 | public partial class ExampleEventBus : Node 6 | { 7 | [Signal] 8 | public delegate void OnMyEventEventHandler(); 9 | 10 | public void Emit() => EmitSignal(SignalName.OnMyEvent); 11 | 12 | public void Connect(Callable callback) => Connect(SignalName.OnMyEvent, callback); 13 | } 14 | -------------------------------------------------------------------------------- /Example/src/Menu.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=3 uid="uid://5hobjg6fgrwy"] 2 | 3 | [node name="Menu" type="Control"] 4 | layout_mode = 3 5 | anchors_preset = 15 6 | anchor_right = 1.0 7 | anchor_bottom = 1.0 8 | grow_horizontal = 2 9 | grow_vertical = 2 10 | 11 | [node name="Button" type="Button" parent="."] 12 | layout_mode = 0 13 | offset_left = 308.0 14 | offset_top = 188.0 15 | offset_right = 465.0 16 | offset_bottom = 228.0 17 | text = "Click me" 18 | -------------------------------------------------------------------------------- /TestAdapter.Test/test/TestUtils.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.TestAdapter.Test; 2 | 3 | internal static class TestUtils 4 | { 5 | public static string GetResourcePath(string resourcePath) 6 | { 7 | var baseDir = AppDomain.CurrentDomain.BaseDirectory; 8 | var projectRoot = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..")); 9 | return Path.Combine(projectRoot, "test", "resources", resourcePath); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Api/src/core/runners/GdUnit4TestRunnerSceneTemplate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace GdUnit4.TestRunner; 6 | 7 | using Core.Runners; 8 | 9 | /// 10 | /// The GdUnit4Net test runner scene. 11 | /// 12 | public partial class GdUnit4TestRunnerSceneTemplate : GdUnit4TestRunnerSceneCore 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/TestSceneWithExceptionTest.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://cjd3v7it80k0u"] 2 | 3 | [ext_resource type="Script" path="res://src/core/resources/scenes/TestSceneWithExceptionTest.cs" id="1_m8etn"] 4 | 5 | [node name="TestSceneWithExceptionTest" type="Control"] 6 | layout_mode = 3 7 | anchors_preset = 15 8 | anchor_right = 1.0 9 | anchor_bottom = 1.0 10 | grow_horizontal = 2 11 | grow_vertical = 2 12 | script = ExtResource("1_m8etn") 13 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/SimpleScene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://cn8ucy2rheu0f"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://c8au0x20kepw0" path="res://icon.svg" id="1"] 4 | [ext_resource type="Script" path="res://src/core/resources/scenes/SimpleScene.gd" id="2"] 5 | 6 | [node name="Node2D" type="Node2D"] 7 | script = ExtResource("2") 8 | 9 | [node name="Sprite2D" type="Sprite2D" parent="."] 10 | position = Vector2(504, 252) 11 | texture = ExtResource("1") 12 | -------------------------------------------------------------------------------- /Api/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/ExampleTestSuiteA.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using static Assertions; 4 | // will be ignored because of missing `[TestSuite]` annotation 5 | // used by executor integration test 6 | public class ExampleTestSuiteA 7 | { 8 | 9 | [TestCase] 10 | public void TestCase1() 11 | => AssertBool(true).IsEqual(false); 12 | 13 | [TestCase] 14 | public void TestCase2() 15 | => AssertBool(true).IsEqual(false); 16 | } 17 | -------------------------------------------------------------------------------- /Api/src/asserts/CompareExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Asserts; 5 | 6 | internal static class CompareExtensions 7 | { 8 | internal static bool IsEquals(this T? c, T? e) => Comparable.IsEqual(c, e).Valid; 9 | 10 | internal static bool IsSame(this T? c, T? e) 11 | where TAssert : IAssert 12 | => AssertBase.IsSame(c, e); 13 | } 14 | -------------------------------------------------------------------------------- /Api.Test/src/core/execution/monitoring/ExampleWithWithEventBus.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Core.Execution.Monitoring; 2 | 3 | using System; 4 | 5 | using Godot; 6 | 7 | public partial class ExampleWithWithEventBus : Node 8 | { 9 | public void Register(ExampleEventBus bus) 10 | => bus.Connect(new Callable(this, nameof(MyCallback))); 11 | 12 | #pragma warning disable CA2201 13 | private void MyCallback() => throw new NullReferenceException("Nope"); 14 | #pragma warning restore CA2201 15 | } 16 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/TestSceneWithInitialization.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | 3 | namespace GdUnit4.Tests.core.resources.scenes; 4 | 5 | using System.Collections.Generic; 6 | 7 | public partial class TestSceneWithInitialization : Node2D 8 | { 9 | private readonly List methodCalls = new(); 10 | public List MethodCalls => methodCalls; 11 | public void Initialize() => methodCalls.Add("Initialize"); 12 | 13 | public override void _Ready() => methodCalls.Add("_Ready"); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteAllStagesSuccess.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using static Assertions; 4 | 5 | // will be ignored because of missing `[TestSuite]` annotation 6 | // used by executor integration test 7 | public class TestSuiteAllStagesSuccess 8 | { 9 | 10 | [TestCase] 11 | public void TestCase1() 12 | => AssertBool(true).IsEqual(true); 13 | 14 | [TestCase] 15 | public void TestCase2() 16 | => AssertBool(true).IsEqual(true); 17 | } 18 | -------------------------------------------------------------------------------- /Api.Test/src/asserts/ValueFormatter.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.asserts; 2 | 3 | using GdUnit4.Asserts; 4 | 5 | public static class ValueFormatter 6 | { 7 | internal static string AsString(object? value) 8 | { 9 | if (value == null) 10 | return "NULL"; 11 | if (value is string s) 12 | return $"\"{s}\""; 13 | if (value.GetType().IsPrimitive) 14 | return value.ToString() ?? "NULL"; 15 | 16 | return AssertFailures.AsObjectId(value); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteWithFileScopedNamespace.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using static Assertions; 4 | // will be ignored because of missing `[TestSuite]` annotation 5 | // used by executor integration test 6 | public class TestSuiteWithFileScopedNamespace 7 | { 8 | 9 | [TestCase] 10 | public void TestCase1() 11 | => AssertBool(true).IsEqual(false); 12 | 13 | [TestCase] 14 | public void TestCase2() 15 | => AssertBool(true).IsEqual(false); 16 | } 17 | -------------------------------------------------------------------------------- /Api/src/asserts/IObjectAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Asserts; 4 | 5 | using Constraints; 6 | 7 | /// 8 | /// An Assertion Tool to verify object values. 9 | /// 10 | /// 11 | /// The object type being tested. 12 | /// 13 | public interface IObjectAssert : IObjectConstraint, IAssertMessage> 14 | { 15 | } 16 | -------------------------------------------------------------------------------- /Api.Test/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="GdUnit4ApiTest" 14 | config/features=PackedStringArray("4.3", "C#", "Forward Plus") 15 | config/icon="res://icon.svg" 16 | 17 | [dotnet] 18 | 19 | project/assembly_name="GdUnit4ApiTest" 20 | -------------------------------------------------------------------------------- /Api/src/asserts/IEnumerableAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Asserts; 4 | 5 | using Constraints; 6 | 7 | /// 8 | /// An Assertion tool to verify enumerating. 9 | /// 10 | /// The type of elements in the enumerable being asserted. 11 | public interface IEnumerableAssert : IEnumerableConstraint, IAssertMessage> 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /Example/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | -------------------------------------------------------------------------------- /TestAdapter.Test/test/resources/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="gdUnit4Test" 14 | config/features=PackedStringArray("4.3", "C#", "Forward Plus") 15 | config/icon="res://icon.svg" 16 | 17 | [dotnet] 18 | 19 | project/assembly_name="gdUnit4Test" 20 | -------------------------------------------------------------------------------- /Example/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="ExampleProject" 14 | config/features=PackedStringArray("4.2.2", "C#", "Forward Plus") 15 | config/icon="res://icon.svg" 16 | run/main_scene="res://src/Menu.tscn" 17 | 18 | [dotnet] 19 | 20 | project/assembly_name="ExampleProject" 21 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/DragAndDrop/DragAndDropControl.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://ca2rr3dan4vvw"] 2 | 3 | [ext_resource type="Script" path="res://src/core/resources/scenes/DragAndDrop/DragAndDropControl.gd" id="1"] 4 | 5 | [node name="Panel" type="PanelContainer"] 6 | offset_left = 245.0 7 | offset_top = 232.0 8 | offset_right = 350.0 9 | offset_bottom = 337.0 10 | size_flags_horizontal = 3 11 | size_flags_vertical = 3 12 | script = ExtResource("1") 13 | 14 | [node name="TextureRect" type="TextureRect" parent="."] 15 | layout_mode = 2 16 | expand_mode = 1 17 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/DragAndDrop/DragAndDropTestScene.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | @onready var texture = preload("res://src/core/resources/scenes/DragAndDrop/icon.png") 4 | 5 | func _ready(): 6 | # set initial drag texture 7 | $left/TextureRect.texture = texture 8 | 9 | 10 | # Virtual method to be implemented by the user. Use this method to process and accept inputs on UI elements. See accept_event(). 11 | func _gui_input(_event): 12 | #prints("Game _gui_input", _event.as_text()) 13 | pass 14 | 15 | 16 | func _on_Button_button_down(): 17 | # print("BUTTON DOWN") 18 | pass 19 | -------------------------------------------------------------------------------- /Api/src/api/LogLevel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Api; 4 | 5 | /// 6 | /// Defines the available logging severity levels. 7 | /// 8 | public enum LogLevel 9 | { 10 | /// 11 | /// Informational message. 12 | /// 13 | Informational = 0, 14 | 15 | /// 16 | /// Warning message. 17 | /// 18 | Warning = 1, 19 | 20 | /// 21 | /// Error message. 22 | /// 23 | Error = 2 24 | } 25 | -------------------------------------------------------------------------------- /Api/src/core/hooks/StdOutHookFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Hooks; 5 | 6 | using System; 7 | 8 | internal static class StdOutHookFactory 9 | { 10 | public static IStdOutHook CreateStdOutHook() 11 | { 12 | if (OperatingSystem.IsWindows()) 13 | return new WindowsStdOutHook(); 14 | if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()) 15 | return new UnixStdOutHook(); 16 | throw new PlatformNotSupportedException("Unsupported operating system"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/sources/TestPerson.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Example.Test.Resources; 2 | 3 | public class TestPerson 4 | { 5 | 6 | public TestPerson(string firstName, string lastName) 7 | { 8 | FirstName = firstName; 9 | LastName = lastName; 10 | } 11 | 12 | public string FirstName { get; } 13 | 14 | public string LastName { get; } 15 | 16 | public string FullName => FirstName + " " + LastName; 17 | 18 | public string FullName2() => FirstName + " " + LastName; 19 | 20 | public string FullName3() 21 | { 22 | var fullName = $"{FirstName} {LastName}"; 23 | return fullName; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/sources/TestPerson2.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CA1050 // Declare types in namespaces 2 | public class TestPerson2 3 | #pragma warning restore CA1050 // Declare types in namespaces 4 | { 5 | 6 | public TestPerson2(string firstName, string lastName) 7 | { 8 | FirstName = firstName; 9 | LastName = lastName; 10 | } 11 | 12 | public string FirstName { get; } 13 | 14 | public string LastName { get; } 15 | 16 | public string FullName => FirstName + " " + LastName; 17 | 18 | public string FullName2() => FirstName + " " + LastName; 19 | 20 | public string FullName3() => FirstName + " " + LastName; 21 | } 22 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Mike Schulze", 6 | "copyrightText": "Copyright (c) 2025 Mike Schulze\nMIT License - See LICENSE file in the repository root for full license text", 7 | "xmlHeader": false, 8 | "headerDecoration": "", 9 | "documentInternalElements": false, 10 | "documentPrivateElements": false, 11 | "documentPrivateFields": false, 12 | "fileNamingConvention": "stylecop" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Example/test/api/CSharpTypes/Player.cs: -------------------------------------------------------------------------------- 1 | namespace Examples.Test.Api.CSharpTypes; 2 | 3 | // Original Player class (without IEquatable for comparison) 4 | public class Player 5 | { 6 | public Player(string name, int level, float health, bool isAlive) 7 | { 8 | Name = name; 9 | Level = level; 10 | Health = health; 11 | IsAlive = isAlive; 12 | } 13 | 14 | public string Name { get; } 15 | 16 | public int Level { get; } 17 | public float Health { get; } 18 | public bool IsAlive { get; } 19 | 20 | public override string ToString() 21 | => $"Player(Name: {Name}, Level: {Level}, Health: {Health}, IsAlive: {IsAlive})"; 22 | } 23 | -------------------------------------------------------------------------------- /Api.Test/src/core/ExternalDataPoints.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Core; 2 | 3 | using System.Collections.Generic; 4 | 5 | public static class ExternalDataPoints 6 | { 7 | public static IEnumerable PublicArrayDataPointProperty => [[1, 2, 3], [4, 5, 9]]; 8 | private static IEnumerable PrivateArrayDataPointProperty => [[1, 2, 3], [4, 5, 9]]; 9 | 10 | public static IEnumerable PublicArrayDataPointMethod() => [[1, 2, 3], [4, 5, 9]]; 11 | #pragma warning disable CA1859 // #warning directive 12 | private static IEnumerable PrivateArrayDataPointMethod() => [[1, 2, 3], [4, 5, 9]]; 13 | #pragma warning restore CA1859 // #warning directive 14 | } 15 | -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | rm -rf ./bin 2 | rm -rf ./obj 3 | rm -rf ./Api/nupkg/* 4 | rm -rf ./Api/obj 5 | 6 | rm -rf .godot 7 | 8 | rm -rf ./Api.Test/.godot 9 | rm -rf ./Api.Test/gdunit4_testadapter 10 | 11 | rm -rf ./Example/.godot 12 | rm -rf ./Example/gdunit4_testadapter 13 | 14 | 15 | rm -rf ./TestAdapter/nupkg/* 16 | rm -rf ./TestAdapter/obj 17 | 18 | 19 | dotnet build ./Api/GdUnit4Api.csproj 20 | dotnet build ./TestAdapter/GdUnit4TestAdapter.csproj 21 | dotnet restore 22 | dotnet build 23 | 24 | 25 | $GODOT_BIN --path ./Example --headless --build-solutions --quit-after 20 26 | # dotnet clean 27 | # dotnet restore 28 | # dotnet build 29 | # "dotnet.unitTests.runSettingsPath": "./test/.runsettings" 30 | -------------------------------------------------------------------------------- /Api.Test/src/asserts/CSharpTypes/Player.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Asserts.CSharpTypes; 2 | 3 | // Original Player class (without IEquatable for comparison) 4 | public class Player 5 | { 6 | public Player(string name, int level, float health, bool isAlive) 7 | { 8 | Name = name; 9 | Level = level; 10 | Health = health; 11 | IsAlive = isAlive; 12 | } 13 | 14 | public string Name { get; } 15 | 16 | public int Level { get; } 17 | public float Health { get; } 18 | public bool IsAlive { get; } 19 | 20 | public override string ToString() 21 | => $"Player(Name: {Name}, Level: {Level}, Health: {Health}, IsAlive: {IsAlive})"; 22 | } 23 | -------------------------------------------------------------------------------- /TestAdapter/src/extensions/TestEventExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.TestAdapter.Extensions; 5 | 6 | using Api; 7 | 8 | using Microsoft.VisualStudio.TestPlatform.ObjectModel; 9 | 10 | internal static class TestEventExtensions 11 | { 12 | public static TestOutcome AsTestOutcome(this ITestEvent e) 13 | { 14 | if (e.IsFailed || e.IsError) 15 | return TestOutcome.Failed; 16 | if (e.IsWarning) 17 | return TestOutcome.Passed; 18 | if (e.IsSkipped) 19 | return TestOutcome.Skipped; 20 | return TestOutcome.Passed; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Analyzers/ReleaseNotes.txt: -------------------------------------------------------------------------------- 1 | v1.0.0 2 | This is the Initial release of the analyzers. 3 | 4 | GdUnit0201: Multiple TestCase attributes not allowed with DataPoint 5 | GdUnit0500: Godot Runtime Required for Test Class 6 | GdUnit0501: Godot Runtime Required for Test Method 7 | 8 | Features: 9 | - Compile-time validation of GdUnit4 attribute combinations 10 | - Provides clear error messages with method name identification 11 | - Integrates seamlessly with Visual Studio and other IDEs 12 | 13 | Integration: 14 | - Built for .NET Standard 2.0 compatibility 15 | 16 | Technical Details: 17 | - Implementation based on Roslyn analyzer framework 18 | - IDE support for real-time error detection 19 | - Includes a help link to documentation 20 | -------------------------------------------------------------------------------- /TestAdapter/src/discovery/TestCaseDiscoverySink.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.TestAdapter.Discovery; 5 | 6 | using System.Collections.Concurrent; 7 | 8 | using Microsoft.VisualStudio.TestPlatform.ObjectModel; 9 | using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; 10 | 11 | internal sealed class TestCaseDiscoverySink : ITestCaseDiscoverySink 12 | { 13 | private readonly ConcurrentBag testCases = []; 14 | 15 | public IReadOnlyList TestCases => [.. testCases.OrderBy(tc => tc.FullyQualifiedName)]; 16 | 17 | public void SendTestCase(TestCase test) => testCases.Add(test); 18 | } 19 | -------------------------------------------------------------------------------- /Api/src/asserts/IDictionaryAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Asserts; 4 | 5 | using Constraints; 6 | 7 | /// 8 | /// An Assertion Tool to verify dictionary values. 9 | /// Provides specialized assertions for validating dictionaries and their contents. 10 | /// 11 | /// The type of keys in the dictionary. 12 | /// The type of values in the dictionary. 13 | public interface IDictionaryAssert : IDictionaryConstraint, IAssertMessage> 14 | where TKey : notnull 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /Api/src/constraints/IBoolConstraint.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Constraints; 5 | 6 | using Asserts; 7 | 8 | /// 9 | /// A set of constrains to verify boolean values. 10 | /// 11 | public interface IBoolConstraint : IAssertBase 12 | { 13 | /// 14 | /// Verifies that the current value is true. 15 | /// 16 | /// IBoolConstrains. 17 | IBoolConstraint IsTrue(); 18 | 19 | /// 20 | /// Verifies that the current value is false. 21 | /// 22 | /// IBoolConstrains. 23 | IBoolConstraint IsFalse(); 24 | } 25 | -------------------------------------------------------------------------------- /Api/src/core/commands/Response.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Commands; 5 | 6 | using System.Net; 7 | 8 | /// 9 | /// Represents a response from a command execution. 10 | /// Contains status code and optional payload data. 11 | /// 12 | public record Response 13 | { 14 | /// 15 | /// Gets hTTP status code indicating the result of the command execution. 16 | /// 17 | public HttpStatusCode StatusCode { get; init; } 18 | 19 | /// 20 | /// Gets optional payload containing command-specific response data. 21 | /// 22 | public string Payload { get; init; } = string.Empty; 23 | } 24 | -------------------------------------------------------------------------------- /Api.Test/src/asserts/Example.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Asserts; 2 | // GdUnit generated TestSuite 3 | 4 | using System; 5 | 6 | internal sealed partial class ExampleNode : Godot.Node, IEquatable 7 | { 8 | private int Value { get; set; } 9 | private string Msg { get; set; } 10 | 11 | public ExampleNode(string msg, int value) 12 | { 13 | Msg = msg; 14 | Value = value; 15 | } 16 | 17 | public override bool Equals(object? obj) 18 | => obj is ExampleNode example 19 | && Value == example.Value 20 | && Msg == example.Msg; 21 | 22 | public bool Equals(ExampleNode? obj) 23 | => obj is ExampleNode example 24 | && Value == example.Value 25 | && Msg == example.Msg; 26 | public override int GetHashCode() => HashCode.Combine(Value, Msg); 27 | } 28 | -------------------------------------------------------------------------------- /Api/src/core/execution/BeforeExecutionStage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Execution; 5 | 6 | using System.Threading.Tasks; 7 | 8 | internal class BeforeExecutionStage : ExecutionStage 9 | { 10 | public BeforeExecutionStage(TestSuite testSuite) 11 | : base("Before", testSuite.Instance.GetType()) 12 | { 13 | } 14 | 15 | public override async Task Execute(ExecutionContext context) 16 | { 17 | context.MemoryPool.SetActive(StageName, true); 18 | await base 19 | .Execute(context) 20 | .ConfigureAwait(true); 21 | context.FireBeforeEvent(); 22 | context.ReportCollector.Clear(); 23 | context.MemoryPool.StopMonitoring(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Api/src/asserts/IVectorAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Asserts; 4 | 5 | using Constraints; 6 | 7 | /// 8 | /// An assertion tool to verify Godot.Vector values in the GdUnit4 testing framework. 9 | /// Provides specialized methods for comparing and validating vector types from the Godot engine. 10 | /// 11 | /// 12 | /// The vector type being tested. Must implement IEquatable{TValue} to enable value comparisons. 13 | /// Typically used with Godot.Vector2, Godot.Vector3, or Godot.Vector4 types. 14 | /// 15 | public interface IVectorAssert : IVectorConstraint, IAssertMessage> 16 | where TValue : IEquatable 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /Api/src/core/execution/monitoring/OrphanNodesMonitor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Execution.Monitoring; 5 | 6 | using static Godot.Performance; 7 | 8 | internal class OrphanNodesMonitor 9 | { 10 | public int OrphanCount { get; private set; } 11 | 12 | private int OrphanNodesStart { get; set; } 13 | 14 | public void Start(bool reset = false) 15 | { 16 | if (reset) 17 | Reset(); 18 | OrphanNodesStart = GetMonitoredOrphanCount(); 19 | } 20 | 21 | public void Stop() => OrphanCount += GetMonitoredOrphanCount() - OrphanNodesStart; 22 | 23 | private int GetMonitoredOrphanCount() => (int)GetMonitor(Monitor.ObjectOrphanNodeCount); 24 | 25 | private void Reset() => OrphanCount = 0; 26 | } 27 | -------------------------------------------------------------------------------- /Api/src/core/execution/monitoring/GodotExceptionPattern.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Core.Execution.Monitoring; 4 | 5 | using System.Text.RegularExpressions; 6 | 7 | internal static partial class GodotExceptionPattern 8 | { 9 | public static Match DebugMode(string value) 10 | => ExceptionPatternDebug().Match(value); 11 | 12 | public static Match ReleaseMode(string value) 13 | => ExceptionPatternRelease().Match(value); 14 | 15 | [GeneratedRegex(@"([\w\.]+Exception):\s*(.*)", RegexOptions.Compiled)] 16 | private static partial Regex ExceptionPatternDebug(); 17 | 18 | [GeneratedRegex(@"([\w\.]+Exception):\s*(.*?)(?:\r\n|\n|$)", RegexOptions.Compiled)] 19 | private static partial Regex ExceptionPatternRelease(); 20 | } 21 | -------------------------------------------------------------------------------- /Api/src/core/reporting/TestReportCollector.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Reporting; 5 | 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | using Api; 10 | 11 | internal sealed class TestReportCollector 12 | { 13 | public List Reports { get; } = []; 14 | 15 | public IEnumerable Failures => Reports.Where(r => r.IsFailure); 16 | 17 | public IEnumerable Errors => Reports.Where(r => r.IsError); 18 | 19 | public IEnumerable Warnings => Reports.Where(r => r.IsWarning); 20 | 21 | public void Consume(ITestReport report) => Reports.Add(report); 22 | 23 | public void PushFront(ITestReport report) => Reports.Insert(0, report); 24 | 25 | public void Clear() => Reports.Clear(); 26 | } 27 | -------------------------------------------------------------------------------- /Api.Test/src/core/hooks/StdOutConsoleHookTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Core.Hooks; 2 | 3 | using System; 4 | 5 | using GdUnit4.Core.Hooks; 6 | 7 | using static Assertions; 8 | 9 | [TestSuite] 10 | public class StdOutConsoleHookTest 11 | { 12 | [TestCase] 13 | public void CaptureStdOut() 14 | { 15 | using var hook = new StdOutConsoleHook(); 16 | Console.WriteLine("Before capture."); 17 | 18 | hook.StartCapture(); 19 | 20 | Console.WriteLine("Hello World A!"); 21 | Console.WriteLine("Hello World B!"); 22 | 23 | hook.StopCapture(); 24 | 25 | Console.WriteLine("After capture."); 26 | 27 | var capturedMessages = hook.GetCapturedOutput(); 28 | AssertThat(capturedMessages) 29 | .IsEqual($"Hello World A!{Environment.NewLine}" + 30 | $"Hello World B!{Environment.NewLine}"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Api/src/core/runners/GodotLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Runners; 5 | 6 | using Api; 7 | 8 | using Godot; 9 | 10 | internal sealed class GodotLogger : ITestEngineLogger 11 | { 12 | /// 13 | public void SendMessage(LogLevel logLevel, string message) 14 | { 15 | switch (logLevel) 16 | { 17 | case LogLevel.Informational: 18 | GD.PrintS(message); 19 | break; 20 | case LogLevel.Warning: 21 | GD.PrintS(message); 22 | break; 23 | case LogLevel.Error: 24 | GD.PrintErr(message); 25 | break; 26 | default: 27 | GD.PrintS(message); 28 | break; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Analyzers.Test/src/ModuleInitializer.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Analyzers.Test; 2 | 3 | using System.Runtime.CompilerServices; 4 | 5 | using Gu.Roslyn.Asserts; 6 | 7 | using Microsoft.CodeAnalysis; 8 | 9 | internal static class ModuleInitializer 10 | { 11 | internal static readonly MetadataReference[] References = 12 | [ 13 | MetadataReference.CreateFromFile(typeof(object).Assembly.Location), MetadataReference.CreateFromFile(typeof(TestSuiteAttribute).Assembly.Location), 14 | MetadataReference.CreateFromFile(typeof(CompilerGeneratedAttribute).Assembly.Location) 15 | ]; 16 | 17 | [ModuleInitializer] 18 | internal static void Initialize() => Settings.Default = Settings.Default 19 | .WithCompilationOptions(x => x.WithSuppressedDiagnostics("CS0281", "CS1701", "CS1702", "CS8019")) 20 | .WithMetadataReferences(MetadataReferences.Transitive(typeof(ModuleInitializer))); 21 | } 22 | -------------------------------------------------------------------------------- /Api/src/core/execution/BeforeTestExecutionStage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Execution; 5 | 6 | using System.Threading.Tasks; 7 | 8 | internal class BeforeTestExecutionStage : ExecutionStage 9 | { 10 | public BeforeTestExecutionStage(TestSuite testSuite) 11 | : base("BeforeTest", testSuite.Instance.GetType()) 12 | { 13 | } 14 | 15 | public override async Task Execute(ExecutionContext context) 16 | { 17 | context.FireBeforeTestEvent(); 18 | if (!context.IsSkipped) 19 | { 20 | context.MemoryPool.SetActive(StageName, true); 21 | await base 22 | .Execute(context) 23 | .ConfigureAwait(true); 24 | context.MemoryPool.StopMonitoring(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Api/src/asserts/BoolAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Asserts; 5 | 6 | using Constraints; 7 | 8 | /// 9 | public sealed class BoolAssert : AssertBase, IBoolAssert 10 | { 11 | internal BoolAssert(bool current) 12 | : base(current) 13 | { 14 | } 15 | 16 | /// 17 | public IBoolConstraint IsFalse() 18 | { 19 | if (true.Equals(Current)) 20 | ThrowTestFailureReport(AssertFailures.IsFalse(), Current, false); 21 | return this; 22 | } 23 | 24 | /// 25 | public IBoolConstraint IsTrue() 26 | { 27 | if (!true.Equals(Current)) 28 | ThrowTestFailureReport(AssertFailures.IsTrue(), Current, true); 29 | return this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Analyzers.Test/src/TestSourceBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Analyzers.Test; 2 | 3 | public static class TestSourceBuilder 4 | { 5 | public static string Instrument(string sourceCode) => 6 | $$""" 7 | using System; 8 | using System.Collections; 9 | using System.Collections.Generic; 10 | using System.Collections.Immutable; 11 | using System.Collections.Specialized; 12 | using GdUnit4.Asserts; 13 | using GdUnit4.Core.Execution.Exceptions; 14 | using GdUnit4.Core.Extensions; 15 | using Godot; 16 | using Godot.Collections; 17 | using static GdUnit4.Assertions; 18 | 19 | namespace GdUnit4.Analyzers.Test.Example 20 | { 21 | [TestSuite] 22 | public class TestClass 23 | { 24 | {{sourceCode}} 25 | } 26 | } 27 | """; 28 | } 29 | -------------------------------------------------------------------------------- /Api.Test/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Example/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ProjectVersions.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.0.0 5 | 5.1.0-rc3 6 | 3.0.1 7 | 8 | 9 | 10 | 11 | 12 | 4.4 13 | $(GodotVersion) 14 | 15 | 16 | 17 | 18 | net8.0;net9.0 19 | 12.0 20 | enable 21 | portable 22 | 23 | 24 | -------------------------------------------------------------------------------- /Analyzers/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | ## Release 1.0.0 2 | 3 | ### New Rules 4 | 5 | | Rule ID | Category | Severity | Notes | 6 | |------------|-----------------|----------|---------------------------------------------------------------------------------------------------------------------------------------| 7 | | GdUnit0201 | Attribute Usage | Error | GdUnit0201_AnalyzerName, [Documentation](https://github.com/MikeSchulze/gdUnit4Net/tree/master/Analyzers/documentation/GdUnit0201.md) | 8 | | GdUnit0500 | Attribute Usage | Error | GdUnit0501_AnalyzerName, [Documentation](https://github.com/MikeSchulze/gdUnit4Net/tree/master/Analyzers/documentation/GdUnit0500.md) | 9 | | GdUnit0501 | Attribute Usage | Error | GdUnit0501_AnalyzerName, [Documentation](https://github.com/MikeSchulze/gdUnit4Net/tree/master/Analyzers/documentation/GdUnit0501.md) | 10 | -------------------------------------------------------------------------------- /Api/src/core/DebuggerUtils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core; 5 | 6 | using System.Reflection; 7 | 8 | using Godot.NativeInterop; 9 | 10 | internal static class DebuggerUtils 11 | { 12 | private static readonly MethodInfo? DebuggerIsActiveMethod = IsDebuggerUtils(); 13 | 14 | public static bool IsDebuggerActive() 15 | { 16 | if (DebuggerIsActiveMethod == null) 17 | return false; 18 | 19 | var isDebuggerActive = (godot_bool)DebuggerIsActiveMethod.Invoke(null, null)!; 20 | return isDebuggerActive.ToBool(); 21 | } 22 | 23 | private static MethodInfo? IsDebuggerUtils() 24 | { 25 | var nativeFuncType = typeof(NativeFuncs); 26 | return nativeFuncType.GetMethod( 27 | "godotsharp_internal_script_debugger_is_active", 28 | BindingFlags.NonPublic | BindingFlags.Static); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/Spell.gd: -------------------------------------------------------------------------------- 1 | class_name Spell 2 | extends Node 3 | 4 | signal spell_explode 5 | 6 | const SPELL_LIVE_TIME = 1000 7 | 8 | @warning_ignore("unused_private_class_variable") 9 | var _spell_fired :bool = false 10 | var _spell_live_time :float = 0 11 | var _spell_pos :Vector3 = Vector3.ZERO 12 | 13 | 14 | func _ready(): 15 | set_name("Spell") 16 | 17 | # only comment in for debugging reasons 18 | #func _notification(what): 19 | # prints("Spell", GdObjects.notification_as_string(what)) 20 | 21 | func _process(delta :float): 22 | # added pseudo yield to check `simulate_frames` works wih custom yielding 23 | await get_tree().process_frame 24 | _spell_live_time += delta * 1000 25 | if _spell_live_time < SPELL_LIVE_TIME: 26 | move(delta) 27 | else: 28 | explode() 29 | 30 | func move(delta :float) -> void: 31 | #await get_tree().create_timer(0.1).timeout 32 | _spell_pos.x += delta 33 | 34 | func explode() -> void: 35 | emit_signal("spell_explode", self) 36 | 37 | -------------------------------------------------------------------------------- /Api/src/core/commands/IsAlive.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Commands; 5 | 6 | using System.Net; 7 | using System.Threading.Tasks; 8 | 9 | using Api; 10 | 11 | using Newtonsoft.Json; 12 | 13 | /// 14 | /// Command to check if the test engine is alive and responding. 15 | /// 16 | internal class IsAlive : BaseCommand 17 | { 18 | [JsonConstructor] 19 | private IsAlive() 20 | { 21 | } 22 | 23 | /// 24 | /// Executes a health check and returns an "alive" status response. 25 | /// 26 | #pragma warning disable SA1611 27 | public override Task Execute(ITestEventListener testEventListener) => Task.FromResult( 28 | new Response 29 | #pragma warning restore SA1611 30 | { 31 | StatusCode = HttpStatusCode.OK, 32 | Payload = "alive: true" 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/TestSceneWithButton.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://c8aagra25qdm4"] 2 | 3 | [ext_resource type="Script" path="res://src/core/resources/scenes/TestSceneWithButton.cs" id="1_vn4rj"] 4 | 5 | [node name="TestScene" type="Control"] 6 | layout_mode = 3 7 | anchors_preset = 15 8 | anchor_right = 1.0 9 | anchor_bottom = 1.0 10 | grow_horizontal = 2 11 | grow_vertical = 2 12 | script = ExtResource("1_vn4rj") 13 | 14 | [node name="ExampleButton" type="Button" parent="."] 15 | unique_name_in_owner = true 16 | layout_mode = 0 17 | offset_left = 168.0 18 | offset_top = 149.0 19 | offset_right = 305.0 20 | offset_bottom = 200.0 21 | text = "Press Me" 22 | 23 | [node name="TextInput" type="LineEdit" parent="."] 24 | unique_name_in_owner = true 25 | layout_mode = 0 26 | offset_left = 181.0 27 | offset_top = 299.0 28 | offset_right = 604.0 29 | offset_bottom = 374.0 30 | caret_blink = true 31 | 32 | [connection signal="pressed" from="ExampleButton" to="." method="OnButtonPressed"] 33 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/TestSceneWithExceptionTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using System; 4 | 5 | using Godot; 6 | 7 | public partial class TestSceneWithExceptionTest : Control 8 | { 9 | private int frameCount; 10 | 11 | public void SomeMethodThatThrowsException() 12 | { 13 | Console.WriteLine("Throw a test exception"); 14 | throw new InvalidOperationException("Test Exception"); 15 | } 16 | 17 | public override void _Process(double delta) 18 | { 19 | frameCount++; 20 | // we throw an example exception 21 | if (frameCount == 10) 22 | throw new InvalidProgramException("Exception during scene processing"); 23 | try 24 | { 25 | if (frameCount == 5) 26 | throw new InvalidProgramException("Exception during scene processing inside a catch block"); 27 | } 28 | catch (InvalidProgramException) 29 | { 30 | // ignore 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Api/src/asserts/INumberAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Asserts; 4 | 5 | using System.Numerics; 6 | 7 | using Constraints; 8 | 9 | /// 10 | /// Base interface for numeric value assertions in the GdUnit4 testing framework. 11 | /// Provides specialized methods for comparing and validating numeric values. 12 | /// 13 | /// 14 | /// The numeric type being tested. Must implement IComparable, IComparable{TValue}, IEquatable{TValue}, 15 | /// IAdditionOperators{TValue, TValue, TValue}, and ISubtractionOperators{TValue, TValue, TValue}. 16 | /// 17 | public interface INumberAssert : INumberConstraint, IAssertMessage> 18 | where TValue : IComparable, IComparable, IEquatable, 19 | IAdditionOperators, ISubtractionOperators 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Example/test/api/AssertionsTest.cs: -------------------------------------------------------------------------------- 1 | namespace Examples.Test.Api; 2 | 3 | using System.Collections; 4 | 5 | using GdUnit4; 6 | 7 | using static GdUnit4.Assertions; 8 | 9 | using Vector2 = System.Numerics.Vector2; 10 | 11 | [TestSuite] 12 | public class AssertionsTest 13 | { 14 | [TestCase] 15 | public void DoAssertNotYetImplemented() 16 | => AssertThrown(() => AssertNotYetImplemented()) 17 | .HasFileLineNumber(16) 18 | .HasMessage("Test not yet implemented!"); 19 | 20 | 21 | [TestCase(Description = "https://github.com/MikeSchulze/gdUnit4Net/issues/84")] 22 | public void AssertThatOnDynamics() 23 | { 24 | // object asserts 25 | AssertThat(Vector2.One).IsEqual(Vector2.One); 26 | AssertThat(TimeSpan.FromMilliseconds(124)).IsEqual(TimeSpan.FromMilliseconds(124)); 27 | 28 | // dictionary asserts 29 | AssertThat(new Hashtable()).HasSize(0); 30 | 31 | // enumerable asserts 32 | AssertThat(new List()).HasSize(0); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Api/src/core/attributes/TestCategoryAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | 10 | /// 11 | /// Specifies a category for a test method or class. 12 | /// 13 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] 14 | public sealed class TestCategoryAttribute : Attribute 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The name of the category. 20 | public TestCategoryAttribute(string category) => Category = category; 21 | 22 | /// 23 | /// Gets the name of the category. 24 | /// 25 | public string Category { get; } 26 | } 27 | -------------------------------------------------------------------------------- /TestAdapter.Test/GdUnit4TestAdapter.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | false 5 | GdUnit4.TestAdapter 6 | 7 | GdUnit4.TestAdapter.Test 8 | 9 | CS8785 10 | true 11 | test.ruleset 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteFailOnTestCase1.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using static Assertions; 4 | // will be ignored because of missing `[TestSuite]` annotation 5 | // used by executor integration test 6 | public class TestSuiteFailOnTestCase1 7 | { 8 | 9 | [Before] 10 | public void Before() 11 | => AssertString("Suite Before()").IsEqual("Suite Before()"); 12 | 13 | [After] 14 | public void After() 15 | => AssertString("Suite After()").IsEqual("Suite After()"); 16 | 17 | [BeforeTest] 18 | public void BeforeTest() 19 | => AssertString("Suite BeforeTest()").IsEqual("Suite BeforeTest()"); 20 | 21 | [AfterTest] 22 | public void AfterTest() 23 | => AssertString("Suite AfterTest()").IsEqual("Suite AfterTest()"); 24 | 25 | [TestCase] 26 | public void TestCase1() 27 | => AssertString("invalid").IsEqual("TestCase1"); 28 | 29 | [TestCase] 30 | public void TestCase2() 31 | => AssertString("TestCase2").IsEqual("TestCase2"); 32 | } 33 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteFailOnStageBeforeTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using static Assertions; 4 | 5 | // will be ignored because of missing `[TestSuite]` annotation 6 | // used by executor integration test 7 | public class TestSuiteFailOnStageBeforeTest 8 | { 9 | 10 | [Before] 11 | public void Before() 12 | => AssertString("Suite Before()").IsEqual("Suite Before()"); 13 | 14 | [After] 15 | public void After() 16 | => AssertString("Suite After()").IsEqual("Suite After()"); 17 | 18 | [BeforeTest] 19 | public void BeforeTest() 20 | => AssertString("Suite BeforeTest()").OverrideFailureMessage("failed on BeforeTest()").IsEmpty(); 21 | 22 | [AfterTest] 23 | public void AfterTest() 24 | => AssertString("Suite AfterTest()").IsEqual("Suite AfterTest()"); 25 | 26 | [TestCase] 27 | public void TestCase1() 28 | => AssertString("TestCase1").IsEqual("TestCase1"); 29 | 30 | [TestCase] 31 | public void TestCase2() => AssertString("TestCase2").IsEqual("TestCase2"); 32 | } 33 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/Spell.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests; 2 | 3 | using Godot; 4 | 5 | public partial class Spell : Node 6 | { 7 | [Signal] 8 | public delegate void SpellExplodeEventHandler(ulong spellId); 9 | 10 | private const float SPELL_LIVE_TIME = 1000f; 11 | private bool spellExploded; 12 | 13 | private bool spellFired; 14 | private double spellLiveTime; 15 | private Vector3 spellPos = Vector3.Zero; 16 | 17 | public override void _Ready() 18 | => Name = "Spell"; 19 | 20 | public override void _Process(double delta) 21 | { 22 | spellLiveTime += delta * 1000; 23 | 24 | if (spellLiveTime < SPELL_LIVE_TIME) 25 | Move((float)delta); 26 | else 27 | Explode(); 28 | } 29 | 30 | private void Move(float delta) => spellPos.X += delta; 31 | 32 | private void Explode() 33 | { 34 | if (spellExploded) 35 | return; 36 | 37 | EmitSignal(SignalName.SpellExplode, GetInstanceId()); 38 | QueueFree(); 39 | spellExploded = true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteFailOnStageAfter.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using static Assertions; 4 | 5 | // will be ignored because of missing `[TestSuite]` annotation 6 | // used by executor integration test 7 | public class TestSuiteFailOnStageAfter 8 | { 9 | 10 | [Before] 11 | public void Before() 12 | => AssertString("Suite Before()").IsEqual("Suite Before()"); 13 | 14 | [After] 15 | public void After() 16 | => AssertString("Suite After()").OverrideFailureMessage("failed on After()").IsEmpty(); 17 | 18 | [BeforeTest] 19 | public void BeforeTest() 20 | => AssertString("Suite BeforeTest()").IsEqual("Suite BeforeTest()"); 21 | 22 | [AfterTest] 23 | public void AfterTest() 24 | => AssertString("Suite AfterTest()").IsEqual("Suite AfterTest()"); 25 | 26 | [TestCase] 27 | public void TestCase1() 28 | => AssertString("TestCase1").IsEqual("TestCase1"); 29 | 30 | [TestCase] 31 | public void TestCase2() 32 | => AssertString("TestCase2").IsEqual("TestCase2"); 33 | } 34 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteFailOnStageBefore.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using static Assertions; 4 | 5 | // will be ignored because of missing `[TestSuite]` annotation 6 | // used by executor integration test 7 | public class TestSuiteFailOnStageBefore 8 | { 9 | 10 | [Before] 11 | public void Before() 12 | => AssertString("Suite Before()").OverrideFailureMessage("failed on Before()").IsEmpty(); 13 | 14 | [After] 15 | public void After() 16 | => AssertString("Suite After()").IsEqual("Suite After()"); 17 | 18 | [BeforeTest] 19 | public void BeforeTest() 20 | => AssertString("Suite BeforeTest()").IsEqual("Suite BeforeTest()"); 21 | 22 | [AfterTest] 23 | public void AfterTest() 24 | => AssertString("Suite AfterTest()").IsEqual("Suite AfterTest()"); 25 | 26 | [TestCase] 27 | public void TestCase1() 28 | => AssertString("TestCase1").IsEqual("TestCase1"); 29 | 30 | [TestCase] 31 | public void TestCase2() 32 | => AssertString("TestCase2").IsEqual("TestCase2"); 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "process", 10 | "args": [ 11 | "build", 12 | "/property:GenerateFullPaths=true", 13 | "/consoleloggerparameters:NoSummary" 14 | ], 15 | "problemMatcher": "$msCompile" 16 | }, 17 | { 18 | "label": "publish", 19 | "command": "dotnet", 20 | "type": "process", 21 | "args": [ 22 | "publish", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/gdUnit4Net.sln" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteFailOnStageAfterTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using static Assertions; 4 | 5 | // will be ignored because of missing `[TestSuite]` annotation 6 | // used by executor integration test 7 | public class TestSuiteFailOnStageAfterTest 8 | { 9 | 10 | [Before] 11 | public void Before() 12 | => AssertString("Suite Before()").IsEqual("Suite Before()"); 13 | 14 | [After] 15 | public void After() 16 | => AssertString("Suite After()").IsEqual("Suite After()"); 17 | 18 | [BeforeTest] 19 | public void BeforeTest() 20 | => AssertString("Suite BeforeTest()").IsEqual("Suite BeforeTest()"); 21 | 22 | [AfterTest] 23 | public void AfterTest() 24 | => AssertString("Suite AfterTest()").OverrideFailureMessage("failed on AfterTest()").IsEmpty(); 25 | 26 | [TestCase] 27 | public void TestCase1() 28 | => AssertString("TestCase1").IsEqual("TestCase1"); 29 | 30 | [TestCase] 31 | public void TestCase2() 32 | => AssertString("TestCase2").IsEqual("TestCase2"); 33 | } 34 | -------------------------------------------------------------------------------- /Api/src/api/TestNode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | using System; 7 | 8 | /// 9 | /// Base record class for all test node types in the GdUnit4 testing framework. 10 | /// Represents a node in the test hierarchy and provides common identification properties. 11 | /// 12 | /// 13 | /// TestNode serves as the foundation for test assemblies, suites, and individual test cases, 14 | /// establishing the parent-child relationship between different test elements. 15 | /// 16 | public record TestNode 17 | { 18 | /// 19 | /// Gets the unique identifier for this test node. 20 | /// 21 | public required Guid Id { get; init; } 22 | 23 | /// 24 | /// Gets or sets the unique identifier of the parent node in the test hierarchy. 25 | /// This establishes the tree structure of the test organization. 26 | /// 27 | public required Guid ParentId { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteFailOnMultiStages.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using static Assertions; 4 | 5 | // will be ignored because of missing `[TestSuite]` annotation 6 | // used by executor integration test 7 | public class TestSuiteFailOnMultiStages 8 | { 9 | 10 | [Before] 11 | public void Before() 12 | => AssertString("Suite Before()").IsEqual("Suite Before()"); 13 | 14 | [After] 15 | public void After() 16 | => AssertString("Suite After()").OverrideFailureMessage("failed on After()").IsEmpty(); 17 | 18 | [BeforeTest] 19 | public void BeforeTest() 20 | => AssertString("Suite BeforeTest()").OverrideFailureMessage("failed on BeforeTest()").IsEmpty(); 21 | 22 | [AfterTest] 23 | public void AfterTest() 24 | => AssertString("Suite AfterTest()").IsEqual("Suite AfterTest()"); 25 | 26 | [TestCase] 27 | public void TestCase1() 28 | => AssertString("TestCase1").IsEmpty(); 29 | 30 | [TestCase] 31 | public void TestCase2() 32 | => AssertString("TestCase2").IsEqual("TestCase2"); 33 | } 34 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteParameterizedTests.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using System; 4 | 5 | using static Assertions; 6 | // will be ignored because of missing `[TestSuite]` annotation 7 | // used by executor integration test 8 | public class TestSuiteParameterizedTests 9 | { 10 | [TestCase(0, false)] 11 | [TestCase(1, true)] 12 | public void ParameterizedBoolValue(int a, bool expected) 13 | => AssertThat(Convert.ToBoolean(a)).IsEqual(expected); 14 | 15 | [TestCase(1, 2, 3, 6)] 16 | [TestCase(3, 4, 5, 12)] 17 | [TestCase(6, 7, 8, 21)] 18 | public void ParameterizedIntValues(int a, int b, int c, int expected) 19 | => AssertThat(a + b + c).IsEqual(expected); 20 | 21 | [TestCase(1, 2, 3, 6)] 22 | [TestCase(3, 4, 5, 11)] 23 | [TestCase(6, 7, 8, 22)] 24 | public void ParameterizedIntValuesFail(int a, int b, int c, int expected) 25 | => AssertThat(a + b + c).IsEqual(expected); 26 | 27 | [TestCase(true)] 28 | public void ParameterizedSingleTest(bool value) 29 | => AssertThat(value).IsTrue(); 30 | } 31 | -------------------------------------------------------------------------------- /Api/src/api/ITestRunner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading; 9 | 10 | /// 11 | /// Defines a test runner interface for executing GdUnit4 test suites. 12 | /// 13 | internal interface ITestRunner : IAsyncDisposable 14 | { 15 | /// 16 | /// Executes a list of test suites synchronously and waits for completion. 17 | /// 18 | /// The list of test suites to execute. 19 | /// The listener for test execution events. 20 | /// Token to support cancellation of the test run. 21 | internal void RunAndWait(List testSuiteNodes, ITestEventListener eventListener, CancellationToken cancellationToken); 22 | 23 | /// 24 | /// Cancels the current test execution. 25 | /// 26 | internal void Cancel(); 27 | } 28 | -------------------------------------------------------------------------------- /TestAdapter/src/DefaultDebuggerFramework.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.TestAdapter; 5 | 6 | using System; 7 | using System.Diagnostics; 8 | 9 | using Api; 10 | 11 | using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; 12 | 13 | internal class DefaultDebuggerFramework : IDebuggerFramework 14 | { 15 | private readonly IFrameworkHandle frameworkHandle; 16 | 17 | public DefaultDebuggerFramework(IFrameworkHandle frameworkHandle) 18 | => this.frameworkHandle = frameworkHandle; 19 | 20 | public bool IsDebugProcess => false; 21 | 22 | public bool IsDebugAttach => Debugger.IsAttached; 23 | 24 | public Process LaunchProcessWithDebuggerAttached(ProcessStartInfo processStartInfo) 25 | => throw new NotImplementedException(); 26 | 27 | public bool AttachDebuggerToProcess(Process process) 28 | { 29 | if (frameworkHandle is IFrameworkHandle2 fh2) 30 | return fh2.AttachDebuggerToProcess(process.Id); 31 | 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Analyzers.Test/GdUnit4Analyzers.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | false 5 | GdUnit4.Analyzers.Test 6 | true 7 | 8 | 9 | 10 | test.ruleset 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Mike Schulze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Example/test/ExampleTest.cs: -------------------------------------------------------------------------------- 1 | namespace Examples.Test; 2 | 3 | #if GDUNIT4NET_API_V5 4 | using GdUnit4; 5 | 6 | using static GdUnit4.Assertions; 7 | 8 | [TestSuite] 9 | public class ExampleTest 10 | { 11 | public static IEnumerable ArrayDataPointProperty => [[1, 2, 3], [4, 5, 9]]; 12 | public static IEnumerable ArrayDataPointMethod() => [[1, 2, 3], [4, 5, 9]]; 13 | 14 | [TestCase] 15 | public void Success() => AssertBool(true).IsTrue(); 16 | 17 | 18 | [TestCase] 19 | public void Failed() => AssertBool(false).IsTrue(); 20 | 21 | [TestCase] 22 | [DataPoint(nameof(ArrayDataPointProperty))] 23 | public void WithDataPointProperty(int a, int b, int expected) => AssertThat(a + b).IsEqual(expected); 24 | 25 | [TestCase] 26 | [DataPoint(nameof(ArrayDataPointMethod))] 27 | public void WithArrayDataPointMethod(int a, int b, int expected) => AssertThat(a + b).IsEqual(expected); 28 | 29 | 30 | [TestCase(0, 1, 2, TestName = "TestA")] 31 | [TestCase(1, 2, 3, TestName = "TestB", Description = "foo ")] 32 | public void DataRows(int a, int b, int c) => AssertBool(true).IsTrue(); 33 | } 34 | #endif 35 | -------------------------------------------------------------------------------- /Analyzers/documentation/index.md: -------------------------------------------------------------------------------- 1 | # GdUnit4Net Diagnostic Analyzer Overview 2 | 3 | | Id | Severity | Title | 4 | |-----------------------------|----------|---------------------------------------------------------| 5 | | [GdUnit0201](GdUnit0201.md) | Error | Multiple TestCase attributes not allowed with DataPoint | 6 | | [GdUnit0500](GdUnit0500.md) | Error | Godot Runtime Required for Test Class | 7 | | [GdUnit0501](GdUnit0501.md) | Error | Godot Runtime Required for Test Method | 8 | 9 | # Category Overview 10 | 11 | All diagnostics in this analyzer focus on attribute usage and test configuration correctness: 12 | 13 | ## Attribute Usage 14 | 15 | GdUnit0201: Ensures correct usage of TestCase attributes with DataPoint 16 | GdUnit0501: Ensures proper test configuration for Godot engine dependencies 17 | 18 | ## General Guidelines 19 | 20 | Use appropriate attributes for your test scenarios 21 | Follow the correct testing patterns for different test types (standard vs Godot) 22 | Pay attention to attribute combinations that might cause conflicts 23 | -------------------------------------------------------------------------------- /Api.Test/src/core/GodotRuntimeAnalyzerExampleTestSuite.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Core; 2 | 3 | [TestSuite] 4 | public class GodotRuntimeAnalyzerExampleTestSuite 5 | { 6 | /* 7 | [TestCase] 8 | public void CaseA() 9 | { 10 | //var tmp1 = Utils.CreateTempDir("build-test-suite-test"); 11 | // UtilsX1 is located in the gdunit4.api.dll 12 | var tmp = UtilsX1.Foo(); 13 | 14 | ProjectSettings.GlobalizePath("res://src/core/resources/sources/TestPerson.cs"); 15 | } 16 | 17 | 18 | [TestCase] 19 | public void CaseB() 20 | { 21 | // UtilsX2 is located locally in the gdunit4.test.dll 22 | var tmp = UtilsX2.Foo(); 23 | 24 | ProjectSettings.GlobalizePath("res://src/core/resources/sources/TestPerson.cs"); 25 | } 26 | 27 | 28 | [TestCase] 29 | public void CaseC() 30 | { 31 | // UtilsX2 is located locally in the gdunit4.test.dll 32 | var tmp = UtilsX2.Bar(); 33 | 34 | Console.WriteLine(""); 35 | } 36 | 37 | 38 | [TestCase] 39 | public void CaseD() => new MyNode(); 40 | 41 | public class MyNode : Node 42 | 43 | { 44 | } 45 | */ 46 | } 47 | -------------------------------------------------------------------------------- /Api/src/core/execution/DirectCommandExecutor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Execution; 5 | 6 | using System; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | using Api; 11 | 12 | using Commands; 13 | 14 | /// 15 | /// Implements a direct command executor that executes commands without additional runtime overhead. 16 | /// Used for direct test execution without interprocess communication. 17 | /// 18 | internal class DirectCommandExecutor : ICommandExecutor 19 | { 20 | public Task StartAsync() => Task.CompletedTask; 21 | 22 | public Task StopAsync() => Task.CompletedTask; 23 | 24 | public ValueTask DisposeAsync() 25 | { 26 | GC.SuppressFinalize(this); 27 | return ValueTask.CompletedTask; 28 | } 29 | 30 | public async Task ExecuteCommand(T command, ITestEventListener testEventListener, CancellationToken cancellationToken) 31 | where T : BaseCommand 32 | => await command 33 | .Execute(testEventListener) 34 | .ConfigureAwait(true); 35 | } 36 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteAllTestsFailWithExceptions.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Core; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | // will be ignored because of missing `[TestSuite]` annotation 7 | // used by executor integration test 8 | public class TestSuiteAllTestsFailWithExceptions 9 | { 10 | [TestCase] 11 | [RequireGodotRuntime] 12 | public void ExceptionIsThrownOnSceneInvoke() 13 | { 14 | var runner = ISceneRunner.Load("res://src/core/resources/scenes/TestSceneWithExceptionTest.tscn"); 15 | 16 | runner.Invoke("SomeMethodThatThrowsException"); 17 | } 18 | 19 | [TestCase] 20 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously 21 | public async Task ExceptionAtAsyncMethod() 22 | #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously 23 | => throw new ArgumentException("outer exception", new ArgumentNullException("inner exception")); 24 | 25 | [TestCase] 26 | public void ExceptionAtSyncMethod() 27 | => throw new ArgumentException("outer exception", new ArgumentNullException("inner exception")); 28 | } 29 | -------------------------------------------------------------------------------- /Api/src/api/ITestEventListener.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | /// 7 | /// Interface for listening to test execution events and maintaining test execution state. 8 | /// 9 | /// 10 | /// The test event listener: 11 | /// 12 | /// Tracks test execution status and completion counts 13 | /// Receives and processes test events during execution 14 | /// 15 | /// 16 | public interface ITestEventListener 17 | { 18 | /// 19 | /// Gets or sets a value indicating whether gets or sets whether any tests have failed. 20 | /// 21 | bool IsFailed { get; protected set; } 22 | 23 | /// 24 | /// Gets or sets the number of completed test cases. 25 | /// 26 | int CompletedTests { get; protected set; } 27 | 28 | /// 29 | /// Processes and publishes a test event. 30 | /// 31 | /// The test event to publish. 32 | void PublishEvent(ITestEvent testEvent); 33 | } 34 | -------------------------------------------------------------------------------- /Api/src/api/TestAssemblyNode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | using Newtonsoft.Json; 7 | 8 | /// 9 | /// Represents a test assembly in the GdUnit4 testing framework. 10 | /// An assembly is the highest level in the test hierarchy, containing multiple test suites. 11 | /// 12 | /// 13 | /// TestAssemblyNode serves as a container for all test suites within a single .NET assembly. 14 | /// It tracks the physical location of the assembly file and organizes the contained test suites. 15 | /// 16 | public record TestAssemblyNode : TestNode 17 | { 18 | /// 19 | /// Gets the file path to the assembly containing this test case. 20 | /// 21 | public required string AssemblyPath { get; init; } 22 | 23 | /// 24 | /// Gets the collection of test suites contained within this assembly. 25 | /// This property is ignored during JSON serialization. 26 | /// 27 | [JsonIgnore] 28 | #pragma warning disable CA1002 29 | public List Suites { get; init; } = []; 30 | #pragma warning restore CA1002 31 | } 32 | -------------------------------------------------------------------------------- /Api/src/asserts/ITuple.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Asserts; 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | /// 10 | /// A tuple implementation to hold two or many values in the GdUnit4 testing framework. 11 | /// Provides a generic container for storing multiple heterogeneous values as a single unit. 12 | /// 13 | /// 14 | /// This interface defines a lightweight tuple data structure used internally by the testing framework 15 | /// for storing and comparing multiple values. It implements IEquatable to support value comparison 16 | /// between tuples, which is essential for assertion-based testing. 17 | /// 18 | public interface ITuple : IEquatable 19 | { 20 | /// 21 | /// Gets or sets the collection of values stored in this tuple. 22 | /// 23 | /// 24 | /// The values can be of any type, including null references. 25 | /// The order of values in the collection is preserved and significant for equality comparisons. 26 | /// 27 | IEnumerable Values { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Analyzers/documentation/GdUnit0201.md: -------------------------------------------------------------------------------- 1 | # GdUnit0201 2 | 3 | ## Multiple TestCase attributes not allowed with DataPoint 4 | 5 | | Id | Category | Severity | Enabled | 6 | |------------|-----------------|----------|---------| 7 | | GdUnit0201 | Attribute Usage | Error | True | 8 | 9 | # Problem Description 10 | 11 | Methods decorated with DataPoint attribute can only have one TestCase attribute. Multiple TestCase attributes on a method that uses DataPoint will result in undefined behavior. 12 | 13 | ### Error example: 14 | 15 | ```csharp 16 | [TestCase] 17 | [TestCase] // ❌ GdUnit0201: Method 'TestDataPointProperty' cannot have multiple TestCase attributes when DataPoint attribute is present 18 | [DataPoint(nameof(ArrayDataPointProperty))] 19 | public void TestDataPointProperty(int a, int b, int expected) 20 | { 21 | AssertThat(a + b).IsEqual(expected); 22 | } 23 | ``` 24 | 25 | ### How to fix: 26 | 27 | Use only one TestCase attribute with DataPoint: 28 | 29 | ```csharp 30 | [TestCase] // ✅ Correct: Single TestCase with DataPoint 31 | [DataPoint(nameof(ArrayDataPointProperty))] 32 | public void TestDataPointProperty(int a, int b, int expected) 33 | { 34 | AssertThat(a + b).IsEqual(expected); 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /Analyzers.Test/.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | . 6 | ./TestResults 7 | net8.0 8 | 9 | 500000 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | normal 18 | 19 | 20 | 21 | 22 | test-result.html 23 | 24 | 25 | 26 | 27 | test-result.trx 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /TestAdapter/src/RiderDebuggerFramework.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.TestAdapter; 5 | 6 | using System; 7 | using System.Diagnostics; 8 | 9 | using Api; 10 | 11 | using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; 12 | 13 | internal class RiderDebuggerFramework : IDebuggerFramework 14 | { 15 | public RiderDebuggerFramework(IFrameworkHandle frameworkHandle) 16 | => FrameworkHandle = frameworkHandle; 17 | 18 | public IFrameworkHandle FrameworkHandle { get; } 19 | 20 | public bool IsDebugProcess { get; } = Debugger.IsAttached; 21 | 22 | public bool IsDebugAttach => false; 23 | 24 | public Process LaunchProcessWithDebuggerAttached(ProcessStartInfo processStartInfo) 25 | { 26 | var processId = FrameworkHandle 27 | .LaunchProcessWithDebuggerAttached( 28 | processStartInfo.FileName, 29 | processStartInfo.WorkingDirectory, 30 | processStartInfo.Arguments, 31 | processStartInfo.Environment); 32 | return Process.GetProcessById(processId); 33 | } 34 | 35 | public bool AttachDebuggerToProcess(Process process) 36 | => throw new NotImplementedException(); 37 | } 38 | -------------------------------------------------------------------------------- /TestAdapter.Test/.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | . 6 | ./TestResults 7 | net8.0 8 | 9 | 500000 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | normal 18 | 19 | 20 | 21 | 22 | test-result.html 23 | 24 | 25 | 26 | 27 | test-result.trx 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Api/src/core/hooks/StdOutConsoleHook.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Hooks; 5 | 6 | using System; 7 | using System.IO; 8 | using System.Text; 9 | 10 | internal sealed class StdOutConsoleHook : IStdOutHook 11 | { 12 | private readonly CaptureWriter captureWriter = new(); 13 | private readonly TextWriter originalOutput = Console.Out; 14 | 15 | public void StartCapture() 16 | { 17 | captureWriter.Clear(); 18 | Console.SetOut(captureWriter); 19 | } 20 | 21 | public void StopCapture() => Console.SetOut(originalOutput); 22 | 23 | public string GetCapturedOutput() => captureWriter.GetCapturedOutput(); 24 | 25 | public void Dispose() 26 | { 27 | captureWriter.Dispose(); 28 | originalOutput.Dispose(); 29 | } 30 | 31 | private class CaptureWriter : TextWriter 32 | { 33 | private readonly StringBuilder capturedOutput = new(); 34 | 35 | public override Encoding Encoding => Encoding.UTF8; 36 | 37 | public override void Write(char value) => capturedOutput.Append(value); 38 | 39 | public string GetCapturedOutput() => capturedOutput.ToString(); 40 | 41 | public void Clear() => capturedOutput.Clear(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Api.Test/src/extensions/GodotVariantExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Extensions; 2 | 3 | using GdUnit4.Core.Extensions; 4 | 5 | using Godot; 6 | 7 | using static Assertions; 8 | 9 | [TestSuite] 10 | public class GodotVariantExtensionsTest 11 | { 12 | [TestCase(null, Variant.Type.Nil)] 13 | [TestCase('A', Variant.Type.Int)] 14 | [TestCase(sbyte.MaxValue, Variant.Type.Int)] 15 | [TestCase(byte.MaxValue, Variant.Type.Int)] 16 | [TestCase(short.MaxValue, Variant.Type.Int)] 17 | [TestCase(ushort.MaxValue, Variant.Type.Int)] 18 | [TestCase(int.MaxValue, Variant.Type.Int)] 19 | [TestCase(uint.MaxValue, Variant.Type.Int)] 20 | [TestCase(long.MaxValue, Variant.Type.Int)] 21 | [TestCase(ulong.MaxValue, Variant.Type.Int)] 22 | [TestCase(float.MaxValue, Variant.Type.Float)] 23 | [TestCase(double.MaxValue, Variant.Type.Float)] 24 | [TestCase("HalloWorld", Variant.Type.String)] 25 | [TestCase(true, Variant.Type.Bool)] 26 | //[TestCase(Decimal.MaxValue, Variant.Type.Float)] 27 | [RequireGodotRuntime] 28 | public void ToVariant(dynamic? value, Variant.Type type) 29 | { 30 | object? val = value; 31 | var v = val.ToVariant(); 32 | AssertObject(v).IsEqual(value == null ? new Variant() : Variant.CreateFrom(value)); 33 | AssertObject(v.VariantType).IsEqual(type); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /TestAdapter.Test/test/utilities/UtilsTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.TestAdapter.Test.Utilities; 2 | 3 | using System; 4 | 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | using static TestAdapter.Utilities.Utils; 8 | 9 | using Environment = Environment; 10 | 11 | [TestClass] 12 | public class UtilsTest 13 | { 14 | [TestMethod] 15 | public void ProjectDirectory() 16 | => StringAssert.Contains(GetProjectDirectory, "TestAdapter.Test", StringComparison.Ordinal); 17 | 18 | [TestMethod] 19 | public void UserDataDirectory() 20 | { 21 | switch (Environment.OSVersion.Platform) 22 | { 23 | case PlatformID.Win32NT: 24 | Assert.IsTrue(GetUserDataDirectory.EndsWith("Godot")); 25 | break; 26 | case PlatformID.Unix: 27 | Assert.IsTrue(GetUserDataDirectory.EndsWith("godot")); 28 | break; 29 | case PlatformID.MacOSX: 30 | Assert.IsTrue(GetUserDataDirectory.EndsWith("Library/Application Support/Godot")); 31 | break; 32 | case PlatformID.Win32S: 33 | case PlatformID.Win32Windows: 34 | case PlatformID.WinCE: 35 | case PlatformID.Xbox: 36 | case PlatformID.Other: 37 | default: 38 | break; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Analyzers.Test/test.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TestAdapter.Test/test.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[csharp]": { 3 | "editor.codeActionsOnSave": { 4 | "source.addMissingImports": "explicit", 5 | "source.fixAll": "explicit", 6 | "source.organizeImports": "explicit" 7 | }, 8 | "editor.formatOnPaste": true, 9 | "editor.formatOnSave": true, 10 | "editor.formatOnType": false 11 | }, 12 | "csharp.semanticHighlighting.enabled": true, 13 | "editor.semanticHighlighting.enabled": true, 14 | "editor.formatOnSave": true, 15 | "terminal.integrated.tabs.title": "${task}", 16 | "omnisharp.enableEditorConfigSupport": true, 17 | "omnisharp.enableMsBuildLoadProjectsOnDemand": false, 18 | "omnisharp.maxFindSymbolsItems": 3000, 19 | "omnisharp.useModernNet": true, 20 | "dotnet.unitTests.runSettingsPath": "./test/.runsettings", 21 | "cSpell.words": [ 22 | "Aabb", 23 | "autofree", 24 | "Callv", 25 | "Clazz", 26 | "Cmdline", 27 | "configfile", 28 | "Deadzone", 29 | "dorny", 30 | "Extr", 31 | "Fullqualified", 32 | "Fuzzer", 33 | "GDUNIT", 34 | "Hashtable", 35 | "Lerp", 36 | "Millis", 37 | "Newtonsoft", 38 | "Predelete", 39 | "testadapter", 40 | "TESTCASE", 41 | "Testrun", 42 | "TESTSUITE", 43 | "testsuites", 44 | "tscn", 45 | "Unconfigured", 46 | "Xbutton" 47 | ], 48 | "dotnet.formatting.organizeImportsOnFormat": true 49 | } 50 | -------------------------------------------------------------------------------- /Api/src/api/IGdUnitAwaitable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace GdUnit4; 6 | 7 | /// 8 | /// Marker interface for GdUnit4 awaitable operations that support timeout functionality. 9 | /// 10 | /// 11 | /// 12 | /// This interface serves as a constraint for the 13 | /// extension method, ensuring that only appropriate GdUnit4 awaitable types can be used with timeout operations. 14 | /// 15 | /// 16 | /// Types implementing this interface indicate they represent asynchronous operations within the GdUnit4 17 | /// testing framework that can be canceled or timed out gracefully. 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// // ISignalAssert implements IGdUnitAwaitable, so this works: 23 | /// await AssertSignal(node).IsEmitted("ready").WithTimeout(5000); 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | #pragma warning disable CA1040 30 | public interface IGdUnitAwaitable 31 | #pragma warning restore CA1040 32 | { 33 | } 34 | -------------------------------------------------------------------------------- /TestAdapter/src/utilities/IdeDetector.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.TestAdapter.Utilities; 5 | 6 | using System; 7 | using System.Diagnostics; 8 | 9 | using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; 10 | 11 | internal enum Ide 12 | { 13 | JetBrainsRider, 14 | VisualStudio, 15 | VisualStudioCode, 16 | DotNet, 17 | Unknown 18 | } 19 | 20 | internal static class IdeDetector 21 | { 22 | public static Ide Detect(IFrameworkHandle frameworkHandle) 23 | { 24 | var runningFramework = frameworkHandle.GetType().ToString(); 25 | if (runningFramework.Contains("JetBrains", StringComparison.Ordinal)) 26 | return Ide.JetBrainsRider; 27 | if (runningFramework.Contains("VisualStudio", StringComparison.Ordinal)) 28 | { 29 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VisualStudioVersion"))) 30 | return Ide.VisualStudio; 31 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VSCODE_PID"))) 32 | return Ide.VisualStudioCode; 33 | if (Process.GetCurrentProcess().ProcessName.Contains("testhost", StringComparison.OrdinalIgnoreCase)) 34 | return Ide.DotNet; 35 | } 36 | 37 | return Ide.Unknown; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Api/src/core/execution/exceptions/ExecutionTimeoutException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Execution.Exceptions; 5 | 6 | using System.Diagnostics; 7 | 8 | #pragma warning disable CA1064 9 | internal sealed class ExecutionTimeoutException : TestFailedException 10 | #pragma warning restore CA1064 11 | { 12 | public ExecutionTimeoutException() 13 | : base("Execution timed out", new StackTrace(true)) 14 | { 15 | } 16 | 17 | public ExecutionTimeoutException(string message) 18 | : base(message, new StackTrace(true)) 19 | { 20 | } 21 | 22 | public ExecutionTimeoutException(string message, StackTrace stackTrace) 23 | : base(message, stackTrace) 24 | { 25 | } 26 | 27 | public ExecutionTimeoutException(string message, Exception innerException) 28 | : base(message, innerException) 29 | => SetCurrentStackTrace(innerException); 30 | 31 | public ExecutionTimeoutException(string message, int line) 32 | : base(message) 33 | => LineNumber = line; 34 | 35 | [StackTraceHidden] 36 | private void SetCurrentStackTrace(Exception innerException) 37 | { 38 | LineNumber = innerException is ExecutionTimeoutException ete ? ete.LineNumber : -1; 39 | OriginalStackTrace = innerException.StackTrace; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Api/src/core/commands/BaseCommand.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Commands; 5 | 6 | using System.Threading.Tasks; 7 | 8 | using Api; 9 | 10 | /// 11 | /// Base class for all test execution commands in the GdUnit4 framework. 12 | /// 13 | /// 14 | /// All command implementations must be serializable to support interprocess communication. 15 | /// Key requirements for derived commands: 16 | /// 17 | /// Must provide a parameterless constructor for JSON deserialization 18 | /// All properties must be marked with [JsonProperty] attribute 19 | /// Properties containing complex types must also be serializable 20 | /// 21 | /// Commands are serialized when passing between test runner and Godot engine processes. 22 | /// 23 | public abstract class BaseCommand 24 | { 25 | /// 26 | /// Executes the command and returns a response containing the execution result. 27 | /// 28 | /// Listener that receives test execution events. 29 | /// Response indicating command execution status and result. 30 | public abstract Task Execute(ITestEventListener testEventListener); 31 | } 32 | -------------------------------------------------------------------------------- /Api/src/core/commands/TerminateGodotInstanceCommand.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Commands; 5 | 6 | using System; 7 | using System.Net; 8 | using System.Threading.Tasks; 9 | 10 | using Api; 11 | 12 | using Godot; 13 | 14 | using Newtonsoft.Json; 15 | 16 | /// 17 | /// Command to safely terminate a running Godot instance. 18 | /// 19 | internal class TerminateGodotInstanceCommand : BaseCommand 20 | { 21 | [JsonConstructor] 22 | public TerminateGodotInstanceCommand() 23 | { 24 | } 25 | 26 | /// 27 | /// Executes the termination command, shutting down the Godot instance. 28 | /// 29 | /// 30 | /// This command gracefully closes the Godot scene tree and engine. 31 | /// 32 | /// 33 | /// A representing the asynchronous operation. 34 | /// 35 | public override Task Execute(ITestEventListener testEventListener) 36 | { 37 | Console.WriteLine("Terminating Godot instance."); 38 | (Engine.GetMainLoop() as SceneTree)?.Quit(); 39 | return Task.FromResult( 40 | new Response 41 | { 42 | StatusCode = HttpStatusCode.OK, 43 | Payload = string.Empty 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Api/src/asserts/IValueExtractor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Asserts; 5 | 6 | /// 7 | /// Interface for extracting values from objects in the GdUnit4 testing framework. 8 | /// Provides a standard mechanism for accessing specific values from various object types. 9 | /// 10 | /// 11 | /// This interface defines a strategy pattern for value extraction, allowing different 12 | /// implementations to handle different object types or extraction techniques. 13 | /// Implementations can extract values based on properties, indices, or custom logic. 14 | /// 15 | public interface IValueExtractor 16 | { 17 | /// 18 | /// Extracts a value by a given implementation. 19 | /// 20 | /// The object containing the value to be extracted. 21 | /// The extracted value, which may be null if the extraction fails or the source contains a null value. 22 | /// 23 | /// The extraction process is implementation-specific and may involve property access, 24 | /// reflection, or conversion operations depending on the object type and extraction strategy. 25 | /// Implementations should handle null input values gracefully. 26 | /// 27 | object? ExtractValue(object? value); 28 | } 29 | -------------------------------------------------------------------------------- /Api/src/core/execution/TestCaseExecutionStage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Execution; 5 | 6 | using System.Threading.Tasks; 7 | 8 | using Asserts; 9 | 10 | using Reporting; 11 | 12 | using static Api.ReportType; 13 | 14 | internal sealed class TestCaseExecutionStage : ExecutionStage 15 | { 16 | public TestCaseExecutionStage(string name, TestCase testCase, TestCaseAttribute stageAttribute) 17 | : base(name, testCase.MethodInfo, stageAttribute) 18 | { 19 | } 20 | 21 | public override async Task Execute(ExecutionContext context) 22 | { 23 | context.MemoryPool.SetActive(StageName, true); 24 | 25 | await base 26 | .Execute(context) 27 | .ConfigureAwait(true); 28 | 29 | await context.MemoryPool 30 | .Gc() 31 | .ConfigureAwait(true); 32 | if (context.MemoryPool.OrphanCount > 0) 33 | context.ReportCollector.PushFront(new TestReport(Warning, context.CurrentTestCase?.Line ?? 0, ReportOrphans(context))); 34 | } 35 | 36 | private static string ReportOrphans(ExecutionContext context) => 37 | $""" 38 | {AssertFailures.FormatValue("WARNING:", AssertFailures.WARN_COLOR, false)} 39 | Detected <{context.MemoryPool.OrphanCount}> orphan nodes during test execution! 40 | """; 41 | } 42 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/DragAndDrop/DragAndDropTestScene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://skueh3d5qn46"] 2 | 3 | [ext_resource type="Script" path="res://src/core/resources/scenes/DragAndDrop/DragAndDropTestScene.gd" id="1"] 4 | [ext_resource type="PackedScene" uid="uid://ca2rr3dan4vvw" path="res://src/core/resources/scenes/DragAndDrop/DragAndDropControl.tscn" id="2_u5ccv"] 5 | 6 | [node name="DragAndDropScene" type="Control"] 7 | layout_mode = 3 8 | anchors_preset = 15 9 | anchor_right = 1.0 10 | anchor_bottom = 1.0 11 | grow_horizontal = 2 12 | grow_vertical = 2 13 | size_flags_horizontal = 3 14 | size_flags_vertical = 3 15 | script = ExtResource("1") 16 | 17 | [node name="left" parent="." instance=ExtResource("2_u5ccv")] 18 | auto_translate_mode = 2 19 | layout_mode = 0 20 | offset_left = 250.0 21 | offset_top = 240.0 22 | offset_right = 355.0 23 | offset_bottom = 345.0 24 | localize_numeral_system = false 25 | metadata/_edit_use_anchors_ = true 26 | 27 | [node name="right" parent="." instance=ExtResource("2_u5ccv")] 28 | layout_mode = 0 29 | offset_left = 370.0 30 | offset_top = 240.0 31 | offset_right = 475.0 32 | offset_bottom = 345.0 33 | 34 | [node name="Button" type="Button" parent="."] 35 | layout_mode = 0 36 | offset_left = 243.0 37 | offset_top = 40.0 38 | offset_right = 479.0 39 | offset_bottom = 200.0 40 | text = "BUTTON" 41 | metadata/_edit_use_anchors_ = true 42 | 43 | [connection signal="button_down" from="Button" to="." method="_on_Button_button_down"] 44 | -------------------------------------------------------------------------------- /Api/src/api/TestSuiteNode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | using System.Collections.Generic; 7 | 8 | /// 9 | /// Represents a test suite in the GdUnit4 testing framework. 10 | /// A test suite corresponds to a test class and contains multiple test cases. 11 | /// 12 | /// 13 | /// TestSuiteNode serves as an organizational unit that groups related test cases. 14 | /// It provides information about the test class type and manages a collection of individual tests. 15 | /// 16 | public record TestSuiteNode : TestNode 17 | { 18 | /// 19 | /// Gets the fully qualified name of the test class type. 20 | /// 21 | public required string ManagedType { get; init; } 22 | 23 | /// 24 | /// Gets the collection of test cases contained within this test suite. 25 | /// 26 | #pragma warning disable CA1002 27 | public required List Tests { get; init; } = []; 28 | #pragma warning restore CA1002 29 | 30 | /// 31 | /// Gets the file path to the assembly containing this test suite. 32 | /// 33 | public required string AssemblyPath { get; init; } 34 | 35 | /// 36 | /// Gets the file path to the source code file that contains this test suite. 37 | /// 38 | public required string SourceFile { get; init; } 39 | } 40 | -------------------------------------------------------------------------------- /Api/src/asserts/IStringAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Asserts; 4 | 5 | using Constraints; 6 | 7 | /// 8 | /// An Assertion Tool to verify string values. 9 | /// 10 | public interface IStringAssert : IStringConstraint, IAssertMessage 11 | { 12 | /// 13 | /// The comparator to compare string length by different modes. 14 | /// 15 | #pragma warning disable SA1400 16 | enum Compare 17 | #pragma warning restore SA1400 18 | { 19 | /// 20 | /// Specifies that the string length should be exactly equal to the expected value. 21 | /// 22 | EQUAL, 23 | 24 | /// 25 | /// Specifies that the string length should be less than the expected value. 26 | /// 27 | LESS_THAN, 28 | 29 | /// 30 | /// Specifies that the string length should be less than or equal to the expected value. 31 | /// 32 | LESS_EQUAL, 33 | 34 | /// 35 | /// Specifies that the string length should be greater than the expected value. 36 | /// 37 | GREATER_THAN, 38 | 39 | /// 40 | /// Specifies that the string length should be greater than or equal to the expected value. 41 | /// 42 | GREATER_EQUAL 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Api/src/api/IDebuggerFramework.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | using System.Diagnostics; 7 | 8 | /// 9 | /// Describes the functionality of the interface for managing debugger operations. 10 | /// 11 | public interface IDebuggerFramework 12 | { 13 | /// 14 | /// Gets a value indicating whether indicates whether a debugger process is currently executing. 15 | /// 16 | bool IsDebugProcess { get; } 17 | 18 | /// 19 | /// Gets a value indicating whether indicates whether a debugger is attached to a running process. 20 | /// 21 | bool IsDebugAttach { get; } 22 | 23 | /// 24 | /// Launches a new process with a debugger attached, based on the provided process start information. 25 | /// 26 | /// The start information of the process. 27 | /// Returns the process started with the attached debugger. 28 | Process LaunchProcessWithDebuggerAttached(ProcessStartInfo processStartInfo); 29 | 30 | /// 31 | /// Attaches a debugger to an already-running process. 32 | /// 33 | /// The process to which the debugger should be attached. 34 | /// Returns true if the debugger was successfully attached; otherwise, false. 35 | bool AttachDebuggerToProcess(Process process); 36 | } 37 | -------------------------------------------------------------------------------- /Api/src/asserts/Comparable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Asserts; 5 | 6 | using Core.Extensions; 7 | 8 | internal static class Comparable 9 | { 10 | public static Result IsEqual(T? left, T? right, GodotObjectExtensions.Mode compareMode = GodotObjectExtensions.Mode.CaseSensitive, Result? r = null) 11 | => new(left.VariantEquals(right, compareMode), left, right, r); 12 | 13 | public class Result 14 | { 15 | public Result(bool valid, object? left, object? right, Result? parent = null) 16 | { 17 | Valid = valid; 18 | Left = left; 19 | Right = right; 20 | Parent = parent; 21 | } 22 | 23 | public static Result Equal => new(true, null, null); 24 | 25 | public bool Valid { get; private set; } 26 | 27 | #pragma warning disable IDE0052 // Remove unread private members 28 | 29 | // ReSharper disable all UnusedAutoPropertyAccessor.Local 30 | private object? Left { get; set; } 31 | 32 | private object? Right { get; set; } 33 | 34 | private string? PropertyName { get; set; } 35 | 36 | // ReSharper enable all UnusedAutoPropertyAccessor.Local 37 | private Result? Parent { get; set; } 38 | #pragma warning restore IDE0052 // Remove unread private members 39 | 40 | public Result WithProperty(string propertyName) 41 | { 42 | PropertyName = propertyName; 43 | return this; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Api/src/core/attributes/RequireGodotRuntimeAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | 10 | /// 11 | /// Indicates that a test class requires the Godot runtime environment. 12 | /// This attribute is used to mark test classes that contain test hooks ([Before], [After], [BeforeTest], [AfterTest]) 13 | /// which use Godot functionality and therefore need a running Godot engine. 14 | /// 15 | /// 16 | /// Use this attribute when your test class: 17 | /// 18 | /// Uses Godot types or functionality in test hooks 19 | /// Requires the Godot engine to be initialized 20 | /// Interacts with Godot's core systems in setup or teardown methods 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// [RequireGodotRuntime] 26 | /// public class SceneRunnerTest 27 | /// { 28 | /// [Before] 29 | /// public void Setup() => 30 | /// Engine.PhysicsTicksPerSecond = 60; 31 | /// 32 | /// [TestCase] 33 | /// public void TestMethod() => 34 | /// AssertThat(true).IsTrue(); 35 | /// } 36 | /// 37 | /// 38 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)] 39 | public sealed class RequireGodotRuntimeAttribute : Attribute 40 | { 41 | } 42 | -------------------------------------------------------------------------------- /Example/.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | ./TestResults 6 | net7.0;net8.0 7 | 180000 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | detailed 16 | 17 | 18 | 19 | 20 | test-result.html 21 | 22 | 23 | 24 | 25 | test-result.trx 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | FullyQualifiedName 37 | 38 | 39 | -------------------------------------------------------------------------------- /Api.Test/src/core/discovery/DiscoverTestUtils.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Core.Discovery; 2 | 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | using Mono.Cecil; 9 | 10 | internal static class DiscoverTestUtils 11 | { 12 | internal static string GetSourceFilePath(string relativeSourcePath) 13 | { 14 | // Get the directory of the executing assembly 15 | var assemblyLocation = Assembly.GetExecutingAssembly().Location; 16 | var projectDir = Path.GetDirectoryName(assemblyLocation)!; 17 | 18 | // Navigate up to find the test file 19 | // Note: Adjust the path based on your project structure 20 | while (Directory.GetFiles(projectDir, "*.csproj").Length == 0 && Directory.GetParent(projectDir) != null) 21 | projectDir = Directory.GetParent(projectDir)!.FullName; 22 | 23 | // Find the test file in the project directory 24 | var sourceFile = Path.Combine(projectDir.Replace('\\', Path.DirectorySeparatorChar), relativeSourcePath.Replace('/', Path.DirectorySeparatorChar)); 25 | return Path.GetFullPath(sourceFile); 26 | } 27 | 28 | internal static MethodDefinition FindMethodDefinition(AssemblyDefinition assemblyDefinition, Type clazzType, string methodName) 29 | { 30 | var methodInfo = clazzType.GetMethod(methodName)!; 31 | var typeDefinition = assemblyDefinition.MainModule.Types 32 | .FirstOrDefault(t => t.FullName == methodInfo.DeclaringType?.FullName)!; 33 | 34 | return typeDefinition.Methods 35 | .First(m => m.Name == methodInfo.Name); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Api/src/core/runners/DefaultTestRunner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Runners; 5 | 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Threading; 9 | 10 | using Api; 11 | 12 | using Execution; 13 | 14 | /// 15 | /// Default test runner implementation that executes tests directly in the current process. 16 | /// 17 | [SuppressMessage( 18 | "Reliability", 19 | "CA2000:Dispose objects before losing scope", 20 | Justification = "DirectCommandExecutor ownership is transferred to base class which handles disposal")] 21 | internal sealed class DefaultTestRunner : BaseTestRunner 22 | { 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// Initializes a new instance of the DefaultTestRunner. 26 | /// 27 | /// The test engine logger for diagnostic output. 28 | /// Test engine configuration settings. 29 | internal DefaultTestRunner(ITestEngineLogger logger, TestEngineSettings settings) 30 | : base(new DirectCommandExecutor(), logger, settings) 31 | { 32 | } 33 | 34 | public new void RunAndWait(List testSuiteNodes, ITestEventListener eventListener, CancellationToken cancellationToken) 35 | { 36 | Logger.LogInfo("======== Running GdUnit4 Default Test Runner ========"); 37 | base.RunAndWait(testSuiteNodes, eventListener, cancellationToken); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Analyzers/documentation/GdUnit0501.md: -------------------------------------------------------------------------------- 1 | # GdUnit0501 2 | 3 | ## Godot Runtime Required for Test Method 4 | 5 | | Id | Category | Severity | Enabled | 6 | |------------|-----------------|----------|---------| 7 | | GdUnit0501 | Attribute Usage | Error | True | 8 | 9 | # Problem Description 10 | 11 | Test methods that use Godot functionality (such as Node, Scene, or other Godot types) 12 | must be annotated with `[RequireGodotRuntime]`. This ensures proper test execution in 13 | the Godot engine environment. 14 | 15 | Add `[RequireGodotRuntime]` to either test method or class level. 16 | 17 | ### Error example: 18 | 19 | ```csharp 20 | [TestCase] // ❌ GdUnit0501: Test method 'TestWithGodot' uses Godot functionality but is not annotated with '[RequireGodotRuntime]'. 21 | public void TestWithGodot() 22 | { 23 | var instance = new Node2D() 24 | AssertThat(instance).IsNotNull(); 25 | } 26 | ``` 27 | 28 | ### How to fix: 29 | 30 | Add `[RequireGodotRuntime]` to method or class level when testing Godot functionality: 31 | 32 | ```csharp 33 | [RequireGodotRuntime] // ✅ Correct: Method level annotation 34 | [TestCase] 35 | public void TestWithGodot() 36 | { 37 | var instance = new Node2D(); 38 | AssertThat(instance).IsNotNull(); 39 | } 40 | 41 | // OR 42 | 43 | [RequireGodotRuntime] // ✅ Correct: Class level annotation 44 | public class MyTestClass 45 | { 46 | [TestCase] 47 | public void TestWithGodot() 48 | { 49 | var instance = new Node2D(); 50 | AssertThat(instance).IsNotNull(); 51 | } 52 | } 53 | ``` 54 | -------------------------------------------------------------------------------- /Api/src/core/extensions/SystemVectorExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Extensions; 5 | 6 | using SystemVector2 = System.Numerics.Vector2; 7 | using SystemVector3 = System.Numerics.Vector3; 8 | using SystemVector4 = System.Numerics.Vector4; 9 | 10 | internal static class SystemVectorExtension 11 | { 12 | internal static bool IsEqualApprox(this SystemVector2 vector, SystemVector2 other, SystemVector2 approx) 13 | { 14 | var min = other - approx; 15 | var max = other + approx; 16 | 17 | var r1 = vector.X >= min.X && vector.Y >= min.Y; 18 | var r2 = vector.X <= max.X && vector.Y <= max.Y; 19 | return r1 && r2; 20 | } 21 | 22 | internal static bool IsEqualApprox(this SystemVector3 vector, SystemVector3 other, SystemVector3 approx) 23 | { 24 | var min = other - approx; 25 | var max = other + approx; 26 | 27 | var r1 = vector.X >= min.X && vector.Y >= min.Y && vector.Z >= min.Z; 28 | var r2 = vector.X <= max.X && vector.Y <= max.Y && vector.Z <= max.Z; 29 | return r1 && r2; 30 | } 31 | 32 | internal static bool IsEqualApprox(this SystemVector4 vector, SystemVector4 other, SystemVector4 approx) 33 | { 34 | var min = other - approx; 35 | var max = other + approx; 36 | 37 | var r1 = vector.X >= min.X && vector.Y >= min.Y && vector.Z >= min.Z && vector.W >= min.W; 38 | var r2 = vector.X <= max.X && vector.Y <= max.Y && vector.Z <= max.Z && vector.W <= max.W; 39 | return r1 && r2; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Analyzers/documentation/GdUnit0500.md: -------------------------------------------------------------------------------- 1 | # GdUnit0500 2 | 3 | ## Godot Runtime Required for Test Class 4 | 5 | | Id | Category | Severity | Enabled | 6 | |------------|-----------------|----------|---------| 7 | | GdUnit0500 | Attribute Usage | Error | True | 8 | 9 | # Problem Description 10 | 11 | Test classes with hooks (`[Before]`, `[After]`, `[BeforeTest]`, `[AfterTest]`) that use Godot functionality 12 | must be annotated with `[RequireGodotRuntime]`. This ensures proper test execution in the 13 | Godot engine environment. 14 | 15 | Add `[RequireGodotRuntime]` to the test class level. 16 | 17 | ### Error example: 18 | 19 | ```csharp 20 | [TestSuite] 21 | public class SceneRunnerTest // ❌ GdUnit0500: Test class 'SceneRunnerTest' contains Godot dependencies in test hooks and requires [RequireGodotRuntime] attribute 22 | { 23 | [Before] 24 | public void Setup() => 25 | Engine.PhysicsTicksPerSecond = 60; // Using Godot Engine in test hook 26 | 27 | [TestCase] 28 | public void TestMethod() => 29 | AssertThat(true).IsTrue(); 30 | } 31 | ``` 32 | 33 | ### How to fix: 34 | 35 | Add the `[RequireGodotRuntime]` attribute to the test class: 36 | 37 | ```csharp 38 | [RequireGodotRuntime] // ✅ Correct: Uses [RequireGodotRuntime] for class with Godot dependencies in hooks 39 | [TestSuite] 40 | public class SceneRunnerTest 41 | { 42 | [Before] 43 | public void Setup() => 44 | Engine.PhysicsTicksPerSecond = 60; 45 | 46 | [TestCase] 47 | public void TestMethod() => 48 | AssertThat(true).IsTrue(); 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /Api/src/api/TestCaseNode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | /// 7 | /// Represents a test case in the GdUnit4 testing framework. 8 | /// Provides essential information about a test's location and identity. 9 | /// 10 | /// 11 | /// TestCaseNode represents an individual test method within a test suite. 12 | /// It contains details about the method's location, identity, and execution requirements. 13 | /// 14 | public record TestCaseNode : TestNode 15 | { 16 | /// 17 | /// Gets the name of the test method. 18 | /// 19 | public required string ManagedMethod { get; init; } 20 | 21 | /// 22 | /// Gets the line number in the source file where this test method is defined. 23 | /// This is useful for source code navigation and error reporting. 24 | /// 25 | public required int LineNumber { get; init; } 26 | 27 | /// 28 | /// Gets the index of the attribute within the method if multiple test attributes exist. 29 | /// This is used to distinguish between multiple test cases on the same method. 30 | /// 31 | public required int AttributeIndex { get; init; } 32 | 33 | /// 34 | /// Gets a value indicating whether this test requires a running Godot engine to execute. 35 | /// Tests that interact with Godot engine functionality need this flag set to true. 36 | /// 37 | public required bool RequireRunningGodotEngine { get; init; } 38 | } 39 | -------------------------------------------------------------------------------- /Api.Test/src/core/TestSuiteWithExpectedExceptions.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Core; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | using GdUnit4.Core.Execution.Exceptions; 7 | 8 | using static Assertions; 9 | 10 | [TestSuite] 11 | public class TestSuiteWithExpectedExceptions 12 | { 13 | [TestCase(Timeout = 100)] 14 | [ThrowsException(typeof(ExecutionTimeoutException), "The execution has timed out after 100ms.")] 15 | public async Task ExpectExecutionTimeoutException() 16 | { 17 | await Task.Delay(500); 18 | // will never be executed because the test is interrupted after a timeout of 100ms 19 | AssertBool(true).IsFalse(); 20 | } 21 | 22 | [TestCase] 23 | [ThrowsException(typeof(TestFailedException), "Expecting: 'False' but is 'True'")] 24 | public void ExpectTestFailedException() => 25 | AssertBool(true).IsFalse(); 26 | 27 | 28 | [TestCase] 29 | [ThrowsException(typeof(TestFailedException), "Expecting: 'False' but is 'True'", 31)] 30 | public void ExpectTestFailedExceptionWithLineNumber() => 31 | AssertBool(true).IsFalse(); 32 | 33 | [TestCase] 34 | [ThrowsException(typeof(ArgumentException), "The argument 'message' is invalid")] 35 | public void ExpectArgumentException() => 36 | throw new ArgumentException("The argument 'message' is invalid"); 37 | 38 | [TestCase] 39 | [ThrowsException(typeof(NullReferenceException))] 40 | public void ExpectNullException() 41 | { 42 | string? value = null; 43 | // ReSharper disable once ReturnValueOfPureMethodIsNotUsed 44 | value!.Contains(""); // This will throw NullReferenceException 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Api/src/asserts/Tuple.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Asserts; 5 | 6 | using Core.Extensions; 7 | 8 | /// 9 | public sealed class Tuple : ITuple 10 | { 11 | private readonly IEnumerable values; 12 | 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// The values holding by the tuple. 17 | public Tuple(params object?[] args) => values = [.. args]; 18 | 19 | /// 20 | public IEnumerable Values 21 | { 22 | get => values; 23 | set => throw new NotImplementedException(); 24 | } 25 | 26 | /// 27 | public static bool operator ==(Tuple? tuple1, Tuple? tuple2) 28 | { 29 | if (ReferenceEquals(tuple1, tuple2)) 30 | return true; 31 | if (tuple1 is null || tuple2 is null) 32 | return false; 33 | return tuple1.Values.VariantEquals(tuple2.Values); 34 | } 35 | 36 | /// 37 | public static bool operator !=(Tuple? tuple1, Tuple? tuple2) => !(tuple1 == tuple2); 38 | 39 | /// 40 | public override bool Equals(object? obj) => obj is Tuple tuple && Values.VariantEquals(tuple.Values); 41 | 42 | /// 43 | public override int GetHashCode() => HashCode.Combine(values); 44 | 45 | /// 46 | public override string ToString() => $"tuple({string.Join(", ", Values.Select(GdUnitExtensions.Formatted)).Indentation(0)})"; 47 | } 48 | -------------------------------------------------------------------------------- /TestAdapter/src/settings/GdUnit4SettingsProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.TestAdapter.Settings; 5 | 6 | using System.Xml; 7 | using System.Xml.Serialization; 8 | 9 | using Microsoft.VisualStudio.TestPlatform.ObjectModel; 10 | using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; 11 | 12 | [SettingsName(GdUnit4Settings.RUN_SETTINGS_XML_NODE)] 13 | 14 | // ReSharper disable once ClassNeverInstantiated.Global 15 | internal sealed class GdUnit4SettingsProvider : ISettingsProvider 16 | { 17 | private GdUnit4Settings Settings { get; set; } = new(); 18 | 19 | private XmlSerializer Serializer { get; } = new(typeof(GdUnit4Settings)); 20 | 21 | public void Load(XmlReader reader) 22 | { 23 | try 24 | { 25 | if (reader.Read() && reader.Name == GdUnit4Settings.RUN_SETTINGS_XML_NODE) 26 | { 27 | var settings = Serializer.Deserialize(reader) as GdUnit4Settings; 28 | Settings = settings ?? new GdUnit4Settings(); 29 | } 30 | } 31 | #pragma warning disable CA1031 32 | catch (Exception e) 33 | #pragma warning restore CA1031 34 | { 35 | Console.WriteLine($"Loading GdUnit4 Adapter settings failed! {e}"); 36 | } 37 | } 38 | 39 | internal static GdUnit4Settings LoadSettings(IDiscoveryContext discoveryContext) 40 | { 41 | var gdUnitSettingsProvider = discoveryContext.RunSettings?.GetSettings(GdUnit4Settings.RUN_SETTINGS_XML_NODE) as GdUnit4SettingsProvider; 42 | return gdUnitSettingsProvider?.Settings ?? new GdUnit4Settings(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Api/src/api/ITestEngineLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | using System.Runtime.CompilerServices; 7 | 8 | /// 9 | /// Interface for test engine logging functionality. 10 | /// Provides standardized logging methods for different severity levels 11 | /// and defines the logging level hierarchy. 12 | /// 13 | public interface ITestEngineLogger 14 | { 15 | /// 16 | /// Logs an informational message. 17 | /// 18 | /// The informational message to log. 19 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 20 | void LogInfo(string message) => SendMessage(LogLevel.Informational, message); 21 | 22 | /// 23 | /// Logs a warning message. 24 | /// 25 | /// The warning message to log. 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | void LogWarning(string message) => SendMessage(LogLevel.Warning, message); 28 | 29 | /// 30 | /// Logs an error message. 31 | /// 32 | /// The error message to log. 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | void LogError(string message) => SendMessage(LogLevel.Error, message); 35 | 36 | /// 37 | /// Sends a message to the enabled loggers. 38 | /// 39 | /// Level of the message. 40 | /// The message to be sent. 41 | protected void SendMessage(LogLevel logLevel, string message); 42 | } 43 | -------------------------------------------------------------------------------- /Api/src/core/attributes/TestSuiteAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | using System.Runtime.CompilerServices; 10 | 11 | /// 12 | /// Attribute that marks a class as a test suite in the GdUnit4 testing framework. 13 | /// A test suite is a collection of related test methods organized in a class. 14 | /// 15 | /// 16 | /// This attribute should be applied to classes containing test methods. 17 | /// Test suites can contain setup and teardown methods marked with Before/After attributes. 18 | /// 19 | /// 20 | /// 21 | /// [TestSuite] 22 | /// public class PlayerTests 23 | /// { 24 | /// // Test methods go here 25 | /// } 26 | /// 27 | /// 28 | [AttributeUsage(AttributeTargets.Class, Inherited = false)] 29 | public sealed class TestSuiteAttribute : TestStageAttribute 30 | { 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The line number where the attribute is applied (automatically provided). 35 | /// The name of the class where the attribute is applied (automatically provided). 36 | #pragma warning disable CA1019 37 | public TestSuiteAttribute([CallerLineNumber] int line = 0, [CallerMemberName] string name = "") 38 | #pragma warning restore CA1019 39 | : base(name, line) 40 | { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Api/src/api/ReportType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Api; 4 | 5 | /// 6 | /// Enum to categorize the type of test report, supporting multiple flags for combined states. 7 | /// 8 | public enum ReportType 9 | { 10 | /// 11 | /// Indicates that the test was executed successfully without any issues. 12 | /// 13 | Success, 14 | 15 | /// 16 | /// Indicates that the test finished with warnings, but no failures occurred. 17 | /// 18 | Warning, 19 | 20 | /// 21 | /// Indicates that the test failed due to some issues or errors. 22 | /// 23 | Failure, 24 | 25 | /// 26 | /// Indicates that the test found orphan nodes. 27 | /// 28 | Orphan, 29 | 30 | /// 31 | /// Denotes that the test was forcibly terminated before it completed execution. 32 | /// 33 | Terminated, 34 | 35 | /// 36 | /// Indicates that the test execution was interrupted, possibly timeout or due to runtime conditions. 37 | /// 38 | Interrupted, 39 | 40 | /// 41 | /// Indicates that the test was aborted, typically due to unrecoverable errors. 42 | /// 43 | Abort, 44 | 45 | /// 46 | /// Marks the test as skipped and not executed during the test run. 47 | /// 48 | Skipped, 49 | 50 | /// 51 | /// Represents standard output logs produced during the test execution. 52 | /// 53 | Stdout 54 | } 55 | -------------------------------------------------------------------------------- /Api/src/api/ICommandExecutor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | using System; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | using Core.Commands; 11 | 12 | /// 13 | /// Defines an interface for executing test commands asynchronously. 14 | /// Provides functionality to start/stop the executor and execute test commands with event handling. 15 | /// 16 | public interface ICommandExecutor : IAsyncDisposable 17 | { 18 | /// 19 | /// Starts the command executor asynchronously. 20 | /// 21 | /// A task representing the asynchronous operation. 22 | Task StartAsync(); 23 | 24 | /// 25 | /// Stops the command executor asynchronously. 26 | /// 27 | /// A task representing the asynchronous operation. 28 | Task StopAsync(); 29 | 30 | /// 31 | /// Executes a test command with event handling and cancellation support. 32 | /// 33 | /// The type of command to execute, must inherit from BaseCommand. 34 | /// The command to execute. 35 | /// The listener for test events. 36 | /// Token to support cancellation of the operation. 37 | /// A task containing the command execution response. 38 | Task ExecuteCommand(T command, ITestEventListener testEventListener, CancellationToken cancellationToken) 39 | where T : BaseCommand; 40 | } 41 | -------------------------------------------------------------------------------- /Api/src/core/attributes/AfterAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | using System.Runtime.CompilerServices; 10 | 11 | /// 12 | /// Attribute that marks methods to be executed once after all tests in the test suite have run. 13 | /// Methods marked with this attribute are used for test suite cleanup. 14 | /// 15 | /// 16 | /// Only one method in a test class should be marked with this attribute. 17 | /// This differs from AfterTest in that it runs only once for the entire test suite, 18 | /// not after each test. 19 | /// 20 | /// 21 | /// 22 | /// [After] 23 | /// public void CleanupTestSuite() 24 | /// { 25 | /// // One-time cleanup code for the entire test suite 26 | /// } 27 | /// 28 | /// 29 | [AttributeUsage(AttributeTargets.Method, Inherited = false)] 30 | public sealed class AfterAttribute : TestStageAttribute 31 | { 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// 35 | /// The line number where the attribute is applied (automatically provided). 36 | /// The name of the method where the attribute is applied (automatically provided). 37 | #pragma warning disable CA1019 38 | public AfterAttribute([CallerLineNumber] int line = 0, [CallerMemberName] string name = "") 39 | #pragma warning restore CA1019 40 | : base(name, line) 41 | { 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Api.Test/src/extensions/GodotObjectExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Extensions; 2 | 3 | using GdUnit4.Core.Extensions; 4 | 5 | using Godot.Collections; 6 | 7 | using static Assertions; 8 | 9 | [TestSuite] 10 | [RequireGodotRuntime] 11 | public class GodotObjectExtensionsTest 12 | { 13 | [TestCase] 14 | public void ToGodotDictionary() 15 | { 16 | var result = new System.Collections.Generic.Dictionary 17 | { 18 | { "path", "res://foo/barTest.cs" }, 19 | { "line", 42 } 20 | }; 21 | var expected = new Dictionary 22 | { 23 | { "path", "res://foo/barTest.cs" }, 24 | { "line", 42 } 25 | }; 26 | AssertThat(result.ToGodotDictionary()).IsEqual(expected); 27 | } 28 | 29 | [TestCase] 30 | public void ToGodotDictionaryNestedDictionary() 31 | { 32 | var result = new System.Collections.Generic.Dictionary 33 | { 34 | { "path", "res://foo/barTest.cs" }, 35 | { "line", 42 }, 36 | { 37 | "statistics", new System.Collections.Generic.Dictionary 38 | { 39 | { "foo", "vale" }, 40 | { "bar", 42 } 41 | } 42 | } 43 | }; 44 | var expected = new Dictionary 45 | { 46 | { "path", "res://foo/barTest.cs" }, 47 | { "line", 42 }, 48 | { 49 | "statistics", new Dictionary 50 | { 51 | { "foo", "vale" }, 52 | { "bar", 42 } 53 | } 54 | } 55 | }; 56 | AssertThat(result.ToGodotDictionary()).IsEqual(expected); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Api/src/core/attributes/BeforeAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | using System.Runtime.CompilerServices; 10 | 11 | /// 12 | /// Attribute that marks methods to be executed once before any test in the test suite is run. 13 | /// Methods marked with this attribute are used for test suite initialization. 14 | /// 15 | /// 16 | /// Only one method in a test class should be marked with this attribute. 17 | /// This differs from BeforeTest in that it runs only once for the entire test suite, 18 | /// not before each test. 19 | /// 20 | /// 21 | /// 22 | /// [Before] 23 | /// public void SetupTestSuite() 24 | /// { 25 | /// // One-time setup code for the entire test suite 26 | /// } 27 | /// 28 | /// 29 | [AttributeUsage(AttributeTargets.Method, Inherited = false)] 30 | public sealed class BeforeAttribute : TestStageAttribute 31 | { 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// 35 | /// The line number where the attribute is applied (automatically provided). 36 | /// The name of the method where the attribute is applied (automatically provided). 37 | #pragma warning disable CA1019 38 | public BeforeAttribute([CallerLineNumber] int line = 0, [CallerMemberName] string name = "") 39 | #pragma warning restore CA1019 40 | : base(name, line) 41 | { 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Api/src/core/attributes/BeforeTestAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | using System.Runtime.CompilerServices; 10 | 11 | /// 12 | /// Attribute that marks methods to be executed before each test method in a test class. 13 | /// Methods marked with this attribute are executed to set up the test environment 14 | /// before each test method runs. 15 | /// 16 | /// 17 | /// Only one method in a test class should be marked with this attribute. 18 | /// The method should not have parameters and should have a void return type. 19 | /// 20 | /// 21 | /// 22 | /// [BeforeTest] 23 | /// public void Setup() 24 | /// { 25 | /// // Setup code to run before each test 26 | /// } 27 | /// 28 | /// 29 | [AttributeUsage(AttributeTargets.Method, Inherited = false)] 30 | public sealed class BeforeTestAttribute : TestStageAttribute 31 | { 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// 35 | /// The line number where the attribute is applied (automatically provided). 36 | /// The name of the method where the attribute is applied (automatically provided). 37 | #pragma warning disable CA1019 38 | public BeforeTestAttribute([CallerLineNumber] int line = 0, [CallerMemberName] string name = "") 39 | #pragma warning restore CA1019 40 | : base(name, line) 41 | { 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Api/src/constraints/IObjectConstraint.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Constraints; 5 | 6 | using Asserts; 7 | 8 | /// 9 | /// A set of constrains to verify object values. 10 | /// 11 | /// 12 | /// The object type being tested. 13 | /// 14 | public interface IObjectConstraint : IAssertBase> 15 | { 16 | /// 17 | /// Verifies that the current value is the same as the given one. 18 | /// 19 | /// The object to be the same. 20 | /// IObjectAssert. 21 | IObjectConstraint IsSame(object expected); 22 | 23 | /// 24 | /// Verifies that the current value is not the same as the given one. 25 | /// 26 | /// The object to be NOT the same. 27 | /// IObjectAssert. 28 | IObjectConstraint IsNotSame(object expected); 29 | 30 | /// 31 | /// Verifies that the current value is an instance of the given type. 32 | /// 33 | /// The type of instance to be expected. 34 | /// IObjectAssert. 35 | IObjectConstraint IsInstanceOf(); 36 | 37 | /// 38 | /// Verifies that the current value is not an instance of the given type. 39 | /// 40 | /// The type of instance to be NOT expected. 41 | /// IObjectAssert. 42 | IObjectConstraint IsNotInstanceOf(); 43 | } 44 | -------------------------------------------------------------------------------- /Example/ExampleProject.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | net9.0 6 | enable 7 | enable 8 | true 9 | Examples 10 | 11 | 12 | 13 | true 14 | 15 | NU1605 16 | 17 | none 18 | GdUnit4 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | none 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Api/src/core/discovery/CodeNavigation.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Discovery; 5 | 6 | /// 7 | /// Value type representing source code navigation information for a test method. 8 | /// 9 | internal readonly struct CodeNavigation 10 | { 11 | /// 12 | /// Gets the method this navigation data refers to. 13 | /// 14 | public required string MethodName { get; init; } 15 | 16 | /// 17 | /// Gets the line number in the source file where the method is defined. 18 | /// 19 | public required int LineNumber { get; init; } 20 | 21 | /// 22 | /// Gets the source code file path containing the method. 23 | /// 24 | public required string? CodeFilePath { get; init; } 25 | 26 | /// 27 | /// Gets a value indicating whether indicates if this navigation data contains valid source information. 28 | /// 29 | public readonly bool IsValid => CodeFilePath != null; 30 | 31 | /// 32 | /// Returns a JSON string representation of the test case descriptor. 33 | /// 34 | /// A formatted JSON string containing all properties of the test case descriptor. 35 | /// 36 | /// This method is primarily used for debugging and logging purposes. 37 | /// The JSON output includes all properties with indented formatting for readability. 38 | /// 39 | public override string ToString() 40 | => $""" 41 | CodeNavigation: 42 | Name: '{MethodName}' 43 | Line: {LineNumber} 44 | CodeFilePath: '{CodeFilePath}'; 45 | """; 46 | } 47 | -------------------------------------------------------------------------------- /Api/src/core/runners/GdUnit4TestRunnerSceneCore.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Runners; 5 | 6 | using System; 7 | using System.Diagnostics.CodeAnalysis; 8 | 9 | using Api; 10 | 11 | using Godot; 12 | 13 | /// 14 | /// The GdUnit4Net test runner scene. 15 | /// 16 | public partial class GdUnit4TestRunnerSceneCore : SceneTree 17 | { 18 | /// 19 | [SuppressMessage( 20 | "Reliability", 21 | "CA2000:Dispose objects before losing scope", 22 | Justification = "TestRunner disposal is managed by Godot internals.")] 23 | public override void _Initialize() 24 | { 25 | try 26 | { 27 | Root.AddChild(new TestRunner()); 28 | } 29 | #pragma warning disable CA1031 30 | catch (Exception e) 31 | #pragma warning restore CA1031 32 | { 33 | GD.PrintErr("Exception", e.Message); 34 | Quit(100); // Exit with error code 35 | } 36 | } 37 | 38 | // ReSharper disable once PartialTypeWithSinglePart 39 | private sealed partial class TestRunner : Node 40 | { 41 | public TestRunner() 42 | { 43 | Logger = new GodotLogger(); 44 | Server = new GodotGdUnit4RestServer(Logger); 45 | } 46 | 47 | private ITestEngineLogger Logger { get; } 48 | 49 | private GodotGdUnit4RestServer Server { get; } 50 | 51 | public override void _Ready() => _ = Server.Start(); 52 | 53 | public override void _Process(double delta) => _ = Server.Process(); 54 | 55 | public override void _Notification(int what) 56 | { 57 | if (what == NotificationPredelete) 58 | Server.Stop(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Api/src/core/data/IValueProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace GdUnit4; 6 | 7 | using System.Collections.Generic; 8 | 9 | /// 10 | /// Defines a contract for providing test data values in data-driven testing scenarios. 11 | /// 12 | /// 13 | /// 14 | /// This interface enables the creation of custom data providers that can supply test values 15 | /// for parameterized tests. Implementations should return a collection of values that will 16 | /// be used as individual test case parameters. 17 | /// 18 | /// 19 | /// 20 | public interface IValueProvider 21 | { 22 | /// 23 | /// Gets a collection of values to be used as test case parameters. 24 | /// 25 | /// 26 | /// An of objects representing the test values. 27 | /// Each object in the collection will be used as a parameter for a separate test case execution. 28 | /// 29 | /// 30 | /// 31 | /// public IEnumerable<object> GetValues() 32 | /// { 33 | /// // Single parameter values 34 | /// yield return "test1"; 35 | /// yield return "test2"; 36 | /// yield return "test3"; 37 | /// 38 | /// // Or complex objects 39 | /// yield return new TestConfiguration { Name = "Config1", Enabled = true }; 40 | /// yield return new TestConfiguration { Name = "Config2", Enabled = false }; 41 | /// } 42 | /// 43 | /// 44 | IEnumerable GetValues(); 45 | } 46 | -------------------------------------------------------------------------------- /Api/src/core/attributes/AfterTestAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | using System.Runtime.CompilerServices; 10 | 11 | /// 12 | /// Attribute that marks methods to be executed after each test method in a test class. 13 | /// Methods marked with this attribute are executed to clean up the test environment 14 | /// after each test method has completed. 15 | /// 16 | /// 17 | /// Only one method in a test class should be marked with this attribute. 18 | /// The method should not have parameters and should have a void return type. 19 | /// This method will be executed even if the test method throws an exception. 20 | /// 21 | /// 22 | /// 23 | /// [AfterTest] 24 | /// public void Cleanup() 25 | /// { 26 | /// // Cleanup code to run after each test 27 | /// } 28 | /// 29 | /// 30 | [AttributeUsage(AttributeTargets.Method, Inherited = false)] 31 | public sealed class AfterTestAttribute : TestStageAttribute 32 | { 33 | #pragma warning disable CA1019 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | /// The line number where the attribute is applied (automatically provided). 38 | /// The name of the method where the attribute is applied (automatically provided). 39 | public AfterTestAttribute([CallerLineNumber] int line = 0, [CallerMemberName] string name = "") 40 | #pragma warning restore CA1019 41 | : base(name, line) 42 | { 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Example/.runsettings-ci: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | ./TestResults 6 | net7.0;net8.0 7 | 180000 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | detailed 16 | 17 | 18 | 19 | 20 | test-result.html 21 | 22 | 23 | 24 | 25 | test-result.trx 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | --audio-driver Dummy --display-driver x11 --rendering-driver opengl3 --screen 0 35 | 37 | FullyQualifiedName 38 | 39 | 40 | -------------------------------------------------------------------------------- /TestAdapter/src/utilities/Utils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.TestAdapter.Utilities; 5 | 6 | using System; 7 | using System.IO; 8 | using System.Linq; 9 | 10 | internal static class Utils 11 | { 12 | internal static string GetUserDataDirectory => Environment.OSVersion.Platform switch 13 | { 14 | PlatformID.Win32NT => Path.Combine( 15 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Godot"), 16 | PlatformID.Unix => Path.Combine( 17 | Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "godot"), 18 | PlatformID.MacOSX => Path.Combine( 19 | Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library/Application Support/Godot"), 20 | PlatformID.Win32S => throw new NotImplementedException(), 21 | PlatformID.Win32Windows => throw new NotImplementedException(), 22 | PlatformID.WinCE => throw new NotImplementedException(), 23 | PlatformID.Xbox => throw new NotImplementedException(), 24 | PlatformID.Other => throw new NotImplementedException(), 25 | _ => throw new PlatformNotSupportedException("Unsupported operating system") 26 | }; 27 | 28 | internal static string GetProjectDirectory 29 | { 30 | get 31 | { 32 | var directory = new DirectoryInfo(AppContext.BaseDirectory); 33 | 34 | while (directory != null && 35 | !directory.EnumerateFiles("*.sln").Any() && 36 | !directory.EnumerateFiles("*.csproj").Any()) 37 | directory = directory.Parent; 38 | 39 | return directory?.FullName ?? throw new FileNotFoundException($"Could not find project root directory {directory}"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/scenes/DragAndDrop/DragAndDropControl.gd: -------------------------------------------------------------------------------- 1 | extends PanelContainer 2 | 3 | 4 | # Godot calls this method to get data that can be dragged and dropped onto controls that expect drop data. 5 | # Returns null if there is no data to drag. 6 | # Controls that want to receive drop data should implement can_drop_data() and drop_data(). 7 | # position is local to this control. Drag may be forced with force_drag(). 8 | func _get_drag_data(_position: Vector2) -> Variant: 9 | var x :TextureRect = $TextureRect 10 | var data: = {texture = x.texture} 11 | var drag_texture := x.duplicate() 12 | drag_texture.size = x.size 13 | drag_texture.position = x.global_position * -0.2 14 | 15 | # set drag preview 16 | var control := Panel.new() 17 | control.add_child(drag_texture) 18 | # center texture relative to mouse pos 19 | set_drag_preview(control) 20 | return data 21 | 22 | 23 | # Godot calls this method to test if data from a control's get_drag_data() can be dropped at position. position is local to this control. 24 | func _can_drop_data(_position: Vector2, data :Variant) -> bool: 25 | return typeof(data) == TYPE_DICTIONARY and data.has("texture") 26 | 27 | 28 | # Godot calls this method to pass you the data from a control's get_drag_data() result. 29 | # Godot first calls can_drop_data() to test if data is allowed to drop at position where position is local to this control. 30 | func _drop_data(_position: Vector2, data :Variant) -> void: 31 | var drag_texture :Texture = data["texture"] 32 | if drag_texture != null: 33 | $TextureRect.texture = drag_texture 34 | 35 | 36 | # Virtual method to be implemented by the user. Use this method to process and accept inputs on UI elements. See accept_event(). 37 | func _gui_input(_event): 38 | #prints("Panel _gui_input", _event.as_text()) 39 | #if _event is InputEventMouseButton: 40 | # prints("Panel _gui_input", _event.as_text()) 41 | pass 42 | -------------------------------------------------------------------------------- /Api/src/asserts/ObjectAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Asserts; 4 | 5 | using Constraints; 6 | 7 | /// 8 | public sealed class ObjectAssert : AssertBase>, IObjectAssert 9 | { 10 | internal ObjectAssert(TValue? current) 11 | : base(current) 12 | { 13 | var type = current?.GetType(); 14 | if (type is { IsPrimitive: true }) 15 | ThrowTestFailureReport($"ObjectAssert initial error: current is primitive <{type}>", Current, null); 16 | } 17 | 18 | /// 19 | public IObjectConstraint IsNotInstanceOf() 20 | { 21 | if (Current is TExpectedType) 22 | ThrowTestFailureReport(AssertFailures.NotInstanceOf(typeof(TExpectedType)), Current, typeof(TExpectedType)); 23 | return this; 24 | } 25 | 26 | /// 27 | public IObjectConstraint IsNotSame(object expected) 28 | { 29 | if (ReferenceEquals(expected, Current)) 30 | ThrowTestFailureReport(AssertFailures.IsNotSame(expected), Current, expected); 31 | return this; 32 | } 33 | 34 | /// 35 | public IObjectConstraint IsSame(object expected) 36 | { 37 | if (!ReferenceEquals(expected, Current)) 38 | ThrowTestFailureReport(AssertFailures.IsSame(Current, expected), Current, expected); 39 | return this; 40 | } 41 | 42 | /// 43 | public IObjectConstraint IsInstanceOf() 44 | { 45 | if (Current is not TExpectedType) 46 | ThrowTestFailureReport(AssertFailures.IsInstanceOf(Current?.GetType(), typeof(TExpectedType)), Current, typeof(TExpectedType)); 47 | return this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Api/src/api/ITestReport.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | using System.Collections.Generic; 7 | 8 | /// 9 | /// Represents an interface for a test report, providing details about the outcome of a test execution. 10 | /// 11 | public interface ITestReport 12 | { 13 | /// 14 | /// Gets the type of the test report, indicating the outcome or status of the test. 15 | /// 16 | ReportType Type { get; } 17 | 18 | /// 19 | /// Gets the line number in the test script or code where the report originated. 20 | /// 21 | int LineNumber { get; } 22 | 23 | /// 24 | /// Gets the message associated with the test report, providing additional details. 25 | /// 26 | string Message { get; } 27 | 28 | /// 29 | /// Gets the stack trace information in case of test failure or error, providing contextual details. 30 | /// 31 | string? StackTrace { get; } 32 | 33 | /// 34 | /// Gets a value indicating whether the test report denotes an error occurred during execution. 35 | /// 36 | bool IsError { get; } 37 | 38 | /// 39 | /// Gets a value indicating whether the test report denotes a failure. 40 | /// 41 | bool IsFailure { get; } 42 | 43 | /// 44 | /// Gets a value indicating whether the test report contains warnings. 45 | /// 46 | bool IsWarning { get; } 47 | 48 | /// 49 | /// Serializes the test report into a dictionary representation containing its properties and values. 50 | /// 51 | /// The report serialized as a dictionary. 52 | IDictionary Serialize(); 53 | } 54 | -------------------------------------------------------------------------------- /TestAdapter/src/utilities/Logger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.TestAdapter.Utilities; 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | using Api; 10 | 11 | using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; 12 | 13 | /// 14 | /// Adapts ITestEngineLogger to work with Visual Studio's IMessageLogger. 15 | /// Provides test logging functionality within the VS test platform. 16 | /// 17 | public class Logger : ITestEngineLogger 18 | { 19 | private static readonly Dictionary LevelMap = new() 20 | { 21 | { LogLevel.Informational, TestMessageLevel.Informational }, 22 | { LogLevel.Warning, TestMessageLevel.Warning }, 23 | { LogLevel.Error, TestMessageLevel.Error } 24 | }; 25 | 26 | private readonly IMessageLogger delegator; 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// The VS test platform message logger to delegate to. 32 | public Logger(IMessageLogger delegator) => this.delegator = delegator ?? throw new ArgumentNullException(nameof(delegator)); 33 | 34 | /// 35 | /// Sends a message to the VS test platform logger with the appropriate level. 36 | /// 37 | /// The severity level of the message. 38 | /// The message to log. 39 | public void SendMessage(LogLevel logLevel, string message) 40 | { 41 | if (LevelMap.TryGetValue(logLevel, out var testLogLevel)) 42 | delegator.SendMessage(testLogLevel, $"[GdUnit4] {message}"); 43 | else 44 | delegator.SendMessage(TestMessageLevel.Error, $"[GdUnit4] Can't parse logging level {logLevel}"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Api.Test/src/UtilsTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests; 2 | 3 | #if GDUNIT4NET_API_V5 4 | using System.IO; 5 | 6 | using Godot; 7 | 8 | using static Utils; 9 | 10 | using static Assertions; 11 | 12 | [TestSuite] 13 | public class UtilsTest 14 | { 15 | [TestCase] 16 | public void CreateTempDirSuccess() 17 | { 18 | var tempDir = CreateTempDir("foo"); 19 | AssertThat(tempDir).IsEqual(Path.Combine(GodotTempDir(), "foo")); 20 | AssertThat(Directory.Exists(tempDir)).IsTrue(); 21 | 22 | tempDir = CreateTempDir("bar1\\test\\foo"); 23 | AssertThat(tempDir).IsEqual(Path.Combine(GodotTempDir(), "bar1\\test\\foo")); 24 | AssertThat(Directory.Exists(tempDir)).IsTrue(); 25 | 26 | tempDir = CreateTempDir("bar2/test/foo"); 27 | AssertThat(tempDir).IsEqual(Path.Combine(GodotTempDir(), "bar2/test/foo")); 28 | AssertThat(Directory.Exists(tempDir)).IsTrue(); 29 | } 30 | 31 | [TestCase] 32 | public void CreateTempDirAtTwice() 33 | { 34 | var tempDir = CreateTempDir("foo"); 35 | // create again 36 | CreateTempDir("foo"); 37 | 38 | AssertThat(tempDir).IsEqual(Path.Combine(GodotTempDir(), "foo")); 39 | AssertThat(Directory.Exists(tempDir)).IsTrue(); 40 | } 41 | 42 | [TestCase] 43 | public void ClearTempDirSuccess() 44 | { 45 | var tempDir = CreateTempDir("foo"); 46 | AssertThat(Directory.Exists(tempDir)).IsTrue(); 47 | 48 | ClearTempDir(); 49 | AssertThat(Directory.Exists(tempDir)).IsFalse(); 50 | } 51 | 52 | [TestCase] 53 | [RequireGodotRuntime] 54 | public void GodotErrorAsString() 55 | { 56 | AssertThat(ErrorAsString(Error.Bug)).IsEqual("Bug"); 57 | AssertThat(ErrorAsString(47)).IsEqual("Bug"); 58 | 59 | // with not existing error number 60 | AssertThat(ErrorAsString(100)).IsEqual("The error: 100 is not defined in Godot."); 61 | } 62 | } 63 | #endif 64 | -------------------------------------------------------------------------------- /Api/src/api/TestRunnerConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | /// 7 | /// Represents the configuration for the test runner, holding information about which test suites should be run. 8 | /// For each test suite, a set of 'TestCaseConfig' instances is attached to specify included tests. 9 | /// 10 | public class TestRunnerConfig 11 | { 12 | /// 13 | /// Gets the included test suites along with their associated test case configurations. 14 | /// 15 | public Dictionary> Included { get; } = []; 16 | 17 | /// 18 | /// Gets holds test run properties to control the test execution. 19 | /// 20 | public Dictionary Properties { get; } = []; 21 | 22 | /// 23 | /// Gets or sets a value indicating whether when set to true, standard output (stdout) from test cases is captured and included in the test result. This can be 24 | /// useful for debugging. 25 | /// Default: false. 26 | /// 27 | public bool CaptureStdOut { get; set; } 28 | 29 | /// 30 | /// Gets or sets the version of the test runner configuration. 31 | /// 32 | public string Version { get; set; } = "2.0"; 33 | } 34 | 35 | /// 36 | /// Represents configuration specific to a test case. 37 | /// 38 | #pragma warning disable SA1402 39 | public class TestCaseConfig 40 | #pragma warning restore SA1402 41 | { 42 | /// 43 | /// Gets or sets the name of the test case. 44 | /// 45 | public string Name { get; set; } = string.Empty; 46 | 47 | /// 48 | /// Gets or sets a value indicating whether the test case is skipped. 49 | /// 50 | public bool Skipped { get; set; } 51 | } 52 | -------------------------------------------------------------------------------- /TestAdapter/src/execution/TestCaseFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.TestAdapter.Execution; 5 | 6 | using Api; 7 | 8 | using Extensions; 9 | 10 | using Microsoft.VisualStudio.TestPlatform.ObjectModel; 11 | using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; 12 | 13 | /// 14 | /// Implementation for filtering test cases. 15 | /// 16 | internal class TestCaseFilter 17 | { 18 | private readonly ITestCaseFilterExpression? filterExpression; 19 | 20 | public TestCaseFilter(IRunContext runContext, ITestEngineLogger logger) 21 | { 22 | try 23 | { 24 | filterExpression = runContext.GetTestCaseFilter( 25 | TestCaseExtensions.SupportedProperties.Keys, 26 | TestCaseExtensions.GetPropertyProvider()); 27 | } 28 | catch (TestPlatformFormatException e) 29 | { 30 | logger.LogError(e.Message); 31 | } 32 | } 33 | 34 | /// 35 | /// Runs the filter on the provided test cases and returns the filtered collection. 36 | /// 37 | /// The collection of test cases to filter. 38 | /// The filtered collection of test cases or the original collection if the filter is null. 39 | public List Execute(List testCases) => 40 | filterExpression == null 41 | ? testCases 42 | : [.. testCases.Where(MatchTestCase)]; 43 | 44 | /// 45 | /// Determines if a test case matches the filter criteria. 46 | /// 47 | /// Test case to evaluate. 48 | /// True if the test case matches the filter, false otherwise. 49 | private bool MatchTestCase(TestCase testCase) 50 | => filterExpression?.MatchTestCase(testCase, testCase.GetPropertyValue) ?? false; 51 | } 52 | -------------------------------------------------------------------------------- /Api/src/core/execution/monitoring/GodotPushErrorPattern.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Core.Execution.Exceptions; 4 | 5 | using System.Text.RegularExpressions; 6 | 7 | /// 8 | /// Provides pattern matching for Godot push_error stack trace format. 9 | /// 10 | /// 11 | /// This class contains regex patterns to parse Godot's error output format, 12 | /// specifically the stack trace information produced by push_error() calls. 13 | /// The expected format is: "at: MethodName() in file.cs:lineNumber". 14 | /// 15 | internal static partial class GodotPushErrorPattern 16 | { 17 | /// 18 | /// Matches a Godot push_error stack trace line against the expected format. 19 | /// 20 | /// The stack trace line to match. 21 | /// 22 | /// A object containing: 23 | /// 24 | /// 25 | /// Groups[1]: Method name and signature 26 | /// 27 | /// 28 | /// Groups[2]: File path 29 | /// 30 | /// 31 | /// Groups[3]: Line number 32 | /// 33 | /// 34 | /// 35 | public static Match Match(string value) => PushErrorFileInfoRegex().Match(value); 36 | 37 | /// 38 | /// Generated regex pattern to match Godot push_error stack trace format. 39 | /// Pattern: "at: (method_info) in (file_path):(line_number)" 40 | /// 41 | /// A compiled regex for parsing Godot error stack traces. 42 | [GeneratedRegex(@"at: (.*) \((.*\.cs):(\d+)\)$", RegexOptions.Compiled)] 43 | private static partial Regex PushErrorFileInfoRegex(); 44 | } 45 | -------------------------------------------------------------------------------- /Api/src/core/MouseMoveTask.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | namespace GdUnit4.Core; 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | using Godot; 8 | 9 | using static Assertions; 10 | 11 | /// 12 | /// A helper to simulate a mouse moving from a source to the final position. 13 | /// 14 | internal partial class MouseMoveTask : Node, IDisposable 15 | { 16 | public MouseMoveTask(Vector2 currentPosition, Vector2 finalPosition) 17 | { 18 | CurrentMousePosition = currentPosition; 19 | FinalMousePosition = finalPosition; 20 | } 21 | 22 | private Vector2 CurrentMousePosition { get; set; } 23 | 24 | private Vector2 FinalMousePosition { get; } 25 | 26 | public new void Dispose() 27 | { 28 | QueueFree(); 29 | Dispose(true); 30 | GC.SuppressFinalize(this); 31 | } 32 | 33 | [SuppressMessage( 34 | "Style", 35 | "IDE0058:Expression value is never used", 36 | Justification = "Method called for side effects only, return value intentionally ignored")] 37 | public async Task WaitOnFinalPosition(ISceneRunner sceneRunner, double time, Tween.TransitionType transitionType) 38 | { 39 | AssertObject(sceneRunner.Scene()).OverrideFailureMessage("No valid scene is loaded.").IsNotNull(); 40 | 41 | // ReSharper disable once NullableWarningSuppressionIsUsed 42 | using var tween = sceneRunner.Scene()!.CreateTween(); 43 | tween.TweenProperty(this, "CurrentMousePosition", FinalMousePosition, time).SetTrans(transitionType); 44 | tween.Play(); 45 | 46 | while (!sceneRunner.GetMousePosition().IsEqualApprox(FinalMousePosition)) 47 | { 48 | sceneRunner.SimulateMouseMove(CurrentMousePosition); 49 | await ISceneRunner.SyncProcessFrame; 50 | } 51 | 52 | sceneRunner.SimulateMouseMove(FinalMousePosition); 53 | await ISceneRunner.SyncProcessFrame; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Api/src/core/execution/monitoring/MemoryPool.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Core.Execution.Monitoring; 5 | 6 | using Extensions; 7 | 8 | using Godot; 9 | 10 | internal class MemoryPool 11 | { 12 | private static readonly ThreadLocal CurrentPool = new(); 13 | private readonly List registeredObjects = []; 14 | 15 | public MemoryPool(bool reportOrphanNodesEnabled) => OrphanMonitor = reportOrphanNodesEnabled ? new OrphanNodesMonitor() : null; 16 | 17 | public int OrphanCount => OrphanMonitor?.OrphanCount ?? 0; 18 | 19 | public string Name { get; set; } = "Unknown"; 20 | 21 | private OrphanNodesMonitor? OrphanMonitor { get; } 22 | 23 | public static T RegisterForAutoFree(T obj) 24 | where T : GodotObject 25 | { 26 | CurrentPool.Value?.registeredObjects.Add(obj); 27 | return obj; 28 | } 29 | 30 | public void SetActive(string name, bool reset = false) 31 | { 32 | Name = name; 33 | CurrentPool.Value = this; 34 | OrphanMonitor?.Start(reset); 35 | } 36 | 37 | public async Task Gc() 38 | { 39 | var currentPool = CurrentPool.Value; 40 | currentPool?.registeredObjects.ForEach(FreeInstance); 41 | currentPool?.registeredObjects.Clear(); 42 | StopMonitoring(); 43 | if (OrphanMonitor != null) 44 | _ = await GodotObjectExtensions.SyncProcessFrame; 45 | } 46 | 47 | public void StopMonitoring() => OrphanMonitor?.Stop(); 48 | 49 | private void FreeInstance(GodotObject obj) 50 | { 51 | // needs to manually exclude JavaClass see https://github.com/godotengine/godot/issues/44932 52 | if (GodotObject.IsInstanceValid(obj) && obj is not JavaClass) 53 | { 54 | if (obj is RefCounted) 55 | obj.Notification((int)GodotObject.NotificationPredelete); 56 | else 57 | obj.Free(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /TestAdapter/src/extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.TestAdapter.Extensions; 5 | 6 | using System.Text; 7 | 8 | using Api; 9 | 10 | using static Api.ReportType; 11 | 12 | internal static class StringExtensions 13 | { 14 | private const string ANSI_RESET = "\u001b[0m"; 15 | private const string ANSI_BLUE = "\u001b[34m"; 16 | private const string ANSI_YELLOW = "\u001b[33m"; 17 | private const string ANSI_BOLD = "\u001b[1m"; 18 | private const string ANSI_ITALIC = "\u001b[3m"; 19 | 20 | public static string Indent(this string str, int count = 1, string indentWith = "\t") 21 | { 22 | if (string.IsNullOrEmpty(str)) 23 | return str; 24 | 25 | var indent = string.Concat(Enumerable.Repeat(indentWith, count)); 26 | return indent + str.Replace("\n", "\n" + indent, StringComparison.Ordinal); 27 | } 28 | 29 | public static string FormatMessageColored(this string message, ReportType reportType) 30 | { 31 | var sb = new StringBuilder(); 32 | 33 | // Header line (always visible) 34 | switch (reportType) 35 | { 36 | case Stdout: 37 | _ = sb.AppendLine($"{ANSI_BLUE}{ANSI_BOLD}Standard Output: {ANSI_ITALIC}{ANSI_RESET}"); 38 | _ = sb.AppendLine($"{ANSI_BLUE}──────────────────────────────────────────{ANSI_RESET}"); 39 | 40 | break; 41 | case Warning: 42 | _ = sb.AppendLine($"{ANSI_YELLOW}{ANSI_BOLD}Warning:{ANSI_ITALIC}{ANSI_RESET}"); 43 | 44 | break; 45 | case Success: 46 | case Failure: 47 | case Orphan: 48 | case Terminated: 49 | case Interrupted: 50 | case Abort: 51 | case Skipped: 52 | default: 53 | break; 54 | } 55 | 56 | _ = sb 57 | .Append(message) 58 | .Append(Environment.NewLine); 59 | return sb.ToString(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteFailAndOrphansDetected.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | using Godot; 7 | 8 | using static Assertions; 9 | 10 | // will be ignored because of missing `[TestSuite]` annotation 11 | // used by executor integration test 12 | [RequireGodotRuntime] 13 | //[TestSuite] 14 | public class TestSuiteFailAndOrphansDetected : IDisposable 15 | { 16 | private readonly List orphans = new(); 17 | 18 | // finally, we manually release the orphans from the simulated test suite to avoid memory leaks 19 | #pragma warning disable CA1816 // Dispose methods should call SuppressFinalize 20 | public void Dispose() 21 | #pragma warning restore CA1816 // Dispose methods should call SuppressFinalize 22 | { 23 | orphans.ForEach(n => n.Free()); 24 | orphans.Clear(); 25 | } 26 | 27 | [Before] 28 | public void SetupSuite() 29 | { 30 | AssertString("Suite Before()").IsEqual("Suite Before()"); 31 | orphans.Add(new Node()); 32 | } 33 | 34 | [After] 35 | public void TearDownSuite() 36 | => AssertString("Suite After()").IsEqual("Suite After()"); 37 | 38 | [BeforeTest] 39 | public void SetupTest() 40 | { 41 | AssertString("Suite BeforeTest()").IsEqual("Suite BeforeTest()"); 42 | orphans.Add(new Node()); 43 | orphans.Add(new Node()); 44 | } 45 | 46 | [AfterTest] 47 | public void TearDownTest() 48 | => AssertString("Suite AfterTest()").IsEqual("Suite AfterTest()"); 49 | 50 | [TestCase] 51 | public void TestCase1() 52 | { 53 | orphans.Add(new Node()); 54 | orphans.Add(new Node()); 55 | orphans.Add(new Node()); 56 | AssertString("TestCase1").IsEqual("TestCase1"); 57 | } 58 | 59 | [TestCase] 60 | public void TestCase2() 61 | { 62 | orphans.Add(new Node()); 63 | orphans.Add(new Node()); 64 | orphans.Add(new Node()); 65 | orphans.Add(new Node()); 66 | AssertString("TestCase2").IsEmpty(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Api/src/core/attributes/TraitAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | 10 | /// 11 | /// Specifies a trait for a test method or class, allowing tests to be categorized and filtered. 12 | /// 13 | /// 14 | /// Traits can be used to categorize tests by various criteria such as category, priority, or area. 15 | /// Multiple traits can be applied to the same test method or class. 16 | /// Traits are useful for selectively running tests based on their characteristics. 17 | /// 18 | /// 19 | /// 20 | /// [TestSuite] 21 | /// [Trait("Category", "Integration")] 22 | /// public class DatabaseTests 23 | /// { 24 | /// [Test] 25 | /// [Trait("Priority", "High")] 26 | /// [Trait("Feature", "Authentication")] 27 | /// public void UserLogin_ValidCredentials_Succeeds() 28 | /// { 29 | /// // Test implementation 30 | /// } 31 | /// } 32 | /// 33 | /// 34 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] 35 | public sealed class TraitAttribute : Attribute 36 | { 37 | /// 38 | /// Initializes a new instance of the class. 39 | /// 40 | /// The name of the trait. 41 | /// The value of the trait. 42 | public TraitAttribute(string name, string value) 43 | { 44 | Name = name ?? throw new ArgumentNullException(nameof(name)); 45 | Value = value ?? throw new ArgumentNullException(nameof(value)); 46 | } 47 | 48 | /// 49 | /// Gets the name of the trait. 50 | /// 51 | public string Name { get; } 52 | 53 | /// 54 | /// Gets the value of the trait. 55 | /// 56 | public string Value { get; } 57 | } 58 | -------------------------------------------------------------------------------- /Analyzers.Test/src/ExampleTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Analyzers.Test; 2 | 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Reflection; 6 | 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | 9 | [TestClass] 10 | [SuppressMessage("Style", "IDE0060", Justification = "Required for DynamicData test method pattern")] 11 | public class ExampleTest 12 | { 13 | // Display name provider method 14 | public static string GetDisplayName(MethodInfo methodInfo, object[] data) 15 | #pragma warning disable CA1062 16 | => $"{methodInfo.Name} with {data[0]} + {data[1]} = {data[2]}"; 17 | #pragma warning restore CA1062 18 | 19 | [TestMethod] 20 | public void SingeTest() 21 | { 22 | } 23 | 24 | [TestMethod("Test with display name")] 25 | public void SingeTestNamed() 26 | { 27 | } 28 | 29 | [TestMethod] 30 | [DataRow(1, 2, DisplayName = "DataRow1")] 31 | [DataRow(2, 2)] 32 | [DataRow(3, 2)] 33 | public void DataRowTest(int a, int b) 34 | { 35 | } 36 | 37 | [TestMethod] 38 | [DynamicData(nameof(TestDataProvider.GetTestData), typeof(TestDataProvider), DynamicDataSourceType.Method)] 39 | public void TestWithDynamicData(int a, int b, int expected) 40 | { 41 | } 42 | 43 | [TestMethod] 44 | [DynamicData(nameof(TestDataProvider.GetTestData), typeof(TestDataProvider), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetDisplayName))] 45 | public void TestWithDisplayName(int a, int b, int expected) 46 | { 47 | } 48 | 49 | [TestMethod] 50 | [DynamicData(nameof(TestDataProvider.GetTestData), typeof(TestDataProvider), DynamicDataSourceType.Method)] 51 | public void TestWithDisplayName2(int a, int b, int expected) 52 | { 53 | } 54 | 55 | #pragma warning disable CA1812 56 | private sealed class TestDataProvider 57 | { 58 | public static IEnumerable GetTestData() 59 | { 60 | yield return [1, 2, 3]; 61 | yield return [5, 5, 10]; 62 | yield return [-1, 1, 0]; 63 | } 64 | } 65 | #pragma warning restore CA1812 66 | } 67 | -------------------------------------------------------------------------------- /Api.Test/src/core/resources/testsuites/mono/TestSuiteAbortOnTestTimeout.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Resources; 2 | 3 | using System.Threading.Tasks; 4 | 5 | using static Assertions; 6 | using static Utils; 7 | 8 | // will be ignored because of missing `[TestSuite]` annotation 9 | // used by executor integration test 10 | // [TestSuite] 11 | public class TestSuiteAbortOnTestTimeout 12 | { 13 | 14 | [Before] 15 | public async Task Before() 16 | => await DoWait(500); 17 | 18 | [After] 19 | public void After() 20 | { } 21 | 22 | [BeforeTest] 23 | public void BeforeTest() 24 | { } 25 | 26 | [AfterTest] 27 | public void AfterTest() 28 | { } 29 | 30 | [TestCase(Timeout = 1000, Description = "This test will be interrupted after a timeout of 1000ms.")] 31 | public async Task TestCase1() 32 | { 33 | AssertBool(true).IsEqual(true); 34 | // wait 1500ms to enforce an test interrupt by a timeout 35 | var elapsedMilliseconds = await DoWait(1500); 36 | AssertBool(true).OverrideFailureMessage($"Expected this test is interrupted after 1000ms but is runs {elapsedMilliseconds}ms").IsFalse(); 37 | } 38 | 39 | [TestCase(Timeout = 1000, Description = "This test will end with a failure and no timeout.")] 40 | public async Task TestCase2() 41 | { 42 | var elapsedMilliseconds = await DoWait(500); 43 | AssertBool(true).IsEqual(false); 44 | } 45 | 46 | [TestCase(Timeout = 1000, Description = "This test will end with a success and no timeout.")] 47 | public async Task TestCase3() 48 | { 49 | var elapsedMilliseconds = await DoWait(500); 50 | AssertBool(true).IsEqual(true); 51 | } 52 | 53 | [TestCase(Timeout = 1000, Description = "This test has a invalid signature and should be end with a failure.")] 54 | #pragma warning disable CS1998 55 | public async void TestCase4() 56 | => AssertBool(true).IsEqual(true); 57 | #pragma warning restore CS1998 58 | 59 | [TestCase(Description = "This test has no timeout definition and expect to end with success.")] 60 | public void TestCase5() 61 | => AssertBool(true).IsEqual(true); 62 | } 63 | -------------------------------------------------------------------------------- /Api/src/core/attributes/GodotExceptionMonitorAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | 10 | /// 11 | /// Indicates that a test method or class should monitor exceptions that occur during Godot's main thread execution. 12 | /// This attribute enables monitoring of exceptions that are caught by CSharpInstanceBridge.Call, which handles 13 | /// exceptions from Godot callbacks and scene tree processing. 14 | /// 15 | /// 16 | /// This attribute is particularly useful for: 17 | /// 18 | ///
  • Testing exception handling in Godot node callbacks (_Ready, _Process, etc.)
  • 19 | ///
  • Verifying exceptions during scene tree operations
  • 20 | ///
  • Monitoring exceptions that would normally be caught silently by Godot's bridge
  • 21 | ///
    22 | /// Example usage for a test method: 23 | /// 24 | /// [TestCase] 25 | /// [GodotExceptionMonitor] 26 | /// public void TestNodeThrowsInReady() 27 | /// { 28 | /// // This will capture the exception thrown in the node's _Ready method 29 | /// var node = new MyNode(); 30 | /// AddChild(node); 31 | /// } 32 | /// 33 | /// Example usage for a test class: 34 | /// 35 | /// [TestSuite] 36 | /// [GodotExceptionMonitor] 37 | /// public class MyNodeTests 38 | /// { 39 | /// // All test methods in this class will monitor Godot exceptions 40 | /// [TestCase] 41 | /// public void TestSceneProcessing() 42 | /// { 43 | /// // Exceptions during scene processing will be monitored 44 | /// var scene = SceneLoader.Load("res://my_scene.tscn"); 45 | /// } 46 | /// } 47 | /// 48 | ///
    49 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] 50 | #pragma warning disable CA1813 51 | public class GodotExceptionMonitorAttribute : Attribute 52 | #pragma warning restore CA1813 53 | { 54 | } 55 | -------------------------------------------------------------------------------- /Api/src/api/ITestEvent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Api; 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | /// 10 | /// Defines the core identification properties of a test event. 11 | /// 12 | public interface ITestEvent 13 | { 14 | /// 15 | /// Gets the type of test event. 16 | /// 17 | EventType Type { get; } 18 | 19 | /// 20 | /// Gets the unique identifier for this test event. 21 | /// 22 | Guid Id { get; } 23 | 24 | /// 25 | /// Gets the full qualified test name, used for console logging. 26 | /// 27 | string FullyQualifiedName { get; } 28 | 29 | /// 30 | /// Gets the test display name. Used for data-driven test e.g., DataPointAttribute. 31 | /// 32 | string? DisplayName { get; } 33 | 34 | /// 35 | /// Gets a value indicating whether the test failed. 36 | /// 37 | bool IsFailed { get; } 38 | 39 | /// 40 | /// Gets a value indicating whether the test encountered an error. 41 | /// 42 | bool IsError { get; } 43 | 44 | /// 45 | /// Gets a value indicating whether the test completed successfully. 46 | /// 47 | bool IsSuccess { get; } 48 | 49 | /// 50 | /// Gets a value indicating whether the test produced warnings. 51 | /// 52 | bool IsWarning { get; } 53 | 54 | /// 55 | /// Gets a value indicating whether the test was skipped. 56 | /// 57 | bool IsSkipped { get; } 58 | 59 | /// 60 | /// Gets the elapsed time of the test execution. 61 | /// 62 | TimeSpan ElapsedInMs { get; } 63 | 64 | /// 65 | /// Gets the collection of reports associated with the test event. 66 | /// Each report provides details about various aspects of the test's execution. 67 | /// 68 | ICollection Reports { get; } 69 | } 70 | -------------------------------------------------------------------------------- /TestAdapter/ReleaseNotes.txt: -------------------------------------------------------------------------------- 1 | v3.0.1 2 | 3 | ✨ Improvements 4 | * GD-311: Increase default compile timeout `CompileProcessTimeout` to 2 minutes. 5 | Added prefix `[GdUnit4]` to the logged messages. 6 | 7 | v3.0.0 8 | * GD-27: Added VSTest filter support with test categories and traits 9 | * GD-138: Added capture test case execution stdout to the test report if `CaptureStdOut` is enabled 10 | * GD-160: Apply runsettings environment variables to the test execution context 11 | 12 | ## ⚙️ Configuration: 13 | 14 | New `.runsettings` options: 15 | ```xml 16 | 17 | 18 | true 19 | --verbose --headless 20 | FullyQualifiedName 21 | 30000 22 | 23 | 24 | ``` 25 | -------------------------------------------------------------------------- 26 | 27 | v2.1.0 28 | Improvements: 29 | * GD-138: Add capture test case execution stdout to the test report if `CaptureStdOut` is enabled 30 | * GD-141: Code cleanup and formatting 31 | 32 | -------------------------------------------------------------------------- 33 | 34 | v2.0.0 35 | - Changed test event sending via std:out to using IPC 36 | - Enable debugging in Rider 2024.2 without using hacks 37 | 'gdUnit4.test.adapter' v2.0.0 requires 'gdUnit4.api' v4.3.0 38 | 39 | -------------------------------------------------------------------------- 40 | 41 | v1.1.2 42 | Bug Fixes: 43 | - Fixes the test adapter gdUnit4.api version resolving 44 | - Fixing Debug tests gets stuck when using --verbose 45 | 46 | -------------------------------------------------------------------------- 47 | 48 | v1.1.1 49 | 50 | Bug Fixes: 51 | - Fixes space in folder name prevents tests from running 52 | 53 | -------------------------------------------------------------------------- 54 | 55 | v1.1.0 56 | 57 | Bug Fixes: 58 | - Fix parameterized tests are incorrect grouped 59 | - Fix DisplayName resolving 60 | 61 | Other Changes: 62 | - Add example to show usage of gdUnit4.test.adapter 63 | - Support both net7 and net8 at once 64 | 65 | -------------------------------------------------------------------------- 66 | 67 | v1.0.0 68 | 69 | - Initial version 70 | -------------------------------------------------------------------------------- /Api.Test/src/core/discovery/TestCaseDescriptorTest.cs: -------------------------------------------------------------------------------- 1 | namespace GdUnit4.Tests.Core.Discovery; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | using GdUnit4.Core.Discovery; 7 | 8 | using static Assertions; 9 | 10 | [TestSuite] 11 | public class TestCaseDescriptorTest 12 | { 13 | [TestCase] 14 | public void IsEqual() 15 | { 16 | var guid = Guid.NewGuid(); 17 | var dsA = new TestCaseDescriptor 18 | { 19 | SimpleName = "TestA", 20 | FullyQualifiedName = "GdUnit4.Tests.Core.Discovery.ExampleTestSuiteToDiscover.TestA", 21 | AssemblyPath = "/path/to/test_assembly.dll", 22 | ManagedType = "GdUnit4.Tests.Core.Discovery.ExampleTestSuiteToDiscover", 23 | ManagedMethod = "SingleTestCaseWithCustomName", 24 | Id = guid, 25 | LineNumber = 29, 26 | CodeFilePath = "d:/projectX/tests/core/discovery/ExampleTestSuiteToDiscover.cs", 27 | AttributeIndex = 0, 28 | RequireRunningGodotEngine = false, 29 | Categories = new List 30 | { 31 | "CategoryA", 32 | "Foo" 33 | }, 34 | Traits = new Dictionary> { ["Category"] = ["Foo"] } 35 | }; 36 | 37 | var dsB = new TestCaseDescriptor 38 | { 39 | SimpleName = "TestA", 40 | FullyQualifiedName = "GdUnit4.Tests.Core.Discovery.ExampleTestSuiteToDiscover.TestA", 41 | AssemblyPath = "/path/to/test_assembly.dll", 42 | ManagedType = "GdUnit4.Tests.Core.Discovery.ExampleTestSuiteToDiscover", 43 | ManagedMethod = "SingleTestCaseWithCustomName", 44 | Id = guid, 45 | LineNumber = 29, 46 | CodeFilePath = "d:/projectX/tests/core/discovery/ExampleTestSuiteToDiscover.cs", 47 | AttributeIndex = 0, 48 | RequireRunningGodotEngine = false, 49 | Categories = new List 50 | { 51 | "CategoryA", 52 | "Foo" 53 | }, 54 | Traits = new Dictionary> { ["Category"] = ["Foo"] } 55 | }; 56 | 57 | AssertBool(dsA.Equals(dsB)).IsTrue(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Api/src/core/attributes/TestStageAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | 10 | /// 11 | /// Base attribute class for all test stage attributes in the GdUnit4 testing framework. 12 | /// TestStageAttribute serves as the foundation for more specific test-related attributes 13 | /// that mark different phases or types of test execution. 14 | /// 15 | /// 16 | /// This is an abstract base class not intended for direct use. Instead, use derived attributes 17 | /// such as TestCaseAttribute, BeforeTestAttribute, AfterTestAttribute, etc. 18 | /// All derived attributes inherit the ability to specify a description and timeout. 19 | /// 20 | [AttributeUsage(AttributeTargets.Delegate)] 21 | public abstract class TestStageAttribute : Attribute 22 | { 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The name of the test stage, typically derived from the method name. 27 | /// The source code line number where the attribute is applied. 28 | protected TestStageAttribute(string name, int line) 29 | { 30 | Name = name; 31 | Line = line; 32 | } 33 | 34 | /// 35 | /// Gets or sets describe the intention of the test, will be shown as a tool tip on the inspector node. 36 | /// 37 | public string Description { get; set; } = string.Empty; 38 | 39 | /// 40 | /// Gets or sets the timeout in ms to interrupt the test if the test execution takes longer as the given value. 41 | /// 42 | public long Timeout { get; set; } = -1; 43 | 44 | /// 45 | /// Gets or sets the test name. 46 | /// 47 | internal string Name { get; set; } 48 | 49 | /// 50 | /// Gets or sets the line of the annotated method. 51 | /// 52 | internal int Line { get; set; } 53 | } 54 | -------------------------------------------------------------------------------- /Api/src/core/attributes/FuzzerAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | // ReSharper disable once CheckNamespace 5 | // Need to be placed in the root namespace to be accessible by the test runner. 6 | namespace GdUnit4; 7 | 8 | using System; 9 | using System.Collections.Generic; 10 | 11 | /// 12 | /// [PROTOTYPE] Attribute for generating test values through fuzzing techniques. 13 | /// This is an early prototype and is not fully implemented yet. 14 | /// 15 | /// 16 | /// This prototype attribute is intended to eventually support fuzzing, which is a testing technique 17 | /// that automatically generates semi-random data as inputs to a program to discover edge cases. 18 | /// Currently, provides a minimal implementation that only increments an initial value. 19 | /// 20 | /// 21 | /// 22 | /// // Example of intended future usage - not fully functional yet 23 | /// [TestCase(Iterations = 40)] 24 | /// public void IsBetween([Fuzzer(-20)] int value) 25 | /// => AssertThat(value).IsBetween(-20, 20); 26 | /// 27 | /// 28 | [AttributeUsage(AttributeTargets.Parameter)] 29 | public sealed class FuzzerAttribute : Attribute, IValueProvider 30 | { 31 | /// 32 | /// Initializes a new instance of the class with a starting value. 33 | /// 34 | /// The initial value for the fuzzer prototype. 35 | public FuzzerAttribute(int value) => Value = value; 36 | 37 | /// 38 | /// Gets the current value of the fuzzer. 39 | /// 40 | public int Value { get; private set; } 41 | 42 | /// 43 | /// Gets a sequence of test values to be used for the annotated parameter. 44 | /// This is a prototype implementation that simply increments the initial value. 45 | /// Future versions will implement more sophisticated fuzzing algorithms. 46 | /// 47 | /// An enumerable sequence containing the incremented value. 48 | public IEnumerable GetValues() 49 | { 50 | Value += 1; 51 | yield return Value; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Api.Test/GdUnit4ApiTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5.0.0 4 | © 2025 Mike Schulze 5 | Mike Schulze 6 | net9.0 7 | enable 8 | true 9 | GdUnit4.Tests 10 | 11 | 12 | false 13 | GdUnit4ApiTest 14 | true 15 | true 16 | true 17 | true 18 | 19 | NU1605,CS8785 20 | 21 | none 22 | GdUnit4 23 | $(DefineConstants);GDUNIT4NET_API_V5 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Api/src/asserts/IExceptionAssert.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Mike Schulze 2 | // MIT License - See LICENSE file in the repository root for full license text 3 | 4 | namespace GdUnit4.Asserts; 5 | 6 | /// 7 | /// An Assertion Tool to verify exceptions. 8 | /// 9 | public interface IExceptionAssert : IAssert 10 | { 11 | /// 12 | /// Verifies the exception message is equal to the expected one. 13 | /// 14 | /// The expected exception message. 15 | /// IExceptionAssert. 16 | IExceptionAssert HasMessage(string expected); 17 | 18 | /// 19 | /// Verifies that the exception message starts with the given value. 20 | /// 21 | /// A string with which the exception message must begin. 22 | /// IExceptionAssert. 23 | IExceptionAssert StartsWithMessage(string value); 24 | 25 | /// 26 | /// Verifies that the exception message starts with the given value. 27 | /// 28 | /// The exception Type. 29 | /// IExceptionAssert. 30 | IExceptionAssert IsInstanceOf(); 31 | 32 | /// 33 | /// Verifies the exception is thrown at the expected file line number. 34 | /// 35 | /// The line number the exception is thrown. 36 | /// IExceptionAssert. 37 | IExceptionAssert HasFileLineNumber(int lineNumber); 38 | 39 | /// 40 | /// Verifies the exception is thrown in the expected file name. 41 | /// 42 | /// The file name where the exception is thrown. 43 | /// IExceptionAssert. 44 | IExceptionAssert HasFileName(string fileName); 45 | 46 | /// 47 | /// Verifies that the exception has the expected property value. 48 | /// 49 | /// The property name. 50 | /// The expected value of the property. 51 | /// IExceptionAssert. 52 | IExceptionAssert HasPropertyValue(string propertyName, object expected); 53 | } 54 | -------------------------------------------------------------------------------- /TestAdapter.Test/test/resources/project.godot2: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="gdUnit4" 14 | config/tags=PackedStringArray("addon", "godot4", "testing") 15 | config/features=PackedStringArray("4.3", "C#") 16 | config/icon="res://icon.png" 17 | 18 | [audio] 19 | 20 | default_bus_layout="" 21 | 22 | [debug] 23 | 24 | file_logging/log_path="res://godot_session.log" 25 | gdscript/warnings/exclude_addons=false 26 | gdscript/warnings/untyped_declaration=2 27 | gdscript/warnings/unsafe_property_access=1 28 | gdscript/warnings/unsafe_method_access=1 29 | gdscript/warnings/unsafe_cast=1 30 | gdscript/warnings/unsafe_call_argument=1 31 | 32 | [dotnet] 33 | 34 | project/assembly_name="gdUnit4" 35 | 36 | [editor_plugins] 37 | 38 | enabled=PackedStringArray("res://addons/gdUnit4/plugin.cfg") 39 | 40 | [gdunit4] 41 | 42 | ui/inspector/node_collapse=false 43 | ui/toolbar/run_overall=true 44 | ui/inspector/tree_sort_mode=1 45 | report/godot/script_error=false 46 | settings/test/flaky_check_enable=true 47 | settings/common/update_notification_enabled=false 48 | 49 | [importer_defaults] 50 | 51 | texture={ 52 | "compress/channel_pack": 0, 53 | "compress/hdr_compression": 1, 54 | "compress/high_quality": false, 55 | "compress/lossy_quality": 0.7, 56 | "compress/mode": 0, 57 | "compress/normal_map": 0, 58 | "detect_3d/compress_to": 1, 59 | "editor/convert_colors_with_editor_theme": true, 60 | "editor/scale_with_editor_scale": true, 61 | "mipmaps/generate": false, 62 | "mipmaps/limit": -1, 63 | "process/fix_alpha_border": true, 64 | "process/hdr_as_srgb": false, 65 | "process/hdr_clamp_exposure": false, 66 | "process/normal_map_invert_y": false, 67 | "process/premult_alpha": false, 68 | "process/size_limit": 0, 69 | "roughness/mode": 0, 70 | "roughness/src_normal": "", 71 | "svg/scale": 1.0 72 | } 73 | 74 | [network] 75 | 76 | limits/debugger_stdout/max_chars_per_second=60048 77 | limits/debugger_stdout/max_messages_per_frame=100 78 | limits/debugger_stdout/max_errors_per_second=1000 79 | limits/debugger_stdout/max_warnings_per_second=1000 80 | 81 | [rendering] 82 | 83 | environment/default_environment="res://default_env.tres" 84 | --------------------------------------------------------------------------------