diff --git a/.github/workflows/beehave-ci.yml b/.github/workflows/beehave-ci.yml index 616a4e14..a5c9dacd 100644 --- a/.github/workflows/beehave-ci.yml +++ b/.github/workflows/beehave-ci.yml @@ -31,7 +31,7 @@ jobs: fail-fast: false max-parallel: 10 matrix: - godot-version: ['4.0.3', '4.1.1'] + godot-version: ['4.0.4', '4.1.3', '4.2'] name: "🤖 CI on Godot ${{ matrix.godot-version }}" uses: ./.github/workflows/unit-tests.yml diff --git a/README.md b/README.md index 3f87969a..06b23db8 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ In order to avoid bugs creeping into the codebase, every feature is covered by u 1. [Download Latest Release](https://github.com/bitbrain/beehave/releases/latest) - (optional) access latest build for [Godot 3.x](https://github.com/bitbrain/beehave/archive/refs/heads/godot-3.x.zip), [Godot 4.x](https://github.com/bitbrain/beehave/archive/refs/heads/godot-4.x.zip) -2. Unpack the `beehave` folder into your `/addons` folder within the Godot project +2. Unpack the `addons/beehave` folder into your `/addons` folder within the Godot project 3. Enable this addon within the Godot settings: `Project > Project Settings > Plugins` +4. Move `script_templates` into your project folder. To better understand what branch to choose from for which Godot version, please refer to this table: |Godot Version|Beehave Branch|Beehave Version| diff --git a/addons/beehave/blackboard.gd b/addons/beehave/blackboard.gd index 5655e89a..d710da9f 100644 --- a/addons/beehave/blackboard.gd +++ b/addons/beehave/blackboard.gd @@ -1,8 +1,9 @@ -## The blackboard is an object that can be used to store and access data between -## multiple nodes of the behavior tree. @icon("icons/blackboard.svg") class_name Blackboard extends Node +## The blackboard is an object that can be used to store and access data between +## multiple nodes of the behavior tree. + var blackboard: Dictionary = {} func keys() -> Array[String]: diff --git a/addons/beehave/debug/debugger_tab.gd b/addons/beehave/debug/debugger_tab.gd index c2774d2a..d8e6f487 100644 --- a/addons/beehave/debug/debugger_tab.gd +++ b/addons/beehave/debug/debugger_tab.gd @@ -37,6 +37,7 @@ func _ready() -> void: var button := Button.new() button.flat = true + button.name = "MakeFloatingButton" button.icon = get_theme_icon(&"ExternalLink", &"EditorIcons") button.pressed.connect(func(): make_floating.emit()) button.tooltip_text = "Make floating" @@ -45,6 +46,7 @@ func _ready() -> void: var toggle_button := Button.new() toggle_button.flat = true + toggle_button.name = "TogglePanelButton" toggle_button.icon = get_theme_icon(&"Back", &"EditorIcons") toggle_button.pressed.connect(_on_toggle_button_pressed.bind(toggle_button)) toggle_button.tooltip_text = "Toggle Panel" diff --git a/addons/beehave/debug/graph_edit.gd b/addons/beehave/debug/graph_edit.gd index f674078c..709d46c9 100644 --- a/addons/beehave/debug/graph_edit.gd +++ b/addons/beehave/debug/graph_edit.gd @@ -55,11 +55,7 @@ func _ready() -> void: layout_button.flat = true layout_button.focus_mode = Control.FOCUS_NONE layout_button.pressed.connect(func(): horizontal_layout = not horizontal_layout) - # Godot 4.2+ - if has_method("get_menu_hbox"): - call("get_menu_hbox").add_child(layout_button) - else: - call("get_zoom_hbox").add_child(layout_button) + get_menu_container().add_child(layout_button) _update_layout_button() @@ -152,7 +148,7 @@ func get_menu_container() -> Control: return call("get_zoom_hbox") # Godot 4.2+ - return call("get_menu_hbox").get_parent() + return call("get_menu_hbox") func get_status(status: int) -> String: diff --git a/addons/beehave/nodes/beehave_node.gd b/addons/beehave/nodes/beehave_node.gd index 0c94b8ce..399efa3f 100644 --- a/addons/beehave/nodes/beehave_node.gd +++ b/addons/beehave/nodes/beehave_node.gd @@ -1,8 +1,9 @@ -## A node in the behavior tree. Every node must return `SUCCESS`, `FAILURE` or -## `RUNNING` when ticked. @tool class_name BeehaveNode extends Node +## A node in the behavior tree. Every node must return `SUCCESS`, `FAILURE` or +## `RUNNING` when ticked. + enum { SUCCESS, FAILURE, diff --git a/addons/beehave/nodes/beehave_tree.gd b/addons/beehave/nodes/beehave_tree.gd index 1dd51043..8cf5c3a9 100644 --- a/addons/beehave/nodes/beehave_tree.gd +++ b/addons/beehave/nodes/beehave_tree.gd @@ -1,23 +1,30 @@ -## Controls the flow of execution of the entire behavior tree. @tool @icon("../icons/tree.svg") class_name BeehaveTree extends Node +## Controls the flow of execution of the entire behavior tree. + enum { SUCCESS, FAILURE, RUNNING } +enum ProcessThread { + IDLE, + PHYSICS +} + signal tree_enabled signal tree_disabled + ## Wether this behavior tree should be enabled or not. @export var enabled: bool = true: set(value): enabled = value - set_physics_process(enabled) - + set_physics_process(enabled and process_thread == ProcessThread.PHYSICS) + set_process(enabled and process_thread == ProcessThread.IDLE) if value: tree_enabled.emit() else: @@ -27,8 +34,27 @@ signal tree_disabled get: return enabled + ## An optional node path this behavior tree should apply to. -@export_node_path var actor_node_path : NodePath +@export_node_path var actor_node_path : NodePath: + set(anp): + actor_node_path = anp + if actor_node_path != null and str(actor_node_path) != "..": + actor = get_node(actor_node_path) + else: + actor = get_parent() + if Engine.is_editor_hint(): + update_configuration_warnings() + + +## Whether to run this tree in a physics or idle thread. +@export var process_thread:ProcessThread = ProcessThread.PHYSICS: + set(value): + process_thread = value + set_physics_process(enabled and process_thread == ProcessThread.PHYSICS) + set_process(enabled and process_thread == ProcessThread.IDLE) + + ## Custom blackboard node. An internal blackboard will be used ## if no blackboard is provided explicitly. @@ -61,6 +87,7 @@ signal tree_disabled BeehaveDebuggerMessages.unregister_tree(get_instance_id()) + var actor : Node var status : int = -1 @@ -69,40 +96,49 @@ var _process_time_metric_name : String var _process_time_metric_value : float = 0.0 var _can_send_message: bool = false -func _ready() -> void: - if Engine.is_editor_hint(): - return - if self.get_child_count() > 0 and not self.get_child(0) is BeehaveNode: - push_warning("Beehave error: Root %s should have only one child of type BeehaveNode (NodePath: %s)" % [self.name, self.get_path()]) - disable() - return +func _ready() -> void: + if not process_thread: + process_thread = ProcessThread.PHYSICS + + if actor_node_path: + actor = get_node(actor_node_path) + else: + actor = get_parent() if not blackboard: _internal_blackboard = Blackboard.new() add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK) - - actor = get_parent() - if actor_node_path: - actor = get_node(actor_node_path) - + # Get the name of the parent node name for metric - var parent_name = actor.name - _process_time_metric_name = "beehave [microseconds]/process_time_%s-%s" % [parent_name, get_instance_id()] + _process_time_metric_name = "beehave [microseconds]/process_time_%s-%s" % [actor.name, get_instance_id()] + set_physics_process(enabled and process_thread == ProcessThread.PHYSICS) + set_process(enabled and process_thread == ProcessThread.IDLE) + # Register custom metric to the engine - if custom_monitor: + if custom_monitor and not Engine.is_editor_hint(): Performance.add_custom_monitor(_process_time_metric_name, _get_process_time_metric_value) BeehaveGlobalMetrics.register_tree(self) - set_physics_process(enabled) - BeehaveGlobalDebugger.register_tree(self) - BeehaveDebuggerMessages.register_tree(_get_debugger_data(self)) + if Engine.is_editor_hint(): + update_configuration_warnings.call_deferred() + else: + BeehaveGlobalDebugger.register_tree(self) + BeehaveDebuggerMessages.register_tree(_get_debugger_data(self)) child_entered_tree.connect(_on_child_entered_tree) func _physics_process(delta: float) -> void: + _process_internally(delta) + + +func _process(delta: float) -> void: + _process_internally(delta) + + +func _process_internally(delta: float) -> void: if Engine.is_editor_hint(): return @@ -125,6 +161,8 @@ func _physics_process(delta: float) -> void: func tick() -> int: + if actor == null or get_child_count() == 0: + return FAILURE var child := self.get_child(0) if status != RUNNING: child.before_run(actor, blackboard) @@ -144,6 +182,9 @@ func tick() -> int: func _get_configuration_warnings() -> PackedStringArray: var warnings:PackedStringArray = [] + + if actor == null: + warnings.append("Configure target node on tree") if get_children().any(func(x): return not (x is BeehaveNode)): warnings.append("All children of this node should inherit from BeehaveNode class.") diff --git a/addons/beehave/nodes/composites/composite.gd b/addons/beehave/nodes/composites/composite.gd index b959eea1..4a8fa5a5 100644 --- a/addons/beehave/nodes/composites/composite.gd +++ b/addons/beehave/nodes/composites/composite.gd @@ -1,20 +1,12 @@ -## A Composite node controls the flow of execution of its children in a specific manner. @tool @icon("../../icons/category_composite.svg") class_name Composite extends BeehaveNode +## A Composite node controls the flow of execution of its children in a specific manner. var running_child: BeehaveNode = null -func _ready(): - if Engine.is_editor_hint(): - return - - if self.get_child_count() < 1: - push_warning("BehaviorTree Error: Composite %s should have at least one child (NodePath: %s)" % [self.name, self.get_path()]) - - func _get_configuration_warnings() -> PackedStringArray: var warnings: PackedStringArray = super._get_configuration_warnings() diff --git a/addons/beehave/nodes/composites/selector.gd b/addons/beehave/nodes/composites/selector.gd index eca71ac3..618f6020 100644 --- a/addons/beehave/nodes/composites/selector.gd +++ b/addons/beehave/nodes/composites/selector.gd @@ -1,11 +1,11 @@ -## Selector nodes will attempt to execute each of its children until one of -## them return `SUCCESS`. If all children return `FAILURE`, this node will also -## return `FAILURE`. -## If a child returns `RUNNING` it will tick again. @tool @icon("../../icons/selector.svg") class_name SelectorComposite extends Composite +## Selector nodes will attempt to execute each of its children until one of +## them return `SUCCESS`. If all children return `FAILURE`, this node will also +## return `FAILURE`. +## If a child returns `RUNNING` it will tick again. var last_execution_index: int = 0 diff --git a/addons/beehave/nodes/composites/selector_random.gd b/addons/beehave/nodes/composites/selector_random.gd index b780b5f1..a85e83cb 100644 --- a/addons/beehave/nodes/composites/selector_random.gd +++ b/addons/beehave/nodes/composites/selector_random.gd @@ -1,10 +1,11 @@ -## This node will attempt to execute all of its children just like a -## [code]SelectorStar[/code] would, with the exception that the children -## will be executed in a random order. @tool @icon("../../icons/selector_random.svg") class_name SelectorRandomComposite extends RandomizedComposite +## This node will attempt to execute all of its children just like a +## [code]SelectorStar[/code] would, with the exception that the children +## will be executed in a random order. + ## A shuffled list of the children that will be executed in reverse order. var _children_bag: Array[Node] = [] var c: Node diff --git a/addons/beehave/nodes/composites/selector_reactive.gd b/addons/beehave/nodes/composites/selector_reactive.gd index 869ef8a2..b0d5d23d 100644 --- a/addons/beehave/nodes/composites/selector_reactive.gd +++ b/addons/beehave/nodes/composites/selector_reactive.gd @@ -1,10 +1,11 @@ +@tool +@icon("../../icons/selector_reactive.svg") +class_name SelectorReactiveComposite extends Composite + ## Selector Reactive nodes will attempt to execute each of its children until one of ## them return `SUCCESS`. If all children return `FAILURE`, this node will also ## return `FAILURE`. ## If a child returns `RUNNING` it will restart. -@tool -@icon("../../icons/selector_reactive.svg") -class_name SelectorReactiveComposite extends Composite func tick(actor: Node, blackboard: Blackboard) -> int: for c in get_children(): diff --git a/addons/beehave/nodes/composites/sequence.gd b/addons/beehave/nodes/composites/sequence.gd index 11b5d79f..4f0d07a5 100644 --- a/addons/beehave/nodes/composites/sequence.gd +++ b/addons/beehave/nodes/composites/sequence.gd @@ -1,12 +1,12 @@ -## Sequence nodes will attempt to execute all of its children and report -## `SUCCESS` in case all of the children report a `SUCCESS` status code. -## If at least one child reports a `FAILURE` status code, this node will also -## return `FAILURE` and restart. -## In case a child returns `RUNNING` this node will tick again. @tool @icon("../../icons/sequence.svg") class_name SequenceComposite extends Composite +## Sequence nodes will attempt to execute all of its children and report +## `SUCCESS` in case all of the children report a `SUCCESS` status code. +## If at least one child reports a `FAILURE` status code, this node will also +## return `FAILURE` and restart. +## In case a child returns `RUNNING` this node will tick again. var successful_index: int = 0 diff --git a/addons/beehave/nodes/composites/sequence_random.gd b/addons/beehave/nodes/composites/sequence_random.gd index ab8eb789..a61bd929 100644 --- a/addons/beehave/nodes/composites/sequence_random.gd +++ b/addons/beehave/nodes/composites/sequence_random.gd @@ -1,10 +1,11 @@ -## This node will attempt to execute all of its children just like a -## [code]SequenceStar[/code] would, with the exception that the children -## will be executed in a random order. @tool @icon("../../icons/sequence_random.svg") class_name SequenceRandomComposite extends RandomizedComposite +## This node will attempt to execute all of its children just like a +## [code]SequenceStar[/code] would, with the exception that the children +## will be executed in a random order. + # Emitted whenever the children are shuffled. signal reset(new_order: Array[Node]) @@ -49,6 +50,9 @@ func tick(actor: Node, blackboard: Blackboard) -> int: c.after_run(actor, blackboard) FAILURE: _children_bag.erase(c) + # Interrupt any child that was RUNNING before + # but do not reset! + super.interrupt(actor, blackboard) c.after_run(actor, blackboard) return FAILURE RUNNING: diff --git a/addons/beehave/nodes/composites/sequence_reactive.gd b/addons/beehave/nodes/composites/sequence_reactive.gd index 50e8b90d..ade4b32a 100644 --- a/addons/beehave/nodes/composites/sequence_reactive.gd +++ b/addons/beehave/nodes/composites/sequence_reactive.gd @@ -1,12 +1,12 @@ +@tool +@icon("../../icons/sequence_reactive.svg") +class_name SequenceReactiveComposite extends Composite + ## Reactive Sequence nodes will attempt to execute all of its children and report ## `SUCCESS` in case all of the children report a `SUCCESS` status code. ## If at least one child reports a `FAILURE` status code, this node will also ## return `FAILURE` and restart. ## In case a child returns `RUNNING` this node will restart. -@tool -@icon("../../icons/sequence_reactive.svg") -class_name SequenceReactiveComposite extends Composite - var successful_index: int = 0 diff --git a/addons/beehave/nodes/composites/sequence_star.gd b/addons/beehave/nodes/composites/sequence_star.gd index 42ceb070..a6400e53 100644 --- a/addons/beehave/nodes/composites/sequence_star.gd +++ b/addons/beehave/nodes/composites/sequence_star.gd @@ -1,12 +1,12 @@ -## Sequence Star nodes will attempt to execute all of its children and report -## `SUCCESS` in case all of the children report a `SUCCESS` status code. -## If at least one child reports a `FAILURE` status code, this node will also -## return `FAILURE` and tick again. -## In case a child returns `RUNNING` this node will restart. @tool @icon("../../icons/sequence_reactive.svg") class_name SequenceStarComposite extends Composite +## Sequence Star nodes will attempt to execute all of its children and report +## `SUCCESS` in case all of the children report a `SUCCESS` status code. +## If at least one child reports a `FAILURE` status code, this node will also +## return `FAILURE` and tick again. +## In case a child returns `RUNNING` this node will tick again. var successful_index: int = 0 @@ -32,6 +32,9 @@ func tick(actor: Node, blackboard: Blackboard) -> int: successful_index += 1 c.after_run(actor, blackboard) FAILURE: + # Interrupt any child that was RUNNING before + # but do not reset! + super.interrupt(actor, blackboard) c.after_run(actor, blackboard) return FAILURE RUNNING: diff --git a/addons/beehave/nodes/decorators/decorator.gd b/addons/beehave/nodes/decorators/decorator.gd index 8cc39443..3b934983 100644 --- a/addons/beehave/nodes/decorators/decorator.gd +++ b/addons/beehave/nodes/decorators/decorator.gd @@ -1,21 +1,13 @@ -## Decorator nodes are used to transform the result received by its child. -## Must only have one child. @tool @icon("../../icons/category_decorator.svg") class_name Decorator extends BeehaveNode +## Decorator nodes are used to transform the result received by its child. +## Must only have one child. var running_child: BeehaveNode = null -func _ready(): - if Engine.is_editor_hint(): - return - - if self.get_child_count() != 1: - push_warning("Beehave Error: Decorator %s should have only one child (NodePath: %s)" % [self.name, self.get_path()]) - - func _get_configuration_warnings() -> PackedStringArray: var warnings: PackedStringArray = super._get_configuration_warnings() diff --git a/addons/beehave/nodes/decorators/failer.gd b/addons/beehave/nodes/decorators/failer.gd index 4a818ede..d5e937c6 100644 --- a/addons/beehave/nodes/decorators/failer.gd +++ b/addons/beehave/nodes/decorators/failer.gd @@ -1,8 +1,8 @@ -## A Failer node will always return a `FAILURE` status code. @tool @icon("../../icons/failer.svg") class_name AlwaysFailDecorator extends Decorator +## A Failer node will always return a `FAILURE` status code. func tick(actor: Node, blackboard: Blackboard) -> int: var c = get_child(0) diff --git a/addons/beehave/nodes/decorators/inverter.gd b/addons/beehave/nodes/decorators/inverter.gd index 16e3f36c..9f2d2389 100644 --- a/addons/beehave/nodes/decorators/inverter.gd +++ b/addons/beehave/nodes/decorators/inverter.gd @@ -1,9 +1,9 @@ -## An inverter will return `FAILURE` in case it's child returns a `SUCCESS` status -## code or `SUCCESS` in case its child returns a `FAILURE` status code. @tool @icon("../../icons/inverter.svg") class_name InverterDecorator extends Decorator +## An inverter will return `FAILURE` in case it's child returns a `SUCCESS` status +## code or `SUCCESS` in case its child returns a `FAILURE` status code. func tick(actor: Node, blackboard: Blackboard) -> int: var c = get_child(0) diff --git a/addons/beehave/nodes/decorators/limiter.gd b/addons/beehave/nodes/decorators/limiter.gd index 72e73565..ae2695b2 100644 --- a/addons/beehave/nodes/decorators/limiter.gd +++ b/addons/beehave/nodes/decorators/limiter.gd @@ -1,19 +1,21 @@ -## The limiter will execute its child `x` amount of times. When the number of -## maximum ticks is reached, it will return a `FAILURE` status code. @tool @icon("../../icons/limiter.svg") class_name LimiterDecorator extends Decorator +## The limiter will execute its `RUNNING` child `x` amount of times. When the number of +## maximum ticks is reached, it will return a `FAILURE` status code. +## The count resets the next time that a child is not `RUNNING` + @onready var cache_key = 'limiter_%s' % self.get_instance_id() @export var max_count : float = 0 func tick(actor: Node, blackboard: Blackboard) -> int: - var child = self.get_child(0) - var current_count = blackboard.get_value(cache_key, 0, str(actor.get_instance_id())) + if not get_child_count() == 1: + return FAILURE - if current_count == 0: - child.before_run(actor, blackboard) + var child = get_child(0) + var current_count = blackboard.get_value(cache_key, 0, str(actor.get_instance_id())) if current_count < max_count: blackboard.set_value(cache_key, current_count + 1, str(actor.get_instance_id())) @@ -28,14 +30,30 @@ func tick(actor: Node, blackboard: Blackboard) -> int: if child is ActionLeaf and response == RUNNING: running_child = child blackboard.set_value("running_action", child, str(actor.get_instance_id())) - + + if response != RUNNING: + child.after_run(actor, blackboard) + return response else: + interrupt(actor, blackboard) child.after_run(actor, blackboard) return FAILURE + + +func before_run(actor: Node, blackboard: Blackboard) -> void: + blackboard.set_value(cache_key, 0, str(actor.get_instance_id())) + if get_child_count() > 0: + get_child(0).before_run(actor, blackboard) func get_class_name() -> Array[StringName]: var classes := super() classes.push_back(&"LimiterDecorator") return classes + + +func _get_configuration_warnings() -> PackedStringArray: + if not get_child_count() == 1: + return ["Requires exactly one child node"] + return [] diff --git a/addons/beehave/nodes/decorators/succeeder.gd b/addons/beehave/nodes/decorators/succeeder.gd index 9d9664b1..d6601deb 100644 --- a/addons/beehave/nodes/decorators/succeeder.gd +++ b/addons/beehave/nodes/decorators/succeeder.gd @@ -1,8 +1,8 @@ -## A succeeder node will always return a `SUCCESS` status code. @tool @icon("../../icons/succeeder.svg") class_name AlwaysSucceedDecorator extends Decorator +## A succeeder node will always return a `SUCCESS` status code. func tick(actor: Node, blackboard: Blackboard) -> int: var c = get_child(0) diff --git a/addons/beehave/nodes/decorators/time_limiter.gd b/addons/beehave/nodes/decorators/time_limiter.gd index cedb3977..b9fa93fd 100644 --- a/addons/beehave/nodes/decorators/time_limiter.gd +++ b/addons/beehave/nodes/decorators/time_limiter.gd @@ -1,20 +1,26 @@ -## The Time Limit Decorator will give its child a set amount of time to finish -## before interrupting it and return a `FAILURE` status code. The timer is reset -## every time before the node runs. @tool @icon("../../icons/limiter.svg") class_name TimeLimiterDecorator extends Decorator -@export var wait_time: = 0.0 +## The Time Limit Decorator will give its `RUNNING` child a set amount of time to finish +## before interrupting it and return a `FAILURE` status code. +## The timer resets the next time that a child is not `RUNNING` -var time_left: = 0.0 +@export var wait_time: = 0.0 -@onready var child: BeehaveNode = get_child(0) +@onready var cache_key = 'time_limiter_%s' % self.get_instance_id() func tick(actor: Node, blackboard: Blackboard) -> int: + if not get_child_count() == 1: + return FAILURE + + var child = self.get_child(0) + var time_left = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id())) + if time_left < wait_time: time_left += get_physics_process_delta_time() + blackboard.set_value(cache_key, time_left, str(actor.get_instance_id())) var response = child.tick(actor, blackboard) if can_send_message(blackboard): BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response) @@ -27,20 +33,28 @@ func tick(actor: Node, blackboard: Blackboard) -> int: running_child = child if child is ActionLeaf: blackboard.set_value("running_action", child, str(actor.get_instance_id())) - + else: + child.after_run(actor, blackboard) return response else: - child.after_run(actor, blackboard) interrupt(actor, blackboard) + child.after_run(actor, blackboard) return FAILURE func before_run(actor: Node, blackboard: Blackboard) -> void: - time_left = 0.0 - child.before_run(actor, blackboard) + blackboard.set_value(cache_key, 0.0, str(actor.get_instance_id())) + if get_child_count() > 0: + get_child(0).before_run(actor, blackboard) func get_class_name() -> Array[StringName]: var classes := super() classes.push_back(&"TimeLimiterDecorator") return classes + + +func _get_configuration_warnings() -> PackedStringArray: + if not get_child_count() == 1: + return ["Requires exactly one child node"] + return [] diff --git a/addons/beehave/nodes/leaves/action.gd b/addons/beehave/nodes/leaves/action.gd index 003bdb4e..7b0957dc 100644 --- a/addons/beehave/nodes/leaves/action.gd +++ b/addons/beehave/nodes/leaves/action.gd @@ -1,11 +1,11 @@ -## Actions are leaf nodes that define a task to be performed by an actor. -## Their execution can be long running, potentially being called across multiple -## frame executions. In this case, the node should return `RUNNING` until the -## action is completed. @tool @icon("../../icons/action.svg") class_name ActionLeaf extends Leaf +## Actions are leaf nodes that define a task to be performed by an actor. +## Their execution can be long running, potentially being called across multiple +## frame executions. In this case, the node should return `RUNNING` until the +## action is completed. func get_class_name() -> Array[StringName]: var classes := super() diff --git a/addons/beehave/nodes/leaves/blackboard_compare.gd b/addons/beehave/nodes/leaves/blackboard_compare.gd index 84fb43fa..3b29cd01 100644 --- a/addons/beehave/nodes/leaves/blackboard_compare.gd +++ b/addons/beehave/nodes/leaves/blackboard_compare.gd @@ -1,9 +1,9 @@ -## Compares two values using the specified comparison operator. -## Returns [code]FAILURE[/code] if any of the expression fails or the -## comparison operation returns [code]false[/code], otherwise it returns [code]SUCCESS[/code]. @tool class_name BlackboardCompareCondition extends ConditionLeaf +## Compares two values using the specified comparison operator. +## Returns [code]FAILURE[/code] if any of the expression fails or the +## comparison operation returns [code]false[/code], otherwise it returns [code]SUCCESS[/code]. enum Operators { EQUAL, diff --git a/addons/beehave/nodes/leaves/blackboard_erase.gd b/addons/beehave/nodes/leaves/blackboard_erase.gd index 89508bd9..31e0a194 100644 --- a/addons/beehave/nodes/leaves/blackboard_erase.gd +++ b/addons/beehave/nodes/leaves/blackboard_erase.gd @@ -1,8 +1,8 @@ -## Erases the specified key from the blackboard. -## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code]. @tool class_name BlackboardEraseAction extends ActionLeaf +## Erases the specified key from the blackboard. +## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code]. ## Expression representing a blackboard key. @export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = "" diff --git a/addons/beehave/nodes/leaves/blackboard_has.gd b/addons/beehave/nodes/leaves/blackboard_has.gd index 868fddfd..ccd46d25 100644 --- a/addons/beehave/nodes/leaves/blackboard_has.gd +++ b/addons/beehave/nodes/leaves/blackboard_has.gd @@ -1,8 +1,8 @@ -## Returns [code]FAILURE[/code] if expression execution fails or the specified key doesn't exist. -## Returns [code]SUCCESS[/code] if blackboard has the specified key. @tool class_name BlackboardHasCondition extends ConditionLeaf +## Returns [code]FAILURE[/code] if expression execution fails or the specified key doesn't exist. +## Returns [code]SUCCESS[/code] if blackboard has the specified key. ## Expression representing a blackboard key. @export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = "" diff --git a/addons/beehave/nodes/leaves/blackboard_set.gd b/addons/beehave/nodes/leaves/blackboard_set.gd index d0d6455c..144336d0 100644 --- a/addons/beehave/nodes/leaves/blackboard_set.gd +++ b/addons/beehave/nodes/leaves/blackboard_set.gd @@ -1,8 +1,8 @@ -## Sets the specified key to the specified value. -## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code]. @tool class_name BlackboardSetAction extends ActionLeaf +## Sets the specified key to the specified value. +## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code]. ## Expression representing a blackboard key. @export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = "" diff --git a/addons/beehave/nodes/leaves/condition.gd b/addons/beehave/nodes/leaves/condition.gd index 55ec6f9c..b3b5c4fe 100644 --- a/addons/beehave/nodes/leaves/condition.gd +++ b/addons/beehave/nodes/leaves/condition.gd @@ -1,9 +1,9 @@ -## Conditions are leaf nodes that either return SUCCESS or FAILURE depending on -## a single simple condition. They should never return `RUNNING`. @tool @icon("../../icons/condition.svg") class_name ConditionLeaf extends Leaf +## Conditions are leaf nodes that either return SUCCESS or FAILURE depending on +## a single simple condition. They should never return `RUNNING`. func get_class_name() -> Array[StringName]: var classes := super() diff --git a/addons/beehave/nodes/leaves/leaf.gd b/addons/beehave/nodes/leaves/leaf.gd index b8c63b85..6f485d72 100644 --- a/addons/beehave/nodes/leaves/leaf.gd +++ b/addons/beehave/nodes/leaves/leaf.gd @@ -1,8 +1,8 @@ -## Base class for all leaf nodes of the tree. @tool @icon("../../icons/category_leaf.svg") class_name Leaf extends BeehaveNode +## Base class for all leaf nodes of the tree. const EXPRESSION_PLACEHOLDER: String = "Insert an expression..." diff --git a/addons/beehave/plugin.cfg b/addons/beehave/plugin.cfg index 8b61a2f1..367d0e32 100644 --- a/addons/beehave/plugin.cfg +++ b/addons/beehave/plugin.cfg @@ -3,5 +3,5 @@ name="Beehave" description="🐝 Behavior Tree addon for Godot Engine" author="bitbrain" -version="2.7.6" +version="2.7.8" script="plugin.gd" diff --git a/addons/beehave/plugin.gd b/addons/beehave/plugin.gd index 3c546274..71130fce 100644 --- a/addons/beehave/plugin.gd +++ b/addons/beehave/plugin.gd @@ -7,8 +7,8 @@ var frames: RefCounted func _init(): name = "BeehavePlugin" - add_autoload_singleton("BeehaveGlobalMetrics", "res://addons/beehave/metrics/beehave_global_metrics.gd") - add_autoload_singleton("BeehaveGlobalDebugger", "res://addons/beehave/debug/global_debugger.gd") + add_autoload_singleton("BeehaveGlobalMetrics", "metrics/beehave_global_metrics.gd") + add_autoload_singleton("BeehaveGlobalDebugger", "debug/global_debugger.gd") print("Beehave initialized!") @@ -20,5 +20,3 @@ func _enter_tree() -> void: func _exit_tree() -> void: remove_debugger_plugin(editor_debugger) - editor_debugger.free() - frames.free() diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd b/addons/gdUnit4/bin/GdUnitCmdTool.gd index a32cc032..6ab26f88 100644 --- a/addons/gdUnit4/bin/GdUnitCmdTool.gd +++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd @@ -1,6 +1,8 @@ #!/usr/bin/env -S godot -s extends SceneTree +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + #warning-ignore-all:return_value_discarded class CLIRunner extends Node: @@ -21,12 +23,12 @@ class CLIRunner extends Node: var _state = READY var _test_suites_to_process :Array var _executor + var _cs_executor var _report :GdUnitHtmlReport var _report_dir: String 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 ", "Adds the given test suite or directory to the execution pipeline.", TYPE_STRING), CmdOption.new("-i, --ignore", "-i ", "Adds the given test suite or test case to the ignore list.", TYPE_STRING), @@ -48,19 +50,18 @@ class CLIRunner extends Node: func _ready(): _state = INIT _report_dir = GdUnitTools.current_dir() + "reports" - _executor = load("res://addons/gdUnit4/src/core/GdUnitExecutor.gd").new() + _executor = load("res://addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd").new() # stop checked first test failure to fail fast _executor.fail_fast(true) - if GdUnitTools.is_mono_supported(): - _cs_executor = GdUnit3MonoAPI.create_executor(self) - + if GdUnit4MonoApiLoader.is_mono_supported(): + prints("GdUnit4Mono Version %s loaded." % GdUnit4MonoApiLoader.version()) + _cs_executor = GdUnit4MonoApiLoader.create_executor(self) var err = GdUnitSignals.instance().gdunit_event.connect(Callable(self, "_on_gdunit_event")) if err != OK: prints("gdUnitSignals failed") push_error("Error checked startup, can't connect executor for 'send_event'") quit(RETURN_ERROR) - add_child(_executor) func _process(_delta): @@ -76,10 +77,11 @@ class CLIRunner extends Node: set_process(false) # process next test suite 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) - await executor.ExecutionCompleted + if _cs_executor != null and _cs_executor.IsExecutable(test_suite): + _cs_executor.Execute(test_suite) + await _cs_executor.ExecutionCompleted + else: + await _executor.execute(test_suite) set_process(true) STOP: _state = EXIT @@ -88,9 +90,8 @@ class CLIRunner extends Node: func quit(code :int) -> void: - if is_instance_valid(_executor): - _executor.free() GdUnitTools.dispose_all() + await GdUnitMemoryObserver.gc_on_guarded_instances() await get_tree().physics_frame get_tree().quit(code) @@ -288,8 +289,8 @@ class CLIRunner extends Node: return total - func PublishEvent(data) -> void: - _on_gdunit_event(GdUnitEvent.new().deserialize(data.AsDictionary())) + func PublishEvent(data :Dictionary) -> void: + _on_gdunit_event(GdUnitEvent.new().deserialize(data)) func _on_gdunit_event(event :GdUnitEvent): @@ -397,9 +398,6 @@ func _initialize(): func _finalize(): prints("Finallize ..") - _cli_runner.free() prints("-Orphan nodes report-----------------------") Window.print_orphan_nodes() - prints("-SceneTree report-----------------------") - root.print_tree_pretty() prints("Finallize .. done") diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd b/addons/gdUnit4/bin/GdUnitCopyLog.gd index 9b6f6696..5f57806f 100644 --- a/addons/gdUnit4/bin/GdUnitCopyLog.gd +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd @@ -1,6 +1,8 @@ #!/usr/bin/env -S godot -s extends MainLoop +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const NO_LOG_TEMPLATE = """ @@ -89,13 +91,13 @@ func _patch_report(report_path :String, godot_log :String) -> void: index_file.seek(0) index_file.store_string(content) -func _copy_and_pach(from_file: String, to_dir: String) -> Result: +func _copy_and_pach(from_file: String, to_dir: String) -> GdUnitResult: var result := GdUnitTools.copy_file(from_file, to_dir) if result.is_error(): return result var file := FileAccess.open(from_file, FileAccess.READ) if file == null: - return Result.error("Can't find file '%s'. Error: %s" % [from_file, GdUnitTools.error_as_string(FileAccess.get_open_error())]) + return GdUnitResult.error("Can't find file '%s'. Error: %s" % [from_file, GdUnitTools.error_as_string(FileAccess.get_open_error())]) var content := file.get_as_text() # patch out console format codes for color_index in range(0, 256): @@ -108,9 +110,9 @@ func _copy_and_pach(from_file: String, to_dir: String) -> Result: var to_file := to_dir + "/" + from_file.get_file() file = FileAccess.open(to_file, FileAccess.WRITE) if file == null: - return Result.error("Can't open to write '%s'. Error: %s" % [to_file, GdUnitTools.error_as_string(FileAccess.get_open_error())]) + return GdUnitResult.error("Can't open to write '%s'. Error: %s" % [to_file, GdUnitTools.error_as_string(FileAccess.get_open_error())]) file.store_string(content) - return Result.empty() + return GdUnitResult.empty() func reports_available() -> bool: return DirAccess.dir_exists_absolute(_report_root_path) diff --git a/addons/gdUnit4/bin/ProjectScanner.gd b/addons/gdUnit4/bin/ProjectScanner.gd index 75070b29..74bbbcf4 100644 --- a/addons/gdUnit4/bin/ProjectScanner.gd +++ b/addons/gdUnit4/bin/ProjectScanner.gd @@ -4,28 +4,28 @@ extends SceneTree const CmdConsole = preload("res://addons/gdUnit4/src/cmd/CmdConsole.gd") -var scanner := ProjectScanner.new() +var scanner := SourceScanner.new() func _initialize(): + set_auto_accept_quit(false) root.add_child(scanner) func _finalize(): - if Engine.get_version_info().hex < 0x40100 or Engine.get_version_info().hex > 0x40101: - print("Finalize scanner ..") - scanner.free() - if Engine.get_version_info().hex < 0x40100 or Engine.get_version_info().hex > 0x40101: - prints("done") + prints("__finalize") -class ProjectScanner extends Node: + +class SourceScanner extends Node: enum { INIT, SCAN, - QUIT + QUIT, + DONE } + var _counter = 0 var WAIT_TIME_IN_MS = 5.000 var _state = INIT @@ -46,7 +46,9 @@ class ProjectScanner extends Node: _console.prints_color("Running project scan:", Color.CORNFLOWER_BLUE) await scan_project() set_process(true) + _state = QUIT if _state == QUIT or _counter >= WAIT_TIME_IN_MS: + _state = DONE _console.prints_color("Scan project done.", Color.CORNFLOWER_BLUE) _console.prints_color("======================================", Color.CORNFLOWER_BLUE) _console.new_line() @@ -58,24 +60,31 @@ class ProjectScanner extends Node: var plugin := EditorPlugin.new() var fs := plugin.get_editor_interface().get_resource_filesystem() - _console.prints_color("Scan :", Color.SANDY_BROWN) - _console.progressBar(0) - fs.scan() - await get_tree().process_frame - while fs.is_scanning(): - await get_tree().process_frame - _console.progressBar(fs.get_scanning_progress() * 100 as int) - _console.progressBar(100) - _console.new_line() - + if fs.has_method("reimport_files--"): + _console.prints_color("Reimport images :", Color.SANDY_BROWN) + for source in ["res://addons/gdUnit4/src/ui/assets/orphan", "res://addons/gdUnit4/src/ui/assets/spinner", "res://addons/gdUnit4/src/ui/assets/"]: + var image_files := Array(DirAccess.get_files_at(source)) + #_console.prints_color("%s" % image_files, Color.SANDY_BROWN) + var files := image_files.map(func full_path(file_name): + return "%s/%s" % [source, file_name] )\ + .filter(func filter_import_files(path :String): + return path.get_extension() != "import") + prints(files) + fs.reimport_files(files) + _console.prints_color("Scan sources: ", Color.SANDY_BROWN) - _console.progressBar(0) fs.scan_sources() + await get_tree().create_timer(5).timeout + await get_tree().process_frame + + _console.prints_color("Scan: ", Color.SANDY_BROWN) + fs.scan() await get_tree().process_frame while fs.is_scanning(): await get_tree().process_frame _console.progressBar(fs.get_scanning_progress() * 100 as int) _console.progressBar(100) _console.new_line() - plugin.free() - _state = QUIT + await get_tree().process_frame + plugin.queue_free() + await get_tree().process_frame diff --git a/addons/gdUnit4/plugin.cfg b/addons/gdUnit4/plugin.cfg index 133bd07d..56c4c0f6 100644 --- a/addons/gdUnit4/plugin.cfg +++ b/addons/gdUnit4/plugin.cfg @@ -3,5 +3,5 @@ name="gdUnit4" description="Unit Testing Framework for Godot Scripts" author="Mike Schulze" -version="4.1.4" +version="4.2.0" script="plugin.gd" diff --git a/addons/gdUnit4/plugin.gd b/addons/gdUnit4/plugin.gd index e533d079..065e0318 100644 --- a/addons/gdUnit4/plugin.gd +++ b/addons/gdUnit4/plugin.gd @@ -1,24 +1,13 @@ @tool extends EditorPlugin +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + var _gd_inspector :Node var _server_node var _gd_console :Node -# removes GdUnit classes inherits from Godot.Node from the node inspecor, ohterwise it takes very long to popup the dialog -func _fixup_node_inspector() -> void: - var classes := PackedStringArray([ - "GdUnitTestSuite", - "_TestCase", - "GdUnitInspecor", - "GdUnitExecutor", - "GdUnitTcpClient", - "GdUnitTcpServer"]) - for clazz in classes: - remove_custom_type(clazz) - - func _enter_tree(): Engine.set_meta("GdUnitEditorPlugin", self) GdUnitSettings.setup() @@ -30,11 +19,12 @@ func _enter_tree(): add_control_to_bottom_panel(_gd_console, "gdUnitConsole") _server_node = load("res://addons/gdUnit4/src/network/GdUnitServer.tscn").instantiate() add_child(_server_node) - _fixup_node_inspector() prints("Loading GdUnit4 Plugin success") if GdUnitSettings.is_update_notification_enabled(): var update_tool = load("res://addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn").instantiate() Engine.get_main_loop().root.call_deferred("add_child", update_tool) + if GdUnit4MonoApiLoader.is_mono_supported(): + prints("GdUnit4Mono Version %s loaded." % GdUnit4MonoApiLoader.version()) func _exit_tree(): diff --git a/addons/gdUnit4/runtest.cmd b/addons/gdUnit4/runtest.cmd index 5c70548d..fbd410e1 100644 --- a/addons/gdUnit4/runtest.cmd +++ b/addons/gdUnit4/runtest.cmd @@ -18,7 +18,7 @@ IF "%GODOT_TYPE%" == "mono" ( %GODOT_BIN% -s -d .\addons\gdUnit4\bin\GdUnitCmdTool.gd %* SET exit_code=%errorlevel% -%GODOT_BIN% --no-window --quiet -s -d .\addons\gdUnit4\bin\GdUnitCopyLog.gd %* +%GODOT_BIN% --headless --quiet -s -d .\addons\gdUnit4\bin\GdUnitCopyLog.gd %* ECHO %exit_code% diff --git a/addons/gdUnit4/src/GdUnitAssert.gd b/addons/gdUnit4/src/GdUnitAssert.gd index 1674d26c..04ba5263 100644 --- a/addons/gdUnit4/src/GdUnitAssert.gd +++ b/addons/gdUnit4/src/GdUnitAssert.gd @@ -3,6 +3,32 @@ class_name GdUnitAssert extends RefCounted +# Scans the current stack trace for the root cause to extract the line number +static func _get_line_number() -> int: + var stack_trace := get_stack() + if stack_trace == null or stack_trace.is_empty(): + return -1 + for stack_info in stack_trace: + var function :String = stack_info.get("function") + # we catch helper asserts to skip over to return the correct line number + if function.begins_with("assert_"): + continue + if function.begins_with("test_"): + return stack_info.get("line") + var source :String = stack_info.get("source") + if source.is_empty() \ + or source.begins_with("user://") \ + or source.ends_with("GdUnitAssert.gd") \ + or source.ends_with("AssertImpl.gd") \ + or source.ends_with("GdUnitTestSuite.gd") \ + or source.ends_with("GdUnitSceneRunnerImpl.gd") \ + or source.ends_with("GdUnitObjectInteractions.gd") \ + or source.ends_with("GdUnitAwaiter.gd"): + continue + return stack_info.get("line") + return -1 + + ## Verifies that the current value is null. func is_null(): return self diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd b/addons/gdUnit4/src/GdUnitAwaiter.gd index 99bf7732..c1fc2ccf 100644 --- a/addons/gdUnit4/src/GdUnitAwaiter.gd +++ b/addons/gdUnit4/src/GdUnitAwaiter.gd @@ -1,14 +1,21 @@ class_name GdUnitAwaiter extends RefCounted +const GdUnitAssertImpl = preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + # Waits for a specified signal in an interval of 50ms sent from the , 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(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: - var line_number := GdUnitAssertImpl._get_line_number() +func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: + # fail fast if the given source instance invalid + var line_number := GdUnitAssert._get_line_number() + if not is_instance_valid(source): + GdUnitAssertImpl.new(signal_name)\ + .report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) + return await Engine.get_main_loop().process_frame # fail fast if the given source instance invalid if not is_instance_valid(source): GdUnitAssertImpl.new(signal_name)\ @@ -27,8 +34,8 @@ static func await_signal_on(source :Object, signal_name :String, args :Array = [ # 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(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: - var line_number := GdUnitAssertImpl._get_line_number() +func await_signal_idle_frames(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: + var line_number := GdUnitAssert._get_line_number() # fail fast if the given source instance invalid if not is_instance_valid(source): GdUnitAssertImpl.new(signal_name)\ @@ -47,17 +54,17 @@ static func await_signal_idle_frames(source :Object, signal_name :String, args : # # waits for 100ms # await GdUnitAwaiter.await_millis(myNode, 100).completed # use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out -static func await_millis(milliSec :int) -> void: +func await_millis(milliSec :int) -> void: var timer :Timer = Timer.new() timer.set_name("gdunit_await_millis_timer_%d" % timer.get_instance_id()) Engine.get_main_loop().root.add_child(timer) timer.add_to_group("GdUnitTimers") timer.set_one_shot(true) - timer.start(milliSec * 0.001) + timer.start(milliSec / 1000.0) await timer.timeout timer.queue_free() # Waits until the next idle frame -static func await_idle_frame() -> void: +func await_idle_frame() -> void: await Engine.get_main_loop().process_frame diff --git a/addons/gdUnit4/src/GdUnitTestSuite.gd b/addons/gdUnit4/src/GdUnitTestSuite.gd index c7a60a59..30734918 100644 --- a/addons/gdUnit4/src/GdUnitTestSuite.gd +++ b/addons/gdUnit4/src/GdUnitTestSuite.gd @@ -3,23 +3,56 @@ ## You have to extend and implement your test cases as described[br] ## e.g MyTests.gd [br] ## [codeblock] -## extends GdUnitTestSuite -## # testcase -## func test_testCaseA(): -## assert_that("value").is_equal("value") -## [/codeblock][br] +## extends GdUnitTestSuite +## # testcase +## func test_case_a(): +## assert_that("value").is_equal("value") +## [/codeblock] ## @tutorial: https://mikeschulze.github.io/gdUnit4/faq/test-suite/ @icon("res://addons/gdUnit4/src/ui/assets/TestSuite.svg") class_name GdUnitTestSuite extends Node - -const NO_ARG = GdUnitConstants.NO_ARG +const NO_ARG :Variant = GdUnitConstants.NO_ARG ### internal runtime variables that must not be overwritten!!! +@warning_ignore("unused_private_class_variable") var __is_skipped := false +@warning_ignore("unused_private_class_variable") var __skip_reason :String = "Unknow." +var __active_test_case :String +var __awaiter := __gdunit_awaiter() +# holds the actual execution context +var __execution_context + + +### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading" +### in order to noticeably reduce the loading time of the test suite. +# We go this hard way to increase the loading performance to avoid reparsing all the used scripts +# for more detailed info -> https://github.com/godotengine/godot/issues/67400 +func __lazy_load(script_path :String) -> GDScript: + return GdUnitAssertions.__lazy_load(script_path) + + +func __gdunit_assert() -> GDScript: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + + +func __gdunit_tools() -> GDScript: + return __lazy_load("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func __gdunit_awaiter() -> Object: + return __lazy_load("res://addons/gdUnit4/src/GdUnitAwaiter.gd").new() + + +func __gdunit_argument_matchers(): + return __lazy_load("res://addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd") + + +func __gdunit_object_interactions(): + return __lazy_load("res://addons/gdUnit4/src/core/GdUnitObjectInteractions.gd") ## This function is called before a test suite starts[br] @@ -50,7 +83,6 @@ func is_failure(_expected_failure :String = NO_ARG) -> bool: return Engine.get_meta("GD_TEST_FAILURE") if Engine.has_meta("GD_TEST_FAILURE") else false -var __active_test_case :String func set_active_test_case(test_case :String) -> void: __active_test_case = test_case @@ -59,60 +91,67 @@ func set_active_test_case(test_case :String) -> void: # Mapps Godot error number to a readable error message. See at ERROR # https://docs.godotengine.org/de/stable/classes/class_@globalscope.html#enum-globalscope-error func error_as_string(error_number :int) -> String: - return GdUnitTools.error_as_string(error_number) + return __gdunit_tools().error_as_string(error_number) ## A litle helper to auto freeing your created objects after test execution func auto_free(obj) -> Variant: - return GdUnitMemoryPool.register_auto_free(obj, get_meta(GdUnitMemoryPool.META_PARAM)) + return __execution_context.register_auto_free(obj) + + +@warning_ignore("native_method_override") +func add_child(node :Node, force_readable_name := false, internal := Node.INTERNAL_MODE_DISABLED) -> void: + super.add_child(node, force_readable_name, internal) + if __execution_context != null: + __execution_context.orphan_monitor_start() ## Discard the error message triggered by a timeout (interruption).[br] ## By default, an interrupted test is reported as an error.[br] ## This function allows you to change the message to Success when an interrupted error is reported. func discard_error_interupted_by_timeout() -> void: - GdUnitTools.register_expect_interupted_by_timeout(self, __active_test_case) + __gdunit_tools().register_expect_interupted_by_timeout(self, __active_test_case) ## Creates a new directory under the temporary directory *user://tmp*[br] ## Useful for storing data during test execution. [br] ## The directory is automatically deleted after test suite execution func create_temp_dir(relative_path :String) -> String: - return GdUnitTools.create_temp_dir(relative_path) + return __gdunit_tools().create_temp_dir(relative_path) ## Deletes the temporary base directory[br] ## Is called automatically after each execution of the test suite func clean_temp_dir(): - GdUnitTools.clear_tmp() + __gdunit_tools().clear_tmp() ## Creates a new file under the temporary directory *user://tmp* + [br] ## with given name and given file (default = File.WRITE)[br] ## If success the returned File is automatically closed after the execution of the test suite func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess: - return GdUnitTools.create_temp_file(relative_path, file_name, mode) + return __gdunit_tools().create_temp_file(relative_path, file_name, mode) ## Reads a resource by given path into a PackedStringArray. func resource_as_array(resource_path :String) -> PackedStringArray: - return GdUnitTools.resource_as_array(resource_path) + return __gdunit_tools().resource_as_array(resource_path) ## Reads a resource by given path and returned the content as String. func resource_as_string(resource_path :String) -> String: - return GdUnitTools.resource_as_string(resource_path) + return __gdunit_tools().resource_as_string(resource_path) ## Reads a resource by given path and return Variand translated by str_to_var func resource_as_var(resource_path :String): - return str_to_var(GdUnitTools.resource_as_string(resource_path)) + return str_to_var(__gdunit_tools().resource_as_string(resource_path)) ## clears the debuger error list[br] ## PROTOTYPE!!!! Don't use it for now func clear_push_errors() -> void: - GdUnitTools.clear_push_errors() + __gdunit_tools().clear_push_errors() ## Waits for given signal is emited by the until a specified timeout to fail[br] @@ -121,17 +160,12 @@ func clear_push_errors() -> void: ## args: the expected signal arguments as an array[br] ## timeout: the timeout in ms, default is set to 2000ms func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout :int = 2000) -> Variant: - # fail fast if the given source instance invalid - if not is_instance_valid(source): - GdUnitAssertImpl.new(signal_name)\ - .report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), GdUnitAssertImpl._get_line_number()) - return await GdUnitAwaiter.await_idle_frame() - return await GdUnitAwaiter.await_signal_on(source, signal_name, args, timeout) + return await __awaiter.await_signal_on(source, signal_name, args, timeout) ## Waits until the next idle frame func await_idle_frame(): - await GdUnitAwaiter.await_idle_frame() + await __awaiter.await_idle_frame() ## Waits for for a given amount of milliseconds[br] @@ -142,7 +176,7 @@ func await_idle_frame(): ## [/codeblock][br] ## use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out func await_millis(timeout :int): - await GdUnitAwaiter.await_millis(timeout) + await __awaiter.await_millis(timeout) ## Creates a new scene runner to allow simulate interactions checked a scene.[br] @@ -157,7 +191,7 @@ func await_millis(timeout :int): ## var runner := scene_runner("res://foo/my_scne.tscn") ## [/codeblock] func scene_runner(scene, verbose := false) -> GdUnitSceneRunner: - return auto_free(GdUnitSceneRunnerImpl.new(scene, verbose)) + return auto_free(__lazy_load("res://addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd").new(scene, verbose)) # === Mocking & Spy =========================================================== @@ -173,12 +207,12 @@ const RETURN_DEEP_STUB = GdUnitMock.RETURN_DEEP_STUB ## Creates a mock for given class name func mock(clazz, mock_mode := RETURN_DEFAULTS) -> Object: - return GdUnitMockBuilder.build(self, clazz, mock_mode) + return __lazy_load("res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd").build(clazz, mock_mode) ## Creates a spy checked given object instance -func spy(instance): - return GdUnitSpyBuilder.build(self, instance) +func spy(instance) -> Object: + return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(instance) ## Configures a return value for the specified function and used arguments.[br] @@ -193,22 +227,22 @@ func do_return(value) -> GdUnitMock: ## Verifies certain behavior happened at least once or exact number of times func verify(obj, times := 1): - return GdUnitObjectInteractions.verify(obj, times) + return __gdunit_object_interactions().verify(obj, times) ## Verifies no interactions is happen checked this mock or spy func verify_no_interactions(obj) -> GdUnitAssert: - return GdUnitObjectInteractions.verify_no_interactions(obj) + return __gdunit_object_interactions().verify_no_interactions(obj) ## Verifies the given mock or spy has any unverified interaction. func verify_no_more_interactions(obj) -> GdUnitAssert: - return GdUnitObjectInteractions.verify_no_more_interactions(obj) + return __gdunit_object_interactions().verify_no_more_interactions(obj) ## Resets the saved function call counters checked a mock or spy func reset(obj) -> void: - GdUnitObjectInteractions.reset(obj) + __gdunit_object_interactions().reset(obj) ## Starts monitoring the specified source to collect all transmitted signals.[br] @@ -224,45 +258,47 @@ func reset(obj) -> void: ## await assert_signal(emitter).is_emitted('my_signal') ## [/codeblock] func monitor_signals(source :Object, _auto_free := true) -> Object: - var signal_collector := GdUnitThreadManager.get_current_context().get_signal_collector() - signal_collector.register_emitter(source) + __lazy_load("res://addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd")\ + .get_current_context()\ + .get_signal_collector()\ + .register_emitter(source) return auto_free(source) if _auto_free else source # === Argument matchers ======================================================== ## Argument matcher to match any argument func any() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.any() + return __gdunit_argument_matchers().any() ## Argument matcher to match any boolean value func any_bool() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_BOOL) + return __gdunit_argument_matchers().by_type(TYPE_BOOL) ## Argument matcher to match any integer value func any_int() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_INT) + return __gdunit_argument_matchers().by_type(TYPE_INT) ## Argument matcher to match any float value func any_float() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_FLOAT) + return __gdunit_argument_matchers().by_type(TYPE_FLOAT) ## Argument matcher to match any string value func any_string() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_STRING) + return __gdunit_argument_matchers().by_type(TYPE_STRING) ## Argument matcher to match any Color value func any_color() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_COLOR) + return __gdunit_argument_matchers().by_type(TYPE_COLOR) ## Argument matcher to match any Vector typed value func any_vector() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_types([ + return __gdunit_argument_matchers().by_types([ TYPE_VECTOR2, TYPE_VECTOR2I, TYPE_VECTOR3, @@ -274,142 +310,151 @@ func any_vector() -> GdUnitArgumentMatcher: ## Argument matcher to match any Vector2 value func any_vector2() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR2) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR2) ## Argument matcher to match any Vector2i value func any_vector2i() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR2I) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR2I) ## Argument matcher to match any Vector3 value func any_vector3() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR3) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR3) ## Argument matcher to match any Vector3i value func any_vector3i() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR3I) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR3I) ## Argument matcher to match any Vector4 value func any_vector4() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR4) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR4) ## Argument matcher to match any Vector3i value func any_vector4i() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR4I) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR4I) ## Argument matcher to match any Rect2 value func any_rect2() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_RECT2) + return __gdunit_argument_matchers().by_type(TYPE_RECT2) ## Argument matcher to match any Plane value func any_plane() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PLANE) + return __gdunit_argument_matchers().by_type(TYPE_PLANE) ## Argument matcher to match any Quaternion value func any_quat() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_QUATERNION) + return __gdunit_argument_matchers().by_type(TYPE_QUATERNION) ## Argument matcher to match any AABB value func any_aabb() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_AABB) + return __gdunit_argument_matchers().by_type(TYPE_AABB) ## Argument matcher to match any Basis value func any_basis() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_BASIS) + return __gdunit_argument_matchers().by_type(TYPE_BASIS) ## Argument matcher to match any Transform3D value func any_transform() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_TRANSFORM3D) + return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM3D) ## Argument matcher to match any Transform2D value func any_transform_2d() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_TRANSFORM2D) + return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM2D) ## Argument matcher to match any NodePath value func any_node_path() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_NODE_PATH) + return __gdunit_argument_matchers().by_type(TYPE_NODE_PATH) ## Argument matcher to match any RID value func any_rid() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_RID) + return __gdunit_argument_matchers().by_type(TYPE_RID) ## Argument matcher to match any Object value func any_object() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_OBJECT) + return __gdunit_argument_matchers().by_type(TYPE_OBJECT) ## Argument matcher to match any Dictionary value func any_dictionary() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_DICTIONARY) + return __gdunit_argument_matchers().by_type(TYPE_DICTIONARY) ## Argument matcher to match any Array value func any_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_ARRAY) ## Argument matcher to match any PackedByteArray value func any_pool_byte_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_BYTE_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_BYTE_ARRAY) ## Argument matcher to match any PackedInt32Array value func any_pool_int_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_INT32_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT32_ARRAY) ## Argument matcher to match any PackedFloat32Array value func any_pool_float_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_FLOAT32_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT32_ARRAY) ## Argument matcher to match any PackedStringArray value func any_pool_string_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_STRING_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_STRING_ARRAY) ## Argument matcher to match any PackedVector2Array value func any_pool_vector2_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_VECTOR2_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR2_ARRAY) ## Argument matcher to match any PackedVector3Array value func any_pool_vector3_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_VECTOR3_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR3_ARRAY) ## Argument matcher to match any PackedColorArray value func any_pool_color_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_COLOR_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_COLOR_ARRAY) ## Argument matcher to match any instance of given class func any_class(clazz :Object) -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.any_class(clazz) + return __gdunit_argument_matchers().any_class(clazz) # === value extract utils ====================================================== ## Builds an extractor by given function name and optional arguments func extr(func_name :String, args := Array()) -> GdUnitValueExtractor: - return GdUnitFuncValueExtractor.new(func_name, args) + return __lazy_load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd").new(func_name, args) ## Constructs a tuple by given arguments -func tuple(arg0, arg1=GdUnitTuple.NO_ARG, arg2=GdUnitTuple.NO_ARG, arg3=GdUnitTuple.NO_ARG, arg4=GdUnitTuple.NO_ARG, arg5=GdUnitTuple.NO_ARG, arg6=GdUnitTuple.NO_ARG, arg7=GdUnitTuple.NO_ARG, arg8=GdUnitTuple.NO_ARG, arg9=GdUnitTuple.NO_ARG) -> GdUnitTuple: +func tuple(arg0 :Variant, + arg1 :Variant=NO_ARG, + arg2 :Variant=NO_ARG, + arg3 :Variant=NO_ARG, + arg4 :Variant=NO_ARG, + arg5 :Variant=NO_ARG, + arg6 :Variant=NO_ARG, + arg7 :Variant=NO_ARG, + arg8 :Variant=NO_ARG, + arg9 :Variant=NO_ARG) -> GdUnitTuple: return GdUnitTuple.new(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) @@ -418,10 +463,6 @@ func tuple(arg0, arg1=GdUnitTuple.NO_ARG, arg2=GdUnitTuple.NO_ARG, arg3=GdUnitTu ## The common assertion tool to verify values. ## It checks the given value by type to fit to the best assert func assert_that(current) -> GdUnitAssert: - - if GdArrayTools.is_array_type(current): - return assert_array(current) - match typeof(current): TYPE_BOOL: return assert_bool(current) @@ -435,32 +476,34 @@ func assert_that(current) -> GdUnitAssert: return assert_vector(current) TYPE_DICTIONARY: return assert_dict(current) - TYPE_ARRAY: + TYPE_ARRAY, TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,\ + TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY,\ + TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_COLOR_ARRAY: return assert_array(current) TYPE_OBJECT, TYPE_NIL: return assert_object(current) _: - return GdUnitAssertImpl.new(current) + return __gdunit_assert().new(current) ## An assertion tool to verify boolean values. func assert_bool(current) -> GdUnitBoolAssert: - return GdUnitBoolAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd").new(current) ## An assertion tool to verify String values. func assert_str(current) -> GdUnitStringAssert: - return GdUnitStringAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd").new(current) ## An assertion tool to verify integer values. func assert_int(current) -> GdUnitIntAssert: - return GdUnitIntAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd").new(current) ## An assertion tool to verify float values. func assert_float(current) -> GdUnitFloatAssert: - return GdUnitFloatAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd").new(current) ## An assertion tool to verify Vector values.[br] @@ -470,55 +513,41 @@ func assert_float(current) -> GdUnitFloatAssert: ## assert_vector(Vector2(1.2, 1.000001)).is_equal(Vector2(1.2, 1.000001)) ## [/codeblock] func assert_vector(current) -> GdUnitVectorAssert: - return GdUnitVectorAssertImpl.new(current) - - -## An assertion tool to verify Vector2 values.[br] -## This function is [b]deprecated[/b] you have to use [method assert_vector] instead -func assert_vector2(current) -> GdUnitVectorAssert: - push_warning("assert_vector2 is deprecated, Use 'assert_vector' instead.") - return assert_vector(current) - - -## An assertion tool to verify Vector3 values.[br] -## This function is [b]deprecated[/b] you have to use [method assert_vector] instead -func assert_vector3(current) -> GdUnitVectorAssert: - push_warning("assert_vector3 is deprecated, Use 'assert_vector' instead.") - return assert_vector(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd").new(current) ## An assertion tool to verify arrays. func assert_array(current) -> GdUnitArrayAssert: - return GdUnitArrayAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current) ## An assertion tool to verify dictionaries. func assert_dict(current) -> GdUnitDictionaryAssert: - return GdUnitDictionaryAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd").new(current) ## An assertion tool to verify FileAccess. func assert_file(current) -> GdUnitFileAssert: - return GdUnitFileAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd").new(current) ## An assertion tool to verify Objects. func assert_object(current) -> GdUnitObjectAssert: - return GdUnitObjectAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd").new(current) func assert_result(current) -> GdUnitResultAssert: - return GdUnitResultAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd").new(current) ## An assertion tool that waits until a certain time for an expected function return value func assert_func(instance :Object, func_name :String, args := Array()) -> GdUnitFuncAssert: - return GdUnitFuncAssertImpl.new(instance, func_name, args) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd").new(instance, func_name, args) ## An Assertion Tool to verify for emitted signals until a certain time. func assert_signal(instance :Object) -> GdUnitSignalAssert: - return GdUnitSignalAssertImpl.new(instance) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd").new(instance) ## An assertion tool to test for failing assertions.[br] @@ -529,7 +558,7 @@ func assert_signal(instance :Object) -> GdUnitSignalAssert: ## .has_message("Expecting:\n 'true'\n not equal to\n 'true'") ## [/codeblock] func assert_failure(assertion :Callable) -> GdUnitFailureAssert: - return GdUnitFailureAssertImpl.new().execute(assertion) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute(assertion) ## An assertion tool to test for failing assertions.[br] @@ -540,7 +569,7 @@ func assert_failure(assertion :Callable) -> GdUnitFailureAssert: ## .has_message("Expecting:\n 'true'\n not equal to\n 'true'") ## [/codeblock] func assert_failure_await(assertion :Callable) -> GdUnitFailureAssert: - return await GdUnitFailureAssertImpl.new().execute_and_await(assertion) + return await __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute_and_await(assertion) ## An assertion tool to verify for Godot errors.[br] @@ -556,26 +585,15 @@ func assert_failure_await(assertion :Callable) -> GdUnitFailureAssert: ## .is_push_error('test error') ## [/codeblock] func assert_error(current :Callable) -> GdUnitGodotErrorAssert: - return GdUnitGodotErrorAssertImpl.new(current) - - -## 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) - GdAssertReports.expect_fail(true) - return is_failed + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd").new(current) func assert_not_yet_implemented(): - GdUnitAssertImpl.new(null).test_fail() + __gdunit_assert().new(null).test_fail() func fail(message :String): - GdUnitAssertImpl.new(null).report_error(message) + __gdunit_assert().new(null).report_error(message) # --- internal stuff do not override!!! diff --git a/addons/gdUnit4/src/GdUnitTuple.gd b/addons/gdUnit4/src/GdUnitTuple.gd index d0ec5524..a6e3c41b 100644 --- a/addons/gdUnit4/src/GdUnitTuple.gd +++ b/addons/gdUnit4/src/GdUnitTuple.gd @@ -7,7 +7,16 @@ const NO_ARG :Variant = GdUnitConstants.NO_ARG 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): +func _init(arg0:Variant, + arg1 :Variant=NO_ARG, + arg2 :Variant=NO_ARG, + arg3 :Variant=NO_ARG, + arg4 :Variant=NO_ARG, + arg5 :Variant=NO_ARG, + arg6 :Variant=NO_ARG, + arg7 :Variant=NO_ARG, + arg8 :Variant=NO_ARG, + arg9 :Variant=NO_ARG): __values = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd b/addons/gdUnit4/src/asserts/GdAssertMessages.gd index 005f015b..42a8ceb5 100644 --- a/addons/gdUnit4/src/asserts/GdAssertMessages.gd +++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd @@ -225,7 +225,7 @@ static func error_not_same_error(current, expected) -> String: return "%s\n %s\n but was\n %s" % [_error("Expecting error message:"), _colored_value(expected), _colored_value(current)] -static func error_is_instanceof(current: Result, expected :Result) -> String: +static func error_is_instanceof(current: GdUnitResult, expected :GdUnitResult) -> String: return "%s\n %s\n But it was %s" % [_error("Expected instance of:"),\ _colored_value(expected.or_else(null)), _colored_value(current.or_else(null))] @@ -408,20 +408,20 @@ static func error_contains_key_value(key, value, current_value, compare_mode :Gd # - ResultAssert specific errors ---------------------------------------------------- -static func error_result_is_empty(current :Result) -> String: - return _result_error_message(current, Result.EMPTY) +static func error_result_is_empty(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.EMPTY) -static func error_result_is_success(current :Result) -> String: - return _result_error_message(current, Result.SUCCESS) +static func error_result_is_success(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.SUCCESS) -static func error_result_is_warning(current :Result) -> String: - return _result_error_message(current, Result.WARN) +static func error_result_is_warning(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.WARN) -static func error_result_is_error(current :Result) -> String: - return _result_error_message(current, Result.ERROR) +static func error_result_is_error(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.ERROR) static func error_result_has_message(current :String, expected :String) -> String: @@ -429,14 +429,14 @@ static func error_result_has_message(current :String, expected :String) -> Strin static func error_result_has_message_on_success(expected :String) -> String: - return "%s\n %s\n but the Result is a success." % [_error("Expecting:"), _colored_value(expected)] + return "%s\n %s\n but the GdUnitResult is a success." % [_error("Expecting:"), _colored_value(expected)] static func error_result_is_value(current, expected) -> String: return "%s\n %s\n but was\n %s." % [_error("Expecting to contain same value:"), _colored_value(expected), _colored_value(current)] -static func _result_error_message(current :Result, expected_type :int) -> String: +static func _result_error_message(current :GdUnitResult, expected_type :int) -> String: if current == null: return _error("Expecting the result must be a %s but was ." % result_type(expected_type)) if current.is_success(): @@ -468,19 +468,19 @@ static func error_await_signal_on_invalid_instance(source, signal_name :String, static func result_type(type :int) -> String: match type: - Result.SUCCESS: return "SUCCESS" - Result.WARN: return "WARNING" - Result.ERROR: return "ERROR" - Result.EMPTY: return "EMPTY" + GdUnitResult.SUCCESS: return "SUCCESS" + GdUnitResult.WARN: return "WARNING" + GdUnitResult.ERROR: return "ERROR" + GdUnitResult.EMPTY: return "EMPTY" return "UNKNOWN" -static func result_message(result :Result) -> String: +static func result_message(result :GdUnitResult) -> String: match result._state: - Result.SUCCESS: return "" - Result.WARN: return result.warn_message() - Result.ERROR: return result.error_message() - Result.EMPTY: return "" + GdUnitResult.SUCCESS: return "" + GdUnitResult.WARN: return result.warn_message() + GdUnitResult.ERROR: return result.error_message() + GdUnitResult.EMPTY: return "" return "UNKNOWN" # ----------------------------------------------------------------------------------- diff --git a/addons/gdUnit4/src/asserts/GdAssertReports.gd b/addons/gdUnit4/src/asserts/GdAssertReports.gd index 8e822173..dc427ed3 100644 --- a/addons/gdUnit4/src/asserts/GdAssertReports.gd +++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd @@ -55,4 +55,5 @@ static func current_failure() -> String: static func send_report(report :GdUnitReport) -> void: - GdUnitSignals.instance().gdunit_report.emit(report) + var execution_context_id := GdUnitThreadManager.get_current_context().get_execution_context_id() + GdUnitSignals.instance().gdunit_report.emit(execution_context_id, report) diff --git a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd index 29aeec05..419f0172 100644 --- a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd @@ -1,13 +1,13 @@ -class_name GdUnitArrayAssertImpl extends GdUnitArrayAssert + var _base :GdUnitAssert var _current_value_provider :ValueProvider func _init(current): _current_value_provider = DefaultValueProvider.new(current) - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not __validate_value_type(current): @@ -50,6 +50,12 @@ func __current() -> Variant: return _current_value_provider.get_value() +func max_length(left, right) -> int: + var ls = str(left).length() + var rs = str(right).length() + return rs if ls < rs else ls + + func _array_equals_div(current, expected, case_sensitive :bool = false) -> Array: var current_ := PackedStringArray(Array(current)) var expected_ := PackedStringArray(Array(expected)) @@ -59,7 +65,7 @@ func _array_equals_div(current, expected, case_sensitive :bool = false) -> Array if index < expected_.size(): var e := expected_[index] if not GdObjects.equals(c, e, case_sensitive): - var length := GdUnitTools.max_length(c, e) + var length := max_length(c, e) current_[index] = GdAssertMessages.format_invalid(c.lpad(length)) expected_[index] = e.lpad(length) index_report_.push_back({"index" : index, "current" :c, "expected": e}) @@ -293,7 +299,7 @@ func is_instanceof(expected) -> GdUnitAssert: func extract(func_name :String, args := Array()) -> GdUnitArrayAssert: var extracted_elements := Array() - var extractor := GdUnitFuncValueExtractor.new(func_name, args) + var extractor :GdUnitValueExtractor = ResourceLoader.load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(func_name, args) var current = __current() if current == null: _current_value_provider = DefaultValueProvider.new(null) diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd index a5852626..6fc87b18 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd @@ -1,4 +1,3 @@ -class_name GdUnitAssertImpl extends GdUnitAssert @@ -7,29 +6,6 @@ var _current_error_message :String = "" var _custom_failure_message :String = "" -# Scans the current stack trace for the root cause to extract the line number -static func _get_line_number() -> int: - var stack_trace := get_stack() - if stack_trace == null or stack_trace.is_empty(): - return -1 - for stack_info in stack_trace: - var function :String = stack_info.get("function") - # we catch helper asserts to skip over to return the correct line number - if function.begins_with("assert_"): - continue - var source :String = stack_info.get("source") - if source.is_empty() \ - or source.begins_with("user://") \ - or source.ends_with("AssertImpl.gd") \ - or source.ends_with("GdUnitTestSuite.gd") \ - or source.ends_with("GdUnitSceneRunnerImpl.gd") \ - or source.ends_with("GdUnitObjectInteractions.gd") \ - or source.ends_with("GdUnitAwaiter.gd"): - continue - return stack_info.get("line") - return -1 - - func _init(current :Variant): _current = current # save the actual assert instance on the current thread context @@ -55,7 +31,7 @@ func report_success() -> GdUnitAssert: func report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: - var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertImpl._get_line_number() + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssert._get_line_number() GdAssertReports.set_last_error_line_number(line_number) _current_error_message = error_message if _custom_failure_message.is_empty() else _custom_failure_message GdAssertReports.report_error(_current_error_message, line_number) @@ -66,10 +42,6 @@ func test_fail(): return report_error(GdAssertMessages.error_not_implemented()) -static func _normalize_bbcode(message :String) -> String: - return GdUnitTools.richtext_normalize(message).replace("\r", "") - - func override_failure_message(message :String): _custom_failure_message = message return self diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd new file mode 100644 index 00000000..0f7219b3 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd @@ -0,0 +1,30 @@ +# Preloads all GdUnit assertions +class_name GdUnitAssertions +extends RefCounted + + +func _init(): + # preload all gdunit assertions to speedup testsuite loading time + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd") + + +### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading" +### in order to noticeably reduce the loading time of the test suite. +# We go this hard way to increase the loading performance to avoid reparsing all the used scripts +# for more detailed info -> https://github.com/godotengine/godot/issues/67400 +static func __lazy_load(script_path :String) -> GDScript: + return ResourceLoader.load(script_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE) diff --git a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd index dc7c31c2..d838c86f 100644 --- a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd @@ -1,10 +1,10 @@ -class_name GdUnitBoolAssertImpl extends GdUnitBoolAssert -var _base: GdUnitAssertImpl +var _base: GdUnitAssert + func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_BOOL): diff --git a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd index eea65385..514894ba 100644 --- a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd @@ -1,11 +1,10 @@ -class_name GdUnitDictionaryAssertImpl extends GdUnitDictionaryAssert var _base :GdUnitAssert func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_DICTIONARY): diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd index ec19413e..8be056e6 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd @@ -1,6 +1,7 @@ -class_name GdUnitFailureAssertImpl extends GdUnitFailureAssert +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + var _is_failed := false var _failure_message :String @@ -75,7 +76,7 @@ func has_line(expected :int) -> GdUnitFailureAssert: func has_message(expected :String) -> GdUnitFailureAssert: var expected_error := GdUnitTools.normalize_text(expected) - var current_error := GdUnitAssertImpl._normalize_bbcode(_failure_message) + var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message)) if current_error != expected_error: var diffs := GdDiffTool.string_diff(current_error, expected_error) var current := GdAssertMessages._colored_array_div(diffs[1]) @@ -85,7 +86,7 @@ func has_message(expected :String) -> GdUnitFailureAssert: func starts_with_message(expected :String) -> GdUnitFailureAssert: var expected_error := GdUnitTools.normalize_text(expected) - var current_error := GdUnitAssertImpl._normalize_bbcode(_failure_message) + var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message)) if current_error.find(expected_error) != 0: var diffs := GdDiffTool.string_diff(current_error, expected_error) var current := GdAssertMessages._colored_array_div(diffs[1]) @@ -94,7 +95,7 @@ func starts_with_message(expected :String) -> GdUnitFailureAssert: func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: - var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertImpl._get_line_number() + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssert._get_line_number() GdAssertReports.report_error(error_message, line_number) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd index 6ccc6645..b4e07008 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd @@ -1,12 +1,12 @@ -class_name GdUnitFileAssertImpl extends GdUnitFileAssert +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") var _base: GdUnitAssert func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_STRING): @@ -89,5 +89,6 @@ func contains_exactly(expected_rows :Array) -> GdUnitFileAssert: var source_code = GdScriptParser.to_unix_format(instance.get_script().source_code) GdUnitTools.free_instance(instance) var rows := Array(source_code.split("\n")) - GdUnitArrayAssertImpl.new(rows).contains_exactly(expected_rows) + ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(rows)\ + .contains_exactly(expected_rows) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd index fbc291b8..d457b00a 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd @@ -1,10 +1,10 @@ -class_name GdUnitFloatAssertImpl extends GdUnitFloatAssert var _base: GdUnitAssert + func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_FLOAT): diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd index 64a5792b..0c9e4b9f 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd @@ -1,43 +1,49 @@ -class_name GdUnitFuncAssertImpl extends GdUnitFuncAssert -signal value_provided(value) +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const DEFAULT_TIMEOUT := 2000 + var _current_value_provider :ValueProvider var _current_error_message :String = "" var _custom_failure_message :String = "" var _line_number := -1 var _timeout := DEFAULT_TIMEOUT var _interrupted := false - var _sleep_timer :Timer = null func _init(instance :Object, func_name :String, args := Array()): - _line_number = GdUnitAssertImpl._get_line_number() + _line_number = GdUnitAssert._get_line_number() GdAssertReports.reset_last_error_line_number() # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) # verify at first the function name exists if not instance.has_method(func_name): report_error("The function '%s' do not exists checked instance '%s'." % [func_name, instance]) + _interrupted = true else: _current_value_provider = CallBackValueProvider.new(instance, func_name, args) func _notification(_what): - if is_instance_valid(self): - dispose() + if is_instance_valid(_current_value_provider): + _current_value_provider.dispose() + _current_value_provider = null + if is_instance_valid(_sleep_timer): + Engine.get_main_loop().root.remove_child(_sleep_timer) + _sleep_timer.stop() + _sleep_timer.free() + _sleep_timer = null -func report_success() -> GdUnitAssert: +func report_success() -> GdUnitFuncAssert: GdAssertReports.report_success() return self -func report_error(error_message :String) -> GdUnitAssert: +func report_error(error_message :String) -> GdUnitFuncAssert: _current_error_message = error_message if _custom_failure_message == "" else _custom_failure_message GdAssertReports.report_error(_current_error_message, _line_number) return self @@ -66,30 +72,49 @@ func wait_until(timeout := 2000) -> GdUnitFuncAssert: func is_null() -> GdUnitFuncAssert: - return await _validate_callback(func is_null(c, _e): return c == null) + await _validate_callback(__is_null) + return self func is_not_null() -> GdUnitFuncAssert: - return await _validate_callback(func is_not_null(c, _e): return c != null) + await _validate_callback(__is_not_null) + return self func is_false() -> GdUnitFuncAssert: - return await _validate_callback(func is_false(c, _e): return c == false) + await _validate_callback(__is_false) + return self func is_true() -> GdUnitFuncAssert: - return await _validate_callback(func is_true(c, _e): return c == true) + await _validate_callback(__is_true) + return self func is_equal(expected) -> GdUnitFuncAssert: - return await _validate_callback(func is_equal(c, e): return GdObjects.equals(c, e), expected) + await _validate_callback(__is_equal, expected) + return self func is_not_equal(expected) -> GdUnitFuncAssert: - return await _validate_callback(func is_not_equal(c, e): return not GdObjects.equals(c, e), expected) + await _validate_callback(__is_not_equal, expected) + return self + +# we need actually to define this Callable as functions otherwise we results into leaked scripts here +# this is actually a Godot bug and needs this kind of workaround +func __is_null(c, _e): return c == null +func __is_not_null(c, _e): return c != null +func __is_false(c, _e): return c == false +func __is_true(c, _e): return c == true +func __is_equal(c, e): return GdObjects.equals(c,e) +func __is_not_equal(c, e): return not GdObjects.equals(c, e) -func _validate_callback(predicate :Callable, expected = null) -> GdUnitFuncAssert: + +func _validate_callback(predicate :Callable, expected = null): + if _interrupted: + return + GdUnitMemoryObserver.guard_instance(self) var time_scale = Engine.get_time_scale() var timer := Timer.new() timer.set_name("gdunit_funcassert_interrupt_timer_%d" % timer.get_instance_id()) @@ -97,8 +122,7 @@ func _validate_callback(predicate :Callable, expected = null) -> GdUnitFuncAsser timer.add_to_group("GdUnitTimers") timer.timeout.connect(func do_interrupt(): _interrupted = true - value_provided.emit(null) - , CONNECT_REFERENCE_COUNTED) + , CONNECT_DEFERRED) timer.set_one_shot(true) timer.start((_timeout/1000.0)*time_scale) _sleep_timer = Timer.new() @@ -106,12 +130,9 @@ func _validate_callback(predicate :Callable, expected = null) -> GdUnitFuncAsser Engine.get_main_loop().root.add_child(_sleep_timer) while true: - next_current_value() - var current = await value_provided - if _interrupted: - break - var is_success = predicate.call(current, expected) - if is_success: + var current = await next_current_value() + # is interupted or predicate success + if _interrupted or predicate.call(current, expected): break if is_instance_valid(_sleep_timer): _sleep_timer.start(0.05) @@ -119,31 +140,20 @@ func _validate_callback(predicate :Callable, expected = null) -> GdUnitFuncAsser _sleep_timer.stop() await Engine.get_main_loop().process_frame - dispose() if _interrupted: # https://github.com/godotengine/godot/issues/73052 #var predicate_name = predicate.get_method() - var predicate_name = str(predicate).split('(')[0] - report_error(GdAssertMessages.error_interrupted(predicate_name, expected, LocalTime.elapsed(_timeout))) + var predicate_name :String = str(predicate).split('::')[1] + report_error(GdAssertMessages.error_interrupted(predicate_name.strip_edges().trim_prefix("__"), expected, LocalTime.elapsed(_timeout))) else: report_success() - return self + _sleep_timer.free() + timer.free() + GdUnitMemoryObserver.unguard_instance(self) -func next_current_value(): +func next_current_value() -> Variant: @warning_ignore("redundant_await") if is_instance_valid(_current_value_provider): - var current = await _current_value_provider.get_value() - call_deferred("emit_signal", "value_provided", current) - - -# it is important to free all references/connections to prevent orphan nodes -func dispose(): - GdUnitTools._release_connections(self) - if is_instance_valid(_current_value_provider): - _current_value_provider.dispose() - _current_value_provider = null - if is_instance_valid(_sleep_timer): - _sleep_timer.stop() - _sleep_timer.free() - _sleep_timer = null + return await _current_value_provider.get_value() + return "invalid value" diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd index 9cfbcb7b..169994be 100644 --- a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd @@ -1,7 +1,5 @@ -class_name GdUnitGodotErrorAssertImpl extends GdUnitGodotErrorAssert - var _current_error_message :String var _callable :Callable @@ -37,7 +35,7 @@ func _report_success() -> GdUnitAssert: func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: - var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertImpl._get_line_number() + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssert._get_line_number() _current_error_message = error_message GdAssertReports.report_error(error_message, line_number) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd index aee77125..7f39c060 100644 --- a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd @@ -1,10 +1,10 @@ -class_name GdUnitIntAssertImpl extends GdUnitIntAssert var _base: GdUnitAssert + func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_INT): diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd index 29b3a1dc..18c67bf7 100644 --- a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd @@ -1,11 +1,10 @@ -class_name GdUnitObjectAssertImpl extends GdUnitObjectAssert var _base :GdUnitAssert func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if (current != null diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd index 8136dc39..ccc92e56 100644 --- a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd @@ -1,10 +1,10 @@ -class_name GdUnitResultAssertImpl extends GdUnitResultAssert var _base :GdUnitAssert + func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not __validate_value_type(current): @@ -19,11 +19,11 @@ func _notification(event): func __validate_value_type(value) -> bool: - return value == null or value is Result + return value == null or value is GdUnitResult -func __current() -> Result: - return _base.__current() as Result +func __current() -> GdUnitResult: + return _base.__current() as GdUnitResult func report_success() -> GdUnitResultAssert: diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd index b3ebba26..b82ebe46 100644 --- a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd @@ -1,10 +1,8 @@ -class_name GdUnitSignalAssertImpl extends GdUnitSignalAssert const DEFAULT_TIMEOUT := 2000 -const NO_ARG = GdUnitConstants.NO_ARG -var _signal_collector :GdUnitSignalAssertImpl.SignalCollector +var _signal_collector :GdUnitSignalCollector var _emitter :Object var _current_error_message :String = "" var _custom_failure_message :String = "" @@ -13,113 +11,12 @@ var _timeout := DEFAULT_TIMEOUT var _interrupted := false -# It connects to all signals of given emitter and collects received signals and arguments -# The collected signals are cleand finally when the emitter is freed. -class SignalCollector extends RefCounted: - const SIGNAL_BLACK_LIST = []#["tree_exiting", "tree_exited", "child_exiting_tree"] - - # { - # emitter : { - # signal_name : [signal_args], - # ... - # } - # } - var _collected_signals :Dictionary = {} - - - func clear() -> void: - for emitter in _collected_signals.keys(): - if is_instance_valid(emitter): - unregister_emitter(emitter) - - - # connect to all possible signals defined by the emitter - # prepares the signal collection to store received signals and arguments - func register_emitter(emitter :Object): - if is_instance_valid(emitter): - # check emitter is already registerd - if _collected_signals.has(emitter): - return - _collected_signals[emitter] = Dictionary() - # connect to 'tree_exiting' of the emitter to finally release all acquired resources/connections. - if emitter is Node and !emitter.tree_exiting.is_connected(unregister_emitter): - emitter.tree_exiting.connect(unregister_emitter.bind(emitter)) - # connect to all signals of the emitter we want to collect - for signal_def in emitter.get_signal_list(): - var signal_name = signal_def["name"] - # set inital collected to empty - if not is_signal_collecting(emitter, signal_name): - _collected_signals[emitter][signal_name] = Array() - if SIGNAL_BLACK_LIST.find(signal_name) != -1: - continue - if !emitter.is_connected(signal_name, _on_signal_emmited): - var err := emitter.connect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) - if err != OK: - push_error("Can't connect to signal %s on %s. Error: %s" % [signal_name, emitter, error_string(err)]) - - - # unregister all acquired resources/connections, otherwise it ends up in orphans - # is called when the emitter is removed from the parent - func unregister_emitter(emitter :Object): - if is_instance_valid(emitter): - for signal_def in emitter.get_signal_list(): - var signal_name = signal_def["name"] - if emitter.is_connected(signal_name, _on_signal_emmited): - emitter.disconnect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) - _collected_signals.erase(emitter) - - - # receives the signal from the emitter with all emitted signal arguments and additional the emitter and signal_name as last two arguements - func _on_signal_emmited( 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, arg10=NO_ARG, arg11=NO_ARG): - var signal_args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11], NO_ARG) - # extract the emitter and signal_name from the last two arguments (see line 61 where is added) - var signal_name :String = signal_args.pop_back() - var emitter :Object = signal_args.pop_back() - # prints("_on_signal_emmited:", emitter, signal_name, signal_args) - if is_signal_collecting(emitter, signal_name): - _collected_signals[emitter][signal_name].append(signal_args) - - - func reset_received_signals(emitter :Object): - # _debug_signal_list("before claer"); - if _collected_signals.has(emitter): - for signal_name in _collected_signals[emitter]: - _collected_signals[emitter][signal_name].clear() - # _debug_signal_list("after claer"); - - - func is_signal_collecting(emitter :Object, signal_name :String) -> bool: - return _collected_signals.has(emitter) and _collected_signals[emitter].has(signal_name) - - - func match(emitter :Object, signal_name :String, args :Array) -> bool: - #prints("match", signal_name, _collected_signals[emitter][signal_name]); - if _collected_signals.is_empty() or not _collected_signals.has(emitter): - return false - for received_args in _collected_signals[emitter][signal_name]: - # prints("testing", signal_name, received_args, "vs", args) - if GdObjects.equals(received_args, args): - return true - return false - - - func _debug_signal_list(message :String): - prints("-----", message, "-------") - prints("senders {") - for emitter in _collected_signals: - prints("\t", emitter) - for signal_name in _collected_signals[emitter]: - var args = _collected_signals[emitter][signal_name] - prints("\t\t", signal_name, args) - prints("}") - - func _init(emitter :Object): # save the actual assert instance on the current thread context var context := GdUnitThreadManager.get_current_context() context.set_assert(self) _signal_collector = context.get_signal_collector() - _line_number = GdUnitAssertImpl._get_line_number() + _line_number = GdUnitAssert._get_line_number() _emitter = emitter GdAssertReports.reset_last_error_line_number() @@ -130,7 +27,7 @@ func report_success() -> GdUnitAssert: func report_warning(message :String) -> GdUnitAssert: - GdAssertReports.report_warning(message, GdUnitAssertImpl._get_line_number()) + GdAssertReports.report_warning(message, GdUnitAssert._get_line_number()) return self @@ -171,13 +68,13 @@ func is_signal_exists(signal_name :String) -> GdUnitSignalAssert: # Verifies that given signal is emitted until waiting time func is_emitted(name :String, args := []) -> GdUnitSignalAssert: - _line_number = GdUnitAssertImpl._get_line_number() + _line_number = GdUnitAssert._get_line_number() return await _wail_until_signal(name, args, false) # Verifies that given signal is NOT emitted until waiting time func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert: - _line_number = GdUnitAssertImpl._get_line_number() + _line_number = GdUnitAssert._get_line_number() return await _wail_until_signal(name, args, true) diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd index a47ed744..154b9e5c 100644 --- a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd @@ -1,11 +1,10 @@ -class_name GdUnitStringAssertImpl extends GdUnitStringAssert var _base :GdUnitAssert func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_STRING): diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd index 4f39a231..43a63f1d 100644 --- a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd @@ -1,4 +1,3 @@ -class_name GdUnitVectorAssertImpl extends GdUnitVectorAssert var _base: GdUnitAssert @@ -6,7 +5,7 @@ var _current_type :int func _init(current :Variant): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _validate_value_type(current): diff --git a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd index 96293226..74adebb1 100644 --- a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd +++ b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd @@ -11,7 +11,7 @@ func _init(p_options :CmdOptions, p_tool_name :String): _tool_name = p_tool_name -func parse(args :Array, ignore_unknown_cmd := false) -> Result: +func parse(args :Array, ignore_unknown_cmd := false) -> GdUnitResult: _parsed_commands.clear() # parse until first program argument @@ -21,7 +21,7 @@ func parse(args :Array, ignore_unknown_cmd := false) -> Result: break if args.is_empty(): - return Result.empty() + return GdUnitResult.empty() # now parse all arguments while not args.is_empty(): @@ -30,10 +30,10 @@ func parse(args :Array, ignore_unknown_cmd := false) -> Result: if option: if _parse_cmd_arguments(option, args) == -1: - return Result.error("The '%s' command requires an argument!" % option.short_command()) + return GdUnitResult.error("The '%s' command requires an argument!" % option.short_command()) elif not ignore_unknown_cmd: - return Result.error("Unknown '%s' command!" % cmd) - return Result.success(_parsed_commands.values()) + return GdUnitResult.error("Unknown '%s' command!" % cmd) + return GdUnitResult.success(_parsed_commands.values()) func options() -> CmdOptions: diff --git a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd index 7105893f..e32557ec 100644 --- a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd +++ b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd @@ -47,7 +47,7 @@ func register_cbv(cmd_name: String, cb: Callable) -> CmdCommandHandler: return self -func _validate() -> Result: +func _validate() -> GdUnitResult: var errors: = PackedStringArray() var registered_cbs: = Dictionary() @@ -66,12 +66,12 @@ func _validate() -> Result: else: registered_cbs[cb_method] = cmd_name if errors.is_empty(): - return Result.success(true) + return GdUnitResult.success(true) else: - return Result.error("\n".join(errors)) + return GdUnitResult.error("\n".join(errors)) -func execute(commands :Array) -> Result: +func execute(commands :Array) -> GdUnitResult: var result := _validate() if result.is_error(): return result @@ -89,4 +89,4 @@ func execute(commands :Array) -> Result: cb_s.call(cmd.arguments()[CB_SINGLE_ARG]) else: cb_m.callv(cmd.arguments()) - return Result.success(true) + return GdUnitResult.success(true) diff --git a/addons/gdUnit4/src/core/GdObjects.gd b/addons/gdUnit4/src/core/GdObjects.gd index 58a6c98a..ee4bf792 100644 --- a/addons/gdUnit4/src/core/GdObjects.gd +++ b/addons/gdUnit4/src/core/GdObjects.gd @@ -2,6 +2,8 @@ class_name GdObjects extends Resource +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const TYPE_VOID = TYPE_MAX + 1000 const TYPE_VARARG = TYPE_MAX + 1001 const TYPE_VARIANT = TYPE_MAX + 1002 @@ -373,7 +375,7 @@ static func is_script(value) -> bool: static func is_test_suite(script :Script) -> bool: - return is_gd_testsuite(script) or GdUnit3MonoAPI.is_test_suite(script.resource_path) + return is_gd_testsuite(script) or GdUnit4MonoApiLoader.is_test_suite(script.resource_path) static func is_native_class(value) -> bool: @@ -397,10 +399,6 @@ static func is_cs_script(script :Script) -> bool: return str(script).find("CSharpScript") != -1 -static func is_cs_test_suite(instance :Node) -> bool: - return instance.get("IsCsTestSuite") == true - - static func is_gd_testsuite(script :Script) -> bool: if is_gd_script(script): var stack := [script] @@ -447,30 +445,30 @@ static func can_be_instantiate(obj :Variant) -> bool: return obj.has_method("new") -static func create_instance(clazz) -> Result: +static func create_instance(clazz) -> GdUnitResult: match typeof(clazz): TYPE_OBJECT: # test is given clazz already an instance if is_instance(clazz): - return Result.success(clazz) - return Result.success(clazz.new()) + return GdUnitResult.success(clazz) + return GdUnitResult.success(clazz.new()) TYPE_STRING: if ClassDB.class_exists(clazz): if Engine.has_singleton(clazz): - return Result.error("Not allowed to create a instance for singelton '%s'." % clazz) + return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz) if not ClassDB.can_instantiate(clazz): - return Result.error("Can't instance Engine class '%s'." % clazz) - return Result.success(ClassDB.instantiate(clazz)) + return GdUnitResult.error("Can't instance Engine class '%s'." % clazz) + return GdUnitResult.success(ClassDB.instantiate(clazz)) else: var clazz_path :String = extract_class_path(clazz)[0] if not FileAccess.file_exists(clazz_path): - return Result.error("Class '%s' not found." % clazz) + return GdUnitResult.error("Class '%s' not found." % clazz) var script = load(clazz_path) if script != null: - return Result.success(script.new()) + return GdUnitResult.success(script.new()) else: - return Result.error("Can't create instance for '%s'." % clazz) - return Result.error("Can't create instance for class '%s'." % clazz) + return GdUnitResult.error("Can't create instance for '%s'." % clazz) + return GdUnitResult.error("Can't create instance for class '%s'." % clazz) static func extract_class_path(clazz) -> PackedStringArray: @@ -515,38 +513,38 @@ static func extract_class_name_from_class_path(clazz_path :PackedStringArray) -> return clazz_name -static func extract_class_name(clazz) -> Result: +static func extract_class_name(clazz) -> GdUnitResult: if clazz == null: - return Result.error("Can't extract class name form a null value.") + return GdUnitResult.error("Can't extract class name form a null value.") if is_instance(clazz): # is instance a script instance? var script := clazz.script as GDScript if script != null: return extract_class_name(script) - return Result.success(clazz.get_class()) + return GdUnitResult.success(clazz.get_class()) # extract name form full qualified class path if clazz is String: if ClassDB.class_exists(clazz): - return Result.success(clazz) + return GdUnitResult.success(clazz) var source_sript :Script = load(clazz) var clazz_name = load("res://addons/gdUnit4/src/core/parse/GdScriptParser.gd").new().get_class_name(source_sript) - return Result.success(to_pascal_case(clazz_name)) + return GdUnitResult.success(to_pascal_case(clazz_name)) if is_primitive_type(clazz): - return Result.error("Can't extract class name for an primitive '%s'" % type_as_string(typeof(clazz))) + return GdUnitResult.error("Can't extract class name for an primitive '%s'" % type_as_string(typeof(clazz))) if is_script(clazz): if clazz.resource_path.is_empty(): var class_path = extract_class_name_from_class_path(extract_class_path(clazz)) - return Result.success(class_path); + return GdUnitResult.success(class_path); return extract_class_name(clazz.resource_path) # need to create an instance for a class typ the extract the class name var instance = clazz.new() if instance == null: - return Result.error("Can't create a instance for class '%s'" % clazz) + return GdUnitResult.error("Can't create a instance for class '%s'" % clazz) var result := extract_class_name(instance) GdUnitTools.free_instance(instance) return result diff --git a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd b/addons/gdUnit4/src/core/GdUnitClassDoubler.gd index d8ff4942..3ff12e9f 100644 --- a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd +++ b/addons/gdUnit4/src/core/GdUnitClassDoubler.gd @@ -2,6 +2,9 @@ class_name GdUnitClassDoubler extends RefCounted + +const DOUBLER_INSTANCE_ID_PREFIX := "gdunit_doubler_instance_id_" +const DOUBLER_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd") const EXCLUDE_VIRTUAL_FUNCTIONS = [ # we have to exclude notifications because NOTIFICATION_PREDELETE is try # to delete already freed spy/mock resources and will result in a conflict @@ -11,7 +14,6 @@ const EXCLUDE_VIRTUAL_FUNCTIONS = [ "get_path", "duplicate", ] - # define functions to be exclude when spy or mock checked a scene const EXLCUDE_SCENE_FUNCTIONS = [ # needs to exclude get/set script functions otherwise it endsup in recursive endless loop @@ -20,28 +22,30 @@ const EXLCUDE_SCENE_FUNCTIONS = [ # needs to exclude otherwise verify fails checked collection arguments checked calling to string "_to_string", ] - const EXCLUDE_FUNCTIONS = ["new", "free", "get_instance_id", "get_tree"] +static func check_leaked_instances() -> void: + ## we check that all registered spy/mock instances are removed from the engine meta data + for key in Engine.get_meta_list(): + if key.begins_with(DOUBLER_INSTANCE_ID_PREFIX): + var instance = Engine.get_meta(key) + push_error("GdUnit internal error: an spy/mock instance '%s', class:'%s' is not removed from the engine and will lead in a leaked instance!" % [instance, instance.__SOURCE_CLASS]) + + # loads the doubler template # class_info = { "class_name": <>, "class_path" : <>} -static func load_template(template :Object, class_info :Dictionary, instance :Object) -> PackedStringArray: - var source_code = template.new().get_script().source_code +static func load_template(template :String, class_info :Dictionary, instance :Object) -> PackedStringArray: # store instance id - source_code = source_code.replace("${instance_id}", "instance_%d" % instance.get_instance_id()) + var source_code = template\ + .replace("${instance_id}", "%s%d" % [DOUBLER_INSTANCE_ID_PREFIX, abs(instance.get_instance_id())])\ + .replace("${source_class}", class_info.get("class_name")) var lines := GdScriptParser.to_unix_format(source_code).split("\n") # replace template class_name with Doubled name and extends form source class - lines.remove_at(2) - lines.insert(2, "class_name Doubled%s" % class_info.get("class_name").replace(".", "_")) - lines.insert(3, extends_clazz(class_info)) - - var eol := lines.size() + lines.insert(0, "class_name Doubled%s" % class_info.get("class_name").replace(".", "_")) + lines.insert(1, extends_clazz(class_info)) # append Object interactions stuff - source_code = GdUnitObjectInteractionsTemplate.new().get_script().source_code - lines.append_array(GdScriptParser.to_unix_format(source_code).split("\n")) - # remove_at the class header from GdUnitObjectInteractionsTemplate - lines.remove_at(eol) + lines.append_array(GdScriptParser.to_unix_format(DOUBLER_TEMPLATE.source_code).split("\n")) return lines diff --git a/addons/gdUnit4/src/core/GdUnitExecutor.gd b/addons/gdUnit4/src/core/GdUnitExecutor.gd deleted file mode 100644 index a9f717ac..00000000 --- a/addons/gdUnit4/src/core/GdUnitExecutor.gd +++ /dev/null @@ -1,400 +0,0 @@ -extends Node - -signal ExecutionCompleted() - - -const INIT = 0 -const STAGE_TEST_SUITE_BEFORE = GdUnitReportCollector.STAGE_TEST_SUITE_BEFORE -const STAGE_TEST_SUITE_AFTER = GdUnitReportCollector.STAGE_TEST_SUITE_AFTER -const STAGE_TEST_CASE_BEFORE = GdUnitReportCollector.STAGE_TEST_CASE_BEFORE -const STAGE_TEST_CASE_EXECUTE = GdUnitReportCollector.STAGE_TEST_CASE_EXECUTE -const STAGE_TEST_CASE_AFTER = GdUnitReportCollector.STAGE_TEST_CASE_AFTER - -var _testsuite_timer :LocalTime -var _testcase_timer :LocalTime - -var _memory_pool :GdUnitMemoryPool = GdUnitMemoryPool.new() -var _report_errors_enabled :bool -var _report_collector : = GdUnitReportCollector.new() - - -var _total_test_execution_orphans :int -var _total_test_warnings :int -var _total_test_failed :int -var _total_test_errors :int -var _fail_fast := false -var _debug := false - - -func _init(debug := false): - set_name("GdUnitExecutor%s" % ("Debug" if debug else "")) - _debug = debug - - -func _ready(): - _report_errors_enabled = GdUnitSettings.is_report_push_errors() - - -func fail_fast(enabled :bool) -> void: - _fail_fast = enabled - - -func set_stage(stage :int) -> void: - _report_collector.set_stage(stage) - - -func set_consume_reports(enabled :bool) -> void: - _report_collector.set_consume_reports(enabled) - - -func fire_event(event :GdUnitEvent) -> void: - if _debug: - GdUnitSignals.instance().gdunit_event_debug.emit(event) - else: - GdUnitSignals.instance().gdunit_event.emit(event) - - -func fire_test_skipped(test_suite :GdUnitTestSuite, test_case :_TestCase): - set_stage(STAGE_TEST_CASE_BEFORE) - fire_event(GdUnitEvent.new()\ - .test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case.get_name())) - var statistics = { - GdUnitEvent.ORPHAN_NODES: 0, - GdUnitEvent.ELAPSED_TIME: 0, - GdUnitEvent.WARNINGS: false, - GdUnitEvent.ERRORS: false, - GdUnitEvent.ERROR_COUNT: 0, - GdUnitEvent.FAILED: false, - GdUnitEvent.FAILED_COUNT: 0, - GdUnitEvent.SKIPPED: true, - GdUnitEvent.SKIPPED_COUNT: 1, - } - set_stage(STAGE_TEST_CASE_AFTER) - var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) - fire_event(GdUnitEvent.new()\ - .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case.get_name(), statistics, [report])) - - -func fire_test_suite_skipped(test_suite :GdUnitTestSuite): - var skip_count := test_suite.get_child_count() - set_stage(STAGE_TEST_SUITE_BEFORE) - fire_event(GdUnitEvent.new()\ - .suite_before(test_suite.get_script().resource_path, test_suite.get_name(), skip_count)) - var statistics = { - GdUnitEvent.ORPHAN_NODES: 0, - GdUnitEvent.ELAPSED_TIME: 0, - GdUnitEvent.WARNINGS: false, - GdUnitEvent.ERRORS: false, - GdUnitEvent.ERROR_COUNT: 0, - GdUnitEvent.FAILED: false, - GdUnitEvent.FAILED_COUNT: 0, - GdUnitEvent.SKIPPED_COUNT: skip_count, - GdUnitEvent.SKIPPED: true - } - set_stage(STAGE_TEST_SUITE_AFTER) - var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, -1, GdAssertMessages.test_suite_skipped(test_suite.__skip_reason, skip_count)) - fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), statistics, [report])) - - -func suite_before(test_suite :GdUnitTestSuite): - set_stage(STAGE_TEST_SUITE_BEFORE) - fire_event(GdUnitEvent.new()\ - .suite_before(test_suite.get_script().resource_path, test_suite.get_name(), test_suite.get_child_count())) - _testsuite_timer = LocalTime.now() - _total_test_errors = 0 - _total_test_failed = 0 - _total_test_warnings = 0 - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.TESTSUITE, true) - @warning_ignore("redundant_await") - await test_suite.before() - _memory_pool.monitor_stop() - - -func suite_after(test_suite :GdUnitTestSuite): - set_stage(STAGE_TEST_SUITE_AFTER) - GdUnitTools.clear_tmp() - - var is_warning := _total_test_warnings != 0 - var is_skipped := test_suite.__is_skipped - var skip_count := test_suite.get_child_count() - var orphan_nodes := 0 - var reports := _report_collector.get_reports(STAGE_TEST_SUITE_BEFORE) - - if not is_skipped: - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.TESTSUITE) - skip_count = 0 - @warning_ignore("redundant_await") - await test_suite.after() - GdUnitTools.append_array(reports, _report_collector.get_reports(STAGE_TEST_SUITE_AFTER)) - _memory_pool.free_pool() - _memory_pool.monitor_stop() - orphan_nodes = _memory_pool.orphan_nodes() - if orphan_nodes > 0: - reports.push_front(GdUnitReport.new() \ - .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphan_nodes))) - - var is_error := _total_test_errors != 0 or _report_collector.has_errors(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER) - var is_failed := _total_test_failed != 0 or _report_collector.has_failures(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER) - # create report - var statistics = { - GdUnitEvent.ORPHAN_NODES: orphan_nodes, - GdUnitEvent.ELAPSED_TIME: _testsuite_timer.elapsed_since_ms(), - GdUnitEvent.WARNINGS: is_warning, - GdUnitEvent.ERRORS: is_error, - GdUnitEvent.ERROR_COUNT: _report_collector.count_errors(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER), - GdUnitEvent.FAILED: is_failed, - GdUnitEvent.FAILED_COUNT: _report_collector.count_failures(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER), - GdUnitEvent.SKIPPED_COUNT: skip_count, - GdUnitEvent.SKIPPED: is_skipped - } - fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), statistics, reports)) - _report_collector.clear_reports(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER) - - -func test_before(test_suite :GdUnitTestSuite, test_case_name :String, do_fire_event := true): - set_stage(STAGE_TEST_CASE_BEFORE) - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.TESTCASE, true) - - _total_test_execution_orphans = 0 - if do_fire_event: - _testcase_timer = LocalTime.now() - fire_event(GdUnitEvent.new()\ - .test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name)) - - @warning_ignore("redundant_await") - await test_suite.before_test() - _memory_pool.monitor_stop() - - -func test_after(test_suite :GdUnitTestSuite, test_case :_TestCase, test_case_name :String, do_fire_event := true): - _memory_pool.free_pool() - # give objects time to finallize - await get_tree().process_frame - _memory_pool.monitor_stop() - var execution_orphan_nodes = _memory_pool.orphan_nodes() - if execution_orphan_nodes > 0: - _total_test_execution_orphans += execution_orphan_nodes - _total_test_warnings += 1 - _report_collector.push_front(STAGE_TEST_CASE_EXECUTE, GdUnitReport.new() \ - .create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test(execution_orphan_nodes))) - - var is_error := false - if test_case.is_interupted() and not test_case.is_expect_interupted(): - _report_collector.add_report(STAGE_TEST_CASE_EXECUTE, test_case.report()) - is_error = true - - set_stage(STAGE_TEST_CASE_AFTER) - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.TESTCASE) - @warning_ignore("redundant_await") - await test_suite.after_test() - _memory_pool.free_pool() - _memory_pool.monitor_stop() - var test_setup_orphan_nodes = _memory_pool.orphan_nodes() - if test_setup_orphan_nodes > 0: - _total_test_warnings += 1 - _total_test_execution_orphans += test_setup_orphan_nodes - _report_collector.push_front(STAGE_TEST_CASE_AFTER, GdUnitReport.new() \ - .create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(test_setup_orphan_nodes))) - - var reports := _report_collector.get_reports(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) - var error_count := _report_collector.count_errors(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) if is_error else 0 - var failure_count := _report_collector.count_failures(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) - var is_warning := _report_collector.has_warnings(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) - - _total_test_errors += error_count - _total_test_failed += failure_count - var statistics = { - GdUnitEvent.ORPHAN_NODES: _total_test_execution_orphans, - GdUnitEvent.ELAPSED_TIME: _testcase_timer.elapsed_since_ms(), - GdUnitEvent.WARNINGS: is_warning, - GdUnitEvent.ERRORS: is_error, - GdUnitEvent.ERROR_COUNT: error_count, - GdUnitEvent.FAILED: failure_count > 0, - GdUnitEvent.FAILED_COUNT: failure_count, - GdUnitEvent.SKIPPED: test_case.is_skipped(), - GdUnitEvent.SKIPPED_COUNT: int(test_case.is_skipped()), - } - - if do_fire_event: - fire_event(GdUnitEvent.new()\ - .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name, statistics, reports.duplicate())) - _report_collector.clear_reports(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) - - -func execute_test_case_single(test_suite :GdUnitTestSuite, test_case :_TestCase): - await test_before(test_suite, test_case.get_name()) - - set_stage(STAGE_TEST_CASE_EXECUTE) - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.EXECUTE, true) - test_case.generate_seed() - await test_case.execute() - test_case.dispose() - await test_after(test_suite, test_case, test_case.get_name()) - - -func execute_test_case_iterative(test_suite :GdUnitTestSuite, test_case :_TestCase): - test_case.generate_seed() - var fuzzers := create_fuzzers(test_suite, test_case) - var is_failure := false - for iteration in test_case.iterations(): - # call before_test for each iteration - await test_before(test_suite, test_case.get_name(), iteration==0) - - set_stage(STAGE_TEST_CASE_EXECUTE) - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.EXECUTE, true) - await test_case.execute(fuzzers, iteration) - - var reports := _report_collector.get_reports(STAGE_TEST_CASE_EXECUTE) - # interrupt at first failure - if not reports.is_empty(): - is_failure = true - var report :GdUnitReport = _report_collector.pop_front(STAGE_TEST_CASE_EXECUTE) - _report_collector.add_report(STAGE_TEST_CASE_EXECUTE, GdUnitReport.new() \ - .create(GdUnitReport.FAILURE, report.line_number(), GdAssertMessages.fuzzer_interuped(iteration, report.message()))) - - if test_case.is_interupted(): - is_failure = true - - # call after_test for each iteration - await test_after(test_suite, test_case, test_case.get_name(), iteration==test_case.iterations()-1 or is_failure) - - if test_case.is_interupted() or is_failure: - break - test_case.dispose() - - -func execute_test_case_parameterized(test_suite :GdUnitTestSuite, test_case :_TestCase): - var testcase_timer = LocalTime.now() - fire_event(GdUnitEvent.new()\ - .test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case.get_name())) - - var current_error_count = _total_test_errors - var current_failed_count = _total_test_failed - var current_warning_count =_total_test_warnings - var test_case_parameters := test_case.test_parameters() - var test_parameter_index := test_case.test_parameter_index() - var test_case_names := test_case.test_case_names() - for test_case_index in test_case.test_parameters().size(): - # is test_parameter_index is set, we run this parameterized test only - if test_parameter_index != -1 and test_parameter_index != test_case_index: - continue - await test_before(test_suite, test_case_names[test_case_index]) - set_stage(STAGE_TEST_CASE_EXECUTE) - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.EXECUTE, true) - await test_case.execute(test_case_parameters[test_case_index]) - await test_after(test_suite, test_case, test_case_names[test_case_index]) - if test_case.is_interupted(): - break - test_case.dispose() - - var statistics = { - GdUnitEvent.ORPHAN_NODES: _total_test_execution_orphans, - GdUnitEvent.ELAPSED_TIME: testcase_timer.elapsed_since_ms(), - GdUnitEvent.WARNINGS: current_warning_count != _total_test_warnings, - GdUnitEvent.ERRORS: current_error_count != _total_test_errors, - GdUnitEvent.ERROR_COUNT: 0, - GdUnitEvent.FAILED: current_failed_count != _total_test_failed, - GdUnitEvent.FAILED_COUNT: 0, - GdUnitEvent.SKIPPED: test_case.is_skipped(), - GdUnitEvent.SKIPPED_COUNT: int(test_case.is_skipped()), - } - fire_event(GdUnitEvent.new()\ - .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case.get_name(), statistics, [])) - - -func execute(test_suite :GdUnitTestSuite): - await Execute(test_suite) - - -func Execute(test_suite :GdUnitTestSuite) -> void: - var context := GdUnitThreadManager.get_current_context() - context.init() - - # stop checked first error if fail fast enabled - if _fail_fast and _total_test_failed > 0: - test_suite.free() - await get_tree().process_frame - ExecutionCompleted.emit() - return - var ts := test_suite - if not ts.__is_skipped and ts.get_child_count() != 0: - await suite_before(ts) - - for test_case_index in ts.get_child_count(): - var test_case := ts.get_child(test_case_index) as _TestCase - # only iterate over test case, we need to filter because of possible adding other child types checked before() or before_test() - if not test_case is _TestCase: - continue - # stop checked first error if fail fast enabled - if _fail_fast and _total_test_failed > 0: - break - ts.set_active_test_case(test_case.get_name()) - if test_case.is_skipped(): - fire_test_skipped(ts, test_case) - await get_tree().process_frame - else: - if test_case.is_parameterized(): - await execute_test_case_parameterized(ts, test_case) - elif test_case.has_fuzzer(): - await execute_test_case_iterative(ts, test_case) - else: - await execute_test_case_single(ts, test_case) - if test_case.is_interupted(): - # it needs to go this hard way to kill the outstanding yields of a test case when the test timed out - # we delete the current test suite where is execute the current test case to kill the function state - # and replace it by a clone without function state - ts = await clone_test_suite(ts) - - await suite_after(ts) - else: - fire_test_suite_skipped(ts) - # needs at least one yielding otherwise the waiting function is blocked - await get_tree().process_frame - ts.free() - context.clear() - ExecutionCompleted.emit() - - -func copy_properties(source :Object, target :Object): - if not source is _TestCase and not source is GdUnitTestSuite: - return - for property in source.get_property_list(): - var property_name = property["name"] - target.set(property_name, source.get(property_name)) - - -# clones a test suite and moves the test cases to new instance -func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite: - dispose_timers(test_suite) - var parent := test_suite.get_parent() - var _test_suite = test_suite.duplicate() - copy_properties(test_suite, _test_suite) - for child in test_suite.get_children(): - copy_properties(child, _test_suite.find_child(child.get_name(), true, false)) - # finally free current test suite instance - parent.remove_child(test_suite) - await get_tree().process_frame - test_suite.free() - parent.add_child(_test_suite) - return _test_suite - - -func dispose_timers(test_suite :GdUnitTestSuite): - GdUnitTools.release_timers() - for child in test_suite.get_children(): - if child is Timer: - child.stop() - test_suite.remove_child(child) - child.free() - - -func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[Fuzzer]: - if not test_case.has_fuzzer(): - return Array() - var fuzzers :Array[Fuzzer] = [] - for fuzzer_arg in test_case.fuzzer_arguments(): - var fuzzer := FuzzerTool.create_fuzzer(test_suite.get_script(), fuzzer_arg) - fuzzer._iteration_index = 0 - fuzzer._iteration_limit = test_case.iterations() - fuzzers.append(fuzzer) - return fuzzers diff --git a/addons/gdUnit4/src/core/GdUnitMemoryPool.gd b/addons/gdUnit4/src/core/GdUnitMemoryPool.gd deleted file mode 100644 index 38ff31a1..00000000 --- a/addons/gdUnit4/src/core/GdUnitMemoryPool.gd +++ /dev/null @@ -1,135 +0,0 @@ -class_name GdUnitMemoryPool -extends GdUnitSingleton - -const META_PARAM := "MEMORY_POOL" - - -enum POOL { - TESTSUITE, - TESTCASE, - EXECUTE, - UNIT_TEST_ONLY, - ALL, -} - - -var _monitors := { - POOL.TESTSUITE : GdUnitMemMonitor.new("TESTSUITE"), - POOL.TESTCASE : GdUnitMemMonitor.new("TESTCASE"), - POOL.EXECUTE : GdUnitMemMonitor.new("EXECUTE"), - POOL.UNIT_TEST_ONLY : GdUnitMemMonitor.new("UNIT_TEST_ONLY"), -} - - -class MemoryStore extends RefCounted: - var _store :Array[Variant] = [] - - - func _notification(what): - if what == NOTIFICATION_PREDELETE: - while not _store.is_empty(): - var value :Variant = _store.pop_front() - GdUnitTools.free_instance(value) - - - static func pool(p_pool :POOL) -> MemoryStore: - var pool_name :String = POOL.keys()[p_pool] - return GdUnitSingleton.instance(pool_name, func(): return MemoryStore.new()) - - - static func append(p_pool :POOL, value :Variant) -> void: - pool(p_pool)._store.append(value) - - - static func contains(p_pool :POOL, value :Variant) -> bool: - return pool(p_pool)._store.has(value) - - - static func push_front(p_pool :POOL, value :Variant) -> void: - pool(p_pool)._store.push_front(value) - - - static func release_objects(p_pool :POOL) -> void: - var store := pool(p_pool)._store - while not store.is_empty(): - var value :Variant = store.pop_front() - GdUnitTools.free_instance(value) - - -var _current :POOL -var _orphan_detection_enabled :bool = true - - -func _init(): - configure(GdUnitSettings.is_verbose_orphans()) - - -func configure(orphan_detection :bool) -> void: - _orphan_detection_enabled = orphan_detection - if not _orphan_detection_enabled: - prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.") - - -func set_pool(obj :Object, pool_id :POOL, reset_monitor: bool = false) -> void: - _current = pool_id - obj.set_meta(META_PARAM, pool_id) - var monitor := get_monitor(pool_id) - if reset_monitor: - monitor.reset() - monitor.start() - - -func monitor_stop() -> void: - var monitor := get_monitor(_current) - monitor.stop() - - -func free_pool() -> void: - GdUnitMemoryPool.run_auto_free(_current) - - -func get_monitor(pool_id :POOL) -> GdUnitMemMonitor: - return _monitors.get(pool_id) - - -func orphan_nodes() -> int: - if _orphan_detection_enabled: - return _monitors.get(_current).orphan_nodes() - return 0 - - -# register an instance to be freed when a test suite is finished -static func register_auto_free(obj, pool :POOL) -> Variant: - # do not register on GDScriptNativeClass - if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") : - return obj - if obj is GDScript or obj is ScriptExtension: - return obj - if obj is MainLoop: - push_error("avoid to add mainloop to auto_free queue %s" % obj) - return - # only register pure objects - if obj is GdUnitSceneRunner: - MemoryStore.push_front(pool, obj) - else: - MemoryStore.append(pool, obj) - return obj - - -# runs over all registered objects and frees it -static func run_auto_free(pool :POOL) -> void: - MemoryStore.release_objects(pool) - - -# tests if given object is registered for auto freeing -static func is_auto_free_registered(obj, pool :POOL = POOL.ALL) -> bool: - # only register real object values - if not is_instance_valid(obj): - return false - # check all pools? - if pool == POOL.ALL: - return is_auto_free_registered(obj, POOL.TESTSUITE)\ - or is_auto_free_registered(obj, POOL.TESTCASE)\ - or is_auto_free_registered(obj, POOL.EXECUTE) - # check checked a specific pool - return MemoryStore.contains(pool, obj) diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd index 2c847c26..b317d392 100644 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd +++ b/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd @@ -9,7 +9,7 @@ static func verify(obj :Object, times): static func verify_no_interactions(obj :Object) -> GdUnitAssert: - var gd_assert := GdUnitAssertImpl.new("") + var gd_assert = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new("") if not _is_mock_or_spy(obj, "__verify"): return gd_assert.report_success() var summary :Dictionary = obj.__verify_no_interactions() @@ -19,7 +19,7 @@ static func verify_no_interactions(obj :Object) -> GdUnitAssert: static func verify_no_more_interactions(obj :Object) -> GdUnitAssert: - var gd_assert := GdUnitAssertImpl.new("") + var gd_assert = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new("") if not _is_mock_or_spy(obj, "__verify_no_more_interactions"): return gd_assert var summary :Dictionary = obj.__verify_no_more_interactions() diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd index d4f10700..5d455abb 100644 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd +++ b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd @@ -1,4 +1,4 @@ -class_name GdUnitObjectInteractionsTemplate +const GdUnitAssertImpl := preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") var __expected_interactions :int = -1 var __saved_interactions := Dictionary() diff --git a/addons/gdUnit4/src/core/Result.gd b/addons/gdUnit4/src/core/GdUnitResult.gd similarity index 72% rename from addons/gdUnit4/src/core/Result.gd rename to addons/gdUnit4/src/core/GdUnitResult.gd index 6b709537..cda3d51a 100644 --- a/addons/gdUnit4/src/core/Result.gd +++ b/addons/gdUnit4/src/core/GdUnitResult.gd @@ -1,4 +1,4 @@ -class_name Result +class_name GdUnitResult extends RefCounted enum { @@ -14,32 +14,32 @@ var _error_message := "" var _value :Variant = null -static func empty() -> Result: - var result := Result.new() +static func empty() -> GdUnitResult: + var result := GdUnitResult.new() result._state = EMPTY return result -static func success(p_value :Variant) -> Result: +static func success(p_value :Variant) -> GdUnitResult: assert(p_value != null, "The value must not be NULL") - var result := Result.new() + var result := GdUnitResult.new() result._value = p_value result._state = SUCCESS return result -static func warn(p_warn_message :String, p_value :Variant = null) -> Result: +static func warn(p_warn_message :String, p_value :Variant = null) -> GdUnitResult: assert(not p_warn_message.is_empty()) #,"The message must not be empty") - var result := Result.new() + var result := GdUnitResult.new() result._value = p_value result._warn_message = p_warn_message result._state = WARN return result -static func error(p_error_message :String) -> Result: +static func error(p_error_message :String) -> GdUnitResult: assert(not p_error_message.is_empty(), "The message must not be empty") - var result := Result.new() + var result := GdUnitResult.new() result._value = null result._error_message = p_error_message result._state = ERROR @@ -81,12 +81,12 @@ func warn_message() -> String: func _to_string() -> String: - return str(Result.serialize(self)) + return str(GdUnitResult.serialize(self)) -static func serialize(result :Result) -> Dictionary: +static func serialize(result :GdUnitResult) -> Dictionary: if result == null: - push_error("Can't serialize a Null object from type Result") + push_error("Can't serialize a Null object from type GdUnitResult") return { "state" : result._state, "value" : var_to_str(result._value), @@ -95,8 +95,8 @@ static func serialize(result :Result) -> Dictionary: } -static func deserialize(config :Dictionary) -> Result: - var result := Result.new() +static func deserialize(config :Dictionary) -> GdUnitResult: + var result := GdUnitResult.new() result._value = str_to_var(config.get("value", "")) result._warn_message = config.get("warn_msg", null) result._error_message = config.get("err_msg", null) diff --git a/addons/gdUnit4/src/core/GdUnitRunner.gd b/addons/gdUnit4/src/core/GdUnitRunner.gd index 0e61f7ea..c383fc3f 100644 --- a/addons/gdUnit4/src/core/GdUnitRunner.gd +++ b/addons/gdUnit4/src/core/GdUnitRunner.gd @@ -2,10 +2,9 @@ extends Node signal sync_rpc_id_result_received -const GdUnitExecutor = preload("res://addons/gdUnit4/src/core/GdUnitExecutor.gd") @onready var _client :GdUnitTcpClient = $GdUnitTcpClient -@onready var _executor :GdUnitExecutor = $GdUnitExecutor +@onready var _executor :GdUnitTestSuiteExecutor = GdUnitTestSuiteExecutor.new() enum { INIT, @@ -25,13 +24,13 @@ var _cs_executor func _init(): # minimize scene window checked debug mode if OS.get_cmdline_args().size() == 1: - DisplayServer.window_set_title("GdUnit3 Runner (Debug Mode)") + DisplayServer.window_set_title("GdUnit4 Runner (Debug Mode)") else: - DisplayServer.window_set_title("GdUnit3 Runner (Release Mode)") + DisplayServer.window_set_title("GdUnit4 Runner (Release Mode)") DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) # store current runner instance to engine meta data to can be access in as a singleton Engine.set_meta(GDUNIT_RUNNER, self) - _cs_executor = GdUnit3MonoAPI.create_executor(self) + _cs_executor = GdUnit4MonoApiLoader.create_executor(self) func _ready(): @@ -79,10 +78,11 @@ func _process(_delta): # process next test suite set_process(false) var test_suite :Node = _test_suites_to_process.pop_front() - add_child(test_suite) - var executor = _cs_executor if GdObjects.is_cs_test_suite(test_suite) else _executor - executor.Execute(test_suite) - await executor.ExecutionCompleted + if _cs_executor != null and _cs_executor.IsExecutable(test_suite): + _cs_executor.Execute(test_suite) + await _cs_executor.ExecutionCompleted + else: + await _executor.execute(test_suite) set_process(true) STOP: _state = EXIT @@ -162,6 +162,7 @@ func _on_gdunit_event(event :GdUnitEvent): _client.rpc_send(RPCGdUnitEvent.of(event)) -func PublishEvent(data) -> void: - var event := GdUnitEvent.new().deserialize(data.AsDictionary()) +# Event bridge from C# GdUnit4.ITestEventListener.cs +func PublishEvent(data :Dictionary) -> void: + var event := GdUnitEvent.new().deserialize(data) _client.rpc_send(RPCGdUnitEvent.of(event)) diff --git a/addons/gdUnit4/src/core/GdUnitRunner.tscn b/addons/gdUnit4/src/core/GdUnitRunner.tscn index 99586a51..c1f67b15 100644 --- a/addons/gdUnit4/src/core/GdUnitRunner.tscn +++ b/addons/gdUnit4/src/core/GdUnitRunner.tscn @@ -1,14 +1,10 @@ -[gd_scene load_steps=4 format=3 uid="uid://belidlfknh74r"] +[gd_scene load_steps=3 format=3 uid="uid://belidlfknh74r"] [ext_resource type="Script" path="res://addons/gdUnit4/src/core/GdUnitRunner.gd" id="1"] [ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitTcpClient.gd" id="2"] -[ext_resource type="Script" path="res://addons/gdUnit4/src/core/GdUnitExecutor.gd" id="3"] [node name="Control" type="Node"] script = ExtResource("1") -[node name="GdUnitExecutor" type="Node" parent="."] -script = ExtResource("3") - [node name="GdUnitTcpClient" type="Node" parent="."] script = ExtResource("2") diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd index 15d3ad0e..f9816cc4 100644 --- a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd @@ -1,6 +1,8 @@ class_name GdUnitRunnerConfig extends Resource +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const CONFIG_VERSION = "1.0" const VERSION = "version" const INCLUDED = "included" @@ -102,35 +104,35 @@ func skipped() -> Dictionary: return _config.get(SKIPPED, PackedStringArray()) -func save_config(path :String = CONFIG_FILE) -> Result: +func save_config(path :String = CONFIG_FILE) -> GdUnitResult: var file := FileAccess.open(path, FileAccess.WRITE) if file == null: var error = FileAccess.get_open_error() - return Result.error("Can't write test runner configuration '%s'! %s" % [path, GdUnitTools.error_as_string(error)]) + return GdUnitResult.error("Can't write test runner configuration '%s'! %s" % [path, GdUnitTools.error_as_string(error)]) _config[VERSION] = CONFIG_VERSION file.store_string(JSON.stringify(_config)) - return Result.success(path) + return GdUnitResult.success(path) -func load_config(path :String = CONFIG_FILE) -> Result: +func load_config(path :String = CONFIG_FILE) -> GdUnitResult: if not FileAccess.file_exists(path): - return Result.error("Can't find test runner configuration '%s'! Please select a test to run." % path) + return GdUnitResult.error("Can't find test runner configuration '%s'! Please select a test to run." % path) var file := FileAccess.open(path, FileAccess.READ) if file == null: var error = FileAccess.get_open_error() - return Result.error("Can't load test runner configuration '%s'! ERROR: %s." % [path, GdUnitTools.error_as_string(error)]) + return GdUnitResult.error("Can't load test runner configuration '%s'! ERROR: %s." % [path, GdUnitTools.error_as_string(error)]) var content := file.get_as_text() if not content.is_empty() and content[0] == '{': # Parse as json var test_json_conv := JSON.new() var error := test_json_conv.parse(content) if error != OK: - return Result.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) + return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) _config = test_json_conv.get_data() as Dictionary if not _config.has(VERSION): - return Result.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) + return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) fix_value_types() - return Result.success(path) + return GdUnitResult.success(path) func fix_value_types(): diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd index e6222389..ed6dffaa 100644 --- a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd @@ -2,6 +2,10 @@ class_name GdUnitSceneRunnerImpl extends GdUnitSceneRunner + +var GdUnitFuncAssertImpl := ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE) + + # mapping of mouse buttons and his masks const MAP_MOUSE_BUTTON_MASKS := { MOUSE_BUTTON_LEFT : MOUSE_BUTTON_MASK_LEFT, @@ -16,6 +20,7 @@ const MAP_MOUSE_BUTTON_MASKS := { var _scene_tree :SceneTree = null var _current_scene :Node = null +var _awaiter :GdUnitAwaiter = GdUnitAwaiter.new() var _verbose :bool var _simulate_start_time :LocalTime var _last_input_event :InputEvent = null @@ -73,7 +78,7 @@ func _notification(what): if is_instance_valid(_current_scene): _scene_tree.root.remove_child(_current_scene) # don't free already memory managed instances - if not GdUnitMemoryPool.is_auto_free_registered(_current_scene): + if not GdUnitMemoryObserver.is_marked_auto_free(_current_scene): _current_scene.free() _scene_tree = null _current_scene = null @@ -208,13 +213,13 @@ func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner: 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: var args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) - await GdUnitAwaiter.await_signal_idle_frames(_current_scene, signal_name, args, 10000) + await _awaiter.await_signal_idle_frames(_current_scene, signal_name, args, 10000) return self 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: var args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) - await GdUnitAwaiter.await_signal_idle_frames(source, signal_name, args, 10000) + await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000) return self @@ -227,11 +232,11 @@ func await_func_on(instance :Object, func_name :String, args := []) -> GdUnitFun func await_signal(signal_name :String, args := [], timeout := 2000 ): - await GdUnitAwaiter.await_signal_on(_current_scene, signal_name, args, timeout) + await _awaiter.await_signal_on(_current_scene, signal_name, args, timeout) func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ): - await GdUnitAwaiter.await_signal_on(source, signal_name, args, timeout) + await _awaiter.await_signal_on(source, signal_name, args, timeout) # maximizes the window to bring the scene visible diff --git a/addons/gdUnit4/src/core/GdUnitScriptType.gd b/addons/gdUnit4/src/core/GdUnitScriptType.gd index 8a691016..7e1be519 100644 --- a/addons/gdUnit4/src/core/GdUnitScriptType.gd +++ b/addons/gdUnit4/src/core/GdUnitScriptType.gd @@ -4,18 +4,13 @@ extends RefCounted const UNKNOWN := "" const CS := "cs" const GD := "gd" -const NATIVE := "gdns" -const VS := "vs" + static func type_of(script :Script) -> String: if script == null: return UNKNOWN if GdObjects.is_gd_script(script): return GD - if GdObjects.is_vs_script(script): - return VS - if GdObjects.is_native_script(script): - return NATIVE if GdObjects.is_cs_script(script): return CS return UNKNOWN diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd b/addons/gdUnit4/src/core/GdUnitSettings.gd index 52ea7a24..d9466a12 100644 --- a/addons/gdUnit4/src/core/GdUnitSettings.gd +++ b/addons/gdUnit4/src/core/GdUnitSettings.gd @@ -12,7 +12,7 @@ const SERVER_TIMEOUT = GROUP_COMMON + "/server_connection_timeout_minutes" const GROUP_TEST = COMMON_SETTINGS + "/test" const TEST_TIMEOUT = GROUP_TEST + "/test_timeout_seconds" -const TEST_ROOT_FOLDER = GROUP_TEST + "/test_root_folder" +const TEST_LOOKUP_FOLDER = GROUP_TEST + "/test_lookup_folder" const TEST_SITE_NAMING_CONVENTION = GROUP_TEST + "/test_suite_naming_convention" @@ -74,7 +74,10 @@ const DEFAULT_SERVER_TIMEOUT :int = 30 # test case runtime timeout in seconds const DEFAULT_TEST_TIMEOUT :int = 60*5 # the folder to create new test-suites -const DEFAULT_TEST_ROOT_FOLDER := "test" +const DEFAULT_TEST_LOOKUP_FOLDER := "test" + +# help texts +const HELP_TEST_LOOKUP_FOLDER := "Sets the subfolder for the search/creation of test suites. (leave empty to use source folder)" enum NAMING_CONVENTIONS { AUTO_DETECT, @@ -87,7 +90,7 @@ static func setup(): create_property_if_need(UPDATE_NOTIFICATION_ENABLED, true, "Enables/Disables the update notification checked startup.") create_property_if_need(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT, "Sets the server connection timeout in minutes.") create_property_if_need(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT, "Sets the test case runtime timeout in seconds.") - create_property_if_need(TEST_ROOT_FOLDER, DEFAULT_TEST_ROOT_FOLDER, "Sets the root folder where test-suites located/generated.") + create_property_if_need(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER, HELP_TEST_LOOKUP_FOLDER) create_property_if_need(TEST_SITE_NAMING_CONVENTION, NAMING_CONVENTIONS.AUTO_DETECT, "Sets test-suite genrate script name convention.", NAMING_CONVENTIONS.keys()) create_property_if_need(REPORT_PUSH_ERRORS, false, "Enables/Disables report of push_error() as failure!") create_property_if_need(REPORT_SCRIPT_ERRORS, true, "Enables/Disables report of script errors as failure!") @@ -99,6 +102,17 @@ static func setup(): create_property_if_need(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, false, "Shows/Hides the 'Run overall Tests' button in the inspector toolbar.") create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Defines the test suite template") create_shortcut_properties_if_need() + migrate_properties() + + +static func migrate_properties() -> void: + var TEST_ROOT_FOLDER := "gdunit4/settings/test/test_root_folder" + if get_property(TEST_ROOT_FOLDER) != null: + migrate_property(TEST_ROOT_FOLDER,\ + TEST_LOOKUP_FOLDER,\ + DEFAULT_TEST_LOOKUP_FOLDER,\ + HELP_TEST_LOOKUP_FOLDER,\ + func(value): return DEFAULT_TEST_LOOKUP_FOLDER if value == null else value) static func create_shortcut_properties_if_need() -> void: @@ -118,18 +132,21 @@ static func create_shortcut_properties_if_need() -> void: static func create_property_if_need(name :String, default :Variant, help :="", value_set := PackedStringArray()) -> void: if not ProjectSettings.has_setting(name): - #prints("GdUnit3: Set inital settings '%s' to '%s'." % [name, str(default)]) + #prints("GdUnit4: Set inital settings '%s' to '%s'." % [name, str(default)]) ProjectSettings.set_setting(name, default) - + ProjectSettings.set_initial_value(name, default) - var hint_string := help + ("" if value_set.is_empty() else " %s" % value_set) - var info = { - "name": name, - "type": typeof(default), - "hint": PROPERTY_HINT_TYPE_STRING, - "hint_string": hint_string - } - ProjectSettings.add_property_info(info) + help += "" if value_set.is_empty() else " %s" % value_set + set_help(name, default, help) + + +static func set_help(property_name :String, value :Variant, help :String) -> void: + ProjectSettings.add_property_info({ + "name": property_name, + "type": typeof(value), + "hint": PROPERTY_HINT_TYPE_STRING, + "hint_string": help + }) static func get_setting(name :String, default :Variant) -> Variant: @@ -171,7 +188,7 @@ static func test_timeout() -> int: # the root folder to store/generate test-suites static func test_root_folder() -> String: - return get_setting(TEST_ROOT_FOLDER, DEFAULT_TEST_ROOT_FOLDER) + return get_setting(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER) static func is_verbose_assert_warnings() -> bool: @@ -233,31 +250,57 @@ static func extract_value_set_from_help(value :String) -> PackedStringArray: return values.replacen(" ", "").replacen("\"", "").split(",", false) -static func update_property(property :GdUnitProperty) -> void: - if get_property(property.name()).value() != property.value(): +static func update_property(property :GdUnitProperty) -> Variant: + var current_value :Variant = ProjectSettings.get_setting(property.name()) + if current_value != property.value(): + var error :Variant = validate_property_value(property) + if error != null: + return error ProjectSettings.set_setting(property.name(), property.value()) GdUnitSignals.instance().gdunit_settings_changed.emit(property) - save() + _save_settings() + return null static func reset_property(property :GdUnitProperty) -> void: ProjectSettings.set_setting(property.name(), property.default()) GdUnitSignals.instance().gdunit_settings_changed.emit(property) - save() + _save_settings() + + +static func validate_property_value(property :GdUnitProperty) -> Variant: + match property.name(): + TEST_LOOKUP_FOLDER: + return validate_lookup_folder(property.value()) + _: return null + + +static func validate_lookup_folder(value :String) -> Variant: + if value.is_empty() or value == "/": + return null + if value.contains("res:"): + return "Test Lookup Folder: do not allowed to contains 'res://'" + if not value.is_valid_filename(): + return "Test Lookup Folder: contains invalid characters! e.g (: / \\ ? * \" | % < >)" + return null static func save_property(name :String, value) -> void: ProjectSettings.set_setting(name, value) - save() + _save_settings() -static func save() -> void: - var err := ProjectSettings.save() +static func _save_settings() -> void: + var err = ProjectSettings.save() if err != OK: - push_error("Save GdUnit3 settings failed : %s" % GdUnitTools.error_as_string(err)) + push_error("Save GdUnit4 settings failed : %s" % error_string(err)) return +static func has_property(name :String) -> bool: + return ProjectSettings.get_property_list().any( func(property): return property["name"] == name) + + static func get_property(name :String) -> GdUnitProperty: for property in ProjectSettings.get_property_list(): var property_name = property["name"] @@ -270,14 +313,15 @@ static func get_property(name :String) -> GdUnitProperty: return null -static func migrate_property(old_property :String, new_property :String, converter := Callable()) -> void: +static func migrate_property(old_property :String, new_property :String, default_value :Variant, help :String, converter := Callable()) -> void: var property := get_property(old_property) if property == null: - prints("Migration not possible, property '%s' not found", old_property) + prints("Migration not possible, property '%s' not found" % old_property) return var value = converter.call(property.value()) if converter.is_valid() else property.value() - create_property_if_need(new_property, property.default(), property.help(), property.value_set()) ProjectSettings.set_setting(new_property, value) + ProjectSettings.set_initial_value(new_property, default_value) + set_help(new_property, value, help) ProjectSettings.clear(old_property) prints("Succesfull migrated property '%s' -> '%s' value: %s" % [old_property, new_property, value]) diff --git a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd new file mode 100644 index 00000000..cf55adcf --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd @@ -0,0 +1,102 @@ +# It connects to all signals of given emitter and collects received signals and arguments +# The collected signals are cleand finally when the emitter is freed. +class_name GdUnitSignalCollector +extends RefCounted + +const NO_ARG :Variant = GdUnitConstants.NO_ARG +const SIGNAL_BLACK_LIST = []#["tree_exiting", "tree_exited", "child_exiting_tree"] + +# { +# emitter : { +# signal_name : [signal_args], +# ... +# } +# } +var _collected_signals :Dictionary = {} + + +func clear() -> void: + for emitter in _collected_signals.keys(): + if is_instance_valid(emitter): + unregister_emitter(emitter) + + +# connect to all possible signals defined by the emitter +# prepares the signal collection to store received signals and arguments +func register_emitter(emitter :Object): + if is_instance_valid(emitter): + # check emitter is already registerd + if _collected_signals.has(emitter): + return + _collected_signals[emitter] = Dictionary() + # connect to 'tree_exiting' of the emitter to finally release all acquired resources/connections. + if emitter is Node and !emitter.tree_exiting.is_connected(unregister_emitter): + emitter.tree_exiting.connect(unregister_emitter.bind(emitter)) + # connect to all signals of the emitter we want to collect + for signal_def in emitter.get_signal_list(): + var signal_name = signal_def["name"] + # set inital collected to empty + if not is_signal_collecting(emitter, signal_name): + _collected_signals[emitter][signal_name] = Array() + if SIGNAL_BLACK_LIST.find(signal_name) != -1: + continue + if !emitter.is_connected(signal_name, _on_signal_emmited): + var err := emitter.connect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + if err != OK: + push_error("Can't connect to signal %s on %s. Error: %s" % [signal_name, emitter, error_string(err)]) + + +# unregister all acquired resources/connections, otherwise it ends up in orphans +# is called when the emitter is removed from the parent +func unregister_emitter(emitter :Object): + if is_instance_valid(emitter): + for signal_def in emitter.get_signal_list(): + var signal_name = signal_def["name"] + if emitter.is_connected(signal_name, _on_signal_emmited): + emitter.disconnect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + _collected_signals.erase(emitter) + + +# receives the signal from the emitter with all emitted signal arguments and additional the emitter and signal_name as last two arguements +func _on_signal_emmited( 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, arg10=NO_ARG, arg11=NO_ARG): + var signal_args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11], NO_ARG) + # extract the emitter and signal_name from the last two arguments (see line 61 where is added) + var signal_name :String = signal_args.pop_back() + var emitter :Object = signal_args.pop_back() + # prints("_on_signal_emmited:", emitter, signal_name, signal_args) + if is_signal_collecting(emitter, signal_name): + _collected_signals[emitter][signal_name].append(signal_args) + + +func reset_received_signals(emitter :Object): + # _debug_signal_list("before claer"); + if _collected_signals.has(emitter): + for signal_name in _collected_signals[emitter]: + _collected_signals[emitter][signal_name].clear() + # _debug_signal_list("after claer"); + + +func is_signal_collecting(emitter :Object, signal_name :String) -> bool: + return _collected_signals.has(emitter) and _collected_signals[emitter].has(signal_name) + + +func match(emitter :Object, signal_name :String, args :Array) -> bool: + #prints("match", signal_name, _collected_signals[emitter][signal_name]); + if _collected_signals.is_empty() or not _collected_signals.has(emitter): + return false + for received_args in _collected_signals[emitter][signal_name]: + # prints("testing", signal_name, received_args, "vs", args) + if GdObjects.equals(received_args, args): + return true + return false + + +func _debug_signal_list(message :String): + prints("-----", message, "-------") + prints("senders {") + for emitter in _collected_signals: + prints("\t", emitter) + for signal_name in _collected_signals[emitter]: + var args = _collected_signals[emitter][signal_name] + prints("\t\t", signal_name, args) + prints("}") diff --git a/addons/gdUnit4/src/core/GdUnitSignals.gd b/addons/gdUnit4/src/core/GdUnitSignals.gd index 9dccdf9c..faf089f4 100644 --- a/addons/gdUnit4/src/core/GdUnitSignals.gd +++ b/addons/gdUnit4/src/core/GdUnitSignals.gd @@ -9,7 +9,7 @@ signal gdunit_event(event :GdUnitEvent) signal gdunit_event_debug(event :GdUnitEvent) signal gdunit_add_test_suite(test_suite :GdUnitTestSuiteDto) signal gdunit_message(message :String) -signal gdunit_report(report :GdUnitReport) +signal gdunit_report(execution_context_id :int, report :GdUnitReport) signal gdunit_set_test_failed(is_failed :bool) signal gdunit_settings_changed(property :GdUnitProperty) diff --git a/addons/gdUnit4/src/core/GdUnitSingleton.gd b/addons/gdUnit4/src/core/GdUnitSingleton.gd index df0ab91e..4fb05619 100644 --- a/addons/gdUnit4/src/core/GdUnitSingleton.gd +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd @@ -8,6 +8,7 @@ class_name GdUnitSingleton extends RefCounted +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const MEATA_KEY := "GdUnitSingletons" @@ -30,10 +31,10 @@ static func unregister(p_singleton :String) -> void: var index := singletons.find(p_singleton) singletons.remove_at(index) var instance_ :Variant = Engine.get_meta(p_singleton) - GdUnitTools.prints_verbose(" Free singeleton instance '%s:%s'" % [p_singleton, instance_]) + GdUnitTools.prints_verbose(" Free singleton instance '%s:%s'" % [p_singleton, instance_]) GdUnitTools.free_instance(instance_) Engine.remove_meta(p_singleton) - GdUnitTools.prints_verbose(" Succesfully freed '%s'" % p_singleton) + GdUnitTools.prints_verbose(" Successfully freed '%s'" % p_singleton) Engine.set_meta(MEATA_KEY, singletons) diff --git a/addons/gdUnit4/src/core/GdUnitStaticDictionary.gd b/addons/gdUnit4/src/core/GdUnitStaticDictionary.gd deleted file mode 100644 index 1fa3606e..00000000 --- a/addons/gdUnit4/src/core/GdUnitStaticDictionary.gd +++ /dev/null @@ -1,66 +0,0 @@ -# implements a Dictionary with static accessors -class_name GdUnitStaticDictionary -extends GdUnitSingleton - - -static func __data() -> Dictionary: - return instance("GdUnitStaticVariables", func(): return {}) - - -static func add_value(key : Variant, value : Variant, overwrite := false) -> Variant: - var data :Dictionary = __data() - if overwrite and data.has(key): - push_error("An value already exists with key: %s" % key) - return null - data[key] = value - #Engine.set_meta("GdUnitStaticVariables", data) - return value - - -static func erase(key: Variant) -> bool: - var data :Dictionary = __data() - if data.has(key): - data.erase(key) - #Engine.set_meta("GdUnitStaticVariables", data) - return true - return false - - -static func clear() -> void: - Engine.set_meta("GdUnitStaticVariables", {}) - - -func find_key(value: Variant) -> Variant: - return GdUnitStaticDictionary.__data().find_key(value) - - -static func get_value(key: Variant, default: Variant = null) -> Variant: - return GdUnitStaticDictionary.__data().get(key, default) - - -static func has_key(key: Variant) -> bool: - return __data().has(key) - - -static func has_keys(keys_: Array) -> bool: - return __data().has_all(keys_) - - -static func is_empty() -> bool: - return __data().is_empty() - - -static func keys() -> Array: - return __data().keys() - - -static func size() -> int: - return __data().size() - - -static func values() -> Array: - return __data().values() - - -func _to_string() -> String: - return str(GdUnitStaticDictionary.__data().keys()) diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd index 0fe3677f..7cd5dfdd 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd @@ -2,19 +2,17 @@ class_name GdUnitTestSuiteBuilder extends RefCounted -static func create(source :Script, line_number :int) -> Result: +static func create(source :Script, line_number :int) -> GdUnitResult: var test_suite_path := GdUnitTestSuiteScanner.resolve_test_suite_path(source.resource_path, GdUnitSettings.test_root_folder()) # we need to save and close the testsuite and source if is current opened before modify ScriptEditorControls.save_an_open_script(source.resource_path) - ScriptEditorControls.save_an_open_script(test_suite_path, true) - + ScriptEditorControls.save_an_open_script(test_suite_path, true) if GdObjects.is_cs_script(source): - return GdUnit3MonoAPI.create_test_suite(source.resource_path, line_number+1, test_suite_path) - + return GdUnit4MonoApiLoader.create_test_suite(source.resource_path, line_number+1, test_suite_path) var parser := GdScriptParser.new() var lines := source.source_code.split("\n") var current_line := lines[line_number] var func_name := parser.parse_func_name(current_line) if func_name.is_empty(): - return Result.error("No function found at line: %d." % line_number) + return GdUnitResult.error("No function found at line: %d." % line_number) return GdUnitTestSuiteScanner.create_test_case(test_suite_path, func_name, source.resource_path) diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd index f751253e..ff7ab6df 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd @@ -8,9 +8,17 @@ func test_${func_name}() -> void: assert_not_yet_implemented() """ + +# we exclude the gdunit source directorys by default +const exclude_scan_directories = [ + "res://addons/gdUnit4/bin", + "res://addons/gdUnit4/src", + "res://reports"] + + var _script_parser := GdScriptParser.new() var _extends_test_suite_classes := Array() -var regex_replace_class_name := GdUnitTools.to_regex("(?m)^class_name .*$") +var _expression_runner := GdUnitExpressionRunner.new() func scan_testsuite_classes() -> void: @@ -39,6 +47,8 @@ func scan(resource_path :String) -> Array[Node]: func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[Node]: + if exclude_scan_directories.has(dir.get_current_dir()): + return collected_suites prints("Scanning for test suites in:", dir.get_current_dir()) dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 var file_name := dir.get_next() @@ -49,9 +59,12 @@ func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[N if sub_dir != null: _scan_test_suites(sub_dir, collected_suites) else: + var time = LocalTime.now() var test_suite := _parse_is_test_suite(resource_path) if test_suite: collected_suites.append(test_suite) + if OS.is_stdout_verbose() and time.elapsed_since_ms() > 300: + push_warning("Scanning of test-suite '%s' took more than 300ms: " % resource_path, time.elapsed_since()) file_name = dir.get_next() return collected_suites @@ -66,8 +79,8 @@ static func _file(dir :DirAccess, file_name :String) -> String: func _parse_is_test_suite(resource_path :String) -> Node: if not GdUnitTestSuiteScanner._is_script_format_supported(resource_path): return null - if GdUnit3MonoAPI.is_test_suite(resource_path): - return GdUnit3MonoAPI.parse_test_suite(resource_path) + if GdUnit4MonoApiLoader.is_test_suite(resource_path): + return GdUnit4MonoApiLoader.parse_test_suite(resource_path) var script :Script = ResourceLoader.load(resource_path) if not GdObjects.is_test_suite(script): return null @@ -80,7 +93,7 @@ static func _is_script_format_supported(resource_path :String) -> bool: var ext := resource_path.get_extension() if ext == "gd": return true - return GdUnit3MonoAPI.is_csharp_file(resource_path) + return GdUnit4MonoApiLoader.is_csharp_file(resource_path) func _parse_test_suite(script :GDScript) -> GdUnitTestSuite: @@ -119,7 +132,7 @@ func _handle_test_suite_arguments(test_suite, script :GDScript, fd :GdFunctionDe for arg in fd.args(): match arg.name(): _TestCase.ARGUMENT_SKIP: - var result = _run_expression(script, arg.value_as_string()) + var result = _expression_runner.execute(script, arg.value_as_string()) if result is bool: test_suite.__is_skipped = result else: @@ -149,7 +162,7 @@ func _handle_test_case_arguments(test_suite, script :GDScript, fd :GdFunctionDes _TestCase.ARGUMENT_TIMEOUT: timeout = arg.default() _TestCase.ARGUMENT_SKIP: - var result = _run_expression(script, arg.value_as_string()) + var result = _expression_runner.execute(script, arg.value_as_string()) if result is bool: is_skipped = result else: @@ -187,23 +200,6 @@ func _parse_and_add_test_cases(test_suite, script :GDScript, test_case_names :Pa _handle_test_case_arguments(test_suite, script, fd) -func _run_expression(src_script :GDScript, expression :String) -> Variant: - var script := GDScript.new() - script.source_code = _remove_class_name(src_script.source_code) - script.source_code += """ - func __run_expression() -> Variant: - return $expression - """.dedent().replace("$expression", expression) - script.reload(false) - var runner := script.new() - runner.queue_free() - return runner.__run_expression() - - -func _remove_class_name(source_code :String) -> String: - return regex_replace_class_name.sub(source_code, "") - - const TEST_CASE_ARGUMENTS = [_TestCase.ARGUMENT_TIMEOUT, _TestCase.ARGUMENT_SKIP, _TestCase.ARGUMENT_SKIP_REASON, Fuzzer.ARGUMENT_ITERATIONS, Fuzzer.ARGUMENT_SEED] func _validate_argument(fd :GdFunctionDescriptor, test_case :_TestCase) -> void: @@ -234,12 +230,12 @@ static func _to_naming_convention(file_name :String) -> String: static func resolve_test_suite_path(source_script_path :String, test_root_folder :String = "test") -> String: var file_name = source_script_path.get_basename().get_file() var suite_name := _to_naming_convention(file_name) - if test_root_folder.is_empty(): + if test_root_folder.is_empty() or test_root_folder == "/": return source_script_path.replace(file_name, suite_name) # is user tmp if source_script_path.begins_with("user://tmp"): - return source_script_path.replace("user://tmp", "user://tmp/" + test_root_folder).replace(file_name, suite_name) + return normalize_path(source_script_path.replace("user://tmp", "user://tmp/" + test_root_folder)).replace(file_name, suite_name) # at first look up is the script under a "src" folder located var test_suite_path :String @@ -258,21 +254,25 @@ static func resolve_test_suite_path(source_script_path :String, test_root_folder test_suite_path = paths[0] + "//" + test_root_folder for index in range(1, paths.size()): test_suite_path += "/" + paths[index] - return test_suite_path.replace(file_name, suite_name) + return normalize_path(test_suite_path).replace(file_name, suite_name) + + +static func normalize_path(path :String) -> String: + return path.replace("///", "/") -static func create_test_suite(test_suite_path :String, source_path :String) -> Result: +static func create_test_suite(test_suite_path :String, source_path :String) -> GdUnitResult: # create directory if not exists if not DirAccess.dir_exists_absolute(test_suite_path.get_base_dir()): var error := DirAccess.make_dir_recursive_absolute(test_suite_path.get_base_dir()) if error != OK: - return Result.error("Can't create directoy at: %s. Error code %s" % [test_suite_path.get_base_dir(), error]) + return GdUnitResult.error("Can't create directoy at: %s. Error code %s" % [test_suite_path.get_base_dir(), error]) var script := GDScript.new() script.source_code = GdUnitTestSuiteTemplate.build_template(source_path) var error := ResourceSaver.save(script, test_suite_path) if error != OK: - return Result.error("Can't create test suite at: %s. Error code %s" % [test_suite_path, error]) - return Result.success(test_suite_path) + return GdUnitResult.error("Can't create test suite at: %s. Error code %s" % [test_suite_path, error]) + return GdUnitResult.success(test_suite_path) static func get_test_case_line_number(resource_path :String, func_name :String) -> int: @@ -292,7 +292,7 @@ static func get_test_case_line_number(resource_path :String, func_name :String) return -1 -static func add_test_case(resource_path :String, func_name :String) -> Result: +static func add_test_case(resource_path :String, func_name :String) -> GdUnitResult: var script := load(resource_path) as GDScript # count all exiting lines and add two as space to add new test case var line_number := count_lines(script) + 2 @@ -307,8 +307,8 @@ static func add_test_case(resource_path :String, func_name :String) -> Result: script.source_code += func_body var error := ResourceSaver.save(script, resource_path) if error != OK: - return Result.error("Can't add test case at: %s to '%s'. Error code %s" % [func_name, resource_path, error]) - return Result.success({ "path" : resource_path, "line" : line_number}) + return GdUnitResult.error("Can't add test case at: %s to '%s'. Error code %s" % [func_name, resource_path, error]) + return GdUnitResult.success({ "path" : resource_path, "line" : line_number}) static func count_lines(script : GDScript) -> int: @@ -327,10 +327,10 @@ static func test_case_exists(test_suite_path :String, func_name :String) -> bool return true return false -static func create_test_case(test_suite_path :String, func_name :String, source_script_path :String) -> Result: +static func create_test_case(test_suite_path :String, func_name :String, source_script_path :String) -> GdUnitResult: if test_case_exists(test_suite_path, func_name): var line_number := get_test_case_line_number(test_suite_path, func_name) - return Result.success({ "path" : test_suite_path, "line" : line_number}) + return GdUnitResult.success({ "path" : test_suite_path, "line" : line_number}) if not test_suite_exists(test_suite_path): var result := create_test_suite(test_suite_path, source_script_path) diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd b/addons/gdUnit4/src/core/GdUnitTools.gd index 771945e7..1e4f660a 100644 --- a/addons/gdUnit4/src/core/GdUnitTools.gd +++ b/addons/gdUnit4/src/core/GdUnitTools.gd @@ -1,4 +1,3 @@ -class_name GdUnitTools extends RefCounted const GDUNIT_TEMP := "user://tmp" @@ -9,15 +8,18 @@ static func temp_dir() -> String: DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP) return GDUNIT_TEMP + static func create_temp_dir(folder_name :String) -> String: var new_folder = temp_dir() + "/" + folder_name if not DirAccess.dir_exists_absolute(new_folder): DirAccess.make_dir_recursive_absolute(new_folder) return new_folder + static func clear_tmp(): delete_directory(GDUNIT_TEMP) - + + # Creates a new file under static func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess: var file_path := create_temp_dir(relative_path) + "/" + file_name @@ -26,9 +28,11 @@ static func create_temp_file(relative_path :String, file_name :String, mode := F push_error("Error creating temporary file at: %s, %s" % [file_path, error_as_string(FileAccess.get_open_error())]) return file + static func current_dir() -> String: return ProjectSettings.globalize_path("res://") + static func delete_directory(path :String, only_content := false) -> void: var dir := DirAccess.open(path) if dir != null: @@ -52,16 +56,16 @@ static func delete_directory(path :String, only_content := false) -> void: push_error("Delete %s failed: %s" % [path, error_as_string(err)]) -static func copy_file(from_file :String, to_dir :String) -> Result: +static func copy_file(from_file :String, to_dir :String) -> GdUnitResult: var dir := DirAccess.open(to_dir) if dir != null: var to_file := to_dir + "/" + from_file.get_file() prints("Copy %s to %s" % [from_file, to_file]) var error = dir.copy(from_file, to_file) if error != OK: - return Result.error("Can't copy file form '%s' to '%s'. Error: '%s'" % [from_file, to_file, error_as_string(error)]) - return Result.success(to_file) - return Result.error("Directory not found: " + to_dir) + return GdUnitResult.error("Can't copy file form '%s' to '%s'. Error: '%s'" % [from_file, to_file, error_as_string(error)]) + return GdUnitResult.success(to_file) + return GdUnitResult.error("Directory not found: " + to_dir) static func copy_directory(from_dir :String, to_dir :String, recursive :bool = false) -> bool: @@ -102,6 +106,7 @@ static func copy_directory(from_dir :String, to_dir :String, recursive :bool = f push_error("Directory not found: " + from_dir) return false + # scans given path for sub directories by given prefix and returns the highest index numer # e.g. static func find_last_path_index(path :String, prefix :String) -> int: @@ -121,6 +126,7 @@ static func find_last_path_index(path :String, prefix :String) -> int: last_iteration = iteration return last_iteration + static func delete_path_index_lower_equals_than(path :String, prefix :String, index :int) -> int: var dir := DirAccess.open(path) if dir == null: @@ -154,6 +160,7 @@ static func scan_dir(path :String) -> PackedStringArray: content.append(next) return content + static func resource_as_array(resource_path :String) -> PackedStringArray: var file := FileAccess.open(resource_path, FileAccess.READ) if file == null: @@ -164,6 +171,7 @@ static func resource_as_array(resource_path :String) -> PackedStringArray: file_content.append(file.get_line()) return file_content + static func resource_as_string(resource_path :String) -> String: var file := FileAccess.open(resource_path, FileAccess.READ) if file == null: @@ -178,20 +186,15 @@ static func normalize_text(text :String) -> String: static func richtext_normalize(input :String) -> String: return GdUnitSingleton.instance("regex_richtext", func _regex_richtext() -> RegEx: - return GdUnitTools.to_regex("\\[/?(b|color|bgcolor|right|table|cell).*?\\]") ).sub(input, "", true) - - -static func max_length(left, right) -> int: - var ls = str(left).length() - var rs = str(right).length() - return rs if ls < rs else ls + return to_regex("\\[/?(b|color|bgcolor|right|table|cell).*?\\]") )\ + .sub(input, "", true).replace("\r", "") static func to_regex(pattern :String) -> RegEx: var regex := RegEx.new() var err := regex.compile(pattern) if err != OK: - push_error("Can't compiling regx '%s'.\n ERROR: %s" % [pattern, GdUnitTools.error_as_string(err)]) + push_error("Can't compiling regx '%s'.\n ERROR: %s" % [pattern, error_string(err)]) return regex @@ -200,7 +203,7 @@ static func prints_verbose(message :String) -> void: prints(message) -static func free_instance(instance :Variant) -> bool: +static func free_instance(instance :Variant, is_stdout_verbose :=false) -> bool: if instance is Array: for element in instance: free_instance(element) @@ -212,20 +215,28 @@ static func free_instance(instance :Variant) -> bool: # do not free a class refernece if typeof(instance) == TYPE_OBJECT and (instance as Object).is_class("GDScriptNativeClass"): return false - if is_instance_valid(instance) and instance is RefCounted: + if is_stdout_verbose: + print_verbose("GdUnit4:gc():free instance ", instance) + release_double(instance) + if instance is RefCounted: instance.notification(Object.NOTIFICATION_PREDELETE) + await Engine.get_main_loop().process_frame return true else: - # is instance already freed? - if not is_instance_valid(instance) or ClassDB.class_get_property(instance, "new"): - return false - release_double(instance) + # is instance already freed? + #if not is_instance_valid(instance) or ClassDB.class_get_property(instance, "new"): + # return false #release_connections(instance) if instance is Timer: instance.stop() - #instance.queue_free() instance.call_deferred("free") + await Engine.get_main_loop().process_frame return true + if instance is Node and instance.get_parent() != null: + if is_stdout_verbose: + print_verbose("GdUnit4:gc():remove node from parent ", instance.get_parent(), instance) + instance.get_parent().remove_child(instance) + instance.set_owner(null) instance.free() return !is_instance_valid(instance) @@ -248,9 +259,9 @@ static func _release_connections(instance :Object): static func release_timers(): # we go the new way to hold all gdunit timers in group 'GdUnitTimers' for node in Engine.get_main_loop().root.get_children(): - if node.is_in_group("GdUnitTimers"): - #prints("found gdunit timer artifact", node, is_instance_valid(node)) + if is_instance_valid(node) and node.is_in_group("GdUnitTimers"): if is_instance_valid(node): + Engine.get_main_loop().root.remove_child(node) node.stop() node.free() @@ -268,11 +279,6 @@ static func release_double(instance :Object) -> void: instance.call("__release_double") -# test is Godot mono running -static func is_mono_supported() -> bool: - return ClassDB.class_exists("CSharpScript") - - static func make_qualified_path(path :String) -> String: if not path.begins_with("res://"): if path.begins_with("//"): @@ -281,33 +287,27 @@ static func make_qualified_path(path :String) -> String: return "res:/" + path return path + static func error_as_string(error_number :int) -> String: return error_string(error_number) - + + static func clear_push_errors() -> void: var runner = Engine.get_meta("GdUnitRunner") if runner != null: runner.clear_push_errors() + static func register_expect_interupted_by_timeout(test_suite :Node, test_case_name :String) -> void: var test_case = test_suite.find_child(test_case_name, false, false) test_case.expect_to_interupt() -static func append_array(array, append :Array) -> void: - var major :int = Engine.get_version_info()["major"] - var minor :int = Engine.get_version_info()["minor"] - if major >= 3 and minor >= 3: - array.append_array(append) - else: - for element in append: - array.append(element) - -static func extract_zip(zip_package :String, dest_path :String) -> Result: +static func extract_zip(zip_package :String, dest_path :String) -> GdUnitResult: var zip: ZIPReader = ZIPReader.new() var err := zip.open(zip_package) if err != OK: - return Result.error("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err]) + return GdUnitResult.error("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err]) var zip_entries: PackedStringArray = zip.get_files() # Get base path and step over archive folder var archive_path = zip_entries[0] @@ -321,4 +321,4 @@ static func extract_zip(zip_package :String, dest_path :String) -> Result: var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE) file.store_buffer(zip.read_file(zip_entry)) zip.close() - return Result.success(dest_path) + return GdUnitResult.success(dest_path) diff --git a/addons/gdUnit4/src/core/GodotVersionFixures.gd b/addons/gdUnit4/src/core/GodotVersionFixures.gd new file mode 100644 index 00000000..d0ce4e14 --- /dev/null +++ b/addons/gdUnit4/src/core/GodotVersionFixures.gd @@ -0,0 +1,11 @@ +## This service class contains helpers to wrap Godot functions and handle them carefully depending on the current Godot version +class_name GodotVersionFixures +extends RefCounted + + + +## Returns the icon property defined by name and theme_type, if it exists. +static func get_icon(control :Control, icon_name :String) -> Texture2D: + if Engine.get_version_info().hex >= 040200: + return control.get_theme_icon(icon_name, "EditorIcons") + return control.theme.get_icon(icon_name, "EditorIcons") diff --git a/addons/gdUnit4/src/core/_TestCase.gd b/addons/gdUnit4/src/core/_TestCase.gd index 376679ac..47ea60ff 100644 --- a/addons/gdUnit4/src/core/_TestCase.gd +++ b/addons/gdUnit4/src/core/_TestCase.gd @@ -23,7 +23,6 @@ var _expect_to_interupt := false var _timer : Timer var _interupted :bool = false var _failed := false -var _timeout :int var _report :GdUnitReport = null @@ -36,22 +35,31 @@ var monitor : GodotGdErrorMonitor = null: return monitor +var timeout : int = DEFAULT_TIMEOUT: + set (value): + timeout = value + get: + if timeout == DEFAULT_TIMEOUT: + timeout = GdUnitSettings.test_timeout() + return timeout + + @warning_ignore("shadowed_variable_base_class") -func configure(p_name: String, p_line_number: int, p_script_path: String, p_timeout :int = DEFAULT_TIMEOUT, p_fuzzers :Array = [], p_iterations: int = 1, p_seed :int = -1) -> _TestCase: +func configure(p_name: String, p_line_number: int, p_script_path: String, p_timeout :int = DEFAULT_TIMEOUT, p_fuzzers :Array[GdFunctionArgument] = [], p_iterations: int = 1, p_seed :int = -1) -> _TestCase: set_name(p_name) _line_number = p_line_number _fuzzers = p_fuzzers _iterations = p_iterations _seed = p_seed _script_path = p_script_path - _timeout = p_timeout if p_timeout != DEFAULT_TIMEOUT else GdUnitSettings.test_timeout() + timeout = p_timeout return self func execute(p_test_parameter := Array(), p_iteration := 0): _failure_received(false) _current_iteration = p_iteration - 1 - if p_iteration == 0: + if _current_iteration == -1: _set_failure_handler() set_timeout() monitor.start() @@ -68,19 +76,37 @@ func execute(p_test_parameter := Array(), p_iteration := 0): _interupted = true +func execute_paramaterized(p_test_parameter :Array): + _failure_received(false) + set_timeout() + monitor.start() + _execute_test_case(name, p_test_parameter) + await completed + monitor.stop() + for report_ in monitor.reports(): + if report_.is_error(): + _report = report_ + _interupted = true + + +var _is_disposed := false + func dispose(): - # unreference last used assert form the test to prevent memory leaks - GdUnitThreadManager.get_current_context().set_assert(null) + if _is_disposed: + return + _is_disposed = true + Engine.remove_meta("GD_TEST_FAILURE") stop_timer() _remove_failure_handler() _fuzzers.clear() + _report = null @warning_ignore("shadowed_variable_base_class", "redundant_await") func _execute_test_case(name :String, test_parameter :Array): - # needs at least on await otherwise it braks the awaiting chain + # needs at least on await otherwise it breaks the awaiting chain await get_parent().callv(name, test_parameter) - await get_tree().create_timer(0.0001).timeout + await Engine.get_main_loop().process_frame completed.emit() @@ -91,18 +117,20 @@ func update_fuzzers(input_values :Array, iteration :int): func set_timeout(): - var time :float = _timeout * 0.001 + if is_instance_valid(_timer): + return + var time :float = timeout / 1000.0 _timer = Timer.new() add_child(_timer) _timer.set_name("gdunit_test_case_timer_%d" % _timer.get_instance_id()) _timer.timeout.connect(func do_interrupt(): - if has_fuzzer(): + if is_fuzzed(): _report = GdUnitReport.new().create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.fuzzer_interuped(_current_iteration, "timedout")) else: - _report = GdUnitReport.new().create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(timeout())) + _report = GdUnitReport.new().create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(timeout)) _interupted = true completed.emit() - , CONNECT_REFERENCE_COUNTED) + , CONNECT_DEFERRED) _timer.set_one_shot(true) _timer.set_wait_time(time) _timer.set_autostart(false) @@ -132,6 +160,7 @@ func stop_timer() : if is_instance_valid(_timer): _timer.stop() _timer.call_deferred("free") + _timer = null func expect_to_interupt() -> void: @@ -170,15 +199,11 @@ func iterations() -> int: return _iterations -func timeout() -> int: - return _timeout - - func seed_value() -> int: return _seed -func has_fuzzer() -> bool: +func is_fuzzed() -> bool: return not _fuzzers.is_empty() @@ -224,9 +249,9 @@ func test_case_names() -> PackedStringArray: var test_cases := PackedStringArray() var test_name = get_name() for index in _test_parameters.size(): - test_cases.append("%s:%d %s" % [test_name, index, str(_test_parameters[index]).replace('"', "'")]) + test_cases.append("%s:%d %s" % [test_name, index, str(_test_parameters[index]).replace('"', "'").replace("&'", "'")]) return test_cases func _to_string(): - return "%s :%d (%dms)" % [get_name(), _line_number, _timeout] + return "%s :%d (%dms)" % [get_name(), _line_number, timeout] diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd index c1893e58..ffe3c125 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd @@ -5,6 +5,8 @@ signal gdunit_runner_start() signal gdunit_runner_stop(client_id :int) +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const CMD_RUN_OVERALL = "Debug Overall TestSuites" const CMD_RUN_TESTCASE = "Run TestCases" const CMD_RUN_TESTCASE_DEBUG = "Run TestCases (Debug)" @@ -54,7 +56,8 @@ func _init(): assert_shortcut_mappings(SETTINGS_SHORTCUT_MAPPING) if Engine.is_editor_hint(): - _editor_interface = Engine.get_meta("GdUnitEditorPlugin").get_editor_interface() + var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + _editor_interface = editor.get_editor_interface() GdUnitSignals.instance().gdunit_event.connect(_on_event) GdUnitSignals.instance().gdunit_client_connected.connect(_on_client_connected) GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_client_disconnected) @@ -63,8 +66,8 @@ func _init(): _runner_config.load_config() init_shortcuts() - var is_running = func(_script :GDScript) : return _is_running - var is_not_running = func(_script :GDScript) : return !_is_running + var is_running = func(_script :Script) : return _is_running + var is_not_running = func(_script :Script) : return !_is_running register_command(GdUnitCommand.new(CMD_RUN_OVERALL, is_not_running, cmd_run_overall.bind(true), GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL)) register_command(GdUnitCommand.new(CMD_RUN_TESTCASE, is_not_running, cmd_editor_run_test.bind(false), GdUnitShortcut.ShortCut.RUN_TESTCASE)) register_command(GdUnitCommand.new(CMD_RUN_TESTCASE_DEBUG, is_not_running, cmd_editor_run_test.bind(true), GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG)) @@ -186,7 +189,7 @@ func cmd_run_test_case(test_suite_resource_path :String, test_case :String, test func cmd_run_overall(debug :bool) -> void: - var test_suite_paths :PackedStringArray = GdUnitCommandHandler.scan_test_directorys("res://", []) + var test_suite_paths :PackedStringArray = GdUnitCommandHandler.scan_test_directorys("res://" , GdUnitSettings.test_root_folder(), []) var result := _runner_config.clear()\ .add_test_suites(test_suite_paths)\ .save_config() @@ -259,20 +262,30 @@ func cmd_create_test() -> void: ScriptEditorControls.edit_script(info.get("path"), info.get("line")) -static func scan_test_directorys(base_directory :String, test_suite_paths :PackedStringArray) -> PackedStringArray: - prints("Scannning for test directories", base_directory) +static func scan_test_directorys(base_directory :String, test_directory: String, test_suite_paths :PackedStringArray) -> PackedStringArray: + print_verbose("Scannning for test directory '%s' at %s" % [test_directory, base_directory]) for directory in DirAccess.get_directories_at(base_directory): if directory.begins_with("."): continue - var current_directory := base_directory + "/" + directory - if directory == "test": - prints(".. ", current_directory) + var current_directory := normalize_path(base_directory + "/" + directory) + if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory): + continue + if match_test_directory(directory, test_directory): + prints("Collect tests at:", current_directory) test_suite_paths.append(current_directory) else: - scan_test_directorys(current_directory, test_suite_paths) + scan_test_directorys(current_directory, test_directory, test_suite_paths) return test_suite_paths +static func normalize_path(path :String) -> String: + return path.replace("///", "//") + + +static func match_test_directory(directory :String, test_directory: String) -> bool: + return directory == test_directory or test_directory.is_empty() or test_directory == "/" or test_directory == "res://" + + func run_debug_mode(): _editor_interface.play_custom_scene("res://addons/gdUnit4/src/core/GdUnitRunner.tscn") _is_running = true diff --git a/addons/gdUnit4/src/core/event/GdUnitEvent.gd b/addons/gdUnit4/src/core/event/GdUnitEvent.gd index 173f64d7..dbfcc326 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEvent.gd +++ b/addons/gdUnit4/src/core/event/GdUnitEvent.gd @@ -11,7 +11,7 @@ const ERROR_COUNT = "error_count" const FAILED_COUNT = "failed_count" const SKIPPED_COUNT = "skipped_count" -enum { +enum { INIT, STOP, TESTSUITE_BEFORE, @@ -139,7 +139,7 @@ func reports() -> Array: func _to_string(): - return "Event: %d %s:%s, %s, %s" % [_event_type, _suite_name, _test_name, _statistics, _reports] + return "Event: %s %s:%s, %s, %s" % [_event_type, _suite_name, _test_name, _statistics, _reports] func serialize() -> Dictionary: diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd new file mode 100644 index 00000000..b55c125f --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd @@ -0,0 +1,171 @@ +## The execution context +## It contains all the necessary information about the executed stage, such as memory observers, reports, orphan monitor +class_name GdUnitExecutionContext + +var _parent_context :GdUnitExecutionContext +var _sub_context :Array[GdUnitExecutionContext] = [] +var _orphan_monitor :GdUnitOrphanNodesMonitor +var _memory_observer :GdUnitMemoryObserver +var _report_collector :GdUnitTestReportCollector +var _timer :LocalTime +var _test_case_name: StringName +var _name :String + + +var test_suite : GdUnitTestSuite = null: + set (value): + test_suite = value + get: + if _parent_context != null: + return _parent_context.test_suite + return test_suite + + +var test_case : _TestCase = null: + get: + if _test_case_name.is_empty(): + return null + return test_suite.find_child(_test_case_name, false, false) + + +func _init(name :String, parent_context :GdUnitExecutionContext = null) -> void: + _name = name + _parent_context = parent_context + _timer = LocalTime.now() + _orphan_monitor = GdUnitOrphanNodesMonitor.new(name) + _orphan_monitor.start() + _memory_observer = GdUnitMemoryObserver.new() + _report_collector = GdUnitTestReportCollector.new(get_instance_id()) + if parent_context != null: + parent_context._sub_context.append(self) + + +func dispose() -> void: + _timer = null + _orphan_monitor = null + _report_collector = null + _memory_observer = null + _parent_context = null + test_suite = null + test_case = null + for context in _sub_context: + context.dispose() + _sub_context.clear() + + +func set_active() -> void: + test_suite.__execution_context = self + GdUnitThreadManager.get_current_context().set_execution_context(self) + + +static func of_test_suite(test_suite_ :GdUnitTestSuite) -> GdUnitExecutionContext: + assert(test_suite_, "test_suite is null") + var context := GdUnitExecutionContext.new(test_suite_.get_name()) + context.test_suite = test_suite_ + context.set_active() + return context + + +static func of_test_case(pe :GdUnitExecutionContext, test_case_name :StringName) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(test_case_name, pe) + context._test_case_name = test_case_name + context.set_active() + return context + + +static func of(pe :GdUnitExecutionContext) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(pe._test_case_name, pe) + context._test_case_name = pe._test_case_name + context.set_active() + return context + + +func test_failed() -> bool: + return has_failures() or has_errors() + + +func orphan_monitor_start() -> void: + _orphan_monitor.start() + + +func orphan_monitor_stop() -> void: + _orphan_monitor.stop() + + +func reports() -> Array[GdUnitReport]: + return _report_collector.reports() + + +func build_report_statistics(orphans :int, recursive := true) -> Dictionary: + return { + GdUnitEvent.ORPHAN_NODES: orphans, + GdUnitEvent.ELAPSED_TIME: _timer.elapsed_since_ms(), + GdUnitEvent.FAILED: has_failures(), + GdUnitEvent.ERRORS: has_errors(), + GdUnitEvent.WARNINGS: has_warnings(), + GdUnitEvent.SKIPPED: has_skipped(), + GdUnitEvent.FAILED_COUNT: count_failures(recursive), + GdUnitEvent.ERROR_COUNT: count_errors(recursive), + GdUnitEvent.SKIPPED_COUNT: count_skipped(recursive) + } + + +func has_failures() -> bool: + return _sub_context.any(func(c): return c.has_failures()) or _report_collector.has_failures() + + +func has_errors() -> bool: + return _sub_context.any(func(c): return c.has_errors()) or _report_collector.has_errors() + + +func has_warnings() -> bool: + return _sub_context.any(func(c): return c.has_warnings()) or _report_collector.has_warnings() + + +func has_skipped() -> bool: + return _sub_context.any(func(c): return c.has_skipped()) or _report_collector.has_skipped() + + +func count_failures(recursive :bool) -> int: + if not recursive: + return _report_collector.count_failures() + return _sub_context\ + .map(func(c): return c.count_failures(recursive))\ + .reduce(sum, _report_collector.count_failures()) + + +func count_errors(recursive :bool) -> int: + if not recursive: + return _report_collector.count_errors() + return _sub_context\ + .map(func(c): return c.count_errors(recursive))\ + .reduce(sum, _report_collector.count_errors()) + + +func count_skipped(recursive :bool) -> int: + if not recursive: + return _report_collector.count_skipped() + return _sub_context\ + .map(func(c): return c.count_skipped(recursive))\ + .reduce(sum, _report_collector.count_skipped()) + + +func count_orphans() -> int: + var orphans := 0 + for c in _sub_context: + orphans += c._orphan_monitor.orphan_nodes() + return _orphan_monitor.orphan_nodes() - orphans + + +func sum(accum :int, number :int) -> int: + return accum + number + + +func register_auto_free(obj :Variant) -> Variant: + return _memory_observer.register_auto_free(obj) + + +## Runs the gdunit garbage collector to free registered object +func gc() -> void: + await _memory_observer.gc() + orphan_monitor_stop() diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd new file mode 100644 index 00000000..6709c136 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd @@ -0,0 +1,136 @@ +## The memory watcher for objects that have been registered and are released when 'gc' is called. +class_name GdUnitMemoryObserver +extends RefCounted + +const TAG_OBSERVE_INSTANCE := "GdUnit4_observe_instance_" +const TAG_AUTO_FREE = "GdUnit4_marked_auto_free" +const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +var _store :Array[Variant] = [] +var _orphan_detection_enabled :bool = true +# enable for debugging purposes +var _is_stdout_verbose := false +const _show_debug := false + + +func _init(): + _orphan_detection_enabled = GdUnitSettings.is_verbose_orphans() + + +## Registration of an instance to be released when an execution phase is completed +func register_auto_free(obj) -> Variant: + if not is_instance_valid(obj): + return obj + # do not register on GDScriptNativeClass + if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") : + return obj + #if obj is GDScript or obj is ScriptExtension: + # return obj + if obj is MainLoop: + push_error("GdUnit4: Avoid to add mainloop to auto_free queue %s" % obj) + return + if _is_stdout_verbose: + print_verbose("GdUnit4:gc():register auto_free(%s)" % obj) + # only register pure objects + if obj is GdUnitSceneRunner: + _store.push_back(obj) + else: + _store.append(obj) + _tag_object(obj) + return obj + + +# to disable instance guard when run into issues. +static func _is_instance_guard_enabled() -> bool: + return false + + +static func debug_observe(name :String, obj :Object, indent :int = 0) -> void: + if not _show_debug: + return + var script :GDScript= obj if obj is GDScript else obj.get_script() + if script: + var base_script :GDScript = script.get_base_script() + prints("".lpad(indent, " "), name, obj, obj.get_class(), "reference_count:", obj.get_reference_count() if obj is RefCounted else 0, "script:", script, script.resource_path) + if base_script: + debug_observe("+", base_script, indent+1) + else: + prints(name, obj, obj.get_class(), obj.get_name()) + + +static func guard_instance(obj :Object) -> Object: + if not _is_instance_guard_enabled(): + return + var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) + if Engine.has_meta(tag): + return + debug_observe("Gard on instance", obj) + Engine.set_meta(tag, obj) + return obj + + +static func unguard_instance(obj :Object, verbose := true) -> void: + if not _is_instance_guard_enabled(): + return + var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) + if verbose: + debug_observe("unguard instance", obj) + if Engine.has_meta(tag): + Engine.remove_meta(tag) + + +static func gc_guarded_instance(name :String, instance :Object) -> void: + if not _is_instance_guard_enabled(): + return + await Engine.get_main_loop().process_frame + unguard_instance(instance, false) + if is_instance_valid(instance) and instance is RefCounted: + # finally do this very hacky stuff + # we need to manually unreferece to avoid leaked scripts + # but still leaked GDScriptFunctionState exists + #var script :GDScript = instance.get_script() + #if script: + # var base_script :GDScript = script.get_base_script() + # if base_script: + # base_script.unreference() + debug_observe(name, instance) + instance.unreference() + await Engine.get_main_loop().process_frame + + +static func gc_on_guarded_instances() -> void: + if not _is_instance_guard_enabled(): + return + for tag in Engine.get_meta_list(): + if tag.begins_with(TAG_OBSERVE_INSTANCE): + var instance = Engine.get_meta(tag) + await gc_guarded_instance("Leaked instance detected:", instance) + await GdUnitTools.free_instance(instance, false) + + +# store the object into global store aswell to be verified by 'is_marked_auto_free' +func _tag_object(obj :Variant) -> void: + var tagged_object := Engine.get_meta(TAG_AUTO_FREE, []) as Array + tagged_object.append(obj) + Engine.set_meta(TAG_AUTO_FREE, tagged_object) + + +## Runs over all registered objects and releases them +func gc() -> void: + if _store.is_empty(): + return + # give engine time to free objects to process objects marked by queue_free() + await Engine.get_main_loop().process_frame + if _is_stdout_verbose: + print_verbose("GdUnit4:gc():running", " freeing %d objects .." % _store.size()) + var tagged_objects := Engine.get_meta(TAG_AUTO_FREE, []) as Array + while not _store.is_empty(): + var value :Variant = _store.pop_front() + tagged_objects.erase(value) + await GdUnitTools.free_instance(value, _is_stdout_verbose) + + +## Checks whether the specified object is registered for automatic release +static func is_marked_auto_free(obj) -> bool: + return Engine.get_meta(TAG_AUTO_FREE, []).has(obj) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd new file mode 100644 index 00000000..cde5cee2 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd @@ -0,0 +1,70 @@ +# Collects all reports seperated as warnings, failures and errors +class_name GdUnitTestReportCollector +extends RefCounted + + +var _execution_context_id :int +var _reports :Array[GdUnitReport] = [] + + +static func __filter_is_error(report :GdUnitReport) -> bool: + return report.is_error() + + +static func __filter_is_failure(report :GdUnitReport) -> bool: + return report.is_failure() + + +static func __filter_is_warning(report :GdUnitReport) -> bool: + return report.is_warning() + + +static func __filter_is_skipped(report :GdUnitReport) -> bool: + return report.is_skipped() + + +func _init(execution_context_id :int): + _execution_context_id = execution_context_id + GdUnitSignals.instance().gdunit_report.connect(on_reports) + + +func count_failures() -> int: + return _reports.filter(__filter_is_failure).size() + + +func count_errors() -> int: + return _reports.filter(__filter_is_error).size() + + +func count_warnings() -> int: + return _reports.filter(__filter_is_warning).size() + + +func count_skipped() -> int: + return _reports.filter(__filter_is_skipped).size() + + +func has_failures() -> bool: + return _reports.any(__filter_is_failure) + + +func has_errors() -> bool: + return _reports.any(__filter_is_error) + + +func has_warnings() -> bool: + return _reports.any(__filter_is_warning) + + +func has_skipped() -> bool: + return _reports.any(__filter_is_skipped) + + +func reports() -> Array[GdUnitReport]: + return _reports + + +# Consumes reports emitted by tests +func on_reports(execution_context_id :int, report :GdUnitReport) -> void: + if execution_context_id == _execution_context_id: + _reports.append(report) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd new file mode 100644 index 00000000..47c02fb8 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd @@ -0,0 +1,26 @@ +## The executor to run a test-suite +class_name GdUnitTestSuiteExecutor + + +# preload all asserts here +@warning_ignore("unused_private_class_variable") +var _assertions := GdUnitAssertions.new() +var _executeStage :IGdUnitExecutionStage = GdUnitTestSuiteExecutionStage.new() + + +func _init(debug_mode :bool = false): + _executeStage.set_debug_mode(debug_mode) + + +func execute(test_suite :GdUnitTestSuite) -> void: + var orphan_detection_enabled = GdUnitSettings.is_verbose_orphans() + if not orphan_detection_enabled: + prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.") + + Engine.get_main_loop().root.call_deferred("add_child", test_suite) + await Engine.get_main_loop().process_frame + await _executeStage.execute(GdUnitExecutionContext.of_test_suite(test_suite)) + + +func fail_fast(enabled :bool) -> void: + _executeStage.fail_fast(enabled) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd new file mode 100644 index 00000000..d8db2c44 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd @@ -0,0 +1,101 @@ +## The test case shutdown hook implementation.[br] +## It executes the 'test_after()' block from the test-suite. +class_name GdUnitTestCaseAfterStage +extends IGdUnitExecutionStage + + +var _test_name :StringName = "" +var _call_stage :bool + + +func _init(call_stage := true): + _call_stage = call_stage + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + if _call_stage: + @warning_ignore("redundant_await") + await test_suite.after_test() + # unreference last used assert form the test to prevent memory leaks + GdUnitThreadManager.get_current_context().set_assert(null) + await context.gc() + + if context.test_case.is_skipped(): + fire_test_skipped(context) + else: + fire_test_ended(context) + if is_instance_valid(context.test_case): + context.test_case.dispose() + + +func set_test_name(test_name :StringName): + _test_name = test_name + + +func fire_test_ended(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var test_name := context._test_case_name if _test_name.is_empty() else _test_name + var reports := collect_reports(context) + var orphans := collect_orphans(context, reports) + + fire_event(GdUnitEvent.new()\ + .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_name, context.build_report_statistics(orphans), reports)) + + +func collect_orphans(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: + var orphans := 0 + if not context._sub_context.is_empty(): + orphans += add_orphan_report_test(context._sub_context[0], reports) + orphans += add_orphan_report_teststage(context, reports) + return orphans + + +func collect_reports(context :GdUnitExecutionContext) -> Array[GdUnitReport]: + var reports := context.reports() + var test_case := context.test_case + if test_case.is_interupted() and not test_case.is_expect_interupted(): + reports.push_back(test_case.report()) + # we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended` + if not context._sub_context.is_empty(): + reports.append_array(context._sub_context[0].reports()) + # needs finally to clean the test reports to avoid counting twice + context._sub_context[0].reports().clear() + return reports + + +func add_orphan_report_test(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: + var orphans := context.count_orphans() + if orphans > 0: + reports.push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans))) + return orphans + + +func add_orphan_report_teststage(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: + var orphans := context.count_orphans() + if orphans > 0: + reports.push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(orphans))) + return orphans + + +func fire_test_skipped(context :GdUnitExecutionContext): + var test_suite := context.test_suite + var test_case := context.test_case + var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name + var statistics = { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: 1, + } + var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) + fire_event(GdUnitEvent.new()\ + .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name, statistics, [report])) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd new file mode 100644 index 00000000..0abc581f --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd @@ -0,0 +1,28 @@ +## The test case startup hook implementation.[br] +## It executes the 'test_before()' block from the test-suite. +class_name GdUnitTestCaseBeforeStage +extends IGdUnitExecutionStage + + +var _test_name :StringName = "" +var _call_stage :bool + + +func _init(call_stage := true): + _call_stage = call_stage + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name + + fire_event(GdUnitEvent.new()\ + .test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name)) + + if _call_stage: + @warning_ignore("redundant_await") + await test_suite.before_test() + + +func set_test_name(test_name :StringName): + _test_name = test_name diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd new file mode 100644 index 00000000..dc8c53d2 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd @@ -0,0 +1,31 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseExecutionStage +extends IGdUnitExecutionStage + + +var _stage_single_test :IGdUnitExecutionStage = GdUnitTestCaseSingleExecutionStage.new() +var _stage_fuzzer_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedExecutionStage.new() +var _stage_parameterized_test :IGdUnitExecutionStage= GdUnitTestCaseParameterizedExecutionStage.new() + + +## Executes the test case 'test_()'.[br] +## It executes synchronized following stages[br] +## -> test_before() [br] +## -> test_case() [br] +## -> test_after() [br] +@warning_ignore("redundant_await") +func _execute(context :GdUnitExecutionContext) -> void: + var test_case := context.test_case + if test_case.is_parameterized(): + await _stage_parameterized_test.execute(context) + elif test_case.is_fuzzed(): + await _stage_fuzzer_test.execute(context) + else: + await _stage_single_test.execute(context) + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_single_test.set_debug_mode(debug_mode) + _stage_fuzzer_test.set_debug_mode(debug_mode) + _stage_parameterized_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd new file mode 100644 index 00000000..dab18da1 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd @@ -0,0 +1,28 @@ +## The test suite shutdown hook implementation.[br] +## It executes the 'after()' block from the test-suite. +class_name GdUnitTestSuiteAfterStage +extends IGdUnitExecutionStage + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + @warning_ignore("redundant_await") + await test_suite.after() + # unreference last used assert form the test to prevent memory leaks + GdUnitThreadManager.get_current_context().set_assert(null) + await context.gc() + + var reports := context.reports() + var orphans := context.count_orphans() + if orphans > 0: + reports.push_front(GdUnitReport.new() \ + .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphans))) + fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), context.build_report_statistics(orphans, false), reports)) + + GdUnitTools.clear_tmp() + # Guard that checks if all doubled (spy/mock) objects are released + GdUnitClassDoubler.check_leaked_instances() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd new file mode 100644 index 00000000..869f5adc --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd @@ -0,0 +1,14 @@ +## The test suite startup hook implementation.[br] +## It executes the 'before()' block from the test-suite. +class_name GdUnitTestSuiteBeforeStage +extends IGdUnitExecutionStage + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + fire_event(GdUnitEvent.new()\ + .suite_before(test_suite.get_script().resource_path, test_suite.get_name(), test_suite.get_child_count())) + + @warning_ignore("redundant_await") + await test_suite.before() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd new file mode 100644 index 00000000..9e796c35 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd @@ -0,0 +1,114 @@ +## The test suite main execution stage.[br] +class_name GdUnitTestSuiteExecutionStage +extends IGdUnitExecutionStage + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _stage_before :IGdUnitExecutionStage = GdUnitTestSuiteBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestSuiteAfterStage.new() +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseExecutionStage.new() +var _fail_fast := false + + +## Executes all tests of an test suite.[br] +## It executes synchronized following stages[br] +## -> before() [br] +## -> run all test cases [br] +## -> after() [br] +func _execute(context :GdUnitExecutionContext) -> void: + if context.test_suite.__is_skipped: + await fire_test_suite_skipped(context) + else: + GdUnitMemoryObserver.guard_instance(context.test_suite.__awaiter) + await _stage_before.execute(context) + for test_case_index in context.test_suite.get_child_count(): + # iterate only over test cases + var test_case := context.test_suite.get_child(test_case_index) as _TestCase + if not is_instance_valid(test_case): + continue + context.test_suite.set_active_test_case(test_case.get_name()) + await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case.get_name())) + # stop on first error or if fail fast is enabled + if _fail_fast and context.test_failed(): + break + if test_case.is_interupted(): + # it needs to go this hard way to kill the outstanding awaits of a test case when the test timed out + # we delete the current test suite where is execute the current test case to kill the function state + # and replace it by a clone without function state + context.test_suite = await clone_test_suite(context.test_suite) + await _stage_after.execute(context) + GdUnitMemoryObserver.unguard_instance(context.test_suite.__awaiter) + await Engine.get_main_loop().process_frame + context.test_suite.free() + context.dispose() + + +# clones a test suite and moves the test cases to new instance +func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite: + await Engine.get_main_loop().process_frame + dispose_timers(test_suite) + await GdUnitMemoryObserver.gc_guarded_instance("Manually free on awaiter", test_suite.__awaiter) + var parent := test_suite.get_parent() + var _test_suite = GdUnitTestSuite.new() + parent.remove_child(test_suite) + copy_properties(test_suite, _test_suite) + for child in test_suite.get_children(): + test_suite.remove_child(child) + _test_suite.add_child(child) + parent.add_child(_test_suite) + GdUnitMemoryObserver.guard_instance(_test_suite.__awaiter) + # finally free current test suite instance + test_suite.free() + await Engine.get_main_loop().process_frame + return _test_suite + + +func dispose_timers(test_suite :GdUnitTestSuite): + GdUnitTools.release_timers() + for child in test_suite.get_children(): + if child is Timer: + child.stop() + test_suite.remove_child(child) + child.free() + + +func copy_properties(source :Object, target :Object): + if not source is _TestCase and not source is GdUnitTestSuite: + return + for property in source.get_property_list(): + var property_name = property["name"] + if property_name == "__awaiter": + continue + target.set(property_name, source.get(property_name)) + + +func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var skip_count := test_suite.get_child_count() + fire_event(GdUnitEvent.new()\ + .suite_before(test_suite.get_script().resource_path, test_suite.get_name(), skip_count)) + var statistics = { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: skip_count, + GdUnitEvent.SKIPPED: true + } + var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, -1, GdAssertMessages.test_suite_skipped(test_suite.__skip_reason, skip_count)) + fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), statistics, [report])) + await Engine.get_main_loop().process_frame + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) + + +func fail_fast(enabled :bool) -> void: + _fail_fast = enabled diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd new file mode 100644 index 00000000..0f6ae93a --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd @@ -0,0 +1,39 @@ +## The interface of execution stage.[br] +## An execution stage is defined as an encapsulated task that can execute 1-n substages covered by its own execution context.[br] +## Execution stage are always called synchronously. +class_name IGdUnitExecutionStage +extends RefCounted + +var _debug_mode := false + + +## Executes synchronized the implemented stage in its own execution context.[br] +## example:[br] +## [codeblock] +## # waits for 100ms +## await MyExecutionStage.new().execute() +## [/codeblock][br] +func execute(context :GdUnitExecutionContext) -> void: + context.set_active() + @warning_ignore("redundant_await") + await _execute(context) + + +## Sends the event to registered listeners +func fire_event(event :GdUnitEvent) -> void: + if _debug_mode: + GdUnitSignals.instance().gdunit_event_debug.emit(event) + else: + GdUnitSignals.instance().gdunit_event.emit(event) + + +## Internal testing stuff.[br] +## Sets the executor into debug mode to emit `GdUnitEvent` via signal `gdunit_event_debug` +func set_debug_mode(debug_mode :bool) -> void: + _debug_mode = debug_mode + + +## The execution phase to be carried out. +func _execute(_context :GdUnitExecutionContext) -> void: + @warning_ignore("assert_always_false") + assert(false, "The execution stage is not implemented") diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd new file mode 100644 index 00000000..269a9ea8 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd @@ -0,0 +1,21 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseFuzzedExecutionStage +extends IGdUnitExecutionStage + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false) +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false) +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedTestStage.new() + + +func _execute(context :GdUnitExecutionContext) -> void: + await _stage_before.execute(context) + if not context.test_case.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(context)) + await _stage_after.execute(context) + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd new file mode 100644 index 00000000..6b91d588 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd @@ -0,0 +1,53 @@ +## The fuzzed test case execution stage.[br] +class_name GdUnitTestCaseFuzzedTestStage +extends IGdUnitExecutionStage + +var _expression_runner := GdUnitExpressionRunner.new() + + +## Executes a test case with given fuzzers 'test_()' iterative.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var test_case := context.test_case + var fuzzers := create_fuzzers(test_suite, test_case) + + # guard on fuzzers + for fuzzer in fuzzers: + GdUnitMemoryObserver.guard_instance(fuzzer) + + for iteration in test_case.iterations(): + @warning_ignore("redundant_await") + await test_suite.before_test() + await test_case.execute(fuzzers, iteration) + @warning_ignore("redundant_await") + await test_suite.after_test() + if test_case.is_interupted(): + break + # interrupt at first failure + var reports := context.reports() + if not reports.is_empty(): + var report :GdUnitReport = reports.pop_front() + reports.append(GdUnitReport.new() \ + .create(GdUnitReport.FAILURE, report.line_number(), GdAssertMessages.fuzzer_interuped(iteration, report.message()))) + break + await context.gc() + + # unguard on fuzzers + if not test_case.is_interupted(): + for fuzzer in fuzzers: + GdUnitMemoryObserver.unguard_instance(fuzzer) + + +func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[Fuzzer]: + if not test_case.is_fuzzed(): + return Array() + test_case.generate_seed() + var fuzzers :Array[Fuzzer] = [] + for fuzzer_arg in test_case.fuzzer_arguments(): + var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script(), fuzzer_arg.value_as_string()) + fuzzer._iteration_index = 0 + fuzzer._iteration_limit = test_case.iterations() + fuzzers.append(fuzzer) + return fuzzers diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd new file mode 100644 index 00000000..52ccdc47 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd @@ -0,0 +1,22 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseParameterizedExecutionStage +extends IGdUnitExecutionStage + + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false) +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false) +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseParamaterizedTestStage.new() + + +func _execute(context :GdUnitExecutionContext) -> void: + await _stage_before.execute(context) + if not context.test_case.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(context)) + await _stage_after.execute(context) + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd new file mode 100644 index 00000000..49202e77 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd @@ -0,0 +1,52 @@ +## The parameterized test case execution stage.[br] +class_name GdUnitTestCaseParamaterizedTestStage +extends IGdUnitExecutionStage + + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() + + +## Executes a paramaterized test case.[br] +## It executes synchronized following stages[br] +## -> test_case( ) [br] +func _execute(context :GdUnitExecutionContext) -> void: + var test_case := context.test_case + var test_case_parameters := test_case.test_parameters() + var test_parameter_index := test_case.test_parameter_index() + var test_case_names := test_case.test_case_names() + var is_fail := false + var is_error := false + var failing_index := 0 + + for test_case_index in test_case.test_parameters().size(): + # is test_parameter_index is set, we run this parameterized test only + if test_parameter_index != -1 and test_parameter_index != test_case_index: + continue + + _stage_before.set_test_name(test_case_names[test_case_index]) + _stage_after.set_test_name(test_case_names[test_case_index]) + + var test_context := GdUnitExecutionContext.of(context) + await _stage_before.execute(test_context) + await test_case.execute_paramaterized(test_case_parameters[test_case_index]) + await _stage_after.execute(test_context) + # we need to clean up the reports here so they are not reported twice + is_fail = is_fail or test_context.count_failures(false) > 0 + is_error = is_error or test_context.count_errors(false) > 0 + failing_index = test_case_index - 1 + test_context.reports().clear() + if test_case.is_interupted(): + break + # add report to parent execution context if failed or an error is found + if is_fail: + context.reports().append(GdUnitReport.new().create(GdUnitReport.FAILURE, test_case.line_number(), "Test failed at parameterized index %d." % failing_index)) + if is_error: + context.reports().append(GdUnitReport.new().create(GdUnitReport.ABORT, test_case.line_number(), "Test aborted at parameterized index %d." % failing_index)) + await context.gc() + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd new file mode 100644 index 00000000..fde7eb29 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd @@ -0,0 +1,22 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseSingleExecutionStage +extends IGdUnitExecutionStage + + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseSingleTestStage.new() + + +func _execute(context :GdUnitExecutionContext) -> void: + await _stage_before.execute(context) + if not context.test_case.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(context)) + await _stage_after.execute(context) + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd new file mode 100644 index 00000000..6882fe29 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd @@ -0,0 +1,11 @@ +## The single test case execution stage.[br] +class_name GdUnitTestCaseSingleTestStage +extends IGdUnitExecutionStage + + +## Executes a single test case 'test_()'.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context :GdUnitExecutionContext) -> void: + await context.test_case.execute() + await context.gc() diff --git a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd index d724a39a..846d414c 100644 --- a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd +++ b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd @@ -1,36 +1,46 @@ -# holds all decodings for default values -class_name GdDefaultValueDecoder +# holds all decodings for default values +class_name GdDefaultValueDecoder extends GdUnitSingleton @warning_ignore("unused_parameter") var _decoders = { - TYPE_NIL: func(value): return "", + TYPE_NIL: func(value): return "null", TYPE_STRING: func(value): return '"%s"' % value, - TYPE_STRING_NAME: func(value): return '"%s"' % value, + TYPE_STRING_NAME: _on_type_StringName, TYPE_BOOL: func(value): return str(value).to_lower(), TYPE_FLOAT: func(value): return '%f' % value, - TYPE_COLOR: func(value): return "Color%s" % value, - TYPE_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_BYTE_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_STRING_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_FLOAT32_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_FLOAT64_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_INT32_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_INT64_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_COLOR_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_VECTOR2_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_VECTOR3_ARRAY: func(value): return GdArrayTools.as_string(value), + TYPE_COLOR: _on_type_Color, + TYPE_ARRAY: _on_type_Array.bind(TYPE_ARRAY), + TYPE_PACKED_BYTE_ARRAY: _on_type_Array.bind(TYPE_PACKED_BYTE_ARRAY), + TYPE_PACKED_STRING_ARRAY: _on_type_Array.bind(TYPE_PACKED_STRING_ARRAY), + TYPE_PACKED_FLOAT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT32_ARRAY), + TYPE_PACKED_FLOAT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT64_ARRAY), + TYPE_PACKED_INT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT32_ARRAY), + TYPE_PACKED_INT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT64_ARRAY), + TYPE_PACKED_COLOR_ARRAY: _on_type_Array.bind(TYPE_PACKED_COLOR_ARRAY), + TYPE_PACKED_VECTOR2_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR2_ARRAY), + TYPE_PACKED_VECTOR3_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR3_ARRAY), + TYPE_DICTIONARY: _on_type_Dictionary, TYPE_RID: _on_type_RID, - TYPE_VECTOR2: func(value): return "Vector2%s" % value, - TYPE_VECTOR2I: func(value): return "Vector2i%s" % value, - TYPE_VECTOR3: func(value): return "Vector3%s" % value, - TYPE_VECTOR3I: func(value): return "Vector3i%s" % value, - TYPE_VECTOR4: func(value): return "Vector4%s" % value, - TYPE_VECTOR4I: func(value): return "Vector4i%s" % value, - TYPE_RECT2: _on_decode_Rect2.bind(GdDefaultValueDecoder._regex("P: ?(\\(.+\\)), S: ?(\\(.+\\))")), - TYPE_RECT2I: _on_decode_Rect2i.bind(GdDefaultValueDecoder._regex("P: ?(\\(.+\\)), S: ?(\\(.+\\))")), + TYPE_NODE_PATH: _on_type_NodePath, + TYPE_VECTOR2: _on_type_Vector.bind(TYPE_VECTOR2), + TYPE_VECTOR2I: _on_type_Vector.bind(TYPE_VECTOR2I), + TYPE_VECTOR3: _on_type_Vector.bind(TYPE_VECTOR3), + TYPE_VECTOR3I: _on_type_Vector.bind(TYPE_VECTOR3I), + TYPE_VECTOR4: _on_type_Vector.bind(TYPE_VECTOR4), + TYPE_VECTOR4I: _on_type_Vector.bind(TYPE_VECTOR4I), + TYPE_RECT2: _on_type_Rect2, + TYPE_RECT2I: _on_type_Rect2i, + TYPE_PLANE: _on_type_Plane, + TYPE_QUATERNION: _on_type_Quaternion, + TYPE_AABB: _on_type_AABB, + TYPE_BASIS: _on_type_Basis, + TYPE_CALLABLE: _on_type_Callable, + TYPE_SIGNAL: _on_type_Signal, TYPE_TRANSFORM2D: _on_type_Transform2D, TYPE_TRANSFORM3D: _on_type_Transform3D, + TYPE_PROJECTION: _on_type_Projection, + TYPE_OBJECT: _on_type_Object } static func _regex(pattern :String) -> RegEx: @@ -46,49 +56,199 @@ func get_decoder(type :int) -> Callable: return _decoders.get(type, func(value): return '%s' % value) -func _on_type_Transform2D(value :Variant) -> String: - var transform := value as Transform2D +func _on_type_StringName(value :StringName) -> String: + if value.is_empty(): + return 'StringName()' + return 'StringName("%s")' % value + + +func _on_type_Object(value :Object, type :int) -> String: + return str(value) + + +func _on_type_Color(color :Color) -> String: + if color == Color.BLACK: + return "Color()" + return "Color%s" % color + + +func _on_type_NodePath(path :NodePath) -> String: + if path.is_empty(): + return 'NodePath()' + return 'NodePath("%s")' % path + + +func _on_type_Callable(cb :Callable) -> String: + return 'Callable()' + + +func _on_type_Signal(s :Signal) -> String: + return 'Signal()' + + +func _on_type_Dictionary(dict :Dictionary) -> String: + if dict.is_empty(): + return '{}' + return str(dict) + + +func _on_type_Array(value, type :int) -> String: + match type: + TYPE_ARRAY: + return str(value) + + TYPE_PACKED_COLOR_ARRAY: + var colors := PackedStringArray() + for color in value as PackedColorArray: + colors.append(_on_type_Color(color)) + if colors.is_empty(): + return "PackedColorArray()" + return "PackedColorArray([%s])" % ", ".join(colors) + + TYPE_PACKED_VECTOR2_ARRAY: + var vectors := PackedStringArray() + for vector in value as PackedVector2Array: + vectors.append(_on_type_Vector(vector, TYPE_VECTOR2)) + if vectors.is_empty(): + return "PackedVector2Array()" + return "PackedVector2Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_VECTOR3_ARRAY: + var vectors := PackedStringArray() + for vector in value as PackedVector3Array: + vectors.append(_on_type_Vector(vector, TYPE_VECTOR3)) + if vectors.is_empty(): + return "PackedVector3Array()" + return "PackedVector3Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_STRING_ARRAY: + var values := PackedStringArray() + for v in value as PackedStringArray: + values.append('"%s"' % v) + if values.is_empty(): + return "PackedStringArray()" + return "PackedStringArray([%s])" % ", ".join(values) + + TYPE_PACKED_BYTE_ARRAY,\ + TYPE_PACKED_FLOAT32_ARRAY,\ + TYPE_PACKED_FLOAT64_ARRAY,\ + TYPE_PACKED_INT32_ARRAY,\ + TYPE_PACKED_INT64_ARRAY: + var vectors := PackedStringArray() + for vector in value as Array: + vectors.append(str(vector)) + if vectors.is_empty(): + return GdObjects.type_as_string(type) + "()" + return "%s([%s])" % [GdObjects.type_as_string(type), ", ".join(vectors)] + return "unknown array type %d" % type + + +func _on_type_Vector(value :Variant, type :int) -> String: + match type: + TYPE_VECTOR2: + if value == Vector2(): + return "Vector2()" + return "Vector2%s" % value + TYPE_VECTOR2I: + if value == Vector2i(): + return "Vector2i()" + return "Vector2i%s" % value + TYPE_VECTOR3: + if value == Vector3(): + return "Vector3()" + return "Vector3%s" % value + TYPE_VECTOR3I: + if value == Vector3i(): + return "Vector3i()" + return "Vector3i%s" % value + TYPE_VECTOR4: + if value == Vector4(): + return "Vector4()" + return "Vector4%s" % value + TYPE_VECTOR4I: + if value == Vector4i(): + return "Vector4i()" + return "Vector4i%s" % value + return "unknown vector type %d" % type + + +func _on_type_Transform2D(transform :Transform2D) -> String: + if transform == Transform2D(): + return "Transform2D()" return "Transform2D(Vector2%s, Vector2%s, Vector2%s)" % [transform.x, transform.y, transform.origin] -func _on_type_Transform3D(value :Variant) -> String: - var transform :Transform3D = value +func _on_type_Transform3D(transform :Transform3D) -> String: + if transform == Transform3D(): + return "Transform3D()" return "Transform3D(Vector3%s, Vector3%s, Vector3%s, Vector3%s)" % [transform.basis.x, transform.basis.y, transform.basis.z, transform.origin] +func _on_type_Projection(projection :Projection) -> String: + return "Projection(Vector4%s, Vector4%s, Vector4%s, Vector4%s)" % [projection.x, projection.y, projection.z, projection.w] + + @warning_ignore("unused_parameter") -func _on_type_RID(value :Variant) -> String: +func _on_type_RID(value :RID) -> String: return "RID()" -func _on_decode_Rect2(value :Variant, regEx :RegEx) -> String: - for reg_match in regEx.search_all(str(value)): - var decodeP = reg_match.get_string(1) - var decodeS = reg_match.get_string(2) - return "Rect2(Vector2%s, Vector2%s)" % [decodeP, decodeS] - return "Rect2()" +func _on_type_Rect2(rect :Rect2) -> String: + if rect == Rect2(): + return "Rect2()" + return "Rect2(Vector2%s, Vector2%s)" % [rect.position, rect.size] + + +func _on_type_Rect2i(rect :Variant) -> String: + if rect == Rect2i(): + return "Rect2i()" + return "Rect2i(Vector2i%s, Vector2i%s)" % [rect.position, rect.size] + + +func _on_type_Plane(plane :Plane) -> String: + if plane == Plane(): + return "Plane()" + return "Plane(%d, %d, %d, %d)" % [plane.x, plane.y, plane.z, plane.d] -func _on_decode_Rect2i(value :Variant, regEx :RegEx) -> String: - for reg_match in regEx.search_all(str(value)): - var decodeP = reg_match.get_string(1) - var decodeS = reg_match.get_string(2) - return "Rect2i(Vector2i%s, Vector2i%s)" % [decodeP, decodeS] - return "Rect2i()" +func _on_type_Quaternion(quaternion :Quaternion) -> String: + if quaternion == Quaternion(): + return "Quaternion()" + return "Quaternion(%d, %d, %d, %d)" % [quaternion.x, quaternion.y, quaternion.z, quaternion.w] + + +func _on_type_AABB(aabb :AABB) -> String: + if aabb == AABB(): + return "AABB()" + return "AABB(Vector3%s, Vector3%s)" % [aabb.position, aabb.size] + + +func _on_type_Basis(basis :Basis) -> String: + if basis == Basis(): + return "Basis()" + return "Basis(Vector3%s, Vector3%s, Vector3%s)" % [basis.x, basis.y, basis.z] static func decode(value :Variant) -> String: - var type := typeof(value) + var type := typeof(value) + if GdArrayTools.is_type_array(type) and value.is_empty(): + return "" var decoder :Callable = instance("GdUnitDefaultValueDecoders", func(): return GdDefaultValueDecoder.new()).get_decoder(type) if decoder == null: push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) return "null" + if type == TYPE_OBJECT: + return decoder.call(value, type) return decoder.call(value) static func decode_typed(type :int, value :Variant) -> String: + if value == null: + return "null" var decoder :Callable = instance("GdUnitDefaultValueDecoders", func(): return GdDefaultValueDecoder.new()).get_decoder(type) if decoder == null: push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) return "null" + if type == TYPE_OBJECT: + return decoder.call(value, type) return decoder.call(value) diff --git a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd index 6c444936..08f70079 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd +++ b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd @@ -5,7 +5,7 @@ var _name: String var _type: int var _default_value :Variant -const UNDEFINED = "<-NO_ARG->" +const UNDEFINED :Variant = "<-NO_ARG->" const ARG_PARAMETERIZED_TEST := "test_parameters" @@ -34,7 +34,7 @@ func type() -> int: func has_default() -> bool: - return _default_value != UNDEFINED + return not is_same(_default_value, UNDEFINED) func is_parameter_set() -> bool: diff --git a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd index e09582c2..51c18b0b 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd +++ b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd @@ -204,9 +204,10 @@ static func _extract_args(descriptor :Dictionary) -> Array[GdFunctionArgument]: var arg :Dictionary = arguments.pop_back() var arg_name := _argument_name(arg) var arg_type := _argument_type(arg) - var arg_default := GdFunctionArgument.UNDEFINED + var arg_default :Variant = GdFunctionArgument.UNDEFINED if not defaults.is_empty(): - arg_default = _argument_default_value(arg, defaults.pop_back()) + var default_value = defaults.pop_back() + arg_default = GdDefaultValueDecoder.decode_typed(arg_type, default_value) args_.push_front(GdFunctionArgument.new(arg_name, arg_type, arg_default)) return args_ @@ -247,32 +248,3 @@ static func _argument_type_as_string(arg :Dictionary) -> String: return "" _: return GdObjects.type_as_string(type) - - -static func _argument_default_value(arg :Dictionary, default_value) -> String: - if default_value == null: - return "null" - var type := _argument_type(arg) - match type: - TYPE_NIL: - return "null" - TYPE_RID: - return GdDefaultValueDecoder.decode_typed(type, default_value) - TYPE_STRING, TYPE_STRING_NAME: - return GdDefaultValueDecoder.decode_typed(type, default_value) - TYPE_BOOL: - return GdDefaultValueDecoder.decode_typed(type, default_value) - TYPE_RECT2, TYPE_RECT2I: - return GdDefaultValueDecoder.decode_typed(type, default_value) - TYPE_TRANSFORM2D, TYPE_TRANSFORM3D: - return GdDefaultValueDecoder.decode_typed(type, default_value) - TYPE_OBJECT: - if default_value == null: - return "null" - if GdObjects.is_primitive_type(default_value): - return str(default_value) - if GdArrayTools.is_type_array(type): - if default_value == null or default_value.is_empty(): - return "[]" - return GdDefaultValueDecoder.decode_typed(type, default_value) - return "%s(%s)" % [GdObjects.type_as_string(type), str(default_value).trim_prefix("(").trim_suffix(")")] diff --git a/addons/gdUnit4/src/core/parse/GdScriptParser.gd b/addons/gdUnit4/src/core/parse/GdScriptParser.gd index 3f78d7f6..0b2e55b6 100644 --- a/addons/gdUnit4/src/core/parse/GdScriptParser.gd +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd @@ -1,6 +1,7 @@ class_name GdScriptParser extends RefCounted +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const ALLOWED_CHARACTERS := "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"" @@ -777,9 +778,9 @@ func extract_functions(script :GDScript, clazz_name :String, clazz_path :PackedS return parse_functions(source_code, clazz_name, clazz_path) -func parse(clazz_name :String, clazz_path :PackedStringArray) -> Result: +func parse(clazz_name :String, clazz_path :PackedStringArray) -> GdUnitResult: if clazz_path.is_empty(): - return Result.error("Invalid script path '%s'" % clazz_path) + return GdUnitResult.error("Invalid script path '%s'" % clazz_path) var is_inner_class_ := is_inner_class(clazz_path) var script :GDScript = load(clazz_path[0]) var function_descriptors := extract_functions(script, clazz_name, clazz_path) @@ -791,4 +792,4 @@ func parse(clazz_name :String, clazz_path :PackedStringArray) -> Result: function_descriptors = extract_functions(script, clazz_name, clazz_path) gd_class.set_parent_clazz(GdClassDescriptor.new(clazz_name, is_inner_class_, function_descriptors)) script = script.get_base_script() - return Result.success(gd_class) + return GdUnitResult.success(gd_class) diff --git a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd new file mode 100644 index 00000000..f3130a07 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd @@ -0,0 +1,26 @@ +class_name GdUnitExpressionRunner +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ExpressionRunner extends '${clazz_path}' + +func __run_expression() -> Variant: + return $expression + +""" + +func execute(src_script :GDScript, expression :String) -> Variant: + var script := GDScript.new() + var resource_path := "res://addons/gdUnit4/src/Fuzzers.gd" if src_script.resource_path.is_empty() else src_script.resource_path + script.source_code = CLASS_TEMPLATE.dedent()\ + .replace("${clazz_path}", resource_path)\ + .replace("$expression", expression) + script.reload(false) + var runner :Variant = script.new() + if runner.has_method("queue_free"): + runner.queue_free() + return runner.__run_expression() + + +func to_fuzzer(src_script :GDScript, expression :String) -> Fuzzer: + return execute(src_script, expression) as Fuzzer diff --git a/addons/gdUnit4/src/core/report/GdUnitReportCollector.gd b/addons/gdUnit4/src/core/report/GdUnitReportCollector.gd deleted file mode 100644 index cdad2d9b..00000000 --- a/addons/gdUnit4/src/core/report/GdUnitReportCollector.gd +++ /dev/null @@ -1,118 +0,0 @@ -# collects all reports seperated as warnings and failures/errors -class_name GdUnitReportCollector -extends RefCounted - -const STAGE_TEST_SUITE_BEFORE = 1 -const STAGE_TEST_SUITE_AFTER = 2 -const STAGE_TEST_CASE_BEFORE = 4 -const STAGE_TEST_CASE_EXECUTE = 8 -const STAGE_TEST_CASE_AFTER = 16 - -var ALL_REPORT_STATES := [STAGE_TEST_SUITE_BEFORE, STAGE_TEST_SUITE_AFTER, STAGE_TEST_CASE_BEFORE, STAGE_TEST_CASE_EXECUTE, STAGE_TEST_CASE_AFTER] -var _current_stage :int -var _consume_reports := true - - -var _reports_by_state :Dictionary = { - STAGE_TEST_SUITE_BEFORE : [] as Array[GdUnitReport], - STAGE_TEST_SUITE_AFTER : [] as Array[GdUnitReport], - STAGE_TEST_CASE_BEFORE : [] as Array[GdUnitReport], - STAGE_TEST_CASE_AFTER : [] as Array[GdUnitReport], - STAGE_TEST_CASE_EXECUTE : [] as Array[GdUnitReport], -} - - -func _init(): - GdUnitSignals.instance().gdunit_report.connect(consume) - - -func get_reports_by_state(execution_state :int) -> Array[GdUnitReport]: - return _reports_by_state.get(execution_state) - - -func add_report(execution_state :int, report :GdUnitReport) -> void: - get_reports_by_state(execution_state).append(report) - - -func push_front(execution_state :int, report :GdUnitReport) -> void: - get_reports_by_state(execution_state).push_front(report) - - -func pop_front(execution_state :int) -> GdUnitReport: - return get_reports_by_state(execution_state).pop_front() - - -func clear_reports(execution_states :int) -> void: - for state in ALL_REPORT_STATES: - if execution_states&state == state: - get_reports_by_state(state).clear() - - -func get_reports(execution_states :int) -> Array[GdUnitReport]: - var reports :Array[GdUnitReport] = [] - for state in ALL_REPORT_STATES: - if execution_states&state == state: - GdUnitTools.append_array(reports, get_reports_by_state(state)) - return reports - - -func has_errors(execution_states :int) -> bool: - for state in ALL_REPORT_STATES: - if execution_states&state == state: - for report in get_reports_by_state(state): - if report.is_error(): - return true - return false - - -func count_errors(execution_states :int) -> int: - var count := 0 - for state in ALL_REPORT_STATES: - if execution_states&state == state: - for report in get_reports_by_state(state): - if report.is_error(): - count += 1 - return count - - -func has_failures(execution_states :int) -> bool: - for state in ALL_REPORT_STATES: - if execution_states&state == state: - for report in get_reports_by_state(state): - if report.type() == GdUnitReport.FAILURE: - return true - return false - - -func count_failures(execution_states :int) -> int: - var count := 0 - for state in ALL_REPORT_STATES: - if execution_states&state == state: - for report in get_reports_by_state(state): - if report.type() == GdUnitReport.FAILURE: - count += 1 - return count - - -func has_warnings(execution_states :int) -> bool: - for state in ALL_REPORT_STATES: - if execution_states&state == state: - for report in get_reports_by_state(state): - if report.type() == GdUnitReport.WARN: - return true - return false - - -func set_stage(stage :int) -> void: - _current_stage = stage - - - -# we need to disable report collection for testing purposes -func set_consume_reports(enabled :bool) -> void: - _consume_reports = enabled - - -func consume(report :GdUnitReport) -> void: - if _consume_reports: - add_report(_current_stage, report) diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd index 91fa72cc..d0eaa16d 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd @@ -1,25 +1,32 @@ class_name GdUnitThreadContext extends RefCounted - var _thread :Thread +var _thread_name :String +var _thread_id :int var _assert :GdUnitAssert -var _signal_collector :GdUnitSignalAssertImpl.SignalCollector +var _signal_collector :GdUnitSignalCollector +var _execution_context :GdUnitExecutionContext func _init(thread :Thread = null): - _thread = thread - _signal_collector = GdUnitSignalAssertImpl.SignalCollector.new() - - -func init() -> void: - clear() + if thread != null: + _thread = thread + _thread_name = thread.get_meta("name") + _thread_id = thread.get_id() as int + else: + _thread_name = "main" + _thread_id = OS.get_main_thread_id() + _signal_collector = GdUnitSignalCollector.new() -func clear() -> void: +func dispose() -> void: _assert = null if is_instance_valid(_signal_collector): _signal_collector.clear() + _signal_collector = null + _execution_context = null + _thread = null func set_assert(value :GdUnitAssert) -> GdUnitThreadContext: @@ -31,12 +38,25 @@ func get_assert() -> GdUnitAssert: return _assert -func get_signal_collector() -> GdUnitSignalAssertImpl.SignalCollector: +func set_execution_context(context :GdUnitExecutionContext) -> void: + _execution_context = context + + +func get_execution_context() -> GdUnitExecutionContext: + return _execution_context + + +func get_execution_context_id() -> int: + return _execution_context.get_instance_id() + + +func get_signal_collector() -> GdUnitSignalCollector: return _signal_collector +func thread_id() -> int: + return _thread_id + + func _to_string() -> String: - var id := OS.get_main_thread_id() if _thread == null else int(_thread.get_id()) - var name := "main" if _thread == null else _thread.get_meta("name") as String - #var assert_ = _assert if is_instance_valid(_assert) else null - return "Thread <%s>: %s " % [name, id] + return "ThreadContext <%s>: %s " % [_thread_name, _thread_id] diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd index 24421b73..532946de 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd @@ -1,40 +1,62 @@ +## A manager to run new thread and crate a ThreadContext shared over the actual test run class_name GdUnitThreadManager extends RefCounted -## { id: = GdUnitThreadContext } -var _threads_by_id := {} +## { = } +var _thread_context_by_id := {} +## holds the current thread id +var _current_thread_id :int = -1 +func _init(): + # add initail the main thread + _current_thread_id = OS.get_thread_caller_id() + _thread_context_by_id[OS.get_main_thread_id()] = GdUnitThreadContext.new() -func _init(): - _threads_by_id[OS.get_main_thread_id()] = GdUnitThreadContext.new() +static func instance() -> GdUnitThreadManager: + return GdUnitSingleton.instance("GdUnitThreadManager", func(): return GdUnitThreadManager.new()) -func _notification(_what): - # prints("_notification", what) - pass +## Runs a new thread by given name and Callable.[br] +## A new GdUnitThreadContext is created, which is used for the actual test execution.[br] +## We need this custom implementation while this bug is not solved +## Godot issue https://github.com/godotengine/godot/issues/79637 +static func run(name :String, cb :Callable) -> Variant: + return await instance()._run(name, cb) -static func instance() -> GdUnitThreadManager: - return GdUnitSingleton.instance("GdUnitThreadManager", func(): return GdUnitThreadManager.new()) +## Returns the current valid thread context +static func get_current_context() -> GdUnitThreadContext: + return instance()._get_current_context() -static func create_thread(name :String, cb :Callable) -> Thread: - var t := Thread.new() - t.set_meta("name", name) - t.start(cb) - instance().register_thread_context(t) - return t +func _run(name :String, cb :Callable): + # we do this hack because of `OS.get_thread_caller_id()` not returns the current id + # when await process_frame is called inside the fread + var save_current_thread_id = _current_thread_id + var thread := Thread.new() + thread.set_meta("name", name) + thread.start(cb) + _current_thread_id = thread.get_id() as int + _register_thread(thread, _current_thread_id) + var result :Variant = await thread.wait_to_finish() + _unregister_thread(_current_thread_id) + # restore original thread id + _current_thread_id = save_current_thread_id + return result -func register_thread_context(thread :Thread): - _threads_by_id[thread.get_id() as int] = GdUnitThreadContext.new(thread) +func _register_thread(thread :Thread, thread_id :int) -> void: + var context := GdUnitThreadContext.new(thread) + _thread_context_by_id[thread_id] = context -func get_context(thread_id :int) -> GdUnitThreadContext: - return _threads_by_id.get(thread_id) +func _unregister_thread(thread_id :int) -> void: + var context := _thread_context_by_id.get(thread_id) as GdUnitThreadContext + if context: + _thread_context_by_id.erase(thread_id) + context.dispose() -static func get_current_context() -> GdUnitThreadContext: - var current_thread_id := OS.get_thread_caller_id() - return instance().get_context(current_thread_id) +func _get_current_context() -> GdUnitThreadContext: + return _thread_context_by_id.get(_current_thread_id) diff --git a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd index acad41cf..74cded24 100644 --- a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd +++ b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd @@ -1,5 +1,4 @@ # This class defines a value extractor by given function name and args -class_name GdUnitFuncValueExtractor extends GdUnitValueExtractor var _func_names :Array @@ -53,14 +52,18 @@ func _call_func(value, func_name :String): if GdArrayTools.is_array_type(value) and func_name == "empty": return value.is_empty() - if not (value is Object): - if GdUnitSettings.is_verbose_assert_warnings(): - push_warning("Extracting value from element '%s' by func '%s' failed! Converting to \"n.a.\"" % [value, func_name]) - return "n.a." - var extract := Callable(value, func_name) - if extract.is_valid(): - return value.call(func_name) if args().is_empty() else value.callv(func_name, args()) - else: - if GdUnitSettings.is_verbose_assert_warnings(): - push_warning("Extracting value from element '%s' by func '%s' failed! Converting to \"n.a.\"" % [value, func_name]) - return "n.a." + if is_instance_valid(value): + # extract from function + if value.has_method(func_name): + var extract := Callable(value, func_name) + if extract.is_valid(): + return value.call(func_name) if args().is_empty() else value.callv(func_name, args()) + else: + # if no function exists than try to extract form parmeters + var parameter = value.get(func_name) + if parameter != null: + return parameter + # nothing found than return 'n.a.' + if GdUnitSettings.is_verbose_assert_warnings(): + push_warning("Extracting value from element '%s' by func '%s' failed! Converting to \"n.a.\"" % [value, func_name]) + return "n.a." diff --git a/addons/gdUnit4/src/fuzzers/Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Fuzzer.gd index b11368e5..b17b6865 100644 --- a/addons/gdUnit4/src/fuzzers/Fuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/Fuzzer.gd @@ -1,7 +1,7 @@ # Base interface for fuzz testing # https://en.wikipedia.org/wiki/Fuzzing class_name Fuzzer -extends Resource +extends RefCounted # To run a test with a specific fuzzer you have to add defailt argument checked your test case # all arguments are optional [] # syntax: @@ -21,16 +21,19 @@ const ARGUMENT_SEED := "fuzzer_seed" var _iteration_index :int = 0 var _iteration_limit :int = ITERATION_DEFAULT_COUNT + # generates the next fuzz value # needs to be implement -func next_value(): +func next_value() -> Variant: push_error("Invalid vall. Fuzzer not implemented 'next_value()'") return null + # returns the current iteration index func iteration_index() -> int: return _iteration_index + # returns the amount of iterations where the fuzzer will be run func iteration_limit() -> int: return _iteration_limit diff --git a/addons/gdUnit4/src/fuzzers/FuzzerTool.gd b/addons/gdUnit4/src/fuzzers/FuzzerTool.gd deleted file mode 100644 index 64a81186..00000000 --- a/addons/gdUnit4/src/fuzzers/FuzzerTool.gd +++ /dev/null @@ -1,35 +0,0 @@ -class_name FuzzerTool -extends Resource - - -const fuzzer_template := """ -${source_code} - -func __fuzzer(): - return ${fuzzer_func} -""" - -static func create_fuzzer(source :GDScript, function: GdFunctionArgument) -> Fuzzer: - var className := source.resource_path.get_file().replace(".gd", "") - var fuzzer_func := function.value_as_string() - var source_code := fuzzer_template\ - .replace("${source_code}", source.source_code)\ - .replace("${fuzzer_func}", fuzzer_func)\ - .replace(className, className + "extented") - var script := GDScript.new() - script.source_code = source_code - var temp_dir := "res://addons/gdUnit4/.tmp" - DirAccess.make_dir_recursive_absolute(temp_dir) - var resource_path_ := "%s/%s" % [temp_dir, "_fuzzer_bulder%d.gd" % Time.get_ticks_msec()] - var err := ResourceSaver.save(script, resource_path_, ResourceSaver.FLAG_BUNDLE_RESOURCES|ResourceSaver.FLAG_REPLACE_SUBRESOURCE_PATHS) - if err != OK: - prints("Script loading error", error_string(err)) - return null - script = ResourceLoader.load(resource_path_, "GDScript", ResourceLoader.CACHE_MODE_IGNORE); - var instance :Object = script.new() - instance.queue_free() - DirAccess.remove_absolute(script.resource_path) - if not instance.has_method("__fuzzer"): - prints("Error", script, "Missing function '__fuzzer'") - return null - return instance.call("__fuzzer") as Fuzzer diff --git a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd index 40015629..0235ee20 100644 --- a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd @@ -19,7 +19,7 @@ func _init(from: int, to: int, mode :int = NORMAL): _mode = mode -func next_value() -> int: +func next_value() -> Variant: var value := randi_range(_from, _to) match _mode: NORMAL: diff --git a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd index b75e0423..b05ee138 100644 --- a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd @@ -52,7 +52,7 @@ static func build_chars(from :int, to :int) -> Array: characters.append(character) return characters -func next_value(): +func next_value() -> Variant: var value := PackedByteArray() var max_char := len(_charset) var length :int = max(_min_length, randi() % _max_length) diff --git a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd index 6bc43c3a..87c88900 100644 --- a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd @@ -10,7 +10,7 @@ func _init(from: Vector2,to: Vector2): _from = from _to = to -func next_value() -> Vector2: +func next_value() -> Variant: var x = randf_range(_from.x, _to.x) var y = randf_range(_from.y, _to.y) return Vector2(x, y) diff --git a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd index 7ff4d8c4..16b6e876 100644 --- a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd @@ -10,7 +10,7 @@ func _init(from: Vector3,to: Vector3): _from = from _to = to -func next_value() -> Vector3: +func next_value() -> Variant: var x = randf_range(_from.x, _to.x) var y = randf_range(_from.y, _to.y) var z = randf_range(_from.z, _to.z) diff --git a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd index 2ceeef99..df7f02c3 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd @@ -1,33 +1,23 @@ class_name GdUnitMockBuilder extends GdUnitClassDoubler - -# holds mocker runtime configuration -const KEY_REPORT_PUSH_ERRORS = "report_push_errors" - -# only for testing -static func do_push_errors(enabled :bool) -> void: - GdUnitStaticDictionary.add_value(KEY_REPORT_PUSH_ERRORS, enabled) - - -static func is_push_errors_enabled() -> bool: - return GdUnitStaticDictionary.get_value(KEY_REPORT_PUSH_ERRORS, false) +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const MOCK_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/mocking/GdUnitMockImpl.gd") static func is_push_errors() -> bool: - return is_push_errors_enabled() or GdUnitSettings.is_report_push_errors() + return GdUnitSettings.is_report_push_errors() -static func build(caller :Object, clazz, mock_mode :String, debug_write := false) -> Object: - var memory_pool :GdUnitMemoryPool.POOL = caller.get_meta(GdUnitMemoryPool.META_PARAM) +static func build(clazz, mock_mode :String, debug_write := false) -> Object: var push_errors := is_push_errors() if not is_mockable(clazz, push_errors): return null # mocking a scene? if GdObjects.is_scene(clazz): - return mock_on_scene(clazz as PackedScene, memory_pool, debug_write) + return mock_on_scene(clazz as PackedScene, debug_write) elif typeof(clazz) == TYPE_STRING and clazz.ends_with(".tscn"): - return mock_on_scene(load(clazz), memory_pool, debug_write) + return mock_on_scene(load(clazz), debug_write) # mocking a script var instance := create_instance(clazz) var mock := mock_on_script(instance, clazz, [ "get_script"], debug_write) @@ -39,7 +29,7 @@ static func build(caller :Object, clazz, mock_mode :String, debug_write := false mock_instance.__set_script(mock) mock_instance.__set_singleton() mock_instance.__set_mode(mock_mode) - return GdUnitMemoryPool.register_auto_free(mock_instance, memory_pool) + return register_auto_free(mock_instance) static func create_instance(clazz) -> Object: @@ -60,7 +50,7 @@ static func create_instance(clazz) -> Object: return null -static func mock_on_scene(scene :PackedScene, memory_pool :int, debug_write :bool) -> Object: +static func mock_on_scene(scene :PackedScene, debug_write :bool) -> Object: var push_errors := is_push_errors() if not scene.can_instantiate(): if push_errors: @@ -81,7 +71,7 @@ static func mock_on_scene(scene :PackedScene, memory_pool :int, debug_write :boo scene_instance.set_script(mock) scene_instance.__set_singleton() scene_instance.__set_mode(GdUnitMock.CALL_REAL_FUNC) - return GdUnitMemoryPool.register_auto_free(scene_instance, memory_pool) + return register_auto_free(scene_instance) static func get_class_info(clazz :Variant) -> Dictionary: @@ -97,7 +87,7 @@ static func mock_on_script(instance :Object, clazz :Variant, function_excludes : var push_errors := is_push_errors() var function_doubler := GdUnitMockFunctionDoubler.new(push_errors) var class_info := get_class_info(clazz) - var lines := load_template(GdUnitMockImpl, class_info, instance) + var lines := load_template(MOCK_TEMPLATE.source_code, class_info, instance) var clazz_name :String = class_info.get("class_name") var clazz_path :PackedStringArray = class_info.get("class_path", [clazz_name]) @@ -171,3 +161,7 @@ static func is_mockable(clazz :Variant, push_errors :bool=false) -> bool: return false # finally check is extending from script return GdObjects.is_script(resource) or GdObjects.is_scene(resource) + + +static func register_auto_free(obj :Variant) -> Variant: + return GdUnitThreadManager.get_current_context().get_execution_context().register_auto_free(obj) diff --git a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd index eea9726a..e544bcef 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd @@ -6,19 +6,17 @@ const TEMPLATE_FUNC_WITH_RETURN_VALUE = """ var args :Array = ["$(func_name)", $(arguments)] if $(instance)__is_prepare_return_value(): - return $(instance)__save_function_return_value(args) + $(instance)__save_function_return_value(args) + return ${default_return_value} if $(instance)__is_verify_interactions(): $(instance)__verify_interactions(args) return ${default_return_value} else: $(instance)__save_function_interaction(args) - if $(instance)__saved_return_values.has(args): - return $(instance)__saved_return_values.get(args) - - if $(instance)__do_call_real_func("$(func_name)"): + if $(instance)__do_call_real_func("$(func_name)", args): return $(await)super($(arguments)) - return ${default_return_value} + return $(instance)__get_mocked_return_value_or_default(args, ${default_return_value}) """ @@ -49,6 +47,7 @@ const TEMPLATE_FUNC_VARARG_RETURN_VALUE = """ if $(instance)__is_prepare_return_value(): if $(push_errors): push_error(\"Mocking a void function '$(func_name)() -> void:' is not allowed.\") + $(instance)__save_function_return_value(args) return ${default_return_value} if $(instance)__is_verify_interactions(): $(instance)__verify_interactions(args) @@ -56,7 +55,7 @@ const TEMPLATE_FUNC_VARARG_RETURN_VALUE = """ else: $(instance)__save_function_interaction(args) - if $(instance)__do_call_real_func("$(func_name)"): + if $(instance)__do_call_real_func("$(func_name)", args): match varargs.size(): 0: return $(await)super($(arguments)) 1: return $(await)super($(arguments), varargs[0]) @@ -69,7 +68,7 @@ const TEMPLATE_FUNC_VARARG_RETURN_VALUE = """ 8: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7]) 9: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8]) 10: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9]) - return ${default_return_value} + return __get_mocked_return_value_or_default(args, ${default_return_value}) """ diff --git a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd index 3a71435b..94110c28 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd @@ -1,20 +1,30 @@ -# warnings-disable -# warning-ignore:unused_argument -class_name GdUnitMockImpl ################################################################################ # internal mocking stuff ################################################################################ const __INSTANCE_ID = "${instance_id}" +const __SOURCE_CLASS = "${source_class}" -var __working_mode :String +var __working_mode := GdUnitMock.RETURN_DEFAULTS var __excluded_methods :PackedStringArray = [] var __do_return_value = null -var __saved_return_values := Dictionary() +var __prepare_return_value := false + +#{ = { +# = +# } +#} +var __mocked_return_values := Dictionary() static func __instance(): - return GdUnitStaticDictionary.get_value(__INSTANCE_ID) + return Engine.get_meta(__INSTANCE_ID) + + +func _notification(what): + if what == NOTIFICATION_PREDELETE: + if Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) func __instance_id() -> String: @@ -23,22 +33,78 @@ func __instance_id() -> String: func __set_singleton(): # store self need to mock static functions - GdUnitStaticDictionary.add_value(__INSTANCE_ID, self) + Engine.set_meta(__INSTANCE_ID, self) func __release_double(): # we need to release the self reference manually to prevent orphan nodes - GdUnitStaticDictionary.erase(__INSTANCE_ID) + Engine.remove_meta(__INSTANCE_ID) func __is_prepare_return_value() -> bool: - return __do_return_value != null + return __prepare_return_value + + +func __sort_by_argument_matcher(left_args :Array, _right_args :Array) -> bool: + for larg in left_args: + if larg is GdUnitArgumentMatcher: + return false + return true + + +# we need to sort by matcher arguments so that they are all at the end of the list +func __sort_dictionary(unsorted_args :Dictionary) -> Dictionary: + # only need to sort if contains more than one entry + if unsorted_args.size() <= 1: + return unsorted_args + var sorted_args := unsorted_args.keys() + sorted_args.sort_custom(__sort_by_argument_matcher) + var sorted_result := {} + for key in sorted_args: + sorted_result[key] = unsorted_args[key] + return sorted_result func __save_function_return_value(args :Array): - __saved_return_values[args] = __do_return_value + var func_name :String = args[0] + var func_args :Array = args.slice(1) + var mocked_return_value_by_args :Dictionary = __mocked_return_values.get(func_name, {}) + mocked_return_value_by_args[func_args] = __do_return_value + __mocked_return_values[func_name] = __sort_dictionary(mocked_return_value_by_args) __do_return_value = null - return __saved_return_values[args] + __prepare_return_value = false + + +func __is_mocked_args_match(func_args :Array, mocked_args :Array) -> bool: + var is_matching := false + for args in mocked_args: + if func_args.size() != args.size(): + continue + is_matching = true + for arg_index in func_args.size(): + var func_arg = func_args[arg_index] + var mock_arg = args[arg_index] + if mock_arg is GdUnitArgumentMatcher: + is_matching = is_matching and mock_arg.is_match(func_arg) + else: + is_matching = is_matching and typeof(func_arg) == typeof(mock_arg) and func_arg == mock_arg + if not is_matching: + break + if is_matching: + break + return is_matching + + +func __get_mocked_return_value_or_default(args :Array, default_return_value :Variant) -> Variant: + var func_name :String = args[0] + if not __mocked_return_values.has(func_name): + return default_return_value + var func_args :Array = args.slice(1) + var mocked_args :Array = __mocked_return_values.get(func_name).keys() + for margs in mocked_args: + if __is_mocked_args_match(func_args, [margs]): + return __mocked_return_values[func_name][margs] + return default_return_value func __set_script(script :GDScript) -> void: @@ -50,8 +116,14 @@ func __set_mode(working_mode :String): return self -func __do_call_real_func(func_name :String) -> bool: - return __working_mode == GdUnitMock.CALL_REAL_FUNC and not __excluded_methods.has(func_name) +func __do_call_real_func(func_name :String, func_args := []) -> bool: + var is_call_real_func := __working_mode == GdUnitMock.CALL_REAL_FUNC and not __excluded_methods.has(func_name) + # do not call real funcions for mocked functions + if is_call_real_func and __mocked_return_values.has(func_name): + var args :Array = func_args.slice(1) + var mocked_args :Array = __mocked_return_values.get(func_name).keys() + return not __is_mocked_args_match(args, mocked_args) + return is_call_real_func func __exclude_method_call(exluded_methods :PackedStringArray) -> void: @@ -60,4 +132,5 @@ func __exclude_method_call(exluded_methods :PackedStringArray) -> void: func __do_return(return_value): __do_return_value = return_value + __prepare_return_value = true return self diff --git a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd index 33136134..f574147e 100644 --- a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd @@ -1,12 +1,16 @@ extends RefCounted class_name ErrorLogEntry + enum TYPE { SCRIPT_ERROR, PUSH_ERROR, PUSH_WARNING } + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const PATTERN_SCRIPT_ERROR := "USER SCRIPT ERROR:" const PATTERN_PUSH_ERROR := "USER ERROR:" const PATTERN_PUSH_WARNING := "USER WARNING:" diff --git a/addons/gdUnit4/src/monitor/GdUnitMemMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitMemMonitor.gd deleted file mode 100644 index f6e3dfa1..00000000 --- a/addons/gdUnit4/src/monitor/GdUnitMemMonitor.gd +++ /dev/null @@ -1,26 +0,0 @@ -class_name GdUnitMemMonitor -extends GdUnitMonitor - -var _orphan_nodes_start :float -var _orphan_nodes_end :float -var _orphan_total :float - -func _init(name :String = ""): - super("MemMonitor:" + name) - _orphan_nodes_start = 0 - _orphan_nodes_end = 0 - _orphan_total = 0 - -func reset(): - _orphan_total = 0 - -func start(): - _orphan_nodes_start = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) - -func stop(): - _orphan_nodes_end = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) - _orphan_total += _orphan_nodes_end - _orphan_nodes_start - -func orphan_nodes() -> int: - return _orphan_total as int - diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd new file mode 100644 index 00000000..9bf76e46 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd @@ -0,0 +1,27 @@ +class_name GdUnitOrphanNodesMonitor +extends GdUnitMonitor + +var _initial_count := 0 +var _orphan_count := 0 +var _orphan_detection_enabled :bool + + +func _init(name :String = ""): + super("OrphanNodesMonitor:" + name) + _orphan_detection_enabled = GdUnitSettings.is_verbose_orphans() + + +func start(): + _initial_count = _orphans() + + +func stop(): + _orphan_count = max(0, _orphans() - _initial_count) + + +func _orphans() -> int: + return Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) as int + + +func orphan_nodes() -> int: + return _orphan_count if _orphan_detection_enabled else 0 diff --git a/addons/gdUnit4/src/mono/GdUnit3MonoAPI.cs b/addons/gdUnit4/src/mono/GdUnit3MonoAPI.cs deleted file mode 100644 index 9ef6caa4..00000000 --- a/addons/gdUnit4/src/mono/GdUnit3MonoAPI.cs +++ /dev/null @@ -1,5 +0,0 @@ - -// GdUnit3 c# API wrapper -public partial class GdUnit3MonoAPI : GdUnit3.GdUnit3MonoAPI -{ -} diff --git a/addons/gdUnit4/src/mono/GdUnit3MonoAPI.gd b/addons/gdUnit4/src/mono/GdUnit3MonoAPI.gd deleted file mode 100644 index 44bbedab..00000000 --- a/addons/gdUnit4/src/mono/GdUnit3MonoAPI.gd +++ /dev/null @@ -1,38 +0,0 @@ -extends RefCounted -class_name GdUnit3MonoAPI - -static func instance() : - return null#GdUnitSingleton.get_or_create_singleton("GdUnit3MonoAPI", "res://addons/gdUnit4/src/mono/GdUnit3MonoAPI.cs") - -static func create_test_suite(source_path :String, line_number :int, test_suite_path :String) -> Result: - if not GdUnitTools.is_mono_supported(): - return Result.error("Can't create test suite. No c# support found.") - var result := instance().CreateTestSuite(source_path, line_number, test_suite_path) as Dictionary - if result.has("error"): - return Result.error(result.get("error")) - return Result.success(result) - -static func is_test_suite(resource_path :String) -> bool: - if not is_csharp_file(resource_path) or not GdUnitTools.is_mono_supported(): - return false - if resource_path.is_empty(): - if GdUnitSettings.is_report_push_errors(): - push_error("Can't create test suite. Missing resource path.") - return false - return instance().IsTestSuite(resource_path) - -static func parse_test_suite(source_path :String) -> Node: - if not GdUnitTools.is_mono_supported(): - if GdUnitSettings.is_report_push_errors(): - push_error("Can't create test suite. No c# support found.") - return null - return instance().ParseTestSuite(source_path) - -static func create_executor(listener :Node) -> Node: - if not GdUnitTools.is_mono_supported(): - return null - return instance().Executor(listener) - -static func is_csharp_file(resource_path :String) -> bool: - var ext := resource_path.get_extension() - return ext == "cs" and GdUnitTools.is_mono_supported() diff --git a/addons/gdUnit4/src/mono/GdUnit4MonoApi.cs b/addons/gdUnit4/src/mono/GdUnit4MonoApi.cs new file mode 100644 index 00000000..0563ef92 --- /dev/null +++ b/addons/gdUnit4/src/mono/GdUnit4MonoApi.cs @@ -0,0 +1,17 @@ +using Godot; +using Godot.Collections; + +// GdUnit4 C# API wrapper +public partial class GdUnit4MonoApi : GdUnit4.GdUnit4MonoAPI +{ + public new string Version() => GdUnit4.GdUnit4MonoAPI.Version(); + + public new bool IsTestSuite(string classPath) => GdUnit4.GdUnit4MonoAPI.IsTestSuite(classPath); + + public new RefCounted Executor(Node listener) => (RefCounted)GdUnit4.GdUnit4MonoAPI.Executor(listener); + + public new GdUnit4.CsNode? ParseTestSuite(string classPath) => GdUnit4.GdUnit4MonoAPI.ParseTestSuite(classPath); + + public new Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) => + GdUnit4.GdUnit4MonoAPI.CreateTestSuite(sourcePath, lineNumber, testSuitePath); +} diff --git a/addons/gdUnit4/src/mono/GdUnit4MonoApiLoader.gd b/addons/gdUnit4/src/mono/GdUnit4MonoApiLoader.gd new file mode 100644 index 00000000..4ad97a48 --- /dev/null +++ b/addons/gdUnit4/src/mono/GdUnit4MonoApiLoader.gd @@ -0,0 +1,64 @@ +extends RefCounted +class_name GdUnit4MonoApiLoader + + +static func instance() -> Object: + return GdUnitSingleton.instance("GdUnit4MonoAPI", func(): + if not GdUnit4MonoApiLoader.is_mono_supported(): + return null + var GdUnit4MonoApi = load("res://addons/gdUnit4/src/mono/GdUnit4MonoApi.cs") + return GdUnit4MonoApi.new() + ) + + +static func is_engine_version_supported(engine_version :int = Engine.get_version_info().hex) -> bool: + return engine_version >= 0x40100 + + +# test is Godot mono running +static func is_mono_supported() -> bool: + return ClassDB.class_exists("CSharpScript") and is_engine_version_supported() + + +static func version() -> String: + if not GdUnit4MonoApiLoader.is_mono_supported(): + return "unknown" + return instance().Version() + + +static func create_test_suite(source_path :String, line_number :int, test_suite_path :String) -> GdUnitResult: + if not GdUnit4MonoApiLoader.is_mono_supported(): + return GdUnitResult.error("Can't create test suite. No c# support found.") + var result := instance().CreateTestSuite(source_path, line_number, test_suite_path) as Dictionary + if result.has("error"): + return GdUnitResult.error(result.get("error")) + return GdUnitResult.success(result) + + +static func is_test_suite(resource_path :String) -> bool: + if not is_csharp_file(resource_path) or not GdUnit4MonoApiLoader.is_mono_supported(): + return false + if resource_path.is_empty(): + if GdUnitSettings.is_report_push_errors(): + push_error("Can't create test suite. Missing resource path.") + return false + return instance().IsTestSuite(resource_path) + + +static func parse_test_suite(source_path :String) -> Node: + if not GdUnit4MonoApiLoader.is_mono_supported(): + if GdUnitSettings.is_report_push_errors(): + push_error("Can't create test suite. No c# support found.") + return null + return instance().ParseTestSuite(source_path) + + +static func create_executor(listener :Node) -> RefCounted: + if not GdUnit4MonoApiLoader.is_mono_supported(): + return null + return instance().Executor(listener) + + +static func is_csharp_file(resource_path :String) -> bool: + var ext := resource_path.get_extension() + return ext == "cs" and GdUnit4MonoApiLoader.is_mono_supported() diff --git a/addons/gdUnit4/src/network/GdUnitTask.gd b/addons/gdUnit4/src/network/GdUnitTask.gd index 662900a4..c27fd982 100644 --- a/addons/gdUnit4/src/network/GdUnitTask.gd +++ b/addons/gdUnit4/src/network/GdUnitTask.gd @@ -16,7 +16,7 @@ func _init(task_name :String,instance :Object,func_name :String): func name() -> String: return _task_name -func execute(args :Array) -> Result: +func execute(args :Array) -> GdUnitResult: if args.is_empty(): return _fref.call() return _fref.callv(args) diff --git a/addons/gdUnit4/src/network/GdUnitTcpClient.gd b/addons/gdUnit4/src/network/GdUnitTcpClient.gd index 83c7c316..0b5ee646 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpClient.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpClient.gd @@ -32,19 +32,19 @@ func stop() -> void: _connected = false -func start(host :String, port :int) -> Result: +func start(host :String, port :int) -> GdUnitResult: _host = host _port = port if _connected: - return Result.warn("Client already connected ... %s:%d" % [_host, _port]) + return GdUnitResult.warn("Client already connected ... %s:%d" % [_host, _port]) # Connect client to server if _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: var err := _stream.connect_to_host(host, port) #prints("connect_to_host", host, port, err) if err != OK: - return Result.error("GdUnit3: Can't establish client, error code: %s" % err) - return Result.success("GdUnit3: Client connected checked port %d" % port) + return GdUnitResult.error("GdUnit3: Can't establish client, error code: %s" % err) + return GdUnitResult.success("GdUnit3: Client connected checked port %d" % port) func _process(_delta): diff --git a/addons/gdUnit4/src/network/GdUnitTcpServer.gd b/addons/gdUnit4/src/network/GdUnitTcpServer.gd index 29207e68..b3ecf59f 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpServer.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd @@ -103,23 +103,23 @@ func _notification(what): stop() -func start() -> Result: +func start() -> GdUnitResult: var server_port := GdUnitServerConstants.GD_TEST_SERVER_PORT var err := OK for retry in GdUnitServerConstants.DEFAULT_SERVER_START_RETRY_TIMES: err = _server.listen(server_port, "127.0.0.1") if err != OK: - prints("GdUnit3: Can't establish server checked port: %d, Error: %s" % [server_port, error_string(err)]) + prints("GdUnit4: Can't establish server checked port: %d, Error: %s" % [server_port, error_string(err)]) server_port += 1 - prints("GdUnit3: Retry (%d) ..." % retry) + prints("GdUnit4: Retry (%d) ..." % retry) else: break if err != OK: if err == ERR_ALREADY_IN_USE: - return Result.error("GdUnit3: Can't establish server, the server is already in use. Error: %s, " % error_string(err)) - return Result.error("GdUnit3: Can't establish server. Error: %s." % error_string(err)) + return GdUnitResult.error("GdUnit3: Can't establish server, the server is already in use. Error: %s, " % error_string(err)) + return GdUnitResult.error("GdUnit3: Can't establish server. Error: %s." % error_string(err)) prints("GdUnit4: Test server successfully started checked port: %d" % server_port) - return Result.success(server_port) + return GdUnitResult.success(server_port) func stop() -> void: diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd index 11884620..d30a0696 100644 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd +++ b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd @@ -4,7 +4,8 @@ extends GdUnitResourceDto var _line_number :int = -1 var _test_case_names :PackedStringArray = [] -func serialize(test_case) -> Dictionary: + +func serialize(test_case :Object) -> Dictionary: var serialized := super.serialize(test_case) if test_case.has_method("line_number"): serialized["line_number"] = test_case.line_number() @@ -12,16 +13,21 @@ func serialize(test_case) -> Dictionary: serialized["line_number"] = test_case.get("LineNumber") if test_case.has_method("test_case_names"): serialized["test_case_names"] = test_case.test_case_names() + elif test_case.has_method("TestCaseNames"): + serialized["test_case_names"] = test_case.TestCaseNames() return serialized + func deserialize(data :Dictionary) -> GdUnitResourceDto: super.deserialize(data) _line_number = data.get("line_number", -1) _test_case_names = data.get("test_case_names", []) return self + func line_number() -> int: return _line_number + func test_case_names() -> PackedStringArray: return _test_case_names diff --git a/addons/gdUnit4/src/report/GdUnitByPathReport.gd b/addons/gdUnit4/src/report/GdUnitByPathReport.gd index ab0d233c..a0c47fa3 100644 --- a/addons/gdUnit4/src/report/GdUnitByPathReport.gd +++ b/addons/gdUnit4/src/report/GdUnitByPathReport.gd @@ -2,14 +2,14 @@ class_name GdUnitByPathReport extends GdUnitReportSummary -func _init(path :String, reports :Array[GdUnitReportSummary]): - _resource_path = path - _reports = reports +func _init(path_ :String, reports_ :Array[GdUnitReportSummary]): + _resource_path = path_ + _reports = reports_ -static func sort_reports_by_path(reports :Array[GdUnitReportSummary]) -> Dictionary: +static func sort_reports_by_path(reports_ :Array[GdUnitReportSummary]) -> Dictionary: var by_path := Dictionary() - for report in reports: + for report in reports_: var suite_path :String = report.path() var suite_report :Array[GdUnitReportSummary] = by_path.get(suite_path, [] as Array[GdUnitReportSummary]) suite_report.append(report) @@ -38,10 +38,10 @@ func write(report_dir :String) -> String: return output_path -static func apply_testsuite_reports(report_dir :String, template :String, reports :Array[GdUnitReportSummary]) -> String: +func apply_testsuite_reports(report_dir :String, template :String, reports_ :Array[GdUnitReportSummary]) -> String: var table_records := PackedStringArray() - for report in reports: + for report in reports_: var report_link = report.output_path(report_dir).replace(report_dir, "..") table_records.append(report.create_record(report_link)) return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) diff --git a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd b/addons/gdUnit4/src/report/GdUnitHtmlReport.gd index 78d55b9a..41ea52a7 100644 --- a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd +++ b/addons/gdUnit4/src/report/GdUnitHtmlReport.gd @@ -7,9 +7,9 @@ var _report_path :String var _iteration :int -func _init(path :String): - _iteration = GdUnitTools.find_last_path_index(path, REPORT_DIR_PREFIX) + 1 - _report_path = "%s/%s%d" % [path, REPORT_DIR_PREFIX, _iteration] +func _init(path_ :String): + _iteration = GdUnitTools.find_last_path_index(path_, REPORT_DIR_PREFIX) + 1 + _report_path = "%s/%s%d" % [path_, REPORT_DIR_PREFIX, _iteration] DirAccess.make_dir_recursive_absolute(_report_path) @@ -17,37 +17,37 @@ func add_testsuite_report(suite_report :GdUnitTestSuiteReport): _reports.append(suite_report) -func add_testcase_report(resource_path :String, suite_report :GdUnitTestCaseReport) -> void: +func add_testcase_report(resource_path_ :String, suite_report :GdUnitTestCaseReport) -> void: for report in _reports: - if report.resource_path() == resource_path: + if report.resource_path() == resource_path_: report.add_report(suite_report) func update_test_suite_report( - resource_path :String, - duration :int, - is_error :bool, - is_failed: bool, - is_warning :bool, - is_skipped :bool, - skipped_count :int, - failed_count :int, - orphan_count :int, - reports :Array = []) -> void: + resource_path_ :String, + duration_ :int, + _is_error :bool, + is_failed_: bool, + _is_warning :bool, + is_skipped_ :bool, + skipped_count_ :int, + failed_count_ :int, + orphan_count_ :int, + reports_ :Array = []) -> void: for report in _reports: - if report.resource_path() == resource_path: - report.set_duration(duration) - report.set_failed(is_failed, failed_count) - report.set_orphans(orphan_count) - report.set_reports(reports) - if is_skipped: - _skipped_count = skipped_count + if report.resource_path() == resource_path_: + report.set_duration(duration_) + report.set_failed(is_failed_, failed_count_) + report.set_orphans(orphan_count_) + report.set_reports(reports_) + if is_skipped_: + _skipped_count = skipped_count_ -func update_testcase_report(resource_path :String, test_report :GdUnitTestCaseReport): +func update_testcase_report(resource_path_ :String, test_report :GdUnitTestCaseReport): for report in _reports: - if report.resource_path() == resource_path: + if report.resource_path() == resource_path_: report.update(test_report) @@ -67,21 +67,21 @@ func delete_history(max_reports :int) -> int: return GdUnitTools.delete_path_index_lower_equals_than(_report_path.get_base_dir(), REPORT_DIR_PREFIX, _iteration-max_reports) -static func apply_path_reports(report_dir :String, template :String, reports :Array) -> String: - var path_report_mapping := GdUnitByPathReport.sort_reports_by_path(reports) +func apply_path_reports(report_dir :String, template :String, reports_ :Array) -> String: + var path_report_mapping := GdUnitByPathReport.sort_reports_by_path(reports_) var table_records := PackedStringArray() var paths := path_report_mapping.keys() paths.sort() - for path in paths: - var report := GdUnitByPathReport.new(path, path_report_mapping.get(path)) + for path_ in paths: + var report := GdUnitByPathReport.new(path_, path_report_mapping.get(path_)) var report_link :String = report.write(report_dir).replace(report_dir, ".") table_records.append(report.create_record(report_link)) return template.replace(GdUnitHtmlPatterns.TABLE_BY_PATHS, "\n".join(table_records)) -static func apply_testsuite_reports(report_dir :String, template :String, reports :Array) -> String: +func apply_testsuite_reports(report_dir :String, template :String, reports_ :Array) -> String: var table_records := PackedStringArray() - for report in reports: + for report in reports_: var report_link :String = report.write(report_dir).replace(report_dir, ".") table_records.append(report.create_record(report_link)) return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) diff --git a/addons/gdUnit4/src/report/GdUnitReportSummary.gd b/addons/gdUnit4/src/report/GdUnitReportSummary.gd index 24b39080..1aefa1fd 100644 --- a/addons/gdUnit4/src/report/GdUnitReportSummary.gd +++ b/addons/gdUnit4/src/report/GdUnitReportSummary.gd @@ -1,6 +1,8 @@ class_name GdUnitReportSummary extends RefCounted +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const CHARACTERS_TO_ENCODE := { '<' : '<', '>' : '>' diff --git a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd b/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd index c4d04afa..68ac30fd 100644 --- a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd +++ b/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd @@ -9,9 +9,9 @@ func _init( p_suite_name :String, test_name :String, is_error := false, - is_failed := false, + _is_failed := false, failed_count :int = 0, - orphan_count :int = 0, + orphan_count_ :int = 0, is_skipped := false, failure_reports :Array = [], p_duration :int = 0): @@ -21,7 +21,7 @@ func _init( _test_count = 1 _error_count = is_error _failure_count = failed_count - _orphan_count = orphan_count + _orphan_count = orphan_count_ _skipped_count = is_skipped _failure_reports = failure_reports _duration = p_duration diff --git a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd b/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd index 633936cd..ca4dc3fc 100644 --- a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd +++ b/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd @@ -85,8 +85,8 @@ func set_failed(failed :bool, count :int) -> void: _failure_count += count -func set_reports(reports :Array) -> void: - _failure_reports = reports +func set_reports(reports_ :Array) -> void: + _failure_reports = reports_ func update(test_report :GdUnitTestCaseReport) -> void: diff --git a/addons/gdUnit4/src/report/JUnitXmlReport.gd b/addons/gdUnit4/src/report/JUnitXmlReport.gd index 551c6bf9..65a708bb 100644 --- a/addons/gdUnit4/src/report/JUnitXmlReport.gd +++ b/addons/gdUnit4/src/report/JUnitXmlReport.gd @@ -3,6 +3,8 @@ class_name JUnitXmlReport extends RefCounted +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const ATTR_CLASSNAME := "classname" const ATTR_ERRORS := "errors" const ATTR_FAILURES := "failures" @@ -76,7 +78,7 @@ func build_test_cases(suite_report :GdUnitTestSuiteReport) -> Array: for index in suite_report.reports().size(): var report :GdUnitTestCaseReport = suite_report.reports()[index] test_cases.append( XmlElement.new("testcase")\ - .attribute(ATTR_NAME, report.name())\ + .attribute(ATTR_NAME, encode_xml(report.name()))\ .attribute(ATTR_CLASSNAME, report.suite_name())\ .attribute(ATTR_TIME, JUnitXmlReport.to_time(report.duration()))\ .add_childs(build_reports(report))) @@ -133,5 +135,9 @@ static func to_time(duration :int) -> String: return "%4.03f" % (duration / 1000.0) +static func encode_xml(value :String) -> String: + return value.xml_escape(true) + + #static func to_ISO8601_datetime() -> String: #return "%04d-%02d-%02dT%02d:%02d:%02d" % [date["year"], date["month"], date["day"], date["hour"], date["minute"], date["second"]] diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd index eb1492d8..b01350ae 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd @@ -1,9 +1,11 @@ class_name GdUnitSpyBuilder extends GdUnitClassDoubler +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const SPY_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/spy/GdUnitSpyImpl.gd") -static func build(caller :Object, to_spy, debug_write = false): - var memory_pool :GdUnitMemoryPool.POOL = caller.get_meta(GdUnitMemoryPool.META_PARAM) + +static func build(to_spy, debug_write = false) -> Object: if GdObjects.is_singleton(to_spy): push_error("Spy on a Singleton is not allowed! '%s'" % to_spy.get_class()) return null @@ -15,10 +17,10 @@ static func build(caller :Object, to_spy, debug_write = false): to_spy = load(to_spy) # spy checked PackedScene if GdObjects.is_scene(to_spy): - return spy_on_scene(to_spy.instantiate(), memory_pool, debug_write) + return spy_on_scene(to_spy.instantiate(), debug_write) # spy checked a scene instance if GdObjects.is_instance_scene(to_spy): - return spy_on_scene(to_spy, memory_pool, debug_write) + return spy_on_scene(to_spy, debug_write) var spy := spy_on_script(to_spy, [], debug_write) if spy == null: @@ -29,7 +31,7 @@ static func build(caller :Object, to_spy, debug_write = false): spy_instance.__set_singleton(to_spy) # we do not call the original implementation for _ready and all input function, this is actualy done by the engine spy_instance.__exclude_method_call([ "_input", "_gui_input", "_input_event", "_unhandled_input"]) - return GdUnitMemoryPool.register_auto_free(spy_instance, memory_pool) + return register_auto_free(spy_instance) static func get_class_info(clazz :Variant) -> Dictionary: @@ -53,7 +55,7 @@ static func spy_on_script(instance, function_excludes :PackedStringArray, debug_ if GdUnitSettings.is_verbose_assert_errors(): push_error("Can't build spy for class type '%s'! Using an instance instead e.g. 'spy()'" % [clazz_name]) return null - var lines := load_template(GdUnitSpyImpl, class_info, instance) + var lines := load_template(SPY_TEMPLATE.source_code, class_info, instance) lines += double_functions(instance, clazz_name, clazz_path, GdUnitSpyFunctionDoubler.new(), function_excludes) var spy := GDScript.new() @@ -71,7 +73,7 @@ static func spy_on_script(instance, function_excludes :PackedStringArray, debug_ return spy -static func spy_on_scene(scene :Node, memory_pool :GdUnitMemoryPool.POOL, debug_write) -> Object: +static func spy_on_scene(scene :Node, debug_write) -> Object: if scene.get_script() == null: if GdUnitSettings.is_verbose_assert_errors(): push_error("Can't create a spy checked a scene without script '%s'" % scene.get_scene_file_path()) @@ -84,7 +86,7 @@ static func spy_on_scene(scene :Node, memory_pool :GdUnitMemoryPool.POOL, debug_ return null # replace original script whit spy scene.set_script(spy) - return GdUnitMemoryPool.register_auto_free(scene, memory_pool) + return register_auto_free(scene) const EXCLUDE_PROPERTIES_TO_COPY = ["script", "type"] @@ -104,3 +106,7 @@ static func copy_properties(source :Object, dest :Object) -> void: dest.set(property_name, ""); continue dest.set(property_name, property_value) + + +static func register_auto_free(obj :Variant) -> Variant: + return GdUnitThreadManager.get_current_context().get_execution_context().register_auto_free(obj) diff --git a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd index e4b4b8d0..8b75a3e0 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd @@ -1,15 +1,19 @@ -# warnings-disable -# warning-ignore:unused_argument -class_name GdUnitSpyImpl const __INSTANCE_ID = "${instance_id}" +const __SOURCE_CLASS = "${source_class}" var __instance_delegator var __excluded_methods :PackedStringArray = [] -static func __instance(): - return GdUnitStaticDictionary.get_value(__INSTANCE_ID) +static func __instance() -> Variant: + return Engine.get_meta(__INSTANCE_ID) + + +func _notification(what): + if what == NOTIFICATION_PREDELETE: + if Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) func __instance_id() -> String: @@ -18,14 +22,13 @@ func __instance_id() -> String: func __set_singleton(delegator): # store self need to mock static functions - GdUnitStaticDictionary.add_value(__INSTANCE_ID, self) + Engine.set_meta(__INSTANCE_ID, self) __instance_delegator = delegator - #assert(__self[0] != null, "Invalid mock") func __release_double(): # we need to release the self reference manually to prevent orphan nodes - GdUnitStaticDictionary.erase(__INSTANCE_ID) + Engine.remove_meta(__INSTANCE_ID) __instance_delegator = null diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd b/addons/gdUnit4/src/ui/GdUnitConsole.gd index 283666e0..f0a3814b 100644 --- a/addons/gdUnit4/src/ui/GdUnitConsole.gd +++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd @@ -58,6 +58,13 @@ func init_statistics(event :GdUnitEvent) : _summary["total_count"] += event.total_count() +func reset_statistics() -> void: + for k in _statistics.keys(): + _statistics[k] = 0 + for k in _summary.keys(): + _summary[k] = 0 + + func update_statistics(event :GdUnitEvent) : _statistics["error_count"] += event.error_count() _statistics["failed_count"] += event.failed_count() @@ -87,7 +94,8 @@ func println_message(message :String, color :Color = _text_color, indent :int = func _on_gdunit_event(event :GdUnitEvent): match event.type(): GdUnitEvent.INIT: - _summary["total_count"] = 0 + reset_statistics() + GdUnitEvent.STOP: print_message("Summary:", Color.DODGER_BLUE) println_message("| %d total | %d error | %d failed | %d skipped | %d orphans |" % [_summary["total_count"], _summary["error_count"], _summary["failed_count"], _summary["skipped_count"], _summary["orphan_nodes"]], _text_color, 1) @@ -96,9 +104,14 @@ func _on_gdunit_event(event :GdUnitEvent): GdUnitEvent.TESTSUITE_BEFORE: init_statistics(event) print_message("Execute: ", Color.DODGER_BLUE) - println_message( event._suite_name, _engine_type_color) + println_message(event._suite_name, _engine_type_color) GdUnitEvent.TESTSUITE_AFTER: + update_statistics(event) + if not event.reports().is_empty(): + var report :GdUnitReport = event.reports().front() + println_message("\t" +event._suite_name, _engine_type_color) + println_message("line %d %s" % [report._line_number, report._message], _text_color, 2) if event.is_success(): print_message("[wave]PASSED[/wave]", Color.LIGHT_GREEN) else: diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd b/addons/gdUnit4/src/ui/GdUnitInspector.gd index adb57f71..1eb0eba6 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.gd +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd @@ -55,7 +55,7 @@ func _getEditorThemes(interface :EditorInterface) -> void: # Context menu registrations ---------------------------------------------------------------------- func add_file_system_dock_context_menu() -> void: - var is_test_suite := func is_visible(script :GDScript, is_test_suite :bool): + var is_test_suite := func is_visible(script :Script, is_test_suite :bool): if script == null: return true return GdObjects.is_test_suite(script) == is_test_suite @@ -67,7 +67,7 @@ func add_file_system_dock_context_menu() -> void: func add_script_editor_context_menu(): - var is_test_suite := func is_visible(script :GDScript, is_test_suite :bool): + var is_test_suite := func is_visible(script :Script, is_test_suite :bool): return GdObjects.is_test_suite(script) == is_test_suite var menu :Array[GdUnitContextMenuItem] = [ GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Tests", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE)), diff --git a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd index 3cca2a46..efb403f0 100644 --- a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd +++ b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd @@ -59,7 +59,7 @@ func collect_testsuites(_menu_item :GdUnitContextMenuItem, file_tree :Tree) -> P var is_dir := DirAccess.dir_exists_absolute(resource_path) if is_dir: selected_test_suites.append(resource_path) - elif is_dir or file_type == "GDScript": + elif is_dir or file_type == "GDScript" or file_type == "CSharpScript": # find a performant way to check if the selected item a testsuite #var resource := ResourceLoader.load(resource_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE) #prints("loaded", resource) diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd index 24a9580d..edb5d80f 100644 --- a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd +++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd @@ -50,11 +50,11 @@ func shortcut() -> Shortcut: return GdUnitCommandHandler.instance().get_shortcut(command.shortcut) -func is_enabled(script :GDScript) -> bool: +func is_enabled(script :Script) -> bool: return command.is_enabled.call(script) -func is_visible(script :GDScript) -> bool: +func is_visible(script :Script) -> bool: return visible.call(script) diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd index 318c9441..cda0ca56 100644 --- a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd @@ -39,8 +39,8 @@ func has_editor_focus() -> bool: return Engine.get_main_loop().root.gui_get_focus_owner() == active_base_editor() -func on_script_changed(script): - if script is GDScript: +func on_script_changed(script :Script): + if script is Script: var popups :Array[Node] = GdObjects.find_nodes_by_class(active_editor(), "PopupMenu", true) for popup in popups: if not popup.about_to_popup.is_connected(on_context_menu_show): @@ -49,7 +49,7 @@ func on_script_changed(script): popup.id_pressed.connect(on_context_menu_pressed) -func on_context_menu_show(script :GDScript, context_menu :PopupMenu): +func on_context_menu_show(script :Script, context_menu :PopupMenu): #prints("on_context_menu_show", _context_menus.keys(), context_menu, self) context_menu.add_separator() var current_index := context_menu.get_item_count() diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd index 716f2b42..08c6e549 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd @@ -17,9 +17,9 @@ func _ready(): _failures.text = "0" _errors.text = "0" var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") - var editiorTheme := editor.get_editor_interface().get_base_control().theme - _button_failure_up.icon = editiorTheme.get_icon("ArrowUp", "EditorIcons") - _button_failure_down.icon = editiorTheme.get_icon("ArrowDown", "EditorIcons") + var editior_control := editor.get_editor_interface().get_base_control() + _button_failure_up.icon = GodotVersionFixures.get_icon(editior_control, "ArrowUp") + _button_failure_down.icon = GodotVersionFixures.get_icon(editior_control, "ArrowDown") func status_changed(errors :int, failed :int): diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd index 40b484ea..0072249f 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd @@ -38,15 +38,15 @@ func _ready(): func init_buttons() -> void: - var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") - var editiorTheme := editor.get_editor_interface().get_base_control().theme + var editor :EditorPlugin = EditorPlugin.new() + var editior_control := editor.get_editor_interface().get_base_control() _button_run_overall.icon = overall_icon_image _button_run_overall.visible = GdUnitSettings.is_inspector_toolbar_button_show() - _button_run.icon = editiorTheme.get_icon("Play", "EditorIcons") + _button_run.icon = GodotVersionFixures.get_icon(editior_control, "Play") _button_run_debug.icon = debug_icon_image - _button_stop.icon = editiorTheme.get_icon("Stop", "EditorIcons") - _tool_button.icon = editiorTheme.get_icon("Tools", "EditorIcons") - _button_wiki.icon = editiorTheme.get_icon("HelpSearch", "EditorIcons") + _button_stop.icon = GodotVersionFixures.get_icon(editior_control, "Stop") + _tool_button.icon = GodotVersionFixures.get_icon(editior_control, "Tools") + _button_wiki.icon = GodotVersionFixures.get_icon(editior_control, "HelpSearch") func init_shortcuts(command_handler :GdUnitCommandHandler) -> void: diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd index a60ca253..8dc7923e 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd @@ -15,10 +15,10 @@ signal run_testsuite # tree icons @onready var ICON_SPINNER = load("res://addons/gdUnit4/src/ui/assets/spinner.tres") -@onready var ICON_TEST_DEFAULT = load_resized_texture("res://addons/gdUnit4/src/ui/assets/TestCase.svg") -@onready var ICON_TEST_SUCCESS = load_resized_texture("res://addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg") -@onready var ICON_TEST_FAILED = load_resized_texture("res://addons/gdUnit4/src/ui/assets/TestCaseFailed.svg") -@onready var ICON_TEST_ERROR = load_resized_texture("res://addons/gdUnit4/src/ui/assets/TestCaseError.svg") +@onready var ICON_TEST_DEFAULT = load("res://addons/gdUnit4/src/ui/assets/TestCase.svg") +@onready var ICON_TEST_SUCCESS = load("res://addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg") +@onready var ICON_TEST_FAILED = load("res://addons/gdUnit4/src/ui/assets/TestCaseFailed.svg") +@onready var ICON_TEST_ERROR = load("res://addons/gdUnit4/src/ui/assets/TestCaseError.svg") @onready var ICON_TEST_SUCCESS_ORPHAN = load("res://addons/gdUnit4/src/ui/assets/TestCase_success_orphan.tres") @onready var ICON_TEST_FAILED_ORPHAN = load("res://addons/gdUnit4/src/ui/assets/TestCase_failed_orphan.tres") @onready var ICON_TEST_ERRORS_ORPHAN = load("res://addons/gdUnit4/src/ui/assets/TestCase_error_orphan.tres") @@ -126,9 +126,9 @@ func is_test_suite(item :TreeItem) -> bool: func _ready(): - init_tree() if Engine.is_editor_hint(): _editor = Engine.get_meta("GdUnitEditorPlugin") + init_tree() GdUnitSignals.instance().gdunit_add_test_suite.connect(_on_gdunit_add_test_suite) GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) var command_handler := GdUnitCommandHandler.instance() @@ -144,20 +144,15 @@ func _process(_delta): queue_redraw() -func load_resized_texture(path :String, width :int = 16, height :int = 16) -> Texture2D: - var texture :Texture2D = load(path) - var image := texture.get_image() - if width > 0 && height > 0: - image.resize(width, height) - return ImageTexture.create_from_image(image) - - func init_tree() -> void: cleanup_tree() _tree.set_hide_root(true) _tree.ensure_cursor_is_visible() _tree.allow_rmb_select = true _tree_root = _tree.create_item() + # fix tree icon scaling + var scale_factor := _editor.get_editor_interface().get_editor_scale() if Engine.is_editor_hint() else 1.0 + _tree.set("theme_override_constants/icon_max_width", 16*scale_factor) func cleanup_tree() -> void: @@ -213,6 +208,9 @@ func set_state_skipped(item :TreeItem) -> void: func set_state_warnings(item :TreeItem) -> void: + # Do not overwrite higher states + if is_state_error(item) or is_state_failed(item): + return item.set_meta(META_GDUNIT_STATE, STATE.WARNING) item.set_custom_color(0, Color.YELLOW) item.set_icon(0, ICON_TEST_SUCCESS) @@ -220,6 +218,9 @@ func set_state_warnings(item :TreeItem) -> void: func set_state_failed(item :TreeItem) -> void: + # Do not overwrite higher states + if is_state_error(item): + return item.set_meta(META_GDUNIT_STATE, STATE.FAILED) item.set_custom_color(0, Color.LIGHT_BLUE) item.set_icon(0, ICON_TEST_FAILED) @@ -256,12 +257,12 @@ func set_state_orphan(item :TreeItem, event: GdUnitEvent) -> void: item.set_meta(META_GDUNIT_ORPHAN, orphan_count) item.set_custom_color(0, Color.YELLOW) item.set_tooltip_text(0, "Total <%d> orphan nodes detected." % orphan_count) - if is_state_warning(item): - item.set_icon(0, ICON_TEST_SUCCESS_ORPHAN) + if is_state_error(item): + item.set_icon(0, ICON_TEST_ERRORS_ORPHAN) elif is_state_failed(item): item.set_icon(0, ICON_TEST_FAILED_ORPHAN) - elif is_state_error(item): - item.set_icon(0, ICON_TEST_ERRORS_ORPHAN) + elif is_state_warning(item): + item.set_icon(0, ICON_TEST_SUCCESS_ORPHAN) func update_state(item: TreeItem, event :GdUnitEvent) -> void: @@ -270,12 +271,12 @@ func update_state(item: TreeItem, event :GdUnitEvent) -> void: else: if event.is_skipped(): set_state_skipped(item) - elif event.is_warning(): - set_state_warnings(item) - elif event.is_failed(): - set_state_failed(item) elif event.is_error(): set_state_error(item) + elif event.is_failed(): + set_state_failed(item) + elif event.is_warning(): + set_state_warnings(item) for report in event.reports(): add_report(item, report) set_state_orphan(item, event) diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd index 692ba3c3..8d2f1fad 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd @@ -2,6 +2,8 @@ extends Window const EAXAMPLE_URL := "https://github.com/MikeSchulze/gdUnit4-examples/archive/refs/heads/master.zip" + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") @onready var _update_client :GdUnitUpdateClient = $GdUnitUpdateClient @@ -15,26 +17,34 @@ const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdate @onready var _properties_shortcuts :Node = %"shortcut-content" @onready var _properties_report :Node = %"report-content" @onready var _input_capture :GdUnitInputCapture = %GdUnitInputCapture +@onready var _property_error :Window = %"propertyError" var _font_size :float func _ready(): + # initialize for testing + if not Engine.is_editor_hint(): + GdUnitSettings.setup() GdUnit4Version.init_version_label(_version_label) _font_size = GdUnitFonts.init_fonts(_version_label) - self.title = "GdUnitSettings" - setup_common_properties(_properties_common, GdUnitSettings.COMMON_SETTINGS) - setup_common_properties(_properties_ui, GdUnitSettings.UI_SETTINGS) - setup_common_properties(_properties_report, GdUnitSettings.REPORT_SETTINGS) - setup_common_properties(_properties_shortcuts, GdUnitSettings.SHORTCUT_SETTINGS) + setup_properties(_properties_common, GdUnitSettings.COMMON_SETTINGS) + setup_properties(_properties_ui, GdUnitSettings.UI_SETTINGS) + setup_properties(_properties_report, GdUnitSettings.REPORT_SETTINGS) + setup_properties(_properties_shortcuts, GdUnitSettings.SHORTCUT_SETTINGS) await get_tree().process_frame - popup_centered_ratio(.75) + if not Engine.is_editor_hint(): + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + DisplayServer.window_set_size(Vector2i(1600, 800)) + popup_centered_ratio(1) + else: + popup_centered_ratio(.75) func _sort_by_key(left :GdUnitProperty, right :GdUnitProperty) -> bool: return left.name() < right.name() -func setup_common_properties(properties_parent :Node, property_category) -> void: +func setup_properties(properties_parent :Node, property_category) -> void: var category_properties := GdUnitSettings.list_settings(property_category) # sort by key category_properties.sort_custom(_sort_by_key) @@ -149,10 +159,14 @@ func _to_human_readable(value :String) -> String: func _get_btn_icon(p_name :String) -> Texture2D: + if not Engine.is_editor_hint(): + var placeholder := PlaceholderTexture2D.new() + placeholder.size = Vector2(8,8) + return placeholder var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") if editor: - var editiorTheme := editor.get_editor_interface().get_base_control().theme - return editiorTheme.get_icon(p_name, "EditorIcons") + var editior_control := editor.get_editor_interface().get_base_control() + return GodotVersionFixures.get_icon(editior_control, p_name) return null @@ -231,7 +245,14 @@ func _on_btn_property_reset_pressed(property: GdUnitProperty, input :Node, reset func _on_property_text_changed(new_value :Variant, property: GdUnitProperty, reset_btn :Button): property.set_value(new_value) reset_btn.disabled = property.value() == property.default() - GdUnitSettings.update_property(property) + var error :Variant = GdUnitSettings.update_property(property) + if error: + var label :Label = _property_error.get_child(0) as Label + label.set_text(error) + var control := gui_get_focus_owner() + _property_error.show() + if control != null: + _property_error.position = control.global_position + Vector2(self.position) + Vector2(40, 40) func _on_option_selected(index :int, property: GdUnitProperty, reset_btn :Button): @@ -269,4 +290,3 @@ func stop_progress() -> void: func update_progress(message :String) -> void: _progress_text.text = message _progress_bar.value += 1 - prints(message) diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn index f569f84b..0650fbac 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=6 format=3 uid="uid://dwgat6j2u77g4"] +[gd_scene load_steps=7 format=3 uid="uid://dwgat6j2u77g4"] [ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd" id="2"] [ext_resource type="Texture2D" uid="uid://c7sk0yhd52lg3" path="res://addons/gdUnit4/src/ui/assets/icon.png" id="2_w63lb"] @@ -6,11 +6,28 @@ [ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn" id="5_xu3j8"] [ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="8_2ggr0"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hbbq5"] +content_margin_left = 10.0 +content_margin_right = 10.0 +bg_color = Color(0.172549, 0.113725, 0.141176, 1) +border_width_left = 4 +border_width_top = 4 +border_width_right = 4 +border_width_bottom = 4 +border_color = Color(0.87451, 0.0705882, 0.160784, 1) +border_blend = true +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 +shadow_color = Color(0, 0, 0, 0.756863) +shadow_size = 10 +shadow_offset = Vector2(10, 10) + [node name="Control" type="Window"] disable_3d = true -gui_embed_subwindows = true +title = "GdUnitSettings" initial_position = 1 -size = Vector2i(384, 384) visible = false wrap_controls = true transient = true @@ -186,7 +203,7 @@ layout_mode = 2 [node name="common-content" type="VBoxContainer" parent="Panel/v/MarginContainer/GridContainer/Properties/Common"] unique_name_in_owner = true clip_contents = true -custom_minimum_size = Vector2(1431, 0) +custom_minimum_size = Vector2(1445, 0) layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 @@ -210,7 +227,7 @@ layout_mode = 2 [node name="shortcut-content" type="VBoxContainer" parent="Panel/v/MarginContainer/GridContainer/Properties/Shortcuts"] unique_name_in_owner = true clip_contents = true -custom_minimum_size = Vector2(941, 0) +custom_minimum_size = Vector2(983, 0) layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 @@ -218,7 +235,7 @@ size_flags_vertical = 3 [node name="GdUnitInputCapture" parent="Panel/v/MarginContainer/GridContainer/Properties/Shortcuts/shortcut-content" instance=ExtResource("5_xu3j8")] unique_name_in_owner = true visible = false -modulate = Color(0.543351, 0.543351, 0.543351, 0.589016) +modulate = Color(0.000201742, 0.000201742, 0.000201742, 0.100182) z_index = 1 z_as_relative = false layout_mode = 2 @@ -241,6 +258,21 @@ size_flags_vertical = 3 visible = false layout_mode = 2 +[node name="propertyError" type="PopupPanel" parent="Panel/v/MarginContainer/GridContainer/Properties"] +unique_name_in_owner = true +initial_position = 1 +size = Vector2i(400, 100) +theme_override_styles/panel = SubResource("StyleBoxFlat_hbbq5") + +[node name="Label" type="Label" parent="Panel/v/MarginContainer/GridContainer/Properties/propertyError"] +offset_left = 10.0 +offset_top = 4.0 +offset_right = 390.0 +offset_bottom = 96.0 +theme_override_colors/font_color = Color(0.858824, 0, 0.109804, 1) +horizontal_alignment = 1 +vertical_alignment = 1 + [node name="MarginContainer2" type="MarginContainer" parent="Panel/v"] layout_mode = 2 size_flags_horizontal = 3 diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd b/addons/gdUnit4/src/update/GdUnitUpdate.gd index 8d517787..dc2c0bec 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdate.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd @@ -1,7 +1,9 @@ @tool extends ConfirmationDialog -const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const GdUnitUpdateClient := preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + const spinner_icon := "res://addons/gdUnit4/src/ui/assets/spinner.tres" @@ -219,7 +221,7 @@ func download_release() -> void: _update_client.queue_free() if response.code() != 200: push_warning("Update information cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.code(), response.response()]) - await message_h4("Update failed! Try it later again.", Color.RED) + message_h4("Update failed! Try it later again.", Color.RED) await get_tree().create_timer(3).timeout return diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd index a98e2732..fb3af570 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd @@ -24,7 +24,8 @@ var _download_zip_url :String func _ready(): - _editor_interface = Engine.get_meta("GdUnitEditorPlugin").get_editor_interface() + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + _editor_interface = plugin.get_editor_interface() _update_button.disabled = true _md_reader.set_http_client(_update_client) GdUnitFonts.init_fonts(_content) diff --git a/docs/manual/composites.md b/docs/manual/composites.md index 68a1dfc7..a0682e58 100644 --- a/docs/manual/composites.md +++ b/docs/manual/composites.md @@ -18,10 +18,15 @@ With composite nodes, you can craft unique and dynamic Behavior Trees for your g ## Restarting a composite -If a parent node restarts a child node, it means that the parent node will start the child node from scratch the next time it is ticked. This means that any progress made by the child node will be reset, and it will start its execution from the beginning. +When a parent node restarts, it means the whole composite node begins its evaluation again from the beginning. This does not interrupt the running child node. ## Ticking again a composite -If a parent node ticks a child node again, it means that the parent node will immediately tick the child node again on the next frame, without waiting for the current frame to finish executing. This allows the child node to continue its execution from where it left off without resetting its progress. +When a composite node ticks again, it will "jump" to the currently `RUNNING` child node and tick it. -In other words, restarting a child node means that the parent node will give the child node a fresh start, while ticking it again means that the parent node will let the child node continue its execution from where it left off. +For [sequence nodes](sequence.md), on the first frame it starts ticking all its children in order, starting from the first one, until either all of them return `SUCCESS` or one of them returns `RUNNING`. If one of them returned `RUNNING`, on the next frame, the sequence will only tick the running child node. Not all of them, just the running one - and possibly the child nodes following it, if the running child has completed and returned `SUCCESS`. +For [selector nodes](selector.md), on the first frame it starts ticking each child in order until one of them returns `SUCCESS` or one of them returns `RUNNING`. If one of them returned `RUNNING`, on the next frame, the selector will only tick the running child node, skipping all the previous ones, and possibly the child nodes following it, if the running child has completed and returned `FAILURE`. + +## Interrupting child nodes + +A sequence may interrupt any `RUNNING` child node. The `interrupt` method will be called recursively on all descendant nodes of the interrupted child node. \ No newline at end of file diff --git a/docs/manual/debugging.md b/docs/manual/debugging.md index 2998a89e..6a07d693 100644 --- a/docs/manual/debugging.md +++ b/docs/manual/debugging.md @@ -16,4 +16,4 @@ In case you want to investigate your behaviors in a separate window, **Beehave** ![how-to-debug-popout](../assets/how-to-debug-popout.png) -If you have any other ideas on how to improve the debug view, please feel free [to provide your feedback here](https://github.com/bitbrain/beehave/discussions/141). \ No newline at end of file +If you have any other ideas on how to improve the debug view, please feel free [to provide your feedback here](https://github.com/bitbrain/beehave/discussions/141). diff --git a/docs/manual/decorators.md b/docs/manual/decorators.md index 5ef90cce..23472d97 100644 --- a/docs/manual/decorators.md +++ b/docs/manual/decorators.md @@ -17,11 +17,15 @@ An `Inverter` node reverses the outcome of its child node. It returns `FAILURE` **Example:** An NPC is patrolling an area and should change its path if it *doesn't* detect an enemy. ## Limiter -The `Limiter` node executes its child a specified number of times (x). When the maximum number of ticks is reached, it returns a `FAILURE` status code. This can be beneficial when you want to limit the number of times an action or condition is executed, such as limiting the number of attempts an NPC makes to perform a task. +The `Limiter` node executes its `RUNNING` child a specified number of times (x). When the maximum number of ticks is reached, it returns a `FAILURE` status code. The limiter resets its counter after its child returns either `SUCCESS` or `FAILURE`. + +This node can be beneficial when you want to limit the number of times an action or condition is executed, such as limiting the number of attempts an NPC makes to perform a task. Once a limiter reaches its maximum number of ticks, it will start interrupting its child on every tick. **Example:** An NPC tries to unlock a door with lockpicks but will give up after three attempts if unsuccessful. ## TimeLimiter -The `TimeLimiter` node only gives its child a set amount of time to finish. When the time is up, it interrupts its child and returns a `FAILURE` status code. This is useful when you want to limit the execution time of a long running action. +The `TimeLimiter` node only gives its `RUNNING` child a set amount of time to finish. When the time is up, it interrupts its child and returns a `FAILURE` status code. The time limiter resets its time after its child returns either `SUCCESS` or `FAILURE`. + +This note is useful when you want to limit the execution time of a long running action. Once a time limiter reaches its time limit, it will start interrupting its child on every tick. **Example:** A mob aggros and tries to chase you, the chase action will last a maximum of 10 seconds before being aborted if not complete. diff --git a/examples/random_tree_example/RandomAction.gd b/examples/random_tree_example/RandomAction.gd index df7f22e8..9d8049f5 100644 --- a/examples/random_tree_example/RandomAction.gd +++ b/examples/random_tree_example/RandomAction.gd @@ -6,28 +6,34 @@ @tool class_name RandomAction extends ActionLeaf + ## How often this action changes its return status, in milliseconds. @export var reset_duration_msec: = 1000 + + var last_step = 0 var action = 0 ## Array of 3 floats signifying the weights of SUCCESS, FAILURE and RUNNING ## statuses respectively. var weights = [3., 3., 1.] + + func _get_random_action(): - var sum = 0. - for w in weights: - sum += w - var rnd = randf_range(0, sum) - for i in weights.size(): - if rnd <= weights[i]: - return i - rnd -= weights[i] - return weights.size() - 1 + var sum = 0. + for w in weights: + sum += w + var rnd = randf_range(0, sum) + for i in weights.size(): + if rnd <= weights[i]: + return i + rnd -= weights[i] + return weights.size() - 1 + func tick(actor: Node, blackboard: Blackboard) -> int: - var step = Time.get_ticks_msec() / reset_duration_msec - if step != last_step: - action = _get_random_action() - last_step = step - return action + var step = Time.get_ticks_msec() / reset_duration_msec + if step != last_step: + action = _get_random_action() + last_step = step + return action diff --git a/examples/random_tree_example/RandomTree.gd b/examples/random_tree_example/RandomTree.gd index 5b9b6545..eb2b914f 100644 --- a/examples/random_tree_example/RandomTree.gd +++ b/examples/random_tree_example/RandomTree.gd @@ -1,63 +1,69 @@ @tool extends BeehaveTree + @export_range(1, 100) var randomize_node_count: int = 20 @export var randomize_tree: bool = false: - set(v): - if !v: - return - randomize_tree = false - _randomize_tree() + set(v): + if !v: + return + randomize_tree = false + _randomize_tree() + class TNode extends RefCounted: - var children = [] + var children = [] + var long_name_suffix = "LooooooooongNameSuffix" + func get_name_suffix(): - var r = randi_range(0, 0) - if r: - return long_name_suffix - return "" + var r = randi_range(0, 0) + if r: + return long_name_suffix + return "" var tree_root: TNode var tree_nodes = [] func _get_random_node(): - return tree_nodes[randi_range(0, tree_nodes.size() - 1)] + return tree_nodes[randi_range(0, tree_nodes.size() - 1)] func _make_random_tree(): - # One node (the root) is already created - tree_root = TNode.new() - tree_nodes = [tree_root] - for i in randomize_node_count - 1: - var n = _get_random_node() - var new_node = TNode.new() - n.children.append(new_node) - tree_nodes.append(new_node) + # One node (the root) is already created + tree_root = TNode.new() + tree_nodes = [tree_root] + for i in randomize_node_count - 1: + var n = _get_random_node() + var new_node = TNode.new() + n.children.append(new_node) + tree_nodes.append(new_node) + func _parse_tree_node(parent, node, index: int): - var n - if node.children.size() > 1: - n = SelectorReactiveComposite.new() - n.name = "SelectorReactiveComposite%s-%d" % [get_name_suffix(), index] - elif node.children.size() == 1: - n = InverterDecorator.new() - n.name = "InverterDecorator%s-%d" % [get_name_suffix(), index] - else: - n = RandomAction.new() - n.name = "RandomAction%s-%d" % [get_name_suffix(), index] - parent.add_child(n) - if Engine.is_editor_hint(): - n.owner = get_tree().get_edited_scene_root() - var i = 0 - for ch in node.children: - _parse_tree_node(n, ch, i) - i += 1 + var n + if node.children.size() > 1: + n = SelectorReactiveComposite.new() + n.name = "SelectorReactiveComposite%s-%d" % [get_name_suffix(), index] + elif node.children.size() == 1: + n = InverterDecorator.new() + n.name = "InverterDecorator%s-%d" % [get_name_suffix(), index] + else: + n = RandomAction.new() + n.name = "RandomAction%s-%d" % [get_name_suffix(), index] + parent.add_child(n) + if Engine.is_editor_hint(): + n.owner = get_tree().get_edited_scene_root() + var i = 0 + for ch in node.children: + _parse_tree_node(n, ch, i) + i += 1 + func _randomize_tree(): - for ch in get_children(): - remove_child(ch) - ch.queue_free() - _make_random_tree() - _parse_tree_node(self, tree_root, 0) + for ch in get_children(): + remove_child(ch) + ch.queue_free() + _make_random_tree() + _parse_tree_node(self, tree_root, 0) diff --git a/project.godot b/project.godot index 5ba5887f..1e021385 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="Beehave" run/main_scene="res://examples/BeehaveTestScene.tscn" -config/features=PackedStringArray("4.1") +config/features=PackedStringArray("4.2") boot_splash/image="res://splash.png" boot_splash/fullsize=false config/icon="res://icon.png" @@ -36,7 +36,7 @@ import/blender/enabled=false [gdunit4] -settings/test/test_root_folder="test/" +settings/test/test_lookup_folder="test/" [input] @@ -68,3 +68,7 @@ ui_down={ , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"echo":false,"script":null) ] } + +[rendering] + +renderer/rendering_method="gl_compatibility" diff --git a/test/UnitTestScene.tscn b/test/UnitTestScene.tscn index 7bca53f4..fc136705 100644 --- a/test/UnitTestScene.tscn +++ b/test/UnitTestScene.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=8 format=3] +[gd_scene load_steps=8 format=3 uid="uid://diw3kjj050wdy"] [ext_resource type="Script" path="res://addons/beehave/blackboard.gd" id="1_27ukk"] [ext_resource type="Script" path="res://test/UnitTestScene.gd" id="1_embiv"] @@ -17,9 +17,10 @@ script = ExtResource("1_27ukk") [node name="Node2D" type="Node2D" parent="."] -[node name="BeehaveTree" type="Node" parent="Node2D"] +[node name="BeehaveTree" type="Node" parent="Node2D" node_paths=PackedStringArray("blackboard")] unique_name_in_owner = true script = ExtResource("2_phgmn") +blackboard = NodePath("@Node@22048") [node name="SequenceComposite" type="Node" parent="Node2D/BeehaveTree"] script = ExtResource("4_px6t4") @@ -80,7 +81,7 @@ script = ExtResource("6_brhwp") [node name="BeehaveTree" type="Node" parent="EmptyTree" node_paths=PackedStringArray("blackboard")] script = ExtResource("2_phgmn") -blackboard = NodePath("") +blackboard = NodePath("@Node@22045") [node name="OnlyOneActionTree" type="Node2D" parent="."] @@ -88,7 +89,7 @@ blackboard = NodePath("") script = ExtResource("2_phgmn") enabled = false actor_node_path = NodePath("..") -blackboard = NodePath("") +blackboard = NodePath("@Node@22046") [node name="CountUpAction" type="Node" parent="OnlyOneActionTree/BeehaveTree"] script = ExtResource("6_brhwp") @@ -98,7 +99,7 @@ script = ExtResource("6_brhwp") [node name="BeehaveTree" type="Node" parent="DeactivatedTree" node_paths=PackedStringArray("blackboard")] script = ExtResource("2_phgmn") actor_node_path = NodePath("..") -blackboard = NodePath("") +blackboard = NodePath("@Node@22047") [node name="CountUpAction" type="Node" parent="DeactivatedTree/BeehaveTree"] script = ExtResource("6_brhwp") diff --git a/test/actions/clear_count_action.gd b/test/actions/clear_count_action.gd index e84dbf98..2436f323 100644 --- a/test/actions/clear_count_action.gd +++ b/test/actions/clear_count_action.gd @@ -2,7 +2,7 @@ class_name ClearCountAction extends ActionLeaf @export var key = "custom_value" -func tick(actor: Node, blackboard: Blackboard) -> int: +func tick(_actor: Node, blackboard: Blackboard) -> int: if blackboard.has_value(key): blackboard.erase_value(key) return SUCCESS diff --git a/test/actions/count_up_action.gd b/test/actions/count_up_action.gd index 91727fd2..e83ae0e5 100644 --- a/test/actions/count_up_action.gd +++ b/test/actions/count_up_action.gd @@ -5,13 +5,13 @@ class_name CountUpAction extends ActionLeaf var count = 0 var status = SUCCESS -func tick(actor: Node, blackboard: Blackboard) -> int: +func tick(_actor: Node, blackboard: Blackboard) -> int: count += 1 blackboard.set_value(key, count) return status -func interrupt(actor: Node, blackboard: Blackboard) -> void: +func interrupt(_actor: Node, blackboard: Blackboard) -> void: count = 0 blackboard.set_value(key, count) status = FAILURE diff --git a/test/actions/mock_action.gd b/test/actions/mock_action.gd index 8e4148e9..5909446c 100644 --- a/test/actions/mock_action.gd +++ b/test/actions/mock_action.gd @@ -16,7 +16,7 @@ func before_run(actor: Node, blackboard: Blackboard) -> void: started_running.emit(actor, blackboard) -func tick(actor: Node, blackboard: Blackboard) -> int: +func tick(_actor: Node, _blackboard: Blackboard) -> int: if tick_count < running_frame_count: tick_count += 1 return RUNNING diff --git a/test/beehave_tree_test.gd b/test/beehave_tree_test.gd index 57fd12a3..3e990175 100644 --- a/test/beehave_tree_test.gd +++ b/test/beehave_tree_test.gd @@ -13,14 +13,14 @@ func create_scene() -> Node2D: func test_normal_tick() -> void: var scene = create_scene() - var runner := scene_runner(scene) + scene_runner(scene) scene.beehave_tree._physics_process(1.0) assert_that(scene.beehave_tree.status).is_equal(BeehaveNode.SUCCESS) func test_nothing_running_before_first_tick() -> void: var scene = create_scene() - var runner := scene_runner(scene) + scene_runner(scene) assert_that(scene.beehave_tree.get_running_action()).is_null() assert_that(scene.beehave_tree.get_last_condition()).is_null() assert_that(scene.beehave_tree.get_last_condition_status()).is_equal("") @@ -55,7 +55,7 @@ func test_reenabled() -> void: func test_interrupt_running_action() -> void: var scene = create_scene() - var runner := scene_runner(scene) + scene_runner(scene) scene.count_up_action.status = BeehaveNode.RUNNING scene.beehave_tree._physics_process(1.0) scene.beehave_tree._physics_process(1.0) diff --git a/test/before_after_run_test.gd b/test/before_after_run_test.gd index 154d46e3..44ec0397 100644 --- a/test/before_after_run_test.gd +++ b/test/before_after_run_test.gd @@ -28,10 +28,10 @@ func before_test() -> void: func test_action_after_run() -> void: - var before_run_callback = func (actor, blackboard): + var before_run_callback = func (_actor, blackboard): blackboard.set_value("entered", true) - var after_run_callback = func (actor, blackboard): + var after_run_callback = func (_actor, blackboard): blackboard.set_value("exited", true) action.started_running.connect(before_run_callback) diff --git a/test/conditions/value_reached_condition.gd b/test/conditions/value_reached_condition.gd index 24fcacda..30aeeb57 100644 --- a/test/conditions/value_reached_condition.gd +++ b/test/conditions/value_reached_condition.gd @@ -3,7 +3,7 @@ class_name ValueReachedCondition extends ConditionLeaf @export var limit = 2 @export var key = "custom_value" -func tick(actor: Node, blackboard: Blackboard) -> int: +func tick(_actor: Node, blackboard: Blackboard) -> int: if blackboard.get_value(key, 0) >= limit: return SUCCESS else: diff --git a/test/debug/debugger_test.gd b/test/debug/debugger_test.gd index 2f0af7b2..8c9d9006 100644 --- a/test/debug/debugger_test.gd +++ b/test/debug/debugger_test.gd @@ -14,8 +14,8 @@ func create_scene() -> Node2D: func test_debugger_renders_correctly(): var scene = create_scene() - var scene_runner = scene_runner(scene) - await scene_runner.simulate_frames(20) - await scene_runner.set_mouse_pos(Vector2(20, 20)) - await scene_runner.simulate_mouse_button_press(1) - await scene_runner.simulate_frames(10) + var runner = scene_runner(scene) + await runner.simulate_frames(20) + runner.set_mouse_pos(Vector2(20, 20)) + runner.simulate_mouse_button_press(1) + await runner.simulate_frames(10) diff --git a/test/nodes/composites/selector_test.gd b/test/nodes/composites/selector_test.gd index 26cccb81..4eaf2477 100644 --- a/test/nodes/composites/selector_test.gd +++ b/test/nodes/composites/selector_test.gd @@ -106,8 +106,6 @@ func test_clear_running_child_after_run() -> void: func test_not_interrupt_first_after_finished() -> void: var action3 = auto_free(load(__count_up_action).new()) selector.add_child(action3) - var running_action: Node - var blackboard_name: String = str(actor.get_instance_id()) action1.status = BeehaveNode.RUNNING action2.status = BeehaveNode.FAILURE diff --git a/test/nodes/composites/sequence_star_test.gd b/test/nodes/composites/sequence_star_test.gd index f9bf7a23..cadd6cc7 100644 --- a/test/nodes/composites/sequence_star_test.gd +++ b/test/nodes/composites/sequence_star_test.gd @@ -91,11 +91,13 @@ func test_keeps_running_child_until_failure() -> void: assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE) assert_that(action1.count).is_equal(1) - assert_that(action2.count).is_equal(3) + # action2 will reset as it failed + assert_that(action2.count).is_equal(0) assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE) assert_that(action1.count).is_equal(1) - assert_that(action2.count).is_equal(4) + # action2 has reset previously but sequence star will tick again + assert_that(action2.count).is_equal(1) func test_tick_again_when_child_returns_failure() -> void: diff --git a/test/nodes/decorators/limiter_test.gd b/test/nodes/decorators/limiter_test.gd index 08a0b252..c5880708 100644 --- a/test/nodes/decorators/limiter_test.gd +++ b/test/nodes/decorators/limiter_test.gd @@ -30,13 +30,27 @@ func before_test() -> void: tree.blackboard = blackboard -func test_max_count(count: int, test_parameters: Array = [[2], [0]]) -> void: +func test_max_count(count: int, _test_parameters: Array = [[2], [0]]) -> void: limiter.max_count = count - + action.status = BeehaveNode.RUNNING for i in range(count): - assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS) + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + assert_that(action.count).is_equal(count) assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE) + # ensure it resets its child after it reached max count + assert_that(action.count).is_equal(0) + + +func test_interrupt_after_run() -> void: + action.status = BeehaveNode.RUNNING + limiter.max_count = 1 + tree.tick() + assert_that(limiter.running_child).is_equal(action) + action.status = BeehaveNode.FAILURE + tree.tick() + assert_that(action.count).is_equal(0) + assert_that(limiter.running_child).is_equal(null) func test_clear_running_child_after_run() -> void: @@ -46,4 +60,5 @@ func test_clear_running_child_after_run() -> void: assert_that(limiter.running_child).is_equal(action) action.status = BeehaveNode.SUCCESS tree.tick() + assert_that(action.count).is_equal(2) assert_that(limiter.running_child).is_equal(null) diff --git a/test/nodes/decorators/repeater_test.gd b/test/nodes/decorators/repeater_test.gd index 14768994..037a5787 100644 --- a/test/nodes/decorators/repeater_test.gd +++ b/test/nodes/decorators/repeater_test.gd @@ -42,7 +42,7 @@ func after_test(): tree.blackboard.set_value("ended", 0) -func test_repetitions(count: int, test_parameters: Array = [[2], [0]]) -> void: +func test_repetitions(count: int, _test_parameters: Array = [[2], [0]]) -> void: repeater.repetitions = count action.final_result = BeehaveNode.SUCCESS @@ -88,11 +88,11 @@ func test_failure(): assert_int(times_ended).is_equal(2) -func _on_action_started(actor, blackboard): +func _on_action_started(_actor, blackboard): var started = blackboard.get_value("started", 0) blackboard.set_value("started", started + 1) -func _on_action_ended(actor, blackboard): +func _on_action_ended(_actor, blackboard): var ended = blackboard.get_value("ended", 0) - blackboard.set_value("ended", ended + 1) \ No newline at end of file + blackboard.set_value("ended", ended + 1) diff --git a/test/nodes/decorators/time_limiter_test.gd b/test/nodes/decorators/time_limiter_test.gd index 3320f2ab..1a1e100d 100644 --- a/test/nodes/decorators/time_limiter_test.gd +++ b/test/nodes/decorators/time_limiter_test.gd @@ -13,47 +13,51 @@ const __blackboard = "res://addons/beehave/blackboard.gd" var tree: BeehaveTree var action: ActionLeaf var time_limiter: TimeLimiterDecorator +var actor: Node2D +var blackboard: Blackboard +var runner:GdUnitSceneRunner func before_test() -> void: tree = auto_free(load(__tree).new()) + actor = auto_free(Node2D.new()) + blackboard = auto_free(load(__blackboard).new()) + + tree.actor = actor + tree.blackboard = blackboard action = auto_free(load(__action).new()) time_limiter = auto_free(load(__source).new()) - var actor = auto_free(Node2D.new()) - var blackboard = auto_free(load(__blackboard).new()) - + time_limiter.add_child(action) tree.add_child(time_limiter) - time_limiter.child = action - tree.actor = actor - tree.blackboard = blackboard + runner = scene_runner(tree) func test_return_failure_when_child_exceeds_time_limiter() -> void: time_limiter.wait_time = 1.0 action.status = BeehaveNode.RUNNING + tree.tick() assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) - time_limiter.time_left = 0.5 - assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) - time_limiter.time_left = 1.0 + await runner.simulate_frames(1, 1500) assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE) func test_reset_when_child_finishes() -> void: - time_limiter.wait_time = 1.0 + time_limiter.wait_time = 0.5 action.status = BeehaveNode.RUNNING + tree.tick() assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) - time_limiter.time_left = 0.5 + await runner.simulate_frames(2, 500) action.status = BeehaveNode.SUCCESS assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS) func test_clear_running_child_after_run() -> void: - time_limiter.wait_time = 1.0 + time_limiter.wait_time = 1.5 action.status = BeehaveNode.RUNNING tree.tick() assert_that(time_limiter.running_child).is_equal(action) action.status = BeehaveNode.SUCCESS - tree.tick() - assert_that(time_limiter.running_child).is_equal(null) + await runner.simulate_frames(1, 1600) + assert_that(time_limiter.running_child).is_null() diff --git a/test/nodes/leaves/actions/blackboard_set_test.gd b/test/nodes/leaves/actions/blackboard_set_test.gd index 63e88ec8..4689e74c 100644 --- a/test/nodes/leaves/actions/blackboard_set_test.gd +++ b/test/nodes/leaves/actions/blackboard_set_test.gd @@ -26,7 +26,7 @@ func before_test() -> void: func test_set_to_constant() -> void: blackboard_set.value = "0" - var runner: GdUnitSceneRunner = scene_runner(blackboard_set) # run it as a scene, so that _ready gets called + scene_runner(blackboard_set) # run it as a scene, so that _ready gets called assert_that(blackboard_set.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS) assert_bool(blackboard.has_value(KEY)).is_true() assert_that(blackboard.get_value(KEY)).is_equal(0) @@ -35,21 +35,21 @@ func test_set_to_constant() -> void: func test_copy_key() -> void: blackboard.set_value(KEY2, 0) blackboard_set.value = "get_value('%s')" % [KEY2] - var runner: GdUnitSceneRunner = scene_runner(blackboard_set) + scene_runner(blackboard_set) assert_that(blackboard_set.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS) assert_that(blackboard.get_value(KEY)).is_equal(blackboard.get_value(KEY2)) # properly copy values from one key to another func test_invalid_expression() -> void: blackboard_set.value = "this is not a valid expression" - var runner: GdUnitSceneRunner = scene_runner(blackboard_set) + scene_runner(blackboard_set) assert_that(blackboard_set.tick(actor, blackboard)).is_equal(BeehaveNode.FAILURE) assert_bool(blackboard.has_value(KEY)).is_false() func test_set_vector3() -> void: blackboard_set.value = "Vector3(0,0,0)" - var runner: GdUnitSceneRunner = scene_runner(blackboard_set) + scene_runner(blackboard_set) assert_that(blackboard_set.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS) assert_bool(blackboard.has_value(KEY)).is_true() assert_that(blackboard.get_value(KEY)).is_equal(Vector3(0,0,0)) @@ -57,11 +57,11 @@ func test_set_vector3() -> void: func test_invalid_key_expression() -> void: blackboard_set.key = "this is invalid!!!" - var runner: GdUnitSceneRunner = scene_runner(blackboard_set) + scene_runner(blackboard_set) assert_that(blackboard_set.tick(actor, blackboard)).is_equal(BeehaveNode.FAILURE) func test_invalid_value_expression() -> void: blackboard_set.value = "this is invalid!!!" - var runner: GdUnitSceneRunner = scene_runner(blackboard_set) + scene_runner(blackboard_set) assert_that(blackboard_set.tick(actor, blackboard)).is_equal(BeehaveNode.FAILURE) diff --git a/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights.gd b/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights.gd index c0e6d0d4..9a63be0d 100644 --- a/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights.gd +++ b/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights.gd @@ -14,7 +14,7 @@ func _ready(): _counter_keys[action.name] = action.key -func _process(delta): +func _process(_delta:float): var debug_text = "" var sample_size = 0 @@ -37,7 +37,6 @@ func get_final_results() -> Dictionary: sample_size += count for action_name in _counter_keys.keys(): - var a = _counter_keys[action_name] var value = blackboard.get_value(_counter_keys[action_name], 0) var perc = (float(value) / float(sample_size)) * 100.0 results[action_name] = perc diff --git a/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights_test.gd b/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights_test.gd index 13a64219..fe30a7eb 100644 --- a/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights_test.gd +++ b/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights_test.gd @@ -3,7 +3,7 @@ extends GdUnitTestSuite const __source = "res://test/randomized_composites/weighted_sampling/selector_random/SelectorRandomWeights.tscn" -const ACCEPTABLE_RANGE = 3.0 +const ACCEPTABLE_RANGE = 5 func create_scene() -> Node2D: