Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GD-517: Fix test discovery guard fails on CSharpScript tests when editing #523

Merged
merged 1 commit into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
68 changes: 40 additions & 28 deletions addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,75 +12,87 @@ 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 map_func_names(method_info :Dictionary) -> String:
return method_info["name"]
func extract_test_functions(test_suite :Node) -> PackedStringArray:
return test_suite.get_children()\
.map(func (child: Node) -> String: return child.get_name())


func filter_test_cases(value :String) -> bool:
return value.begins_with("test_")
# 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()

func filter_by_test_cases(method_info :Dictionary, value :String) -> bool:
return method_info["name"] == value
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);