diff --git a/cmake/godotcpp.cmake b/cmake/godotcpp.cmake index 7c4ea686b..b83fb50c4 100644 --- a/cmake/godotcpp.cmake +++ b/cmake/godotcpp.cmake @@ -139,6 +139,9 @@ function(godotcpp_options) #TODO compiledb_file set(GODOTCPP_BUILD_PROFILE "" CACHE PATH "Path to a file containing a feature build profile") + if(GODOTCPP_BUILD_PROFILE AND EXISTS "${GODOTCPP_BUILD_PROFILE}") + set_property(SOURCE APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${GODOTCPP_BUILD_PROFILE}") + endif() set(GODOTCPP_USE_HOT_RELOAD "" CACHE BOOL "Enable the extra accounting required to support hot reload. (ON|OFF)") diff --git a/test/build_profile.json b/test/build_profile.json index 57d847a11..5dd836763 100644 --- a/test/build_profile.json +++ b/test/build_profile.json @@ -8,6 +8,11 @@ "OS", "TileMap", "TileSet", - "Viewport" + "Viewport", + "Engine", + "DirAccess", + "EditorInterface", + "GDExtensionManager", + "Thread" ] } diff --git a/test/project/example.gdextension b/test/project/example.gdextension index 8e2f794d1..fd0ff2d7e 100644 --- a/test/project/example.gdextension +++ b/test/project/example.gdextension @@ -2,6 +2,7 @@ entry_symbol = "example_library_init" compatibility_minimum = "4.1" +reloadable = true [libraries] diff --git a/test/project/icon.png.import b/test/project/icon.png.import index 8a7c8b0ce..9cf191ddf 100644 --- a/test/project/icon.png.import +++ b/test/project/icon.png.import @@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.cte compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -25,6 +27,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/test/project/project.godot b/test/project/project.godot index 9b711dd65..ec8d34149 100644 --- a/test/project/project.godot +++ b/test/project/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="GDExtension Test Project" run/main_scene="res://main.tscn" -config/features=PackedStringArray("4.4") +config/features=PackedStringArray("4.5") config/icon="res://icon.png" [native_extensions] diff --git a/test/project/reload.gd b/test/project/reload.gd new file mode 100644 index 000000000..47df4a71a --- /dev/null +++ b/test/project/reload.gd @@ -0,0 +1,26 @@ +@tool +extends Example + +func _on_custom_signal( _msg, _value ) -> void: + print_rich("[color=green] ******** PASSED ******** [/color]") + get_tree().quit(0) + + +func _on_timeout(): + print_rich("[color=red] ******** FAILED ********[/color]") + get_tree().quit(1) + + +func _ready() -> void: + # Dont quit if the reload test isn't specified on the command line. + if not 'test_reload' in OS.get_cmdline_args(): return + + print("gdscript:Reload Test is Enabled") + # Connect to the custom signal of Example + custom_signal.connect(_on_custom_signal) + + # Start 5s watchdog timer + var timer = get_tree().create_timer(10.0) + timer.timeout.connect(_on_timeout) + + print("Awaiting Custom Signal (with 10s timeout)") diff --git a/test/project/reload.gd.uid b/test/project/reload.gd.uid new file mode 100644 index 000000000..bcc95a5ce --- /dev/null +++ b/test/project/reload.gd.uid @@ -0,0 +1 @@ +uid://c8v75k3p4pnjj diff --git a/test/project/reload.tscn b/test/project/reload.tscn new file mode 100644 index 000000000..41d06a190 --- /dev/null +++ b/test/project/reload.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://b4wrd36npp8ov"] + +[ext_resource type="Script" uid="uid://c8v75k3p4pnjj" path="res://reload.gd" id="1_icw2l"] + +[node name="Reload" type="Example"] +script = ExtResource("1_icw2l") diff --git a/test/run-tests.ps1 b/test/run-tests.ps1 new file mode 100644 index 000000000..1ec9c67be --- /dev/null +++ b/test/run-tests.ps1 @@ -0,0 +1,82 @@ +# Define Godot executable (uses environment variable if set, else defaults to 'godot') +$GODOT = if ($env:GODOT) { $env:GODOT } else { 'godot' } + +$END_STRING = "==== TESTS FINISHED ====" +$FAILURE_STRING = "******** FAILED ********" +$HAS_FAILURE = 0 + +# Function to filter spam from output +function Filter-Output { + param([string[]]$Lines) + $Lines | ForEach-Object { $_.TrimEnd() } | Where-Object { + $_ -notmatch "Narrowing conversion" -and + $_ -notmatch "at:\s+GDScript::reload" -and + $_ -notmatch "\[\s*\d+%\s*\]" -and + $_ -notmatch "first_scan_filesystem" -and + $_ -notmatch "loading_editor_layout" + } +} + +# Run Godot and capture output and exit code +try { + $OUTPUT = & $GODOT --path project --debug --headless --quit 2>&1 + $ERRCODE = $LASTEXITCODE +} +catch { + $OUTPUT = $_.Exception.Message + $ERRCODE = 1 +} + +# Output the results +Write-Output $OUTPUT +Write-Output "" + +# Check if tests completed +if (-not ($OUTPUT -match [regex]::Escape($END_STRING))) { + $HAS_FAILURE += 1 +} + +# Check for test failures +if ($OUTPUT -match [regex]::Escape($FAILURE_STRING)) { + $HAS_FAILURE += 1 +} + +# Lock file path (relative to project dir) +$LOCK_PATH = "project/test_reload_lock" + +# Delete lock file before reload test if it exists +Remove-Item -Path $LOCK_PATH -Force -ErrorAction SilentlyContinue + +# Run Godot and capture output and exit code +try { + $OUTPUT = & $GODOT -e --path project --scene reload.tscn --headless --debug test_reload 2>&1 + $ERRCODE = $LASTEXITCODE +} +catch { + $OUTPUT = $_.Exception.Message + $ERRCODE = 1 +} + +# Filter and output the results +$FilteredOutput = Filter-Output -Lines ($OUTPUT -split "`n") +Write-Output ($FilteredOutput -join "`n") +Write-Output "" + +# Check for test failures +if ($OUTPUT -match [regex]::Escape($FAILURE_STRING)) { + $HAS_FAILURE += 1 +} + +# Lock file path (relative to project dir) +$LOCK_PATH = "project/test_reload_lock" + +# Delete lock file before reload test if it exists +Remove-Item -Path $LOCK_PATH -Force -ErrorAction SilentlyContinue + +if ($HAS_FAILURE -gt 0 ){ + Write-Output "ERROR: Tests failed to complete" + exit 1 +} + +# Success! +exit 0 diff --git a/test/src/example.cpp b/test/src/example.cpp index 9d3e62b91..44e18012d 100644 --- a/test/src/example.cpp +++ b/test/src/example.cpp @@ -114,6 +114,9 @@ void Example::_notification(int p_what) { rpc_config("test_rpc", opts); } //UtilityFunctions::print("Notification: ", String::num(p_what)); + if (p_what == NOTIFICATION_EXTENSION_RELOADED) { + emit_custom_signal("NOTIFICATION_EXTENSION_RELOADED ", 0); + } } bool Example::_set(const StringName &p_name, const Variant &p_value) { diff --git a/test/src/register_types.cpp b/test/src/register_types.cpp index c68176d92..53b23a41c 100644 --- a/test/src/register_types.cpp +++ b/test/src/register_types.cpp @@ -12,38 +12,118 @@ #include #include "example.h" +#include "godot_cpp/classes/dir_access.hpp" +#include "godot_cpp/classes/editor_interface.hpp" +#include "godot_cpp/classes/file_access.hpp" +#include "godot_cpp/classes/gd_extension_manager.hpp" +#include "godot_cpp/classes/thread.hpp" #include "tests.h" +#include + using namespace godot; +using std::chrono_literals::operator""s; -void initialize_example_module(ModuleInitializationLevel p_level) { - if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { +// Global flag to control the timer thread +std::atomic keep_running(true); + +// Minimal static worker, no class needed +static void timer_thread_function(int interval_ms); + +// Global thread ref +static Ref g_reload_thread; + +// Function to simulate extension reload +void trigger_extension_reload() { + // Loop if the scene is not yet loaded. + if (const EditorInterface *ei = EditorInterface::get_singleton(); + !ei->get_edited_scene_root()) { return; } + keep_running = false; + + // Trigger the Reload + const auto gdextension_path = String{ "res://example.gdextension" }; + if (GDExtensionManager *ext_manager = GDExtensionManager::get_singleton()) { + ext_manager->call_deferred("reload_extension", gdextension_path); + UtilityFunctions::print("Simulating GDExtension reload..."); + } +} + +void timer_thread_function(const int interval_ms) { + while (keep_running) { + OS::get_singleton()->delay_msec(interval_ms); + trigger_extension_reload(); + } +} + +void initialize_example_module(ModuleInitializationLevel p_level) { + if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { + UtilityFunctions::print("Initializing Integration Testing Extension"); + GDREGISTER_CLASS(ExampleRef); + GDREGISTER_CLASS(ExampleMin); + GDREGISTER_CLASS(Example); + GDREGISTER_VIRTUAL_CLASS(ExampleVirtual); + GDREGISTER_ABSTRACT_CLASS(ExampleAbstractBase); + GDREGISTER_CLASS(ExampleConcrete); + GDREGISTER_CLASS(ExampleBase); + GDREGISTER_CLASS(ExampleChild); + GDREGISTER_RUNTIME_CLASS(ExampleRuntime); + GDREGISTER_CLASS(ExamplePrzykład); + GDREGISTER_INTERNAL_CLASS(ExampleInternal); + } else if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) { + if (OS::get_singleton()->get_cmdline_args().has("test_reload")) { + UtilityFunctions::print("Test Reload is enabled"); + const String lock_path = "res://test_reload_lock"; + if (FileAccess::file_exists(lock_path)) { + UtilityFunctions::print("Lock File Exists"); + } else { + UtilityFunctions::print("Creating Timer Thread"); + // Start reload thread with a 3s loop timer. + g_reload_thread.instantiate(); + g_reload_thread->start(callable_mp_static(&timer_thread_function).bind(3000)); - GDREGISTER_CLASS(ExampleRef); - GDREGISTER_CLASS(ExampleMin); - GDREGISTER_CLASS(Example); - GDREGISTER_VIRTUAL_CLASS(ExampleVirtual); - GDREGISTER_ABSTRACT_CLASS(ExampleAbstractBase); - GDREGISTER_CLASS(ExampleConcrete); - GDREGISTER_CLASS(ExampleBase); - GDREGISTER_CLASS(ExampleChild); - GDREGISTER_RUNTIME_CLASS(ExampleRuntime); - GDREGISTER_CLASS(ExamplePrzykład); - GDREGISTER_INTERNAL_CLASS(ExampleInternal); + // Create the lock file to prevent future inits from starting duplicates. + if (const Ref lock_file = FileAccess::open(lock_path, FileAccess::WRITE); lock_file.is_valid()) { + UtilityFunctions::print("Creating Lock File"); + lock_file->close(); + } else { + UtilityFunctions::print("Warning: Failed to create lock file."); + } + } + } + } } void uninitialize_example_module(ModuleInitializationLevel p_level) { - if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { - return; + if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { + UtilityFunctions::print("Uninitializing Integration Testing Extension"); + } else if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) { + // Stop the timer thread when deinitializing + keep_running = false; + + // Join reload thread if ever started + if (g_reload_thread.is_valid()) { + UtilityFunctions::print("Waiting for reload thread to finish..."); + // Loop-check for race safety (up to ~3s max, your interval) + for (int i = 0; i < 10; ++i) { // Arbitrary retries; adjust if needed + if (g_reload_thread->is_alive()) { + OS::get_singleton()->delay_msec(300); // Short poll delay + } else { + break; + } + } + g_reload_thread->wait_to_finish(); // Blocks until done; safe even if not alive + g_reload_thread.unref(); + UtilityFunctions::print("Reload thread joined."); + } } } extern "C" { // Initialization. GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { - godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); + GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); init_obj.register_initializer(initialize_example_module); init_obj.register_terminator(uninitialize_example_module);