-
-
Notifications
You must be signed in to change notification settings - Fork 1
Fix main scene orphan leaks and tune parallax decor with tests #550
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
Merged
ikostan
merged 32 commits into
main
from
orphan-node-leak-from-placeholder-sprites-in-main_scenegd
Apr 16, 2026
Merged
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 faa58f0
[QA] Orphan Node Leak Fix – Test Plan #549
ikostan 4782dba
Update test/gut/test_main_scene_orphan_nodes.gd
ikostan 49ac64a
Update main_scene.gd
ikostan 7bfa831
Add orphan leak helper and update tests
ikostan 1bfd05f
Update main_scene.gd
ikostan 2a647aa
suggestion: Consider tightening the type of stats_panel instead of us…
ikostan 3713757
Update test_main_scene_orphan_nodes.gd
ikostan 437369f
Update test_player_lifecycle.gd
ikostan c36a488
Update main_scene.gd
ikostan 5822274
Update test_player_lifecycle.gd
ikostan 608020e
[FEATURE] Randomize rotation for organic decor sprites in main scene …
ikostan 8eb7436
Update main_scene.gd
ikostan 237b33b
Make the background look as random and organic as possible
ikostan 4d999f4
Update main_scene.gd
ikostan 95b2b9e
[FEATURE] Increase parallax background chunk size to prevent visual r…
ikostan d63252e
Add GUT tests for parallax chunk sizes
ikostan 26f42a0
GUT test file dedicated specifically to verifying the randomized tran…
ikostan f38072c
Adjust bushes/decor layer height and density
ikostan cb04302
assertions that lock in the "Goldilocks Zone"
ikostan f7df782
Update test_main_scene_parallax_chunks.gd
ikostan 221cbd6
Update scripts/main_scene.gd
ikostan 60e24c2
Update main_scene.gd
ikostan 909d4a1
Merge branch 'orphan-node-leak-from-placeholder-sprites-in-main_scene…
ikostan aaddd37
Update test_decor_layer_transformations.gd
ikostan 7127f19
Rename performance test & add parallax checks
ikostan 3740a4c
Improve test teardown and prevent orphan nodes
ikostan deb1ae2
Performance assertion is likely to be CI-flaky, and _process side eff…
ikostan 2588460
suggestion: The hardcoded screens_tall = 8.0 is duplicated and would …
ikostan 2f94791
Update gut_test_helper.gd
ikostan a1c6821
Update test_main_scene_parallax_and_performance.gd
ikostan b9cfc17
Use GutHelper.safe_hard_free in tests
ikostan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
| # 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.") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| uid://c63067f3sp0l4 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.