Skip to content

Commit

Permalink
Merge pull request #17 from bitwes/3_1
Browse files Browse the repository at this point in the history
3 1
  • Loading branch information
bitwes authored Jan 28, 2017
2 parents 9e390a5 + b54f22f commit f4c5b1c
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 51 deletions.
Binary file added .DS_Store
Binary file not shown.
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ This guy warrants his own section. I found that making tests for most getters a
* `gut.p(text, level=0, indent=0)` print info to the GUI and console (if enabled.)

* `gut.pause_before_teardown()` This method will cause Gut to pause before it moves on to the next test. This is useful for debugging, for instance if you want to investigate the screen or anything else after a test has finished executing. See also `set_ignore_pause_before_teardown`
* `yield_for(time_in_seconds)` Basically this simplifies the code needed to pause the test execution for a number of seconds. This is useful if your test requires things to play out in real time before making an assertion. There are more details in the Yielding section. It is designed to be used with the `yield` built in. The following example will pause your test execution (and only the test execution) for 5 seconds before continuing. You must call `end_test()` in any test that has a yield in it or execution will not continue.
* `yield_for(time_in_seconds)` Basically this simplifies the code needed to pause the test execution for a number of seconds. This is useful if your test requires things to play out in real time before making an assertion. There are more details in the Yielding section. It is designed to be used with the `yield` built in. The following example will pause your test execution (and only the test execution) for 5 seconds before continuing. You must call an assert or `pending` or `end_test()` after a yield or the test will never stop running.
* `yield(yield_for(5), YIELD)`
* `end_test()` This must be called in any test that has a `yield` in it (regardless of what is yielded to) so that Gut knows when the test has completed.
* `end_test()` This can be called instead of an assert or `pending` to end a test that has yielded.

### <a name="gut_methods"> Methods for Configuring the Execution of Tests
These methods would be used inside the Scene's script (`templates/gut_main.gd`) to load and execute scripts as well as inspect the results of a run and change how Gut behaves. These methods must all be called on the Gut object that was instantiated. In the case of the provided template, this would be `tester`.
Expand All @@ -151,7 +151,7 @@ These methods would be used inside the Scene's script (`templates/gut_main.gd`)
# <a name="extras"> Extras

## <a name="strict"> Strict type checking
Gut performs type checks in the asserts where it applies. This is done for a few reasons. The first is that invalid comparisons can cause runtime errors which will stop your tests from running. With the type checking enabled your test will fail instead of crashing. The other reason is that you can get false positives/negatives when comparing things like a Real/Float and an Integer. With strict type checking enabled these become a lot more obvious. It's also a sanity check to make sure your classes are using the expected types of values which can save time in the long run.
Gut performs type checks in the asserts when comparing two differnt types would normally cause a runtime error. With the type checking enabled (on be default) your test will fail instead of crashing. Some types are ok to be compared such as Floats and Integers but if you attempt to compare a String with a Float your test will fail instead of blowing up.

You can disable this behavior if you like by calling `tester.disable_strict_datatype_checks(true)` inside `gut_main.gd`.

Expand Down Expand Up @@ -250,7 +250,7 @@ func test_does_something_each_loop():
```
## <a name="yielding"> Yielding during a test

I'm not going to try and explain yielding here. It's can be a bit confusing and [Godot does a pretty good job of it already](http://docs.godotengine.org/en/latest/reference/gdscript.html#coroutines). Gut has support for yielding though, so you can yield at anytime in your test. The one caveat is that you must tell Gut when your test has completed so it can continue running tests. You do this by calling `end_test()`.
I'm not going to try and explain yielding here. It's can be a bit confusing and [Godot does a pretty good job of it already](http://docs.godotengine.org/en/latest/reference/gdscript.html#coroutines). Gut has support for yielding though, so you can yield at anytime in your test. The one caveat is that you must use one of the various asserts or `pending()` after the yield. Otherwise Gut won't know that the yield has finished. You can optionally use `end_test()` if an assert or `pending` doesn't make sense for some reason.

When might you want to yield? Yielding is very handy when you want to wait for a signal to occur instead of running for a finite amount of time. For example, you could have your test yield until your character gets hit by something (`yield(my_char, 'hit')`). An added bonus of this approach is that you can watch everything happen. In your test you create your character, the object to hit it, and then watch the interaction play out.

Expand All @@ -261,7 +261,6 @@ func test_yield_to_custom_signal():
add_child(my_object)
yield(my_object, 'custom_signal')
assert_true(some_condition, 'After signal fired, this should be true')
end_test()
```

Another use case I have come across is when creating integration tests and you want to verify that a complex interaction ends with an expected result. In this case you might have an idea of how long the interaction will take to play out but you don't have a signal that you can attach to. Instead you want to pause your test execution until that time has elapsed. For this, Gut has the `yield_for` method. This method has a little magic in it, but all you really need to know is that the following line will pause your test execution for 5 seconds while the rest of your code executes as expected: `yield(yield_for(5), YIELD)`.
Expand All @@ -274,12 +273,11 @@ func test_wait_for_a_bit():
#wait 5 seconds
yield(yield_for(5), YIELD)
gut.assert_eq(my_object.some_property, 'some value', 'After waiting 5 seconds, this property should be set')
end_test()
```
Sometimes it's also helpful to just watch things play out. Yield is great for that, you just create a couple objects, set them to interact and then yield. You can leave the yields in or take them out if your test passes without them. You can also use the `pause_before_teardown` method that will pause test execution before it runs `teardown` and moves onto the next test. This keeps the game loop running after the test has finished and you can see what everything looks like.

### How Yielding and Gut Works
For those that are interested, Gut is able to detect when a test has called yield because the method returns a special class back. Gut itself will then `yield` to an internal timer and check to see if `end_test` has been called every second, if not it waits again. It continues to do this until `end_test` has been called.
For those that are interested, Gut is able to detect when a test has called yield because the method returns a special class back. Gut itself will then `yield` to an internal timer and check to see if an assertion or `pending()` or `end_test()` has been called every second, if not it waits again.

If you only yielded using `yield_for` then Gut would always know when to resume the test and could handle it itself. You can yield to anything though and Gut cannot tell the difference. Also, when you yield to something else Gut has no way of knowing when the method has continued so you have to tell it when you are done so it will stop waiting. One side effect of this is that if you `yield` multiple times in the same test, Gut can't tell. It continues to wait from the first yield and you won't see any additional "yield detected" outputs in the GUI or console.

Expand Down
2 changes: 1 addition & 1 deletion engine.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[application]

name="GUT"
main_scene="res://scenes/main.scn"
main_scene="res://scenes/main.tscn"
icon="icon.png"

[autoload]
Expand Down
Binary file removed scenes/main.scn
Binary file not shown.
10 changes: 10 additions & 0 deletions scenes/main.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[gd_scene load_steps=2 format=1]

[ext_resource path="res://scripts/main.gd" type="Script" id=1]

[node name="main" type="Node2D"]

script/script = ExtResource( 1 )
__meta__ = { "__editor_plugin_screen__":"Script" }


Binary file added test/.DS_Store
Binary file not shown.
128 changes: 96 additions & 32 deletions test/gut/gut.gd
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
################################################################################
# View readme for usage details.
#
# Version 3.0
# Version 3.1
################################################################################
extends WindowDialog

Expand Down Expand Up @@ -71,8 +71,6 @@ var _yield_between = {

var types = {}



var _set_yield_time_called = false
# used when yielding to gut instead of some other
# signal. Start with set_yield_time()
Expand Down Expand Up @@ -105,7 +103,9 @@ var _ctrls = {
script_progress = ProgressBar.new(),
test_progress = ProgressBar.new(),
runtime_label = Label.new(),
ignore_continue_checkbox = CheckBox.new()
ignore_continue_checkbox = CheckBox.new(),
pass_count = Label.new(),
run_rest = Button.new()
}

var _mouse_down = false
Expand All @@ -119,6 +119,12 @@ var min_size = Vector2(650, 400)
const SIGNAL_TESTS_FINISHED = 'tests_finished'
const SIGNAL_STOP_YIELD_BEFORE_TEARDOWN = 'stop_yeild_before_teardown'


func _set_anchor_top_right(obj):
obj.set_anchor(MARGIN_RIGHT, ANCHOR_BEGIN)
obj.set_anchor(MARGIN_LEFT, ANCHOR_END)
obj.set_anchor(MARGIN_TOP, ANCHOR_BEGIN)

func _set_anchor_bottom_right(obj):
obj.set_anchor(MARGIN_LEFT, ANCHOR_END)
obj.set_anchor(MARGIN_RIGHT, ANCHOR_END)
Expand Down Expand Up @@ -192,12 +198,13 @@ func setup_controls():
_ctrls.clear_button.set_pos(_ctrls.copy_button.get_pos() - Vector2(button_size.x, 0) - button_spacing)
_ctrls.clear_button.connect("pressed", self, "clear_text")
_set_anchor_bottom_right(_ctrls.clear_button)

add_child(_ctrls.runtime_label)
_ctrls.runtime_label.set_text('0.0')
_ctrls.runtime_label.set_size(Vector2(50, 30))
_ctrls.runtime_label.set_pos(Vector2(_ctrls.clear_button.get_pos().x - 60, _ctrls.clear_button.get_pos().y + 10))
_set_anchor_bottom_right(_ctrls.runtime_label)

add_child(_ctrls.pass_count)
_ctrls.pass_count.set_text('0 - 0')
_ctrls.pass_count.set_size(Vector2(100, 30))
_ctrls.pass_count.set_pos(Vector2(550, 0))
_ctrls.pass_count.set_align(HALIGN_RIGHT)
_set_anchor_top_right(_ctrls.pass_count)

add_child(_ctrls.continue_button)
_ctrls.continue_button.set_text("Continue")
Expand Down Expand Up @@ -262,16 +269,10 @@ func setup_controls():
_ctrls.test_progress.set_unit_value(1)
_set_anchor_bottom_left(_ctrls.test_progress)

add_child(_ctrls.scripts_drop_down)
_ctrls.scripts_drop_down.set_size(Vector2(375, 25))
_ctrls.scripts_drop_down.set_pos(Vector2(10, _ctrls.log_level_slider.get_pos().y + 50))
_ctrls.scripts_drop_down.add_item("Run All")
_set_anchor_bottom_left(_ctrls.scripts_drop_down)

add_child(_ctrls.previous_button)
_ctrls.previous_button.set_size(Vector2(50, 25))
pos = _ctrls.scripts_drop_down.get_pos() + Vector2(_ctrls.scripts_drop_down.get_size().x, -30)
pos.x -= 240
pos = _ctrls.test_progress.get_pos() + Vector2(250, 25)
pos.x -= 300
_ctrls.previous_button.set_pos(pos)
_ctrls.previous_button.set_text("<")
_ctrls.previous_button.connect("pressed", self, '_on_previous_button_pressed')
Expand All @@ -285,13 +286,13 @@ func setup_controls():
_ctrls.stop_button.connect("pressed", self, '_on_stop_button_pressed')
_set_anchor_bottom_left(_ctrls.stop_button)

add_child(_ctrls.run_button)
_ctrls.run_button.set_text("run")
_ctrls.run_button.set_size(Vector2(50, 25))
add_child(_ctrls.run_rest)
_ctrls.run_rest.set_text('run')
_ctrls.run_rest.set_size(Vector2(50, 25))
pos.x += 60
_ctrls.run_button.set_pos(pos)
_ctrls.run_button.connect("pressed", self, "_on_run_button_pressed")
_set_anchor_bottom_left(_ctrls.run_button)
_ctrls.run_rest.set_pos(pos)
_ctrls.run_rest.connect('pressed', self, '_on_run_rest_pressed')
_set_anchor_bottom_left(_ctrls.run_rest)

add_child(_ctrls.next_button)
_ctrls.next_button.set_size(Vector2(50, 25))
Expand All @@ -301,6 +302,30 @@ func setup_controls():
_ctrls.next_button.connect("pressed", self, '_on_next_button_pressed')
_set_anchor_bottom_left(_ctrls.next_button)

add_child(_ctrls.runtime_label)
_ctrls.runtime_label.set_text('0.0')
_ctrls.runtime_label.set_size(Vector2(50, 30))
_ctrls.runtime_label.set_pos(Vector2(_ctrls.clear_button.get_pos().x - 90, _ctrls.next_button.get_pos().y))
_set_anchor_bottom_right(_ctrls.runtime_label)

# the drop down has to be one of the last added so that when then list of
# scripts is displayed, other controls do not get in the way of selecting
# an item in the list.
add_child(_ctrls.scripts_drop_down)
_ctrls.scripts_drop_down.set_size(Vector2(375, 25))
_ctrls.scripts_drop_down.set_pos(Vector2(10, _ctrls.log_level_slider.get_pos().y + 50))
_set_anchor_bottom_left(_ctrls.scripts_drop_down)
_ctrls.scripts_drop_down.connect('item_selected', self, '_on_script_selected')
_ctrls.scripts_drop_down.set_clip_text(true)

add_child(_ctrls.run_button)
_ctrls.run_button.set_text('<- run')
_ctrls.run_button.set_size(Vector2(50, 25))
_ctrls.run_button.set_pos(_ctrls.scripts_drop_down.get_pos() + Vector2(_ctrls.scripts_drop_down.get_size().x + 5, 0))
_ctrls.run_button.connect("pressed", self, "_on_run_button_pressed")
_set_anchor_bottom_left(_ctrls.run_button)


#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
func _init():
Expand Down Expand Up @@ -344,6 +369,8 @@ func _ready():
set_process(true)

set_pause_mode(PAUSE_MODE_PROCESS)
_update_controls()

#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
func _process(delta):
Expand Down Expand Up @@ -492,6 +519,16 @@ func _on_ignore_continue_checkbox_pressed():
if(!_ctrls.continue_button.is_disabled()):
_on_continue_button_pressed()

#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
func _on_script_selected(id):
_update_controls()

#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
func _on_run_rest_pressed():
test_scripts(true)

#####################
#
# Private
Expand All @@ -504,17 +541,20 @@ func _update_controls():
if(_is_running):
_ctrls.previous_button.set_disabled(true)
_ctrls.next_button.set_disabled(true)
_ctrls.pass_count.show()
else:
_ctrls.previous_button.set_disabled(_ctrls.scripts_drop_down.get_selected() == 0)
_ctrls.next_button.set_disabled(_ctrls.scripts_drop_down.get_selected() == _ctrls.scripts_drop_down.get_item_count() -1)
_ctrls.pass_count.hide()

# disabled during run
_ctrls.run_button.set_disabled(_is_running)
_ctrls.run_rest.set_disabled(_is_running)
_ctrls.scripts_drop_down.set_disabled(_is_running)

# enabled during run
_ctrls.stop_button.set_disabled(!_is_running)

_ctrls.pass_count.set_text(str( _summary.passed, ' - ', _summary.failed))



Expand Down Expand Up @@ -553,6 +593,9 @@ func _fail(text):
p('FAILED: ' + text, LOG_LEVEL_FAIL_ONLY)
if(_current_test != null):
p(' at line ' + str(_current_test.line_number), LOG_LEVEL_FAIL_ONLY)
_update_controls()
end_yielded_test()


#-------------------------------------------------------------------------------
#Pass an assertion.
Expand All @@ -562,6 +605,9 @@ func _pass(text):
_summary.passed += 1
if(_log_level >= LOG_LEVEL_ALL_ASSERTS):
p("PASSED: " + text, LOG_LEVEL_ALL_ASSERTS)
_update_controls()
end_yielded_test()


#-------------------------------------------------------------------------------
#Convert the _summary struct into text for display
Expand Down Expand Up @@ -821,12 +867,12 @@ func p(text, level=0, indent=0):
#-------------------------------------------------------------------------------
#Runs all the scripts that were added using add_script
#-------------------------------------------------------------------------------
func test_scripts():
func test_scripts(run_rest=false):
clear_text()
_test_scripts.clear()

if(_ctrls.scripts_drop_down.get_selected() == 0):
for idx in range(1, _ctrls.scripts_drop_down.get_item_count()):
if(run_rest):
for idx in range(_ctrls.scripts_drop_down.get_selected(), _ctrls.scripts_drop_down.get_item_count()):
_test_scripts.append(_ctrls.scripts_drop_down.get_item_text(idx))
else:
_test_scripts.append(_ctrls.scripts_drop_down.get_item_text(_ctrls.scripts_drop_down.get_selected()))
Expand All @@ -849,6 +895,11 @@ func test_script(script):
func add_script(script, select_this_one=false):
_test_scripts.append(script)
_ctrls.scripts_drop_down.add_item(script)
# Move the run_button in case the size of the path of the script caused the
# drop down to resize.
_ctrls.run_button.set_pos(_ctrls.scripts_drop_down.get_pos() + \
Vector2(_ctrls.scripts_drop_down.get_size().x + 5, 0))

if(select_this_one):
_ctrls.scripts_drop_down.select(_ctrls.scripts_drop_down.get_item_count() -1)

Expand Down Expand Up @@ -903,10 +954,19 @@ func select_script(script_name):
################
func _pass_if_datatypes_match(got, expected, text):
var passed = true

if(!_disable_strict_datatype_checks):
if(typeof(got) != typeof(expected) and got != null and expected != null):
_fail('Cannot compare ' + types[typeof(got)] + '[' + str(got) + '] to ' + types[typeof(expected)] + '[' + str(expected) + ']. ' + text)
passed = false
var got_type = typeof(got)
var expect_type = typeof(expected)
if(got_type != expect_type and got != null and expected != null):
# If we have a mismatch between float and int (types 2 and 3) then
# print out a warning but do not fail.
if([2, 3].has(got_type) and [2, 3].has(expect_type)):
p(str('Warn: Float/Int comparison. Got ', types[got_type], ' but expected ', types[expect_type]), 1)
else:
_fail('Cannot compare ' + types[got_type] + '[' + str(got) + '] to ' + types[expect_type] + '[' + str(expected) + ']. ' + text)
passed = false

return passed

#-------------------------------------------------------------------------------
Expand Down Expand Up @@ -1056,7 +1116,7 @@ func pending(text=""):
p("Pending")
else:
p("Pending: " + text)

end_yielded_test()
################
#
# MISC
Expand Down Expand Up @@ -1156,6 +1216,10 @@ func pause_before_teardown():
#-------------------------------------------------------------------------------
func set_ignore_pause_before_teardown(should_ignore):
_ignore_pause_before_teardown = should_ignore
_ctrls.ignore_continue_checkbox.set_pressed(should_ignore)

func get_ignore_pause_before_teardown():
return _ignore_pause_before_teardown

#-------------------------------------------------------------------------------
#Set to true so that painting of the screen will occur between tests. Allows you
Expand Down
7 changes: 5 additions & 2 deletions test/gut/gut_cmdln.gd
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
# See the readme for a list of options and examples. You can also use the -gh
# option to get more information about how to use the command line interface.
#
# Version 3.0
# Version 3.1
################################################################################
extends SceneTree

Expand Down Expand Up @@ -243,6 +243,8 @@ var options = {
# set to false if you specify a script to run with the -gselect
# option and it cannot find the script.
var _auto_run = true
# flag to indicate if only a single script should be run.
var _run_single = false

func setup_options():
var opts = Options.new()
Expand Down Expand Up @@ -304,6 +306,7 @@ func apply_options():

if(options.selected != ''):
_auto_run = _tester.select_script(options.selected)
_run_single = true
if(!_auto_run):
_tester.p("Could not find a script that matched: " + options.selected)

Expand Down Expand Up @@ -334,7 +337,7 @@ func _init():
apply_options()

if(_auto_run):
_tester.test_scripts()
_tester.test_scripts(!_run_single)

# exit if option is set.
func _on_tests_finished():
Expand Down
3 changes: 2 additions & 1 deletion test/gut/gut_main.gd
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ func _run_tests():
tester.set_should_print_to_console(true)
tester.set_yield_between_tests(true)
tester.add_directory('res://test/unit')
tester.test_scripts()
tester.add_directory('res://test/integration')
#tester.test_scripts()
Binary file removed test/gut/gut_main.scn
Binary file not shown.
Loading

0 comments on commit f4c5b1c

Please sign in to comment.