Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gutconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

"include_subdirs": true,
"dirs": [
"res://test/unit/",
"res://test/integration/"
"res://test/samples/cs/"
],
"should_exit": true,
"log_level": 3,
Expand Down
8 changes: 8 additions & 0 deletions Gut.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Godot.NET.Sdk/4.3.0">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
</Project>
19 changes: 19 additions & 0 deletions Gut.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gut", "Gut.csproj", "{AED1B5D9-B5A4-4898-9EE6-55B809D51881}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AED1B5D9-B5A4-4898-9EE6-55B809D51881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AED1B5D9-B5A4-4898-9EE6-55B809D51881}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AED1B5D9-B5A4-4898-9EE6-55B809D51881}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{AED1B5D9-B5A4-4898-9EE6-55B809D51881}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{AED1B5D9-B5A4-4898-9EE6-55B809D51881}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{AED1B5D9-B5A4-4898-9EE6-55B809D51881}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal
68 changes: 68 additions & 0 deletions addons/gut/CSharpScriptInspector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Godot;
using System;
using System.Reflection;

// namespace GutCSharp
// {
/// <summary>
/// Utility class to handle inspection of C# scripts for the GUT testing framework.
/// This provides reflection-based methods that help GUT understand C# class inheritance.
/// </summary>
public partial class CSharpScriptInspector : RefCounted
{
/// <summary>
/// Checks if a C# type inherits from GutTest
/// </summary>
/// <param name="type">The Type to check</param>
/// <returns>True if the type inherits from GutTest</returns>
private bool TypeInheritsFromGutTest(Type type)
{
if (type == null)
return false;

// If this is the GutTest class itself (base case for successful inheritance)
if (type.FullName == "GutCSharp.GutTest")
{
GD.Print("FULL NAME: ", type.FullName);
return true;
}

// Check the base type
return TypeInheritsFromGutTest(type.BaseType);
}

/// <summary>
/// Determines if a CSharpScript inherits from GutTest
/// </summary>
/// <param name="script">The CSharpScript to check</param>
/// <returns>True if the script inherits from GutTest</returns>
public bool InheritsFromTest(CSharpScript script)
{

if (script == null)
return false;
Variant instance = script.New();
var tryCast = instance.As<GutTest>();

return tryCast != null;
}

/// <summary>
/// Determines if an object instance inherits from GutTest
/// </summary>
/// <param name="instance">The object instance to check</param>
/// <returns>True if the object inherits from GutTest</returns>
public bool InheritsFromTest(object instance)
{

GD.Print("here");


if (instance == null)
return false;


return TypeInheritsFromGutTest(instance.GetType());
}
}
// }
67 changes: 67 additions & 0 deletions addons/gut/GutRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Godot;
using System;
using System.Reflection;
using System.Collections.Generic;

namespace GutCSharp
{
public partial class GutCSharpRunner : RefCounted
{
private Node _gut;

public GutCSharpRunner(Node gut)
{
_gut = gut;
}

public void RunTests(string scriptPath)
{
// Load the C# script/class
var script = GD.Load<CSharpScript>(scriptPath);
if (script == null) return;

var instance = script.New().As<GutTest>();
if (instance == null) return;

instance.gut = _gut;

// Get all methods from the instance
Type type = instance.GetType();
MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);

// Filter for test methods (prefixed with "Test")
List<MethodInfo> testMethods = new List<MethodInfo>();
foreach (var method in methods)
{
if (method.Name.StartsWith("Test") && method.GetParameters().Length == 0)
{
testMethods.Add(method);
}
}

// Run lifecycle and test methods
try
{
instance.BeforeAll();

foreach (var testMethod in testMethods)
{
string testName = testMethod.Name;
_gut.Call("_pre_run_test", testName);

instance.BeforeEach();
testMethod.Invoke(instance, null);
instance.AfterEach();

_gut.Call("_post_run_test");
}

instance.AfterAll();
}
catch (Exception e)
{
_gut.Call("_fail", "Error in test: " + e.Message);
}
}
}
}
162 changes: 162 additions & 0 deletions addons/gut/GutTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using Godot;
using System;
using System.Reflection;

// namespace GutCSharp
// {
public partial class GutTest : Godot.Node
{
// Public property that proxies to _gdGutTest
public Node gut
{
get
{
if (_gdGutTest != null)
{
return (Node)_gdGutTest.Get("gut");
}
return null;
}
set
{
if (_gdGutTest != null)
{
_gdGutTest.Set("gut", value);
}
}
}

// Reference to GDScript GutTest instance that will handle the actual implementation
private Node _gdGutTest;

// Flag to track if ready was called
public bool _was_ready_called
{
get
{
if (_gdGutTest != null)
{
return (bool)_gdGutTest.Get("_was_ready_called");
}
return false;
}
}

// Constructor - creates the GDScript GutTest instance
public GutTest()
{
// Load the GDScript GutTest scene/script
var gdTestScript = GD.Load<Script>("res://addons/gut/test.gd");
_gdGutTest = new Node();
_gdGutTest.SetScript(gdTestScript);
}

// Override _Ready to setup the GDScript instance
public override void _Ready()
{
base._Ready();
// Add the GDScript instance as a child so it gets properly initialized
AddChild(_gdGutTest);
}

// Lifecycle methods - these are virtual and meant to be overridden
public virtual void BeforeAll() { }
public void before_all() {
BeforeAll();
}

public virtual void AfterAll() { }
public void after_all() {
AfterAll();
}

public virtual void BeforeEach() { }
public void before_each() {
BeforeEach();
}

public virtual void AfterEach() { }
public void after_each() {
AfterEach();
// Call GDScript implementation
if (_gdGutTest != null)
{
_gdGutTest.Call("after_each");
}
}

// Assertion methods - proxied to GDScript implementation when possible
public void AssertEq(object a, object b, string text = "")
{
// Using direct implementation instead of proxying due to type conversion issues
if (a.Equals(b))
gut.Call("_pass", $"Expecting {a} to equal {b}. {text}");
else
gut.Call("_fail", $"Expecting {a} to equal {b}. {text}");
}

public void AssertNe(object a, object b, string text = "")
{
// Using direct implementation instead of proxying due to type conversion issues
if (!a.Equals(b))
gut.Call("_pass", $"Expecting {a} to not equal {b}. {text}");
else
gut.Call("_fail", $"Expecting {a} to not equal {b}. {text}");
}

// Virtual method that can be overridden
public virtual bool should_skip_script() {
return false;
}

// Proxy methods to GDScript implementation
public void set_logger(RefCounted logger) {
if (_gdGutTest != null)
{
_gdGutTest.Call("set_logger", logger);
}
}

public Variant get_assert_count() {
if (_gdGutTest != null)
{
return _gdGutTest.Call("get_assert_count");
}
return 0;
}

public void _do_ready_stuff() {
if (_gdGutTest != null)
{
_gdGutTest.Call("_do_ready_stuff");
}
}

public void clear_signal_watcher() {
if (_gdGutTest != null)
{
_gdGutTest.Call("clear_signal_watcher");
}
}

// Additional methods to proxy common GutTest functionality

// Example of how to add more assertion methods:
public void AssertTrue(bool condition, string text = "")
{
if (condition)
gut.Call("_pass", $"Expecting true. {text}");
else
gut.Call("_fail", $"Expecting true. {text}");
}

public void AssertFalse(bool condition, string text = "")
{
if (!condition)
gut.Call("_pass", $"Expecting false. {text}");
else
gut.Call("_fail", $"Expecting false. {text}");
}

}
//}
16 changes: 15 additions & 1 deletion addons/gut/gut.gd
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,16 @@ func _test_the_scripts(indexes=[]):
_setup_script(test_script, coll_script)
_doubler.set_strategy(_double_strategy)

# Skip based on indexes if needed
if(indexes != [] and !indexes.has(test_indexes)):
continue

# C# specific handling
# if test_script.resource_path.ends_with(".cs"):
# var runner = load("res://addons/gut/GutCSharpRunner.cs").new(self)
# runner.RunTests(test_script.path)
# continue

# ----
# SHORTCIRCUIT
# skip_script logic
Expand Down Expand Up @@ -924,7 +934,9 @@ func _get_files(path, prefix, suffix):
# MUST use FileAccess since d.file_exists returns false for exported
# projects
if(FileAccess.file_exists(full_path)):
if(fs_item.begins_with(prefix) and fs_item.ends_with(suffix)):
# Add support for C# files
if((fs_item.begins_with(prefix) and fs_item.ends_with(suffix)) or
(fs_item.begins_with("Test") and fs_item.ends_with(".cs"))):
files.append(full_path)
# MUST use DirAccess, d.dir_exists is false for exported projects.
elif(include_subdirectories and DirAccess.dir_exists_absolute(full_path)):
Expand Down Expand Up @@ -1026,6 +1038,7 @@ func add_directory(path, prefix=_file_prefix, suffix=".gd"):
# check for '' b/c the calls to addin the exported directories 1-6 will pass
# '' if the field has not been populated. This will cause res:// to be
# processed which will include all files if include_subdirectories is true.
print('path: ', path)
if(path == '' or path == null):
return

Expand All @@ -1034,6 +1047,7 @@ func add_directory(path, prefix=_file_prefix, suffix=".gd"):
_lgr.error(str('The path [', path, '] does not exist.'))
else:
var files = _get_files(path, prefix, suffix)
print(files)
for i in range(files.size()):
if(_script_name == null or _script_name == '' or \
(_script_name != null and files[i].findn(_script_name) != -1)):
Expand Down
Loading