Skip to content

Commit

Permalink
GD-320: Add support for parameterized tests (#321)
Browse files Browse the repository at this point in the history
* GD-320: Add support for parameterized tests

- rework on test suite scanner by use existing script parser
- improved/simplify script parser
- addapt parse function descriptor to get all test parameters

test case syntax
```
func test_parameterized_bool_value(a: int, expected :bool, test_parameters := [
	[0, false],
	[1, true]]):
```
  • Loading branch information
MikeSchulze authored Sep 17, 2022
1 parent 9db9bb4 commit d1165a9
Show file tree
Hide file tree
Showing 25 changed files with 1,105 additions and 585 deletions.
4 changes: 4 additions & 0 deletions addons/gdUnit3/src/asserts/GdAssertMessages.gd
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ static func _current(value, delimiter ="\n") -> String:
TYPE_REAL:
return "'[color=%s]%f[/color]'" % [VALUE_COLOR, value]
TYPE_OBJECT:
#if value.has_method("_to_string"):
# return "[color=%s]<%s>[/color]" % [VALUE_COLOR, value._to_string()]
return "[color=%s]<%s>[/color]" % [VALUE_COLOR, value.get_class()]
_:
if GdObjects.is_array_type(value):
Expand All @@ -45,6 +47,8 @@ static func _expected(value, delimiter ="\n") -> String:
TYPE_REAL:
return "'[color=%s]%f[/color]'" % [VALUE_COLOR, value]
TYPE_OBJECT:
#if value.has_method("_to_string"):
# return "[color=%s]<%s>[/color]" % [VALUE_COLOR, value._to_string()]
return "[color=%s]<%s>[/color]" % [VALUE_COLOR, value.get_class()]
_:
if GdObjects.is_array_type(value):
Expand Down
2 changes: 1 addition & 1 deletion addons/gdUnit3/src/core/GdFunctionDoubler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ static func extract_constructor_args(args :Array) -> PoolStringArray:
return constructor_args

static func get_default(arg :GdFunctionArgument):
if arg.default():
if arg.default() != GdFunctionArgument.UNDEFINED:
return arg.default()
else:
var arg_type := GdObjects.string_to_type(arg._type)
Expand Down
6 changes: 4 additions & 2 deletions addons/gdUnit3/src/core/GdObjects.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
class_name GdObjects
extends Object

const TYPE_VOID = TYPE_MAX + 1000
const TYPE_VARARG = TYPE_MAX + 1001
const TYPE_VOID = TYPE_MAX + 1000
const TYPE_VARARG = TYPE_MAX + 1001
const TYPE_FUNC = TYPE_MAX + 1002

# used as default value for varargs
const TYPE_VARARG_PLACEHOLDER_VALUE = "__null__"
Expand Down Expand Up @@ -38,6 +39,7 @@ const TYPE_AS_STRING_MAPPINGS := {
TYPE_COLOR_ARRAY: "PoolColorArray",
TYPE_VOID: "void",
TYPE_VARARG: "VarArg",
TYPE_FUNC: "Func"
}

# holds flipped copy of TYPE_AS_STRING_MAPPINGS initalisized by func 'string_as_typeof'
Expand Down
64 changes: 56 additions & 8 deletions addons/gdUnit3/src/core/GdUnitExecutor.gd
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func fire_test_skipped(test_suite :GdUnitTestSuite, test_case :_TestCase):
GdUnitEvent.SKIPPED: true,
GdUnitEvent.SKIPPED_COUNT: 1,
}
var report := GdUnitReport.new().create(GdUnitReport.WARN, test_case.line_number(), "Test skipped")
var report := GdUnitReport.new().create(GdUnitReport.WARN, test_case.line_number(), "Test skipped %s" % test_case.error())
fire_event(GdUnitEvent.new()\
.test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case.get_name(), statistics, [report]))

Expand Down Expand Up @@ -129,15 +129,16 @@ func suite_after(test_suite :GdUnitTestSuite) -> GDScriptFunctionState:
_report_collector.clear_reports(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER)
return null

func test_before(test_suite :GdUnitTestSuite, test_case :_TestCase, fire_event := true) -> GDScriptFunctionState:
func test_before(test_suite :GdUnitTestSuite, test_case :_TestCase, input_values :Array = [], fire_event := true) -> GDScriptFunctionState:
set_stage(STAGE_TEST_CASE_BEFORE)
_memory_pool.set_pool(test_suite, GdUnitMemoryPool.TEST_SETUP, true)

_total_test_execution_orphans = 0
if fire_event:
_testcase_timer = LocalTime.now()
var test_case_name = build_test_case_name(test_case.get_name(), input_values)
fire_event(GdUnitEvent.new()\
.test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case.get_name()))
.test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name))

test_suite.set_meta(GdUnitAssertImpl.GD_TEST_FAILURE, false)
var fstate = test_suite.before_test()
Expand All @@ -148,7 +149,7 @@ func test_before(test_suite :GdUnitTestSuite, test_case :_TestCase, fire_event :
GdUnitTools.run_auto_close()
return null

func test_after(test_suite :GdUnitTestSuite, test_case :_TestCase, fire_event := true) -> GDScriptFunctionState:
func test_after(test_suite :GdUnitTestSuite, test_case :_TestCase, input_values :Array = [], fire_event := true) -> GDScriptFunctionState:
_memory_pool.free_pool()
# give objects time to finallize
yield(get_tree(), "idle_frame")
Expand Down Expand Up @@ -200,8 +201,9 @@ func test_after(test_suite :GdUnitTestSuite, test_case :_TestCase, fire_event :=
}

if fire_event:
var test_case_name = build_test_case_name(test_case.get_name(), input_values)
fire_event(GdUnitEvent.new()\
.test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case.get_name(), statistics, reports.duplicate()))
.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)
return null

Expand Down Expand Up @@ -231,7 +233,7 @@ func execute_test_case_iterative(test_suite :GdUnitTestSuite, test_case :_TestCa
var is_failure := false
for iteration in test_case.iterations():
# call before_test for each iteration
_test_run_state = test_before(test_suite, test_case, iteration==0 )
_test_run_state = test_before(test_suite, test_case, [], iteration==0)
if GdUnitTools.is_yielded(_test_run_state):
yield(_test_run_state, "completed")
_test_run_state = null
Expand All @@ -256,7 +258,7 @@ func execute_test_case_iterative(test_suite :GdUnitTestSuite, test_case :_TestCa
.create(GdUnitReport.INTERUPTED, test_case.line_number(), GdAssertMessages.fuzzer_interuped(iteration, "timedout")))

# call after_test for each iteration
_test_run_state = test_after(test_suite, test_case, iteration==test_case.iterations()-1 or is_failure)
_test_run_state = test_after(test_suite, test_case, [], iteration==test_case.iterations()-1 or is_failure)
if GdUnitTools.is_yielded(_test_run_state):
yield(_test_run_state, "completed")
_test_run_state = null
Expand All @@ -265,6 +267,44 @@ func execute_test_case_iterative(test_suite :GdUnitTestSuite, test_case :_TestCa
break
return _test_run_state

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
for test_parameter in test_case.test_parameters():
var fs = test_before(test_suite, test_case, test_parameter)
if GdUnitTools.is_yielded(fs):
yield(fs, "completed")
set_stage(STAGE_TEST_CASE_EXECUTE)
_memory_pool.set_pool(test_suite, GdUnitMemoryPool.TEST_EXECUTE, true)
fs = test_case.execute(test_parameter)
if GdUnitTools.is_yielded(fs):
yield(_test_run_state, "completed")
fs = test_after(test_suite, test_case, test_parameter)
if GdUnitTools.is_yielded(fs):
yield(fs, "completed")
if test_case.is_interupted():
break

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) -> GDScriptFunctionState:
return Execute(test_suite)

Expand Down Expand Up @@ -298,8 +338,11 @@ func Execute(test_suite :GdUnitTestSuite) -> GDScriptFunctionState:
test_suite.set_active_test_case(test_case.get_name())
if test_case.is_skipped():
fire_test_skipped(test_suite, test_case)
yield(get_tree(), "idle_frame")
else:
if test_case.has_fuzzer():
if test_case.is_parameterized():
fs = execute_test_case_parameterized(test_suite, test_case)
elif test_case.has_fuzzer():
fs = execute_test_case_iterative(test_suite, test_case)
else:
fs = execute_test_case_single(test_suite, test_case)
Expand Down Expand Up @@ -357,3 +400,8 @@ static func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) ->
fuzzer._iteration_limit = test_case.iterations()
fuzzers.append(fuzzer)
return fuzzers

static func build_test_case_name(name :String, input_values :Array) -> String:
if input_values.empty():
return name
return "%s %s" % [name, str(input_values)]
3 changes: 3 additions & 0 deletions addons/gdUnit3/src/core/GdUnitRunner.gd
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ func _process(delta):
INIT:
# wait until client is connected to the GdUnitServer
if _client.is_client_connected():
var time = LocalTime.now()
prints("Scan for test suites.")
_test_suites_to_process = load_test_suits()
prints("Scanning of %d test suites took" % _test_suites_to_process.size(), time.elapsed_since())
gdUnitInit()
_state = RUN
RUN:
Expand Down
44 changes: 30 additions & 14 deletions addons/gdUnit3/src/core/_TestCase.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ const ARGUMENT_TIMEOUT := "timeout"
var _iterations: int = 1
var _seed: int
var _fuzzers: PoolStringArray = PoolStringArray()
var _test_parameters := Array()
var _line_number: int = -1
var _script_path: String
var _skipped := false
var _error := ""
var _expect_to_interupt := false

var _timer : Timer
Expand All @@ -37,13 +39,13 @@ func configure(name: String, line_number: int, script_path: String, timeout :int
_timeout = timeout
return self

func execute(fuzzers := Array(), iteration := 0):
func execute(test_parameter := Array(), iteration := 0):
if iteration == 0:
set_timeout()
_monitor.start()
if not fuzzers.empty():
update_fuzzers(fuzzers, iteration)
_fs = get_parent().callv(name, fuzzers)
if not test_parameter.empty():
update_fuzzers(test_parameter, iteration)
_fs = get_parent().callv(name, test_parameter)
else:
_fs = get_parent().call(name)
if GdUnitTools.is_yielded(_fs):
Expand All @@ -56,9 +58,10 @@ func execute(fuzzers := Array(), iteration := 0):
stop_timer()
return _fs

func update_fuzzers(fuzzers :Array, iteration :int):
for fuzzer in fuzzers:
fuzzer._iteration_index = iteration + 1
func update_fuzzers(input_values :Array, iteration :int):
for fuzzer in input_values:
if fuzzer is Fuzzer:
fuzzer._iteration_index = iteration + 1

func set_timeout():
var time :float = _timeout * 0.001
Expand All @@ -71,7 +74,7 @@ func set_timeout():
_timer.start()

func _test_case_timeout():
prints("interrupted by timeout", self, _timer, _timer.time_left)
#prints("interrupted by timeout", self, _timer, _timer.time_left)
_interupted = true
if _fs is GDScriptFunctionState:
_fs.emit_signal("completed")
Expand All @@ -84,15 +87,24 @@ func stop_timer() :
_timer.stop()
_timer.call_deferred("free")

func is_interupted() -> bool:
return _interupted

func expect_to_interupt() -> void:
_expect_to_interupt = true

func is_interupted() -> bool:
return _interupted

func is_expect_interupted() -> bool:
return _expect_to_interupt

func is_parameterized() -> bool:
return _test_parameters.size() != 0

func is_skipped() -> bool:
return _skipped

func error() -> String:
return _error

func line_number() -> int:
return _line_number

Expand Down Expand Up @@ -121,11 +133,15 @@ func generate_seed() -> void:
if _seed != -1:
seed(_seed)

func skip(skipped :bool) -> void:
func skip(skipped :bool, error :String = "") -> void:
_skipped = skipped
_error = error

func is_skipped() -> bool:
return _skipped
func set_test_parameters(test_parameters :Array) -> void:
_test_parameters = test_parameters

func test_parameters() -> Array:
return _test_parameters

func _to_string():
return "%s :%d (%dms)" % [get_name(), _line_number, _timeout]
Loading

0 comments on commit d1165a9

Please sign in to comment.