Skip to content

Commit

Permalink
GD-129: Enable CSharp test support - Initial Commit (#210)
Browse files Browse the repository at this point in the history
* GD-129: Enable CSharp test support - Initial Commit

- Decoupling TestSuite and TestCase class from Godot class and mark it as internal
- introduce the new await functions
- addapt test coverage
- simplify/cleanup GdUnitRumner
- Fixed a number of timer issues that were causing print errors.
- Rework on test interruption handling and added a new awaiter class to prevent unreleased timers
- bump to version 2.0.0-beta
  • Loading branch information
MikeSchulze authored Apr 3, 2022
1 parent 39a596d commit ad8c17b
Show file tree
Hide file tree
Showing 142 changed files with 8,514 additions and 600 deletions.
39 changes: 39 additions & 0 deletions .github/workflows/selftest-3.3.x-mono.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI GdUnit3 on Godot Mono 3.3.x
on: [push]

jobs:
testing:
strategy:
matrix:
godot: [mono-3.3.3, mono-3.3.4, mono-3.4.1, mono-3.4.2]

name: GdUnit3 Selftest on Godot ${{ matrix.godot }}
runs-on: ubuntu-latest
container:
image: barichello/godot-ci:${{ matrix.godot }}

steps:
- name: Checkout
uses: actions/checkout@v2
with:
lfs: true

- name: Compile
run: |
nuget restore
mkdir -p .mono/assemblies/Debug
cp /usr/local/bin/GodotSharp/Api/Release/* .mono/assemblies/Debug
msbuild
- name: Run Selftes
timeout-minutes: 7
env:
GODOT_BIN: "/usr/local/bin/godot"
shell: bash
run: ./runtest.sh --selftest

- name: Collect Test Reports
uses: actions/upload-artifact@v2
with:
name: Report_${{ matrix.godot }}
path: reports/**
2 changes: 1 addition & 1 deletion .github/workflows/selftest-3.3.x.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
testing:
strategy:
matrix:
godot: [3.3, 3.3.1, 3.3.2, 3.3.3, 3.3.4, mono-3.3.3]
godot: [3.3.3, 3.3.4]

name: CI Godot ${{ matrix.godot }}
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/selftest-3.4.x.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
testing:
strategy:
matrix:
godot: [3.4]
godot: [3.4, 3.4.1, 3.4.2]

name: CI Godot ${{ matrix.godot }}
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ gdUnit3-examples/
# temporary generated files
GdUnitRunner.cfg
reports/

.fake
.ionide
34 changes: 23 additions & 11 deletions addons/gdUnit3/bin/GdUnitCmdTool.gd
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class CLIRunner extends Node:
const RETURN_ERROR = 100
const RETURN_WARNING = 101


var _state = INIT
var _signal_handler
var _test_suites_to_process :Array
Expand All @@ -24,6 +25,7 @@ class CLIRunner extends Node:
var _report_max: int = DEFAULT_REPORT_COUNT
var _runner_config := GdUnitRunnerConfig.new()
var _console := CmdConsole.new()
var _cs_executor

var _cmd_options: = CmdOptions.new([
CmdOption.new("-a, --add", "-a <directory|path of testsuite>", "Adds the given test suite or directory to the execution pipeline.", TYPE_STRING),
Expand All @@ -48,6 +50,11 @@ class CLIRunner extends Node:
_executor = GdUnitExecutor.new()
# stop on first test failure to fail fast
_executor.fail_fast(true)

if GdUnitTools.is_mono_supported():
_cs_executor = load("res://addons/gdUnit3/src/core/execution/Executor.cs").new()
_cs_executor.AddGdTestEventListener(self)

var err := _executor.connect("send_event", self, "_on_executor_event")
if err != OK:
push_error("Error on startup, can't connect executor for 'send_event'")
Expand All @@ -60,17 +67,18 @@ class CLIRunner extends Node:
gdUnitInit()
_state = RUN
RUN:
set_process(false)
# all test suites executed
if _test_suites_to_process.empty():
_state = STOP
else:
set_process(false)
# process next test suite
var test_suite := _test_suites_to_process.pop_front() as GdUnitTestSuite
var fs = _executor.execute(test_suite)
if fs is GDScriptFunctionState:
yield(fs, "completed")
set_process(true)
var test_suite := _test_suites_to_process.pop_front() as Node
add_child(test_suite)
var executor = _cs_executor if GdObjects.is_cs_test_suite(test_suite) else _executor
executor.Execute(test_suite)
yield(executor, "ExecutionCompleted")
set_process(true)
STOP:
_state = EXIT
_on_executor_event(GdUnitStop.new())
Expand Down Expand Up @@ -217,7 +225,7 @@ class CLIRunner extends Node:
for test_suite in test_suites:
skip_suite(test_suite, skipped)

func skip_suite(test_suite :GdUnitTestSuite, skipped :Dictionary) -> void:
func skip_suite(test_suite :Node, skipped :Dictionary) -> void:
var skipped_suites := skipped.keys()
if skipped_suites.empty():
return
Expand All @@ -233,7 +241,7 @@ class CLIRunner extends Node:
else:
# skip tests
for test_to_skip in skipped_tests:
var test_case :_TestCase = test_suite.find_node(test_to_skip, true, false)
var test_case :_TestCase = test_suite.get_test_case_by_name(test_to_skip)
if test_case:
test_case.skip(true)
else:
Expand All @@ -245,6 +253,9 @@ class CLIRunner extends Node:
total += (test_suite as Node).get_child_count()
return total

func PublishEvent(data) -> void:
_on_executor_event(GdUnitEvent.new().deserialize(data.AsDictionary()))

func _on_executor_event(event :GdUnitEvent):
match event.type():
GdUnitEvent.INIT:
Expand All @@ -260,21 +271,22 @@ class CLIRunner extends Node:
_report.add_testsuite_report(GdUnitTestSuiteReport.new(event.resource_path(), event.suite_name()))

GdUnitEvent.TESTSUITE_AFTER:
_report.update_test_suite_report(event.suite_name(), event.is_failed(), event.skipped_count(), event.orphan_nodes(), event.elapsed_time())
_report.update_test_suite_report(event.resource_path(), event.is_failed(), event.skipped_count(), event.orphan_nodes(), event.elapsed_time())

GdUnitEvent.TESTCASE_BEFORE:
_report.add_testcase_report(event.suite_name(), GdUnitTestCaseReport.new(event.test_name()))
_report.add_testcase_report(event.resource_path(), GdUnitTestCaseReport.new(event.resource_path(), event.test_name()))

GdUnitEvent.TESTCASE_AFTER:
var test_report := GdUnitTestCaseReport.new(
event.resource_path(),
event.test_name(),
event.is_error(),
event.is_failed(),
event.orphan_nodes(),
event.skipped_count(),
event.reports(),
event.elapsed_time())
_report.update_testcase_report(event.suite_name(), test_report)
_report.update_testcase_report(event.resource_path(), test_report)
print_status(event)

func report_exit_code(report :GdUnitHtmlReport) -> int:
Expand Down
2 changes: 1 addition & 1 deletion addons/gdUnit3/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="gdUnit3"
description="Unit Testing Framework for Godot Scripts"
author="Mike Schulze"
version="1.1.5"
version="2.0.0-beta"
script="plugin.gd"
160 changes: 160 additions & 0 deletions addons/gdUnit3/src/Assertions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace GdUnit3
{
using Asserts;

/// <summary>
/// A collection of assertions and helpers to verify values
/// </summary>
public sealed class Assertions
{
/// <summary>
/// An Assertion to verify boolean values
/// </summary>
/// <param name="current">The current boolean value to verify</param>
/// <returns>IBoolAssert</returns>
public static IBoolAssert AssertBool(bool current) => new BoolAssert(current);

/// <summary>
/// An Assertion to verify string values
/// </summary>
/// <param name="current">The current string value to verify</param>
/// <param name="expectResult"></param>
/// <returns></returns>
public static IStringAssert AssertString(string current) => new StringAssert(current);

/// <summary>
/// An Assertion to verify integer values
/// </summary>
/// <param name="current">The current integer value to verify</param>
/// <param name="expectResult"></param>
/// <returns></returns>
public static IIntAssert AssertInt(int current) => new IntAssert(current);

/// <summary>
/// An Assertion to verify double values
/// </summary>
/// <param name="current">The current double value to verify</param>
/// <param name="expectResult"></param>
/// <returns></returns>
public static IDoubleAssert AssertFloat(double current) => new DoubleAssert(current);

/// <summary>
/// An Assertion to verify object values
/// </summary>
/// <param name="current">The current double value to verify</param>
/// <param name="expectResult"></param>
/// <returns></returns>
public static IObjectAssert AssertObject(object current) => new ObjectAssert(current);

/// <summary>
/// An Assertion to verify array values
/// </summary>
/// <param name="current">The current array value to verify</param>
/// <param name="expectResult"></param>
/// <returns></returns>
public static IArrayAssert AssertArray(IEnumerable current) => new ArrayAssert(current);


public static IAssertBase<T> AssertThat<T>(T current)
{
if (typeof(string) == typeof(T))
return (IAssertBase<T>)AssertString(Convert.ToString(current));
if (typeof(bool) == typeof(T))
return (IAssertBase<T>)AssertBool(Convert.ToBoolean(current));
if (typeof(int) == typeof(T))
return (IAssertBase<T>)AssertInt(Convert.ToInt32(current));
if (typeof(double) == typeof(T))
return (IAssertBase<T>)AssertFloat(Convert.ToDouble(current));
if (typeof(IEnumerable) == typeof(T))
return (IAssertBase<T>)AssertArray(current as IEnumerable);
return (IAssertBase<T>)AssertObject(current as object);
}

public static IStringAssert AssertThat(string current) => AssertString(current);
public static IBoolAssert AssertThat(bool current) => AssertBool(current);
public static IIntAssert AssertThat(int current) => AssertInt(current);
public static IDoubleAssert AssertThat(double current) => AssertFloat(current);
public static IObjectAssert AssertThat(object current) => AssertObject(current);
public static IArrayAssert AssertThat(IEnumerable current) => AssertArray(current);


/// <summary>
/// An Assertion to verify for expecting exceptions
/// </summary>
/// <param name="supplier">A function callback where throw possible exceptions</param>
/// <returns>IExceptionAssert</returns>
public static IExceptionAssert AssertThrown<T>(Func<T> supplier) => new ExceptionAssert<T>(supplier);

/// <summary>
/// An Assertion to verify for expecting exceptions when performing a task.
/// <example>
/// <code>
/// await AssertThrown(task.WithTimeout(500))
/// .ContinueWith(result => result.Result.HasMessage("timed out after 500ms."));
/// </code>
/// </example>
/// </summary>
/// <param name="task">A task where throw possible exceptions</param>
/// <returns>a task of <c>IExceptionAssert</c> to await</returns>
public async static Task<IExceptionAssert> AssertThrown<T>(Task<T> task)
{
try
{
await task;
return default;
}
catch (Exception e)
{
return new ExceptionAssert<T>(e);
}
}

public async static Task<IExceptionAssert> AssertThrown(Task task)
{
try
{
await task;
return default;
}
catch (Exception e)
{
return new ExceptionAssert<object>(e);
}
}

/// ----------- Helpers -------------------------------------------------------------------------------------------------------

///<summary>
/// A litle helper to auto freeing your created objects after test execution
/// </summary>
public static T AutoFree<T>(T obj) => Executions.Monitors.MemoryPool.RegisterForAutoFree(obj);

/// <summary>
/// Buils a tuple by given values
/// </summary>
public static ITuple Tuple(params object[] args) => new GdUnit3.Asserts.Tuple(args);

/// <summary>
/// Builds an extractor by given method name and optional arguments
/// </summary>
public static IValueExtractor Extr(string methodName, params object[] args) => new ValueExtractor(methodName, args);

/// <summary>
/// A helper to return given enumerable as string representation
/// </summary>
public static string AaString(IEnumerable values)
{
var items = new List<string>();
foreach (var value in values)
{
items.Add(value != null ? value.ToString() : "Null");
}
return string.Join(", ", items);
}
}
}
47 changes: 47 additions & 0 deletions addons/gdUnit3/src/GdUnitAwaiter.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class_name GdUnitAwaiter
extends Reference


# Waits for a specified signal in an interval of 50ms sent from the <source>, and terminates with an error after the specified timeout has elapsed.
# source: the object from which the signal is emitted
# signal_name: signal name
# args: the expected signal arguments as an array
# timeout: the timeout in ms, default is set to 2000ms
static func await_signal_on(test_suite :WeakRef, source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> GDScriptFunctionState:
var line_number := GdUnitAssertImpl._get_line_number();
var awaiter = GdUnitSignalAwaiter.new(timeout_millis)
yield(awaiter.on_signal(source, signal_name, args), "completed")
if awaiter.is_interrupted():
var failure = "await_signal_on(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis]
GdUnitAssertImpl.new(test_suite.get_ref(), signal_name).report_error(failure, line_number)
return

# Waits for a specified signal sent from the <source> between idle frames and aborts with an error after the specified timeout has elapsed
# source: the object from which the signal is emitted
# signal_name: signal name
# args: the expected signal arguments as an array
# timeout: the timeout in ms, default is set to 2000ms
static func await_signal_idle_frames(test_suite :WeakRef, source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> GDScriptFunctionState:
var line_number := GdUnitAssertImpl._get_line_number();
var awaiter = GdUnitSignalAwaiter.new(timeout_millis, true)
yield(awaiter.on_signal(source, signal_name, args), "completed")
if awaiter.is_interrupted():
var failure = "await_signal_idle_frames(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis]
GdUnitAssertImpl.new(test_suite.get_ref(), signal_name).report_error(failure, line_number)
return

# Waits for for a given amount of milliseconds
# example:
# # waits for 100ms
# yield(GdUnitAwaiter.await_millis(myNode, 100), "completed")
# use this waiter and not `yield(get_tree().create_timer(), "timeout") to prevent errors when a test case is timed out
static func await_millis(parent: Node, milliSec :int) -> GDScriptFunctionState:
var timer :Timer = parent.auto_free(Timer.new())
parent.add_child(timer)
timer.set_one_shot(true)
timer.start(milliSec * 0.001)
return yield(timer, "timeout")

# Waits until the next idle frame
static func await_idle_frame() -> GDScriptFunctionState:
return yield(Engine.get_main_loop(), "idle_frame")
4 changes: 4 additions & 0 deletions addons/gdUnit3/src/GdUnitConstants.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class_name GdUnitConstants
extends Reference

const NO_ARG = "<--null-->"
Loading

0 comments on commit ad8c17b

Please sign in to comment.