Skip to content

Commit

Permalink
GD-129-5: improve GdUnitSceneRunner according to c# scenerunner (#224)
Browse files Browse the repository at this point in the history
* GD-129-5: improve GdUnitSceneRunner according to c# scenerunner

- 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
  • Loading branch information
MikeSchulze authored Apr 3, 2022
1 parent 560fe0a commit 7780af9
Show file tree
Hide file tree
Showing 31 changed files with 1,238 additions and 561 deletions.
37 changes: 37 additions & 0 deletions addons/gdUnit3/src/Assertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ public sealed class Assertions
/// <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>
Expand Down Expand Up @@ -90,6 +114,19 @@ public async static Task<IExceptionAssert> AssertThrown<T>(Task<T> task)
}
}

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

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

///<summary>
Expand Down
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-->"
138 changes: 138 additions & 0 deletions addons/gdUnit3/src/GdUnitSceneRunner.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
class_name GdUnitSceneRunner
extends Node

const NO_ARG = GdUnitConstants.NO_ARG

func simulate(frames: int, delta_peer_frame :float) -> GdUnitSceneRunner:
push_warning("DEPRECATED!: 'simulate(<frames>, <delta_peer_frame>)' is deprecated. Use 'simulate_frames(<frames>, <delta_milli>) instead.'")
return simulate_frames(frames, delta_peer_frame * 1000)

func wait_emit_signal(instance :Object, signal_name :String, args := [], timeout := 2000, expeced := GdUnitAssert.EXPECT_SUCCESS) -> GDScriptFunctionState:
push_warning("DEPRECATED!: 'wait_emit_signal(<instance>, <signal_name>, <timeout>)' is deprecated. Use 'await_signal_on(<source>, <signal_name>, <timeout>) instead.'")
return yield(await_signal_on(instance, signal_name, args, timeout), "completed")

func wait_func(source :Object, func_name :String, args := [], expeced := GdUnitAssert.EXPECT_SUCCESS) -> GdUnitFuncAssert:
push_warning("DEPRECATED!: 'wait_func(<source>, <func_name>, <args>)' is deprecated. Use 'await_func(<func_name>, <args>)' or 'await_func_on(<source>, <func_name>, <args>)' instead.")
return await_func_on(source, func_name, args, expeced)

# Sets the mouse cursor to given position relative to the viewport.
func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner:
return self

# Simulates that a key has been pressed
# key_code : the key code e.g. 'KEY_ENTER'
# shift : false by default set to true if simmulate shift is press
# control : false by default set to true if simmulate control is press
func simulate_key_pressed(key_code :int, shift :bool = false, control := false) -> GdUnitSceneRunner:
return self

# Simulates that a key is pressed
# key_code : the key code e.g. 'KEY_ENTER'
# shift : false by default set to true if simmulate shift is press
# control : false by default set to true if simmulate control is press
func simulate_key_press(key_code :int, shift :bool = false, control := false) -> GdUnitSceneRunner:
return self

# Simulates that a key has been released
# key_code : the key code e.g. 'KEY_ENTER'
# shift : false by default set to true if simmulate shift is press
# control : false by default set to true if simmulate control is press
func simulate_key_release(key_code :int, shift :bool = false, control := false) -> GdUnitSceneRunner:
return self

# Simulates a mouse moved to relative position by given speed
# relative: The mouse position relative to the previous position (position at the last frame).
# speed : The mouse speed in pixels per second.‚
func simulate_mouse_move(relative :Vector2, speed :Vector2 = Vector2.ONE) -> GdUnitSceneRunner:
return self

# Simulates a mouse button pressed
# buttonIndex: The mouse button identifier, one of the ButtonList button or button wheel constants.
func simulate_mouse_button_pressed(buttonIndex :int) -> GdUnitSceneRunner:
return self

# Simulates a mouse button press (holding)
# buttonIndex: The mouse button identifier, one of the ButtonList button or button wheel constants.
func simulate_mouse_button_press(buttonIndex :int) -> GdUnitSceneRunner:
return self

# Simulates a mouse button released
# buttonIndex: The mouse button identifier, one of the ButtonList button or button wheel constants.
func simulate_mouse_button_release(buttonIndex :int) -> GdUnitSceneRunner:
return self

# Sets how fast or slow the scene simulation is processed (clock ticks versus the real).
# It defaults to 1.0. A value of 2.0 means the game moves twice as fast as real life,
# whilst a value of 0.5 means the game moves at half the regular speed.
func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner:
return self

# Simulates scene processing for a certain number of frames
# frames: amount of frames to process
# delta_milli: the time delta between a frame in milliseconds
func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner:
return self

# Simulates scene processing until the given signal is emitted by the scene
# signal_name: the signal to stop the simulation
# arg..: optional signal arguments to be matched for stop
func simulate_until_signal(signal_name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG) -> GdUnitSceneRunner:
return self

# Simulates scene processing until the given signal is emitted by the given object
# source: the object that should emit the signal
# signal_name: the signal to stop the simulation
# arg..: optional signal arguments to be matched for stop
func simulate_until_object_signal(source :Object, signal_name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG) -> GdUnitSceneRunner:
return self

# Waits for the function return value until specified timeout or fails
# args : optional function arguments
func await_func(func_name :String, args := [], expeced := GdUnitAssert.EXPECT_SUCCESS) -> GdUnitFuncAssert:
return null

# Waits for the function return value of specified source until specified timeout or fails
# source: the object where implements the function
# args : optional function arguments
func await_func_on(source :Object, func_name :String, args := [], expeced := GdUnitAssert.EXPECT_SUCCESS) -> GdUnitFuncAssert:
return null

# Waits for given signal is emited by the scene until a specified timeout to fail
# signal_name: signal name
# args: the expected signal arguments as an array
# timeout: the timeout in ms, default is set to 2000ms
func await_signal(signal_name :String, args := [], timeout := 2000 ):
pass

# Waits for given signal is emited by the <source> until a specified timeout to fail
# 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
func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ):
pass

# maximizes the window to bring the scene visible
func maximize_view() -> GdUnitSceneRunner:
return self

# Return the current value of the property with the name <name>.
# name: name of property
# retuen: the value of the property
func get_property(name :String):
pass

# executes the function specified by <name> in the scene and returns the result
# name: the name of the function to execute
# optional function args 0..9
# return: the function result
func invoke(name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG):
pass

# Searches for the specified node with the name in the current scene and returns it, otherwise null
# name: the name of the node to find
# recursive: enables/disables seraching recursive
# return: the node if find otherwise null
func find_node(name :String, recursive :bool = true, owned :bool = false) -> Node:
return null

35 changes: 33 additions & 2 deletions addons/gdUnit3/src/GdUnitTestSuite.gd
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
class_name GdUnitTestSuite
extends Node

const NO_ARG = GdUnitConstants.NO_ARG

# This function is called before a test suite starts
# You can overwrite to prepare test data or initalizize necessary variables
func before() -> void:
Expand All @@ -38,9 +40,18 @@ func after_test() -> void:
func skip(skipped :bool) -> void:
set_meta("gd_skipped", skipped)

func is_failure() -> bool:
func is_failure(expected_failure :String = NO_ARG) -> bool:
return get_meta(GdUnitAssertImpl.GD_TEST_FAILURE) if has_meta(GdUnitAssertImpl.GD_TEST_FAILURE) else false

# Utility to check if a test has failed in a particular line and if there is an error message
func assert_failed_at(line_number :int, expected_failure :String) -> bool:
var is_failed = is_failure()
var last_failure = GdAssertReports.current_failure()
var last_failure_line = GdAssertReports.get_last_error_line_number()
assert_str(last_failure).is_equal(expected_failure)
assert_int(last_failure_line).is_equal(line_number)
return is_failed

func is_skipped() -> bool:
return get_meta("gd_skipped") if has_meta("gd_skipped") else false

Expand Down Expand Up @@ -98,9 +109,29 @@ func resource_as_var(resource_path :String):
func clear_push_errors() -> void:
GdUnitTools.clear_push_errors()

# Waits for given signal is emited by the <source> until a specified timeout to fail
# 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
func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout :int = 2000) -> GDScriptFunctionState:
return yield(GdUnitAwaiter.await_signal_on(weakref(self), source, signal_name, args, timeout), "completed")

# Waits until the next idle frame
func await_idle_frame() -> GDScriptFunctionState:
return yield(GdUnitAwaiter.await_idle_frame(), "completed")

# Waits for for a given amount of milliseconds
# example:
# # waits for 100ms
# yield(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
func await_millis(timeout :int) -> GDScriptFunctionState:
return yield(GdUnitAwaiter.await_millis(self, timeout), "completed")

# Creates a new scene runner to allow simulate interactions on a scene
func scene_runner(scene :Node, verbose := false) -> GdUnitSceneRunner:
return auto_free(GdUnitSceneRunner.new(weakref(self), scene, verbose))
return auto_free(GdUnitSceneRunnerImpl.new(weakref(self), scene, verbose))

# === Mocking & Spy ===========================================================

Expand Down
2 changes: 1 addition & 1 deletion addons/gdUnit3/src/GdUnitTuple.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
class_name GdUnitTuple
extends Reference

const NO_ARG = GdUnitConstants.NO_ARG

const NO_ARG = "<--null-->"
var __values :Array = Array()

func _init(arg0, arg1, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG):
Expand Down
Loading

0 comments on commit 7780af9

Please sign in to comment.