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