Skip to content
Draft
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
3 changes: 3 additions & 0 deletions cmake/godotcpp.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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)")

Expand Down
7 changes: 6 additions & 1 deletion test/build_profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
"OS",
"TileMap",
"TileSet",
"Viewport"
"Viewport",
"Engine",
"DirAccess",
"EditorInterface",
"GDExtensionManager",
"Thread"
]
}
1 change: 1 addition & 0 deletions test/project/example.gdextension
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

entry_symbol = "example_library_init"
compatibility_minimum = "4.1"
reloadable = true

[libraries]

Expand Down
6 changes: 6 additions & 0 deletions test/project/icon.png.import
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ 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
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
Expand Down
2 changes: 1 addition & 1 deletion test/project/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
26 changes: 26 additions & 0 deletions test/project/reload.gd
Original file line number Diff line number Diff line change
@@ -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)")
1 change: 1 addition & 0 deletions test/project/reload.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c8v75k3p4pnjj
6 changes: 6 additions & 0 deletions test/project/reload.tscn
Original file line number Diff line number Diff line change
@@ -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")
82 changes: 82 additions & 0 deletions test/run-tests.ps1
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions test/src/example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
112 changes: 96 additions & 16 deletions test/src/register_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,118 @@
#include <godot_cpp/godot.hpp>

#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 <chrono>

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<Thread> 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<FileAccess> 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);
Expand Down
Loading