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,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
|
||||
Reference in New Issue
Block a user