Skip to content

Commit

Permalink
GD-517: Fic test discovery guard fails on CSharpScript tests when edi…
Browse files Browse the repository at this point in the history
…ting

# Why
see #517

# What
- added rebuild cs scripts before run discovery
- fixed invalid script path resolving by using `localize_path` to convert cs script paths
- fixes test suite scanner to run on Script class to accept GDScript and CSharpScript
  • Loading branch information
MikeSchulze committed Jun 26, 2024
1 parent b5dfc3c commit 7ba08c7
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 38 deletions.
2 changes: 1 addition & 1 deletion addons/gdUnit4/plugin.gd
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ func check_running_in_test_env() -> bool:

func _on_resource_saved(resource: Resource) -> void:
if resource is Script:
_guard.discover(resource)
await _guard.discover(resource)
7 changes: 6 additions & 1 deletion addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,15 @@ static func _is_script_format_supported(resource_path :String) -> bool:
return GdUnit4CSharpApiLoader.is_csharp_file(resource_path)


func _parse_test_suite(script :GDScript) -> GdUnitTestSuite:
func _parse_test_suite(script :Script) -> GdUnitTestSuite:
if not GdObjects.is_test_suite(script):
return null

# If test suite a C# script
if GdUnit4CSharpApiLoader.is_test_suite(script.resource_path):
return GdUnit4CSharpApiLoader.parse_test_suite(script.resource_path)

# Do pares as GDScript
var test_suite :GdUnitTestSuite = script.new()
test_suite.set_name(GdUnitTestSuiteScanner.parse_test_suite_name(script))
# add test cases to test suite and parse test case line nummber
Expand Down
63 changes: 43 additions & 20 deletions addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,66 +12,67 @@ func _init() -> void:


func sync_cache(dto :GdUnitTestSuiteDto) -> void:
var resource_path := dto.path()
var resource_path := ProjectSettings.localize_path(dto.path())
var discovered_test_cases :Array[String] = []
for test_case in dto.test_cases():
discovered_test_cases.append(test_case.name())
_discover_cache[resource_path] = discovered_test_cases


func discover(script: Script) -> void:
# for cs scripts we need to recomplie before discover new tests
if GdObjects.is_cs_script(script):
await rebuild_project(script)

if GdObjects.is_test_suite(script):
# a new test suite is discovered
if not _discover_cache.has(script.resource_path):
var scanner := GdUnitTestSuiteScanner.new()
var test_suite := scanner._parse_test_suite(script)
var script_path := ProjectSettings.localize_path(script.resource_path)
var scanner := GdUnitTestSuiteScanner.new()
var test_suite := scanner._parse_test_suite(script)
var suite_name := test_suite.get_name()

if not _discover_cache.has(script_path):
var dto :GdUnitTestSuiteDto = GdUnitTestSuiteDto.of(test_suite)
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestSuiteAdded.new(script.resource_path, test_suite.get_name(), dto))
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestSuiteAdded.new(script_path, suite_name, dto))
sync_cache(dto)
test_suite.queue_free()
return

var tests_added :Array[String] = []
var tests_removed := PackedStringArray()
var script_test_cases := extract_test_functions(script)
var discovered_test_cases :Array[String] = _discover_cache.get(script.resource_path, [] as Array[String])
var discovered_test_cases :Array[String] = _discover_cache.get(script_path, [] as Array[String])
var script_test_cases := extract_test_functions(test_suite)

# first detect removed/renamed tests
var tests_removed := PackedStringArray()
for test_case in discovered_test_cases:
if not script_test_cases.has(test_case):
tests_removed.append(test_case)
# second detect new added tests
var tests_added :Array[String] = []
for test_case in script_test_cases:
if not discovered_test_cases.has(test_case):
tests_added.append(test_case)

# finally notify changes to the inspector
if not tests_removed.is_empty() or not tests_added.is_empty():
var scanner := GdUnitTestSuiteScanner.new()
var test_suite := scanner._parse_test_suite(script)
var suite_name := test_suite.get_name()

# emit deleted tests
for test_name in tests_removed:
discovered_test_cases.erase(test_name)
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestRemoved.new(script.resource_path, suite_name, test_name))
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestRemoved.new(script_path, suite_name, test_name))

# emit new discovered tests
for test_name in tests_added:
discovered_test_cases.append(test_name)
var test_case := test_suite.find_child(test_name, false, false)
var dto := GdUnitTestCaseDto.new()
dto = dto.deserialize(dto.serialize(test_case))
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestAdded.new(script.resource_path, suite_name, dto))
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestAdded.new(script_path, suite_name, dto))
# update the cache
_discover_cache[script.resource_path] = discovered_test_cases
_discover_cache[script_path] = discovered_test_cases
test_suite.queue_free()


func extract_test_functions(script :Script) -> PackedStringArray:
return script.get_script_method_list()\
.map(map_func_names)\
.filter(filter_test_cases)
func extract_test_functions(test_suite :Node) -> PackedStringArray:
return test_suite.get_children().map(func (child: Node) -> String: return child.get_name())


func map_func_names(method_info :Dictionary) -> String:
Expand All @@ -84,3 +85,25 @@ func filter_test_cases(value :String) -> bool:

func filter_by_test_cases(method_info :Dictionary, value :String) -> bool:
return method_info["name"] == value


# do rebuild the entire project, there is actual no way to enforce the Godot engine itself to do this
func rebuild_project(script: Script) -> void:
var class_path := ProjectSettings.globalize_path(script.resource_path)
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard: CSharpScript change detected on: '%s' [/color]" % class_path)
await Engine.get_main_loop().process_frame

var output := []
var exit_code := OS.execute("dotnet", ["--version"], output)
if exit_code == -1:
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=RED]Rebuild the project failed.[/color]")
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=RED]Can't find installed `dotnet`! Please check your environment is setup correctly.[/color]")
return
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Found dotnet v%s[/color]" % output[0].strip_edges())
output.clear()

exit_code = OS.execute("dotnet", ["build"], output)
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Rebuild the project ... [/color]")
for out:Variant in output:
print_rich("[color=DEEP_SKY_BLUE] %s" % out.strip_edges())
await Engine.get_main_loop().process_frame
6 changes: 3 additions & 3 deletions addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ func discover_test_removed(event: GdUnitEventTestDiscoverTestRemoved) -> void:
func do_add_test_suite(test_suite: GdUnitTestSuiteDto) -> void:
var item := create_tree_item(test_suite)
var suite_name := test_suite.name()

var resource_path := ProjectSettings.localize_path(test_suite.path())
item.set_text(0, suite_name)
item.set_meta(META_GDUNIT_ORIGINAL_INDEX, item.get_index())
item.set_meta(META_GDUNIT_STATE, STATE.INITIAL)
Expand All @@ -786,12 +786,12 @@ func do_add_test_suite(test_suite: GdUnitTestSuiteDto) -> void:
item.set_meta(META_GDUNIT_TOTAL_TESTS, test_suite.test_case_count())
item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0)
item.set_meta(META_GDUNIT_EXECUTION_TIME, 0)
item.set_meta(META_RESOURCE_PATH, test_suite.path())
item.set_meta(META_RESOURCE_PATH, resource_path)
item.set_meta(META_LINE_NUMBER, 1)
item.collapsed = true
set_item_icon_by_state(item)
init_item_counter(item)
add_tree_item_to_cache(test_suite.path(), suite_name, item)
add_tree_item_to_cache(resource_path, suite_name, item)
for test_case in test_suite.test_cases():
add_test(item, test_case)

Expand Down
26 changes: 14 additions & 12 deletions addons/gdUnit4/test/GdUnitTestResourceLoader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@ static func load_cs_script(resource_path :String, debug_write := false) -> Scrip
return null
var script :Script = ClassDB.instantiate("CSharpScript")
script.source_code = GdUnitFileAccess.resource_as_string(resource_path)
script.resource_path = GdUnitFileAccess.create_temp_dir("test") + "/%s" % resource_path.get_file().replace(".resource", ".cs")
var script_resource_path := resource_path.replace(resource_path.get_extension(), "cs")
if debug_write:
print_debug("save resource:", script.resource_path)
DirAccess.remove_absolute(script.resource_path)
var err := ResourceSaver.save(script, script.resource_path)
script_resource_path = GdUnitFileAccess.create_temp_dir("test") + "/%s" % script_resource_path.get_file()
print_debug("save resource:", script_resource_path)
DirAccess.remove_absolute(script_resource_path)
var err := ResourceSaver.save(script, script_resource_path)
if err != OK:
print_debug("Can't save debug resource", script.resource_path, "Error:", error_string(err))
script.take_over_path(script.resource_path)
print_debug("Can't save debug resource",script_resource_path, "Error:", error_string(err))
script.take_over_path(script_resource_path)
else:
script.take_over_path(resource_path)
script.reload()
Expand All @@ -63,14 +64,15 @@ static func load_cs_script(resource_path :String, debug_write := false) -> Scrip
static func load_gd_script(resource_path :String, debug_write := false) -> GDScript:
var script := GDScript.new()
script.source_code = GdUnitFileAccess.resource_as_string(resource_path)
script.resource_path = GdUnitFileAccess.create_temp_dir("test") + "/%s" % resource_path.get_file().replace(".resource", ".gd")
var script_resource_path := resource_path.replace(resource_path.get_extension(), "gd")
if debug_write:
print_debug("save resource:", script.resource_path)
DirAccess.remove_absolute(script.resource_path)
var err := ResourceSaver.save(script, script.resource_path)
script_resource_path = GdUnitFileAccess.create_temp_dir("test") + "/%s" % script_resource_path.get_file()
print_debug("save resource:", script_resource_path)
DirAccess.remove_absolute(script_resource_path)
var err := ResourceSaver.save(script, script_resource_path)
if err != OK:
print_debug("Can't save debug resource", script.resource_path, "Error:", error_string(err))
script.take_over_path(script.resource_path)
print_debug("Can't save debug resource", script_resource_path, "Error:", error_string(err))
script.take_over_path(script_resource_path)
else:
script.take_over_path(resource_path)
script.reload()
Expand Down
2 changes: 1 addition & 1 deletion addons/gdUnit4/test/core/ExampleTestSuite.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace GdUnit4.Tests.Resource
{
using static Assertions;

[TestSuite]
public partial class ExampleTestSuiteA
{
Expand Down
79 changes: 79 additions & 0 deletions addons/gdUnit4/test/core/discovery/GdUnitTestDiscoverGuardTest.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# GdUnit generated TestSuite
class_name GdUnitTestDiscoverGuardTest
extends GdUnitTestSuite
@warning_ignore('unused_parameter')
@warning_ignore('return_value_discarded')

# TestSuite generated from
const GdUnitTestDiscoverGuard = preload("res://addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd")





func test_inital() -> void:
var discoverer := GdUnitTestDiscoverGuard.new()

assert_dict(discoverer._discover_cache).is_empty()


func test_sync_cache() -> void:
var discoverer := GdUnitTestDiscoverGuard.new()

var dto := create_test_dto("res://test/my_test_suite.gd", ["test_a", "test_b"])
discoverer.sync_cache(dto)

assert_dict(discoverer._discover_cache).contains_key_value("res://test/my_test_suite.gd", ["test_a", "test_b"])


func test_discover_on_GDScript() -> void:
var discoverer :GdUnitTestDiscoverGuard = spy(GdUnitTestDiscoverGuard.new())

# connect to catch the events emitted by the test discoverer
var emitted_events :Array[GdUnitEvent] = []
GdUnitSignals.instance().gdunit_event.connect(func on_gdunit_event(event :GdUnitEvent) -> void:
emitted_events.append(event)
)

var script := load("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd")
assert_that(script).is_not_null()
if script == null:
return

await discoverer.discover(script)
# verify the rebuild is NOT called for gd scripts
verify(discoverer, 0).rebuild_project(script)

assert_array(emitted_events).has_size(1)
assert_object(emitted_events[0]).is_instanceof(GdUnitEventTestDiscoverTestSuiteAdded)
assert_dict(discoverer._discover_cache).contains_key_value("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.gd", ["test_case1", "test_case2"])


func test_discover_on_CSharpScript(do_skip := !GdUnit4CSharpApiLoader.is_mono_supported()) -> void:
var discoverer :GdUnitTestDiscoverGuard = spy(GdUnitTestDiscoverGuard.new())

# connect to catch the events emitted by the test discoverer
var emitted_events :Array[GdUnitEvent] = []
GdUnitSignals.instance().gdunit_event.connect(func on_gdunit_event(event :GdUnitEvent) -> void:
emitted_events.append(event)
)

var script :Script = load("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.cs")

await discoverer.discover(script)
# verify the rebuild is called for cs scripts
verify(discoverer, 1).rebuild_project(script)
assert_array(emitted_events).has_size(1)
assert_object(emitted_events[0]).is_instanceof(GdUnitEventTestDiscoverTestSuiteAdded)
assert_dict(discoverer._discover_cache).contains_key_value("res://addons/gdUnit4/test/core/discovery/resources/DiscoverExampleTestSuite.cs", ["TestCase1", "TestCase2"])


func create_test_dto(path: String, test_cases: PackedStringArray) -> GdUnitTestSuiteDto:
var dto := GdUnitTestSuiteDto.new()
dto._path = path
for test_case in test_cases:
var test_dto := GdUnitTestCaseDto.new()
test_dto._name = test_case
test_dto._line_number = 42
dto.add_test_case(test_dto)
return dto
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace GdUnit4.Tests.Resource
{
using static Assertions;

[TestSuite]
public partial class ExampleTestSuite
{

[TestCase]
public void TestCase1()
{
AssertBool(true).IsEqual(true);
}

[TestCase]
public void TestCase2()
{
AssertBool(false).IsEqual(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
extends GdUnitTestSuite


func test_case1() -> void:
assert_bool(true).is_equal(true);


func test_case2() -> void:
assert_bool(false).is_equal(false);

0 comments on commit 7ba08c7

Please sign in to comment.