├── .gitignore
├── image.png
├── docs
└── assets
│ └── image.png
├── .gitattributes
├── addons
└── GDMUT
│ ├── plugin.cfg
│ ├── TestFunction.cs
│ ├── TestResult.tscn
│ ├── GDMUT.cs
│ ├── Result.cs
│ ├── MethodResult.tscn
│ ├── MethodResult.cs
│ ├── TestResult.cs
│ ├── Dock.tscn
│ ├── TestLoader.cs
│ └── Dock.cs
├── Gdmut.csproj.old
├── .editorconfig
├── project.godot
├── Gdmut.csproj
├── icon.svg
├── icon.svg.import
├── LICENSE
├── Gdmut.sln
├── TestClass.cs
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # Godot 4+ specific ignores
2 | .godot/
3 |
4 | *.import
--------------------------------------------------------------------------------
/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Spycemyster/GDMUT/HEAD/image.png
--------------------------------------------------------------------------------
/docs/assets/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Spycemyster/GDMUT/HEAD/docs/assets/image.png
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Normalize EOL for all files that Git considers text files.
2 | * text=auto eol=lf
3 |
--------------------------------------------------------------------------------
/addons/GDMUT/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="GDMUT"
4 | description="A C# Unit Testing framework for Godot."
5 | author="Spyce"
6 | version="1.0.0"
7 | script="GDMUT.cs"
8 |
--------------------------------------------------------------------------------
/Gdmut.csproj.old:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 | true
5 |
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 | dotnet_diagnostic.SA1200.severity = None
3 | dotnet_diagnostic.SA1309.severity = None
4 | dotnet_diagnostic.SA1502.severity = None
5 | dotnet_diagnostic.SA1633.severity = None
6 | dotnet_diagnostic.SA1623.severity = None
7 | dotnet_diagnostic.SA1011.severity = None
8 | dotnet_diagnostic.SA1504.severity = None
9 | dotnet_diagnostic.SA1516.severity = None
10 | dotnet_diagnostic.SA1310.severity = None
11 | dotnet_diagnostic.SA1111.severity = None
12 | dotnet_diagnostic.SA1101.severity = None
13 | dotnet_diagnostic.SA1009.severity = None
14 | dotnet_diagnostic.SA1000.severity = None
15 | dotnet_diagnostic.S2583.severity = None
16 |
--------------------------------------------------------------------------------
/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="Gdmut"
14 | config/features=PackedStringArray("4.2", "C#", "GL Compatibility")
15 | config/icon="res://icon.svg"
16 |
17 | [dotnet]
18 |
19 | project/assembly_name="Gdmut"
20 |
21 | [editor_plugins]
22 |
23 | enabled=PackedStringArray("res://addons/GDMUT/plugin.cfg")
24 |
25 | [rendering]
26 |
27 | renderer/rendering_method="gl_compatibility"
28 | renderer/rendering_method.mobile="gl_compatibility"
29 |
--------------------------------------------------------------------------------
/Gdmut.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 | true
5 |
6 |
7 |
8 | runtime; build; native; contentfiles; analyzers; buildtransitive
9 | all
10 |
11 |
12 | runtime; build; native; contentfiles; analyzers; buildtransitive
13 | all
14 |
15 |
16 |
--------------------------------------------------------------------------------
/addons/GDMUT/TestFunction.cs:
--------------------------------------------------------------------------------
1 | #if TOOLS
2 | using System;
3 | using System.Reflection;
4 |
5 | namespace GdMUT;
6 |
7 | ///
8 | /// A single test function.
9 | ///
10 | public class TestFunction
11 | {
12 | ///
13 | /// The name of the function.
14 | ///
15 | public string Name { get; set; }
16 |
17 | ///
18 | /// The type of the function.
19 | ///
20 | public Type Type { get; set; }
21 |
22 | ///
23 | /// The method info of the function.
24 | ///
25 | public MethodInfo Method { get; set; }
26 |
27 | ///
28 | /// The result of the test function.
29 | ///
30 | public Result Result { get; set; }
31 | }
32 | #endif
33 |
--------------------------------------------------------------------------------
/addons/GDMUT/TestResult.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=2 format=3 uid="uid://dstbxq6v3bavo"]
2 |
3 | [ext_resource type="Script" path="res://addons/GDMUT/TestResult.cs" id="1_oamte"]
4 |
5 | [node name="TestResult" type="VBoxContainer" node_paths=PackedStringArray("_typeName", "_methodList")]
6 | anchors_preset = 15
7 | anchor_right = 1.0
8 | anchor_bottom = 1.0
9 | offset_right = -3.0
10 | offset_bottom = 1.0
11 | grow_horizontal = 2
12 | grow_vertical = 2
13 | script = ExtResource("1_oamte")
14 | _typeName = NodePath("Type")
15 | _methodList = NodePath("MethodList")
16 |
17 | [node name="Type" type="RichTextLabel" parent="."]
18 | layout_mode = 2
19 | bbcode_enabled = true
20 | text = "Type name"
21 | fit_content = true
22 |
23 | [node name="MethodList" type="VBoxContainer" parent="."]
24 | layout_mode = 2
25 | size_flags_vertical = 3
26 |
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/addons/GDMUT/GDMUT.cs:
--------------------------------------------------------------------------------
1 | #if TOOLS
2 | using Godot;
3 |
4 | namespace GdMUT;
5 |
6 | ///
7 | /// Entry point for the plugin.
8 | ///
9 | [Tool]
10 | public partial class GDMUT : EditorPlugin
11 | {
12 | private const string DOCK_SCENE = "res://addons/GDMUT/Dock.tscn";
13 | private Control _dock;
14 |
15 | ///
16 | /// Called when the node enters the scene tree for the first time.
17 | ///
18 | public override void _EnterTree()
19 | {
20 | base._EnterTree();
21 | _dock = GD.Load(DOCK_SCENE).Instantiate();
22 | AddControlToDock(DockSlot.RightUl, _dock);
23 | GD.Print("Successfully loaded GDMUT");
24 | }
25 |
26 | ///
27 | /// Called when the node exits the scene tree.
28 | ///
29 | public override void _ExitTree()
30 | {
31 | base._ExitTree();
32 | RemoveControlFromDocks(_dock);
33 | _dock?.Free();
34 | }
35 | }
36 | #endif
37 |
--------------------------------------------------------------------------------
/icon.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://c1m8130cydwt2"
6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://icon.svg"
14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Spencer (spycemyster) Chang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
--------------------------------------------------------------------------------
/Gdmut.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio 2012
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gdmut", "Gdmut.csproj", "{0E38E110-E39E-4756-9486-112909B18D8F}"
4 | EndProject
5 | Global
6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
7 | Debug|Any CPU = Debug|Any CPU
8 | ExportDebug|Any CPU = ExportDebug|Any CPU
9 | ExportRelease|Any CPU = ExportRelease|Any CPU
10 | EndGlobalSection
11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
12 | {0E38E110-E39E-4756-9486-112909B18D8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
13 | {0E38E110-E39E-4756-9486-112909B18D8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
14 | {0E38E110-E39E-4756-9486-112909B18D8F}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
15 | {0E38E110-E39E-4756-9486-112909B18D8F}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
16 | {0E38E110-E39E-4756-9486-112909B18D8F}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
17 | {0E38E110-E39E-4756-9486-112909B18D8F}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
18 | EndGlobalSection
19 | EndGlobal
20 |
--------------------------------------------------------------------------------
/addons/GDMUT/Result.cs:
--------------------------------------------------------------------------------
1 | #if TOOLS
2 | using System;
3 |
4 | namespace GdMUT;
5 |
6 | ///
7 | /// An objecting denoting the result of an operation.
8 | ///
9 | public struct Result
10 | {
11 | ///
12 | /// A successful result.
13 | ///
14 | public static readonly Result Success = new(true, string.Empty);
15 |
16 | ///
17 | /// A failed result.
18 | ///
19 | public static readonly Result Failure = new(false, string.Empty);
20 |
21 | ///
22 | /// Initializes a new instance of the struct.
23 | ///
24 | /// Whether the operation was a success.
25 | /// Custom message.
26 | public Result(bool success, string message = "")
27 | {
28 | IsSuccess = success;
29 | Message = message;
30 | }
31 |
32 | ///
33 | /// Whether the operation was a success or not.
34 | ///
35 | public bool IsSuccess { get; set; }
36 |
37 | ///
38 | /// The message of the result.
39 | ///
40 | public string Message { get; set; }
41 | }
42 | #endif
43 |
--------------------------------------------------------------------------------
/addons/GDMUT/MethodResult.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=2 format=3 uid="uid://d1f6c4yvl5r2c"]
2 |
3 | [ext_resource type="Script" path="res://addons/GDMUT/MethodResult.cs" id="1_pc067"]
4 |
5 | [node name="MethodResult" type="PanelContainer" node_paths=PackedStringArray("_methodName", "_result")]
6 | anchors_preset = 15
7 | anchor_right = 1.0
8 | anchor_bottom = 1.0
9 | grow_horizontal = 2
10 | grow_vertical = 2
11 | size_flags_horizontal = 3
12 | size_flags_vertical = 3
13 | script = ExtResource("1_pc067")
14 | _methodName = NodePath("VBoxContainer/MarginContainer/Name")
15 | _result = NodePath("VBoxContainer/MarginContainer2/Result")
16 |
17 | [node name="VBoxContainer" type="VBoxContainer" parent="."]
18 | layout_mode = 2
19 |
20 | [node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
21 | layout_mode = 2
22 |
23 | [node name="Name" type="RichTextLabel" parent="VBoxContainer/MarginContainer"]
24 | layout_mode = 2
25 | bbcode_enabled = true
26 | text = "Method Name"
27 | fit_content = true
28 |
29 | [node name="MarginContainer2" type="MarginContainer" parent="VBoxContainer"]
30 | layout_mode = 2
31 |
32 | [node name="Result" type="RichTextLabel" parent="VBoxContainer/MarginContainer2"]
33 | layout_mode = 2
34 | bbcode_enabled = true
35 | text = "Result"
36 | fit_content = true
37 |
--------------------------------------------------------------------------------
/TestClass.cs:
--------------------------------------------------------------------------------
1 | namespace GdMUT;
2 |
3 | ///
4 | /// This is a test class for GDMUT. This is purely for demonstration. If you added
5 | /// this into your project, feel free to delete it =).
6 | ///
7 | public static class TestClass
8 | {
9 | #if TOOLS
10 | ///
11 | /// An example of a result that will pass.
12 | ///
13 | /// The result.
14 | [CSTestFunction]
15 | public static Result ExamplePass()
16 | {
17 | int x = 0;
18 | x *= 100;
19 | return (x == 0) ? Result.Success : Result.Failure;
20 | }
21 |
22 | ///
23 | /// An example of a result that will fail.
24 | ///
25 | /// The result.
26 | [CSTestFunction]
27 | public static Result ExampleFail()
28 | {
29 | int x = 0;
30 | x *= 100;
31 | return (x != 0) ? Result.Success : Result.Failure;
32 | }
33 |
34 | ///
35 | /// An example of a result that will fail with a custom message.
36 | ///
37 | /// The result.
38 | [CSTestFunction]
39 | public static Result ExampleCustomFail()
40 | {
41 | int x = 0;
42 | x *= 100;
43 | return (x != 0)
44 | ? Result.Success
45 | : new Result(false, "You can't multiply 0 and expect anything else than 0!");
46 | }
47 |
48 | ///
49 | /// An example of a result that will pass with a custom message.
50 | ///
51 | /// The result.
52 | [CSTestFunction]
53 | public static Result ExampleCustomSuccess()
54 | {
55 | int x = 0;
56 | x *= 100;
57 | return (x == 0) ? new Result(true, "Proved that 0 * 100 = 0") : Result.Failure;
58 | }
59 | #endif
60 | }
61 |
--------------------------------------------------------------------------------
/addons/GDMUT/MethodResult.cs:
--------------------------------------------------------------------------------
1 | #if TOOLS
2 | using Godot;
3 |
4 | namespace GdMUT.Components;
5 |
6 | ///
7 | /// A single test result.
8 | ///
9 | [Tool]
10 | public partial class MethodResult : Control
11 | {
12 | [Export]
13 | private RichTextLabel _methodName;
14 |
15 | [Export]
16 | private RichTextLabel _result;
17 | private TestFunction _function;
18 |
19 | ///
20 | /// Called when the node enters the scene tree for the first time.
21 | ///
22 | public override void _EnterTree()
23 | {
24 | base._EnterTree();
25 | }
26 |
27 | ///
28 | /// Sets the method result to display.
29 | ///
30 | /// The test function.
31 | public void SetMethodResult(TestFunction function)
32 | {
33 | _function = function;
34 | _methodName.Text = function.Method.Name;
35 | Reset();
36 | }
37 |
38 | ///
39 | /// Updates the result of the test function.
40 | ///
41 | public void Update()
42 | {
43 | SetSuccess(_function.Result.IsSuccess, _function.Result.Message);
44 | }
45 |
46 | ///
47 | /// Resets the result to the default state.
48 | ///
49 | public void Reset()
50 | {
51 | _result.Text = string.Empty;
52 | SelfModulate = new Color(1, 1, 1);
53 | }
54 |
55 | ///
56 | /// Sets the status of the test function.
57 | ///
58 | /// Whether the test was a success or not.
59 | /// The result string.
60 | public void SetSuccess(bool isSuccess, string result = "")
61 | {
62 | _result.Text = (isSuccess ? "Success: " : "Failure: ") + result;
63 | Modulate = isSuccess ? new Color(0, 1, 0) : new Color(1, 0, 0);
64 | GD.Print($"{result} {isSuccess}");
65 | }
66 | }
67 | #endif
68 |
--------------------------------------------------------------------------------
/addons/GDMUT/TestResult.cs:
--------------------------------------------------------------------------------
1 | #if TOOLS
2 | using System.Collections.Generic;
3 | using Godot;
4 |
5 | namespace GdMUT.Components;
6 |
7 | ///
8 | /// A test result object within the test result list.
9 | ///
10 | [Tool]
11 | public partial class TestResult : Control
12 | {
13 | private const string TYPE_NAME_FORMAT = "[b][font_size=24][center]{0}[/center][/font_size][/b]";
14 | private const string METHOD_RESULT_SCENE = "res://addons/GDMUT/MethodResult.tscn";
15 |
16 | [Export]
17 | private RichTextLabel _typeName;
18 |
19 | [Export]
20 | private VBoxContainer _methodList;
21 |
22 | private List<(MethodResult, TestFunction)> _functions = new();
23 | private string _typeNameStr;
24 |
25 | ///
26 | public override void _EnterTree()
27 | {
28 | base._EnterTree();
29 | foreach (Node child in _methodList.GetChildren())
30 | {
31 | child.QueueFree();
32 | }
33 |
34 | _functions.Clear();
35 | }
36 |
37 | ///
38 | /// Set the type name of the test result.
39 | ///
40 | /// Type name.
41 | public void SetTypeName(string typeName)
42 | {
43 | _typeNameStr = typeName;
44 | _typeName.Text = string.Format(TYPE_NAME_FORMAT, typeName);
45 | }
46 |
47 | ///
48 | /// Update the test result.
49 | ///
50 | public void UpdateResult()
51 | {
52 | int numSuccess = 0;
53 | foreach (var (methodResult, function) in _functions)
54 | {
55 | methodResult.Update();
56 | numSuccess += function.Result.IsSuccess ? 1 : 0;
57 | }
58 |
59 | _typeName.Text = string.Format(
60 | TYPE_NAME_FORMAT,
61 | _typeNameStr + $" ({numSuccess}/{_functions.Count})"
62 | );
63 | if (numSuccess == _functions.Count)
64 | {
65 | _typeName.Modulate = new Color(0, 1, 0);
66 | }
67 | else if (numSuccess == 0)
68 | {
69 | _typeName.Modulate = new Color(1, 0, 0);
70 | }
71 | else
72 | {
73 | _typeName.Modulate = new Color(1, 0.9f, 0);
74 | }
75 | }
76 |
77 | ///
78 | /// Adds a method result to the test result.
79 | ///
80 | /// The test function.
81 | public void AddMethodResult(TestFunction function)
82 | {
83 | var methodResultScene = GD.Load(METHOD_RESULT_SCENE);
84 | var methodResult = methodResultScene.Instantiate();
85 | methodResult.SetMethodResult(function);
86 | _methodList.AddChild(methodResult);
87 | _functions.Add((methodResult, function));
88 | }
89 | }
90 | #endif
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GDMUT
2 |
3 | A unit testing framework for C# scripts in Godot. See the Godot Asset Page Here: https://godotengine.org/asset-library/asset/2100
4 |
5 | ## Features
6 | ### Blazingly Fast 🚀
7 | Optimized and multithreaded to allow you to run your tests at blazingly fast speeds! 🔥🚀
8 |
9 | ### Visual
10 | A minimal and simple user-interface that displays all the unit tests and results all in a single dock.
11 |
12 | 
13 |
14 | ### Filtering
15 | Sometimes you might just want to run a certain set of tests. That's now possible with the filter option. Include a filter to match whatever tests you need to run.
16 |
17 | ### Simple To Use
18 | Minimal overhead added to create unit tests. Declare a unit test function by adding a ```[CSTestFunction]``` onto a static, parameterless function with a ```Result``` return type. After that, you can load and run your tests effortlessly.
19 |
20 | ## How to Use
21 | 1. Press the build button before enabling the plugin. This is to allow the plugin to build the necessary files to run.
22 | 2. Ensure the plugin is enabled in the Godot editor.
23 | 3. In a script, write a test function. Test functions must be static, parameterless, be prepended with the ```[CSTestFunction]``` attribute, and return a ```Result``` type, indicating it's success or failure.
24 | - If an exception is thrown in your function, it will be counted as a fail.
25 | 4. Open the "C# Testing" ui on the editor (it should be on the right by default).
26 | 5. Click "Load Tests". This should populate the dock with a list of all your test functions and the types that they reside in.
27 | 6. Click on "Run Tests" to run each of the tests.
28 |
29 | ## Example Test Functions
30 | ```c#
31 | using GdMUT;
32 |
33 | ///
34 | /// This is a test class for GDMUT. This is purely for demonstration. If you added
35 | /// this into your project, feel free to delete it =)
36 | ///
37 | public class TestClass
38 | {
39 | #if TOOLS
40 | [CSTestFunction]
41 | public static Result ExamplePass()
42 | {
43 | int x = 0;
44 | x *= 100;
45 | return (x == 0) ? Result.Success : Result.Failure;
46 | }
47 |
48 | [CSTestFunction]
49 | public static Result ExampleFail()
50 | {
51 | int x = 0;
52 | x *= 100;
53 | return (x != 0) ? Result.Success : Result.Failure;
54 | }
55 |
56 | [CSTestFunction]
57 | public static Result ExampleCustomFail()
58 | {
59 | int x = 0;
60 | x *= 100;
61 | return (x != 0)
62 | ? Result.Success
63 | : new Result(false, "You can't multiply 0 and expect anything else than 0!");
64 | }
65 |
66 | [CSTestFunction]
67 | public static Result ExampleCustomSuccess()
68 | {
69 | int x = 0;
70 | x *= 100;
71 | return (x == 0) ? new Result(true, "Proved that 0 * 100 = 0") : Result.Failure;
72 | }
73 | #endif
74 | }
75 | ```
76 | NOTE: The '``#region``' and '``#if TOOLS``' preprocessor directives are optional. Just good practice :)
77 |
78 | ## Planned Features
79 | - ~~Multithreaded tests~~
80 | - ~~Test filtering (filter by name which tests to run)~~
81 | - Parameterized functions
82 | - Async functions
83 |
--------------------------------------------------------------------------------
/addons/GDMUT/Dock.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=2 format=3 uid="uid://ce83kbl7xjwpr"]
2 |
3 | [ext_resource type="Script" path="res://addons/GDMUT/Dock.cs" id="1_dxr4g"]
4 |
5 | [node name="C# Testing" type="ScrollContainer" node_paths=PackedStringArray("_filter", "_multithreadedEnabled", "_numThreads", "_runTests", "_loadTests", "_testList")]
6 | anchors_preset = 15
7 | anchor_right = 1.0
8 | anchor_bottom = 1.0
9 | offset_right = -835.0
10 | grow_horizontal = 2
11 | grow_vertical = 2
12 | script = ExtResource("1_dxr4g")
13 | _filter = NodePath("VBoxContainer/HBoxContainer/Filter")
14 | _multithreadedEnabled = NodePath("VBoxContainer/HBoxContainer2/Multithreading")
15 | _numThreads = NodePath("VBoxContainer/HBoxContainer2/HBoxContainer/Threads")
16 | _runTests = NodePath("VBoxContainer/Run")
17 | _loadTests = NodePath("VBoxContainer/Load")
18 | _testList = NodePath("VBoxContainer/TestList")
19 |
20 | [node name="VBoxContainer" type="VBoxContainer" parent="."]
21 | layout_mode = 2
22 | size_flags_horizontal = 3
23 | size_flags_vertical = 3
24 |
25 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
26 | layout_mode = 2
27 |
28 | [node name="RichTextLabel" type="RichTextLabel" parent="VBoxContainer/HBoxContainer"]
29 | custom_minimum_size = Vector2(50.585, 0)
30 | layout_mode = 2
31 | size_flags_vertical = 4
32 | tooltip_text = "Matches test with the inputted string. Filters out any tests that don't match name.
33 | For example: Loading tests with \"Filtered\" as the filter text could load these potential test names \"Filtered_Test\", \"FilteredTest\", \"Filtered\", \"ExampleFilteredTest\". But would filter out \"Test1\", \"ExampleTest\", \"Filter\""
34 | bbcode_enabled = true
35 | text = "[center]Filter[/center]"
36 | fit_content = true
37 |
38 | [node name="Filter" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
39 | layout_mode = 2
40 | size_flags_horizontal = 3
41 |
42 | [node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer"]
43 | layout_mode = 2
44 |
45 | [node name="Multithreading" type="CheckBox" parent="VBoxContainer/HBoxContainer2"]
46 | layout_mode = 2
47 | size_flags_horizontal = 3
48 | tooltip_text = "Enables tests to be run in parallel. Not recommended for low number of tests."
49 | text = "Multithreaded"
50 |
51 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer2"]
52 | layout_mode = 2
53 |
54 | [node name="RichTextLabel" type="RichTextLabel" parent="VBoxContainer/HBoxContainer2/HBoxContainer"]
55 | custom_minimum_size = Vector2(73.315, 0)
56 | layout_mode = 2
57 | size_flags_vertical = 4
58 | tooltip_text = "Number of threads to use if multithreading is enabled. If not a valid number, then it will run singlethreaded."
59 | bbcode_enabled = true
60 | text = "Threads"
61 | fit_content = true
62 |
63 | [node name="Threads" type="LineEdit" parent="VBoxContainer/HBoxContainer2/HBoxContainer"]
64 | layout_mode = 2
65 | size_flags_horizontal = 0
66 | size_flags_vertical = 4
67 | text = "4"
68 | placeholder_text = "1"
69 | alignment = 1
70 |
71 | [node name="Load" type="Button" parent="VBoxContainer"]
72 | layout_mode = 2
73 | text = "Load Tests"
74 |
75 | [node name="Run" type="Button" parent="VBoxContainer"]
76 | layout_mode = 2
77 | text = "Run"
78 |
79 | [node name="RichTextLabel" type="RichTextLabel" parent="VBoxContainer"]
80 | layout_mode = 2
81 | bbcode_enabled = true
82 | text = "[font_size=32][center]Tests[/center][/font_size]"
83 | fit_content = true
84 |
85 | [node name="TestList" type="VBoxContainer" parent="VBoxContainer"]
86 | layout_mode = 2
87 | size_flags_vertical = 3
88 |
--------------------------------------------------------------------------------
/addons/GDMUT/TestLoader.cs:
--------------------------------------------------------------------------------
1 | #if TOOLS
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Reflection;
5 | using Godot;
6 |
7 | namespace GdMUT;
8 |
9 | ///
10 | /// Utility class for loading test functions.
11 | ///
12 | public static class TestLoader
13 | {
14 | ///
15 | /// Search for all tests in the project.
16 | ///
17 | /// List of found test functions.
18 | public static List SearchForAllTests()
19 | {
20 | List tests = new();
21 |
22 | // get all functions with MonoTestFunctionAttribute
23 | ReadOnlySpan assemblies = AppDomain.CurrentDomain.GetAssemblies();
24 | for (int assemblyIndex = 0; assemblyIndex < assemblies.Length; assemblyIndex++)
25 | {
26 | Assembly assembly = assemblies[assemblyIndex];
27 | if (
28 | assembly.FullName.StartsWith("System.")
29 | || assembly.FullName.Equals("System")
30 | || assembly.FullName.StartsWith("Microsoft.")
31 | || assembly.FullName.StartsWith("GodotSharp")
32 | || assembly.FullName.StartsWith("GodotTools")
33 | || assembly.FullName.StartsWith("GodotPlugins")
34 | || assembly.FullName.StartsWith("JetBrains")
35 | || assembly.FullName.Equals("netstandard")
36 | )
37 | {
38 | continue;
39 | }
40 |
41 | GD.Print($"Loading tests from {assembly.FullName}");
42 | LoadFunctionsFromAssembly(tests, assembly);
43 | }
44 |
45 | return tests;
46 | }
47 |
48 | private static void LoadFunctionsFromAssembly(List tests, Assembly assembly)
49 | {
50 | ReadOnlySpan types = assembly.GetTypes();
51 | for (int typeIndex = 0; typeIndex < types.Length; typeIndex++)
52 | {
53 | LoadFunctionsFromType(tests, types[typeIndex]);
54 | }
55 | }
56 |
57 | private static void LoadFunctionsFromType(List tests, Type type)
58 | {
59 | ReadOnlySpan methods = type.GetMethods();
60 | foreach (var method in methods)
61 | {
62 | var attribute = method.GetCustomAttributes(typeof(CSTestFunctionAttribute), false);
63 |
64 | if (attribute.Length > 0)
65 | {
66 | if (method.ReturnType != typeof(Result))
67 | {
68 | GD.PushError(
69 | $"Method {method.Name} in {method.DeclaringType} does not return Result. Skipping it..."
70 | );
71 | continue;
72 | }
73 | else if (!method.IsStatic)
74 | {
75 | GD.PushError(
76 | $"Method {method.Name} in {method.DeclaringType} is not static. Skipping it..."
77 | );
78 | continue;
79 | }
80 |
81 | tests.Add(
82 | new TestFunction()
83 | {
84 | Name = method.Name,
85 | Type = method.DeclaringType,
86 | Method = method,
87 | }
88 | );
89 | }
90 | }
91 | }
92 | }
93 |
94 | ///
95 | /// Attribute for marking a method as a test function.
96 | ///
97 | [AttributeUsage(AttributeTargets.Method)]
98 | public class CSTestFunctionAttribute : Attribute
99 | {
100 | ///
101 | /// Initializes a new instance of the class.
102 | ///
103 | public CSTestFunctionAttribute() { }
104 | }
105 | #endif
106 |
--------------------------------------------------------------------------------
/addons/GDMUT/Dock.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Spencer (Spycemyster) Chang, LLC. All Rights Reserved.
2 | // Licensed under the MIT License. See LICENSE in the project root for license information.
3 | namespace GdMUT.Components;
4 |
5 | using Godot;
6 | using System;
7 | using System.Diagnostics;
8 | using System.Threading;
9 |
10 | #if TOOLS
11 | ///
12 | /// The dock UI that contains all the test results and the controls to run tests.
13 | ///
14 | [Tool]
15 | public partial class Dock : Control
16 | {
17 | private const string TEST_RESULT_SCENE = "res://addons/GDMUT/TestResult.tscn";
18 | private readonly System.Collections.Generic.Dictionary<
19 | Type,
20 | System.Collections.Generic.List
21 | > _testDictionary = new();
22 | private readonly System.Collections.Generic.Dictionary _testResultDictionary =
23 | new();
24 |
25 | [Export]
26 | private LineEdit _filter;
27 |
28 | [Export]
29 | private CheckBox _multithreadedEnabled;
30 |
31 | [Export]
32 | private LineEdit _numThreads;
33 |
34 | [Export]
35 | private Button _runTests;
36 |
37 | [Export]
38 | private Button _loadTests;
39 |
40 | [Export]
41 | private VBoxContainer _testList;
42 |
43 | private System.Collections.Generic.List _tests = new();
44 |
45 | ///
46 | /// Called when the node enters the scene tree for the first time.
47 | ///
48 | public override void _EnterTree()
49 | {
50 | base._EnterTree();
51 | _runTests.Pressed += RunTests;
52 | _loadTests.Pressed += LoadTests;
53 | }
54 |
55 | private void LoadTests()
56 | {
57 | var stopwatch = new Stopwatch();
58 | stopwatch.Start();
59 | foreach (Node node in _testList.GetChildren())
60 | {
61 | node.QueueFree();
62 | }
63 |
64 | _tests?.Clear();
65 | _tests = TestLoader.SearchForAllTests();
66 | _testDictionary.Clear();
67 | for (int testIndex = 0; testIndex < _tests.Count; testIndex++)
68 | {
69 | TestFunction function = _tests[testIndex];
70 | if (!function.Name.Contains(_filter.Text))
71 | {
72 | continue;
73 | }
74 |
75 | if (
76 | _testDictionary.TryGetValue(
77 | function.Type,
78 | out System.Collections.Generic.List testList
79 | )
80 | )
81 | {
82 | testList.Add(function);
83 | }
84 | else
85 | {
86 | _testDictionary.Add(
87 | function.Type,
88 | new System.Collections.Generic.List() { function }
89 | );
90 | }
91 | }
92 |
93 | _testResultDictionary.Clear();
94 | var testResultScene = GD.Load(TEST_RESULT_SCENE);
95 | foreach (Type type in _testDictionary.Keys)
96 | {
97 | var functions = _testDictionary[type];
98 | var testResult = testResultScene.Instantiate();
99 | testResult.SetTypeName(type.Name);
100 | _testList.AddChild(testResult);
101 | _testResultDictionary.Add(type, testResult);
102 | foreach (TestFunction function in functions)
103 | {
104 | testResult.AddMethodResult(function);
105 | }
106 | }
107 |
108 | stopwatch.Stop();
109 | GD.Print($"Loading tests took {stopwatch.ElapsedMilliseconds}ms");
110 | }
111 |
112 | private void RunTestsInRange(int startIndex, int endIndex)
113 | {
114 | for (int testIndex = startIndex; testIndex < endIndex; testIndex++)
115 | {
116 | var test = _tests[testIndex];
117 | GD.Print(test.Name);
118 | Result testResult;
119 | try
120 | {
121 | testResult = (Result)test.Method.Invoke(null, null);
122 | }
123 | catch (Exception e)
124 | {
125 | testResult = new Result(false, $"Exception thrown: {e.Message}");
126 | }
127 |
128 | test.Result = testResult;
129 | }
130 | }
131 |
132 | private void RunTests()
133 | {
134 | if (_tests.Count == 0)
135 | {
136 | GD.Print("No tests loaded");
137 | return;
138 | }
139 |
140 | var stopwatch = new Stopwatch();
141 | stopwatch.Start();
142 |
143 | if (
144 | _multithreadedEnabled.ButtonPressed
145 | && int.TryParse(_numThreads.Text, out int numThreads)
146 | && numThreads > 0
147 | )
148 | {
149 | GD.Print("Run Tests multithreaded");
150 | Thread[] threads = new Thread[numThreads];
151 | int testsPerThread =
152 | (_tests.Count / numThreads) + (_tests.Count % numThreads > 0 ? 1 : 0);
153 | for (int threadIndex = 0; threadIndex < numThreads; threadIndex++)
154 | {
155 | int startIndex = threadIndex * testsPerThread;
156 | int endIndex = Math.Min((threadIndex + 1) * testsPerThread, _tests.Count);
157 | threads[threadIndex] = new Thread(() => RunTestsInRange(startIndex, endIndex));
158 | threads[threadIndex].Start();
159 | }
160 |
161 | foreach (Thread thread in threads)
162 | {
163 | thread.Join();
164 | }
165 | }
166 | else
167 | {
168 | GD.Print("Run Tests singlethreaded");
169 | RunTestsInRange(0, _tests.Count);
170 | }
171 |
172 | stopwatch.Stop();
173 | UpdateUIWithResults();
174 | GD.Print($"Tests took {stopwatch.ElapsedMilliseconds}ms");
175 | }
176 |
177 | private void UpdateUIWithResults()
178 | {
179 | foreach (TestResult result in _testResultDictionary.Values)
180 | {
181 | result.UpdateResult();
182 | }
183 | }
184 | }
185 | #endif
186 |
--------------------------------------------------------------------------------