Files
TheTree_mock/tests/helpers/ui_audio_teardown.gd
Takeshi b84bade045 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.
2026-10-11 15:58:00 +09:00

99 lines
4.8 KiB
GDScript

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