Save The Tree implementation and acceptance work in progress
Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
extends RefCounted
|
||||
|
||||
## QA ordinary Main destruction only; application/window close is separate QA.
|
||||
## Reuse the existing four fixtures' real WeakRef/mixer drain without editing it.
|
||||
const ExistingCleanup := preload("res://tests/helpers/ui_audio_teardown.gd")
|
||||
const OWNER_META := &"_the_tree_ui_close_owners"
|
||||
|
||||
static func free_owner_and_wait(tree: SceneTree, owner: Node) -> Dictionary:
|
||||
var owner_id: int = owner.get_instance_id() if is_instance_valid(owner) else 0
|
||||
var controls_before: Dictionary = _controls(tree)
|
||||
# ExistingCleanup captures weak audio targets synchronously before queue_free.
|
||||
# Only weak targets cross its await. It neither changes Clock flags nor calls
|
||||
# application close, and Main._exit_tree keeps the ordinary owner contract.
|
||||
var audio: Dictionary = await ExistingCleanup.free_owner_and_wait(tree, owner, 500000)
|
||||
var controls_after: Dictionary = _controls(tree)
|
||||
var before_owners: Array = controls_before[&"owner_ids"]
|
||||
var expected_remaining: Array = before_owners.duplicate()
|
||||
expected_remaining.erase(owner_id)
|
||||
var ordinary_release: bool = bool(controls_before[&"owner_meta_present"]) and not bool(controls_before[&"closing"]) and before_owners.has(owner_id)
|
||||
if expected_remaining.is_empty():
|
||||
ordinary_release = ordinary_release and not bool(controls_after[&"owner_meta_present"]) and bool(controls_after[&"auto_accept_quit"]) == bool(controls_before[&"original_auto_accept_quit"])
|
||||
else:
|
||||
ordinary_release = ordinary_release and bool(controls_after[&"owner_meta_present"]) and not bool(controls_after[&"closing"]) and controls_after[&"owner_ids"] == expected_remaining and not bool(controls_after[&"auto_accept_quit"])
|
||||
var clock_unchanged: bool = controls_before[&"clock_process"] == controls_after[&"clock_process"] and controls_before[&"clock_physics_process"] == controls_after[&"clock_physics_process"] and controls_before[&"clock_process_mode"] == controls_after[&"clock_process_mode"]
|
||||
var root_input_unchanged: bool = controls_before[&"root_gui_disable_input"] == controls_after[&"root_gui_disable_input"]
|
||||
var report: Dictionary = {&"owned_engine_pid": OS.get_process_id(), &"owner_instance_id": owner_id,
|
||||
&"before": controls_before, &"after": controls_after, &"expected_remaining_owner_ids": expected_remaining,
|
||||
&"ordinary_owner_release_verified": ordinary_release, &"clock_process_flags_unchanged": clock_unchanged,
|
||||
&"root_gui_input_flag_unchanged": root_input_unchanged, &"audio_teardown_passed": bool(audio.get(&"passed", false)),
|
||||
&"maximum_wait_us": 500000, &"whole_Runtime_field_deletions": 0,
|
||||
&"gameplay_expectations_changed": false, &"clock_flags_intentionally_modified": false,
|
||||
&"application_close_called": false, &"SceneTree_quit_overridden": false,
|
||||
&"WindowQuit_verified": false, &"formal_acceptance": false}
|
||||
report[&"passed"] = bool(audio.get(&"passed", false)) and ordinary_release and clock_unchanged and root_input_unchanged
|
||||
var path: String = OS.get_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF")
|
||||
if path.is_empty() or not path.is_absolute_path() or FileAccess.file_exists(path):
|
||||
report[&"passed"] = false
|
||||
return report
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
report[&"passed"] = false
|
||||
return report
|
||||
file.store_string(JSON.stringify(report, "\t") + "\n")
|
||||
file.close()
|
||||
return report
|
||||
|
||||
static func _controls(tree: SceneTree) -> Dictionary:
|
||||
# Local strong Node/WeakRef.get_ref results are released on helper return.
|
||||
var clock: Node = tree.root.get_node("GameClock")
|
||||
var present: bool = tree.root.has_meta(OWNER_META)
|
||||
var state: Dictionary = tree.root.get_meta(OWNER_META) if present else {}
|
||||
var ids: Array = []
|
||||
for reference: WeakRef in state.get(&"owners", []):
|
||||
var node: Variant = reference.get_ref()
|
||||
if is_instance_valid(node):
|
||||
ids.append(node.get_instance_id())
|
||||
ids.sort()
|
||||
return {&"clock_process": clock.is_processing(), &"clock_physics_process": clock.is_physics_processing(),
|
||||
&"clock_process_mode": clock.process_mode, &"root_gui_disable_input": tree.root.gui_disable_input,
|
||||
&"auto_accept_quit": tree.auto_accept_quit, &"owner_meta_present": present,
|
||||
&"original_auto_accept_quit": bool(state.get(&"original_auto_accept_quit", tree.auto_accept_quit)),
|
||||
&"closing": bool(state.get(&"closing", false)), &"owner_ids": ids}
|
||||
@@ -0,0 +1 @@
|
||||
uid://csl3kffjxjnfh
|
||||
@@ -0,0 +1,98 @@
|
||||
extends RefCounted
|
||||
|
||||
## QA teardown only. Real cues run during the fixture; never mute or delete buses.
|
||||
## Godot stop queues a mixer fade. Wait for actual owned WeakRef release, bounded
|
||||
## by wall time, before quitting a short test process. WindowQuit is separate QA.
|
||||
static func free_owner_and_wait(tree: SceneTree, owner: Node, maximum_wait_us := 500000) -> Dictionary:
|
||||
var runtime: Variant = tree.root.get_node("SimulationRuntime")
|
||||
var before: Dictionary = runtime.create_snapshot()
|
||||
var targets := _capture_targets(owner)
|
||||
var initial := _states(targets)
|
||||
var owner_id := owner.get_instance_id() if is_instance_valid(owner) else 0
|
||||
if is_instance_valid(owner):
|
||||
owner.queue_free()
|
||||
var started := Time.get_ticks_usec()
|
||||
await tree.process_frame
|
||||
while Time.get_ticks_usec() - started < maximum_wait_us and (is_instance_valid(owner) or _any_alive(targets)):
|
||||
await tree.process_frame
|
||||
var waited_us := Time.get_ticks_usec() - started
|
||||
var deadline_reached := waited_us >= maximum_wait_us
|
||||
var after: Dictionary = runtime.create_snapshot()
|
||||
var report := {
|
||||
&"owned_engine_pid": OS.get_process_id(), &"owner_instance_id": owner_id,
|
||||
&"waited_actual_us": waited_us, &"maximum_wait_us": maximum_wait_us, &"deadline_reached": deadline_reached,
|
||||
&"owner_freed": not is_instance_valid(owner), &"all_owned_targets_released": not _any_alive(targets),
|
||||
&"initial_targets": initial, &"final_targets": _states(targets),
|
||||
&"whole_runtime_unchanged": var_to_bytes(before) == var_to_bytes(after),
|
||||
&"audio_driver": AudioServer.get_driver_name(), &"bus_deleted": false, &"cues_muted_for_test": false,
|
||||
&"actual_human_hearing": false, &"WindowQuit_verified": false, &"formal_acceptance": false,
|
||||
&"full_typed_snapshots": {},
|
||||
}
|
||||
var directory := OS.get_environment("THE_TREE_AUDIO_TEARDOWN_PROOF_DIR")
|
||||
if directory.is_empty():
|
||||
directory = "user://tests/audio_teardown_%d" % OS.get_process_id()
|
||||
DirAccess.make_dir_recursive_absolute(directory)
|
||||
var path := directory.path_join("owner_%d.json" % owner_id)
|
||||
var written := not FileAccess.file_exists(path)
|
||||
for part: StringName in {&"before": before, &"after": after}:
|
||||
var raw_path := path.get_basename() + "-" + String(part) + ".varbytes"
|
||||
if FileAccess.file_exists(raw_path):
|
||||
written = false
|
||||
continue
|
||||
var bytes := var_to_bytes(before if part == &"before" else after)
|
||||
var raw := FileAccess.open(raw_path, FileAccess.WRITE)
|
||||
if raw == null:
|
||||
written = false
|
||||
continue
|
||||
raw.store_buffer(bytes)
|
||||
raw.close()
|
||||
report[&"full_typed_snapshots"][part] = {&"path": raw_path, &"bytes": bytes.size(), &"sha256": FileAccess.get_sha256(raw_path), &"field_deletions": 0}
|
||||
report[&"passed"] = written and not deadline_reached and bool(report[&"owner_freed"]) and bool(report[&"all_owned_targets_released"]) and bool(report[&"whole_runtime_unchanged"])
|
||||
if written:
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
report[&"passed"] = false
|
||||
else:
|
||||
file.store_string(JSON.stringify(report, "\t") + "\n")
|
||||
file.close()
|
||||
return report
|
||||
|
||||
|
||||
static func _capture_targets(owner: Node) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
if not is_instance_valid(owner):
|
||||
return result
|
||||
if not owner.has_signal(&"cue_requested"):
|
||||
var music: Variant = owner.get(&"_paired_bgm")
|
||||
if is_instance_valid(music) and music.has_method(&"capture_owned_targets"):
|
||||
result.append_array(music.capture_owned_targets())
|
||||
var audio: Variant = owner if owner.has_signal(&"cue_requested") else owner.get(&"_audio_feedback")
|
||||
if not is_instance_valid(audio):
|
||||
return result
|
||||
for role: StringName in audio.get(&"_players"):
|
||||
var player: AudioStreamPlayer = audio.get(&"_players")[role]
|
||||
if player.stream != null:
|
||||
result.append({&"role": role, &"kind": &"stream", &"instance_id": player.stream.get_instance_id(), &"weak": weakref(player.stream)})
|
||||
var packets: Variant = player.stream.get(&"packet_sequence")
|
||||
if packets is Resource:
|
||||
result.append({&"role": role, &"kind": &"packet_sequence", &"instance_id": packets.get_instance_id(), &"weak": weakref(packets)})
|
||||
if player.has_stream_playback():
|
||||
var playback := player.get_stream_playback()
|
||||
result.append({&"role": role, &"kind": &"last_playback", &"instance_id": playback.get_instance_id(), &"weak": weakref(playback)})
|
||||
return result
|
||||
|
||||
|
||||
static func _states(targets: Array[Dictionary]) -> Array[Dictionary]:
|
||||
# Strong targets are local to this helper and released before every await.
|
||||
var rows: Array[Dictionary] = []
|
||||
for entry: Dictionary in targets:
|
||||
var target: Variant = (entry[&"weak"] as WeakRef).get_ref()
|
||||
rows.append({&"role": entry[&"role"], &"kind": entry[&"kind"], &"instance_id": entry[&"instance_id"], &"alive": target != null, &"class": target.get_class() if target != null else &""})
|
||||
return rows
|
||||
|
||||
|
||||
static func _any_alive(targets: Array[Dictionary]) -> bool:
|
||||
for row: Dictionary in _states(targets):
|
||||
if row[&"alive"]:
|
||||
return true
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://csat0urychvsq
|
||||
Reference in New Issue
Block a user