Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GD-129: Enable CSharp test support - Initial Commit #210

Merged
merged 13 commits into from
Apr 3, 2022
Merged
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: CI GdUnit3 on Godot 3.2.x
name: CI GdUnit3 on Godot Mono 3.3.x
on: [push]

jobs:
testing:
strategy:
matrix:
godot: [3.2.3]
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
Expand All @@ -14,20 +14,25 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2
with:
lfs: true
- name: Setup
shell: bash
run: echo "GODOT_BIN=/usr/local/bin/godot" >> $GITHUB_ENV

- 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: 5
timeout-minutes: 7
env:
GODOT_BIN: "/usr/local/bin/godot"
shell: bash
run: ./runtest.sh --selftest

- name: Collect Test Report
if: always()
- name: Collect Test Reports
uses: actions/upload-artifact@v2
with:
name: Report_${{ matrix.godot }}
Expand Down
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")
Loading