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.
99 lines
4.8 KiB
GDScript
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
|