Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8a26313
[BUG] Orphan Node Leak from Placeholder Sprites in main_scene.gd #540
ikostan Apr 16, 2026
faa58f0
[QA] Orphan Node Leak Fix – Test Plan #549
ikostan Apr 16, 2026
4782dba
Update test/gut/test_main_scene_orphan_nodes.gd
ikostan Apr 16, 2026
49ac64a
Update main_scene.gd
ikostan Apr 16, 2026
7bfa831
Add orphan leak helper and update tests
ikostan Apr 16, 2026
1bfd05f
Update main_scene.gd
ikostan Apr 16, 2026
2a647aa
suggestion: Consider tightening the type of stats_panel instead of us…
ikostan Apr 16, 2026
3713757
Update test_main_scene_orphan_nodes.gd
ikostan Apr 16, 2026
437369f
Update test_player_lifecycle.gd
ikostan Apr 16, 2026
c36a488
Update main_scene.gd
ikostan Apr 16, 2026
5822274
Update test_player_lifecycle.gd
ikostan Apr 16, 2026
608020e
[FEATURE] Randomize rotation for organic decor sprites in main scene …
ikostan Apr 16, 2026
8eb7436
Update main_scene.gd
ikostan Apr 16, 2026
237b33b
Make the background look as random and organic as possible
ikostan Apr 16, 2026
4d999f4
Update main_scene.gd
ikostan Apr 16, 2026
95b2b9e
[FEATURE] Increase parallax background chunk size to prevent visual r…
ikostan Apr 16, 2026
d63252e
Add GUT tests for parallax chunk sizes
ikostan Apr 16, 2026
26f42a0
GUT test file dedicated specifically to verifying the randomized tran…
ikostan Apr 16, 2026
f38072c
Adjust bushes/decor layer height and density
ikostan Apr 16, 2026
cb04302
assertions that lock in the "Goldilocks Zone"
ikostan Apr 16, 2026
f7df782
Update test_main_scene_parallax_chunks.gd
ikostan Apr 16, 2026
221cbd6
Update scripts/main_scene.gd
ikostan Apr 16, 2026
60e24c2
Update main_scene.gd
ikostan Apr 16, 2026
909d4a1
Merge branch 'orphan-node-leak-from-placeholder-sprites-in-main_scene…
ikostan Apr 16, 2026
aaddd37
Update test_decor_layer_transformations.gd
ikostan Apr 16, 2026
7127f19
Rename performance test & add parallax checks
ikostan Apr 16, 2026
3740a4c
Improve test teardown and prevent orphan nodes
ikostan Apr 16, 2026
deb1ae2
Performance assertion is likely to be CI-flaky, and _process side eff…
ikostan Apr 16, 2026
2588460
suggestion: The hardcoded screens_tall = 8.0 is duplicated and would …
ikostan Apr 16, 2026
2f94791
Update gut_test_helper.gd
ikostan Apr 16, 2026
a1c6821
Update test_main_scene_parallax_and_performance.gd
ikostan Apr 16, 2026
b9cfc17
Use GutHelper.safe_hard_free in tests
ikostan Apr 16, 2026
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
14 changes: 8 additions & 6 deletions scripts/main_scene.gd
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,15 @@ func setup_bushes_layer(viewport: Vector2) -> void:

# Clear existing children
for child in bushes_layer.get_children():
bushes_layer.remove_child(child)
child.queue_free()
# bushes_layer.remove_child(child)
if is_instance_valid(child):
child.free()

# Get bush IDs from preloader (Array[String])
var bush_ids: Array = Array(texture_preloader.get_resource_list()).filter(
func(id: String) -> bool: return id.begins_with("bush_")
)
print("Loaded ", bush_ids.size(), " bush textures")
# print("Loaded ", bush_ids.size(), " bush textures")

if bush_ids.is_empty():
return
Expand Down Expand Up @@ -161,14 +162,15 @@ func setup_decor_layer(viewport: Vector2) -> void:

# Clear existing children
for child in decor_layer.get_children():
decor_layer.remove_child(child)
child.queue_free()
# decor_layer.remove_child(child)
if is_instance_valid(child):
child.free()

# Get decor IDs from preloader (Array[String])
var decor_ids: Array = Array(texture_preloader.get_resource_list()).filter(
func(id: String) -> bool: return id.begins_with("decor_")
)
print("Loaded ", decor_ids.size(), " decor textures")
# print("Loaded ", decor_ids.size(), " decor textures")

if decor_ids.is_empty():
return
Expand Down
156 changes: 156 additions & 0 deletions test/gut/test_main_scene_orphan_nodes.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
## Copyright (C) 2026 Egor Kostan
## SPDX-License-Identifier: GPL-3.0-or-later
## test_main_scene_orphan_nodes.gd
##
## GUT unit tests for verifying the absence of orphan node leaks in MainScene.
## Covers the Orphan Node Leak Fix Test Plan (Issue #549).

extends "res://addons/gut/test.gd"

var main_scene: MainScene
var viewport_mock: Vector2 = Vector2(1920, 1080)


## Per-test setup: Instantiate MainScene and allow it to initialize.
## :rtype: void
func before_each() -> void:
main_scene = preload("res://scenes/main_scene.tscn").instantiate()
add_child_autofree(main_scene)

# Allow the scene to initialize (_ready, etc.) before running tests
await get_tree().process_frame


## Manual Orphan Node Check & GUT Teardown Memory Test (Frame Sync) |
## Instantiate MainScene, call setup methods multiple times, flush the frame,
## free the scene, and verify no orphan nodes exist.
## :rtype: void
func test_teardown_memory_sync() -> void:
var baseline_orphans: int = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)

main_scene.setup_bushes_layer(viewport_mock)
main_scene.setup_decor_layer(viewport_mock)

# Re-trigger to execute the clearing logic on existing sprites
main_scene.setup_bushes_layer(viewport_mock)
main_scene.setup_decor_layer(viewport_mock)

# CRITICAL: Flush the frame to allow queue_free() to complete its cleanup
await get_tree().process_frame
Comment thread
ikostan marked this conversation as resolved.
Outdated

# Free the scene explicitly to test teardown
main_scene.queue_free()
await get_tree().process_frame

var current_orphans: int = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)
assert_eq(current_orphans, baseline_orphans, "Expected orphan nodes to return to baseline after frame sync and teardown.")


## Repeated Setup Call Stability Test |
## Call setup methods 50 times in a tight loop to simulate heavy reset load,
## then await a frame and check for memory leaks or node accumulation.
## :rtype: void
func test_repeated_setup_call_stability() -> void:
var baseline_orphans: int = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)

# 1. Call setup_bushes_layer() 50 times in a loop
for i in range(50):
main_scene.setup_bushes_layer(viewport_mock)

# 2. Await one frame after loop
await get_tree().process_frame

# 3. Check node count and memory
var current_orphans: int = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)
assert_eq(current_orphans, baseline_orphans, "No accumulated orphan nodes after 50 rapid setup calls.")


## Immediate Rebuild Integrity Test |
## Call setup, then immediately repopulate the layer in the exact same frame.
## Verifies old nodes do not double-up with new nodes.
## :rtype: void
func test_immediate_rebuild_integrity() -> void:
main_scene.setup_bushes_layer(viewport_mock)
var initial_child_count: int = main_scene.bushes_layer.get_child_count()

# Immediately repopulate in the same frame
main_scene.setup_bushes_layer(viewport_mock)
var new_child_count: int = main_scene.bushes_layer.get_child_count()

# The count should remain consistent, confirming old nodes aren't interfering
assert_eq(new_child_count, initial_child_count, "Child count should remain stable during rapid repopulation.")


## Layer Isolation Test |
## Runs setup on one layer and inspects the other to ensure no unintended
## cross-layer deletions occur.
## :rtype: void
func test_layer_isolation() -> void:
# Populate both layers first to establish a baseline
main_scene.setup_bushes_layer(viewport_mock)
main_scene.setup_decor_layer(viewport_mock)

var initial_decor_count: int = main_scene.decor_layer.get_child_count()
var initial_bushes_count: int = main_scene.bushes_layer.get_child_count()

# 1. Run setup_bushes_layer() only.
main_scene.setup_bushes_layer(viewport_mock)
await get_tree().process_frame

# 2. Verify decor layer operates independently
var final_decor_count: int = main_scene.decor_layer.get_child_count()
assert_eq(final_decor_count, initial_decor_count, "Decor layer child count should not change when bushes layer is reset.")

# 3. Run setup_decor_layer() only.
main_scene.setup_decor_layer(viewport_mock)
await get_tree().process_frame

# 4. Verify bushes layer operates independently
var final_bushes_count: int = main_scene.bushes_layer.get_child_count()
assert_eq(final_bushes_count, initial_bushes_count, "Bushes layer child count should not change when decor layer is reset.")


## Scene Reload Lifecycle Test |
## Simulates a full scene reload via tree structure replacements.
## Monitors orphan nodes before and after to ensure clean teardown.
## :rtype: void
func test_scene_reload_lifecycle() -> void:
var baseline_orphans: int = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)

main_scene.setup_bushes_layer(viewport_mock)
main_scene.setup_decor_layer(viewport_mock)

# Simulate change_scene by tearing down and instantiating a new one
main_scene.queue_free()
await get_tree().process_frame

var reloaded_scene: MainScene = preload("res://scenes/main_scene.tscn").instantiate()
add_child_autofree(reloaded_scene)
await get_tree().process_frame

reloaded_scene.setup_bushes_layer(viewport_mock)
reloaded_scene.setup_decor_layer(viewport_mock)
await get_tree().process_frame

var current_orphans: int = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)
assert_eq(current_orphans, baseline_orphans, "No orphan nodes should persist across scene reload simulation.")


## Stress Input Test (Runtime Simulation) |
## Simulates a user rapidly spamming a debug key across multiple frames
## to ensure no compounding leaks or engine crashes happen.
## :rtype: void
func test_stress_input_simulation() -> void:
var baseline_orphans: int = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)

# Spam setups across consecutive frames
for i in range(10):
main_scene.setup_bushes_layer(viewport_mock)
main_scene.setup_decor_layer(viewport_mock)
await get_tree().process_frame

# Final flush and check
await get_tree().process_frame
var current_orphans: int = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)

assert_eq(current_orphans, baseline_orphans, "Memory must remain completely stable after sustained stress input.")
1 change: 1 addition & 0 deletions test/gut/test_main_scene_orphan_nodes.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c63067f3sp0l4
Loading