Files
open-wc/src/tools/verify_wmo_scene_resource_finalizer.gd
T

347 lines
11 KiB
GDScript

extends SceneTree
## Asset-free terminal-I/O, probe validation/lifetime, adoption, boundary and
## timing regression for the cached WMO PackedScene finalizer.
const FINALIZER_SCRIPT := preload("res://src/render/wmo/wmo_scene_resource_finalizer.gd")
const CACHE_STATE_SCRIPT := preload("res://src/render/wmo/wmo_scene_resource_cache_state.gd")
const FINALIZER_PATH := "res://src/render/wmo/wmo_scene_resource_finalizer.gd"
const CACHE_STATE_PATH := "res://src/render/wmo/wmo_scene_resource_cache_state.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
class FakeSceneCacheValidator extends RefCounted:
var validated_nodes: Array[Node] = []
func is_scene_cache_current(node: Node) -> bool:
validated_nodes.append(node)
return bool(node.get_meta("current_wmo_cache", false))
class FakeResourceLoaderAdapter extends RefCounted:
var status_by_path: Dictionary = {}
var resource_by_path: Dictionary = {}
var status_paths: Array[String] = []
var loaded_paths: Array[String] = []
func load_threaded_get_status(resource_path: String) -> int:
status_paths.append(resource_path)
return int(status_by_path.get(resource_path, ResourceLoader.THREAD_LOAD_IN_PROGRESS))
func load_threaded_get(resource_path: String) -> Resource:
loaded_paths.append(resource_path)
return resource_by_path.get(resource_path) as Resource
func _initialize() -> void:
var failures: Array[String] = []
_verify_null_and_non_terminal_poll(failures)
_verify_failed_and_loaded_poll_order(failures)
_verify_loaded_resource_rejections(failures)
_verify_current_scene_adoption_and_probe_lifetime(failures)
_verify_validation_contract(failures)
_verify_source_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("WMO_SCENE_RESOURCE_FINALIZER: %s" % failure)
quit(1)
return
print(
"WMO_SCENE_RESOURCE_FINALIZER PASS cases=26 iterations=1000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_null_and_non_terminal_poll(failures: Array[String]) -> void:
var validator := FakeSceneCacheValidator.new()
var adapter := FakeResourceLoaderAdapter.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
_expect_equal(
int(finalizer.call("poll_terminal_requests", null)),
0,
"null cache state rejected",
failures
)
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
cache_state.call("remember_request", "world/pending.wmo", "res://cache/pending.tscn")
_expect_equal(
int(finalizer.call("poll_terminal_requests", cache_state)),
0,
"non-terminal request retained",
failures
)
_expect_true(
bool(cache_state.call("has_request", "world/pending.wmo")),
"pending state remains",
failures
)
_expect_string_array(
adapter.status_paths,
["res://cache/pending.tscn"],
"pending path polled once",
failures
)
_expect_equal(adapter.loaded_paths.size(), 0, "pending scene not loaded", failures)
func _verify_failed_and_loaded_poll_order(failures: Array[String]) -> void:
var validator := FakeSceneCacheValidator.new()
var adapter := FakeResourceLoaderAdapter.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
var current_scene := _packed_scene(true, true, failures)
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
var requests := {
"world/pending.wmo": "res://cache/pending.tscn",
"world/failed.wmo": "res://cache/failed.tscn",
"world/loaded.wmo": "res://cache/loaded.tscn",
}
for normalized_relative_path in requests:
cache_state.call(
"remember_request",
normalized_relative_path,
requests[normalized_relative_path]
)
adapter.status_by_path = {
"res://cache/failed.tscn": ResourceLoader.THREAD_LOAD_FAILED,
"res://cache/loaded.tscn": ResourceLoader.THREAD_LOAD_LOADED,
}
adapter.resource_by_path["res://cache/loaded.tscn"] = current_scene
_expect_equal(
int(finalizer.call("poll_terminal_requests", cache_state)),
2,
"failed and loaded scenes complete",
failures
)
_expect_string_array(
adapter.status_paths,
[
"res://cache/pending.tscn",
"res://cache/failed.tscn",
"res://cache/loaded.tscn",
],
"detached insertion order preserved",
failures
)
_expect_string_array(
adapter.loaded_paths,
["res://cache/loaded.tscn"],
"only loaded terminal scene retrieved",
failures
)
_expect_true(
bool(cache_state.call("is_missing", "world/failed.wmo")),
"failed request publishes missing",
failures
)
_expect_true(
cache_state.call("scene_for", "world/loaded.wmo") == current_scene,
"loaded scene identity adopted",
failures
)
func _verify_loaded_resource_rejections(failures: Array[String]) -> void:
var validator := FakeSceneCacheValidator.new()
var adapter := FakeResourceLoaderAdapter.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
var rejected_resources := {
"null": null,
"wrong_type": Resource.new(),
"stale": _packed_scene(false, true, failures),
"non_node_3d": _packed_scene(true, false, failures),
}
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
for rejection_name in rejected_resources:
var normalized_relative_path := "world/%s.wmo" % rejection_name
var cache_path := "res://cache/%s.tscn" % rejection_name
cache_state.call("remember_request", normalized_relative_path, cache_path)
adapter.status_by_path[cache_path] = ResourceLoader.THREAD_LOAD_LOADED
adapter.resource_by_path[cache_path] = rejected_resources[rejection_name]
_expect_equal(
int(finalizer.call("poll_terminal_requests", cache_state)),
4,
"all invalid loaded Resources complete",
failures
)
for rejection_name in rejected_resources:
_expect_true(
bool(cache_state.call("is_missing", "world/%s.wmo" % rejection_name)),
"%s loaded Resource publishes missing" % rejection_name,
failures
)
_expect_equal(
validator.validated_nodes.size(),
1,
"only Node3D stale scene reaches validator",
failures
)
_expect_false(
is_instance_valid(validator.validated_nodes[0]),
"stale validation probe released",
failures
)
func _verify_current_scene_adoption_and_probe_lifetime(failures: Array[String]) -> void:
var validator := FakeSceneCacheValidator.new()
var adapter := FakeResourceLoaderAdapter.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
var current_scene := _packed_scene(true, true, failures)
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
cache_state.call("remember_request", "world/current.wmo", "res://cache/current.tscn")
adapter.status_by_path["res://cache/current.tscn"] = ResourceLoader.THREAD_LOAD_LOADED
adapter.resource_by_path["res://cache/current.tscn"] = current_scene
finalizer.call("poll_terminal_requests", cache_state)
_expect_true(
cache_state.call("scene_for", "world/current.wmo") == current_scene,
"current PackedScene exact identity adopted",
failures
)
_expect_equal(
validator.validated_nodes.size(),
1,
"current scene instantiated exactly once",
failures
)
_expect_false(
is_instance_valid(validator.validated_nodes[0]),
"accepted validation probe released",
failures
)
func _verify_validation_contract(failures: Array[String]) -> void:
var validator := FakeSceneCacheValidator.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator)
var current_scene := _packed_scene(true, true, failures)
_expect_true(
bool(finalizer.call("is_scene_cache_current", current_scene)),
"current Node3D scene accepted",
failures
)
_expect_false(
bool(finalizer.call("is_scene_cache_current", null)),
"null scene rejected",
failures
)
var missing_validator_finalizer: RefCounted = FINALIZER_SCRIPT.new(null)
_expect_false(
bool(missing_validator_finalizer.call("is_scene_cache_current", current_scene)),
"missing validator rejects scene without instantiation",
failures
)
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var finalizer_source := _read_text(FINALIZER_PATH, failures)
var cache_source := _read_text(CACHE_STATE_PATH, failures)
_expect_true(
loader_source.contains("WMO_SCENE_RESOURCE_FINALIZER_SCRIPT.new("),
"loader composes scene finalizer",
failures
)
_expect_true(
loader_source.contains("_wmo_scene_resource_finalizer.poll_terminal_requests("),
"loader delegates scene terminal polling",
failures
)
_expect_false(
loader_source.contains("func _is_wmo_scene_cache_current("),
"loader releases PackedScene probe validation",
failures
)
for required_token in [
"ResourceLoader.load_threaded_get_status(resource_path)",
"ResourceLoader.load_threaded_get(resource_path)",
"scene.instantiate()",
"is_scene_cache_current",
"instance.free()",
"complete_request_with_scene",
"complete_request_as_missing",
]:
_expect_true(
finalizer_source.contains(required_token),
"finalizer owns %s" % required_token,
failures
)
_expect_false(
cache_source.contains("ResourceLoader."),
"cache state remains free of ResourceLoader",
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var adapter := FakeResourceLoaderAdapter.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(
FakeSceneCacheValidator.new(),
adapter
)
var started_microseconds := Time.get_ticks_usec()
for iteration in range(1000):
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
var normalized_relative_path := "world/object_%d.wmo" % iteration
var cache_path := "res://cache/object_%d.tscn" % iteration
cache_state.call("remember_request", normalized_relative_path, cache_path)
adapter.status_by_path[cache_path] = ResourceLoader.THREAD_LOAD_FAILED
finalizer.call("poll_terminal_requests", cache_state)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
if elapsed_milliseconds >= 1000.0:
failures.append(
"1000 terminal polls took %.3fms (budget < 1000ms)" % elapsed_milliseconds
)
return elapsed_milliseconds
func _packed_scene(
is_current: bool,
use_node_3d_root: bool,
failures: Array[String]
) -> PackedScene:
var root: Node = Node3D.new() if use_node_3d_root else Node.new()
root.set_meta("current_wmo_cache", is_current)
var scene := PackedScene.new()
var pack_error := scene.pack(root)
root.free()
if pack_error != OK:
failures.append("cannot pack synthetic scene: %d" % pack_error)
return scene
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot read %s" % path)
return ""
var source := file.get_as_text()
file.close()
return source
func _expect_true(value: bool, label: String, failures: Array[String]) -> void:
if not value:
failures.append(label)
func _expect_false(value: bool, label: String, failures: Array[String]) -> void:
if value:
failures.append(label)
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
if actual != expected:
failures.append("%s: expected %d, got %d" % [label, expected, actual])
func _expect_string_array(
actual: Array,
expected: Array,
label: String,
failures: Array[String]
) -> void:
if actual != expected:
failures.append("%s: expected %s, got %s" % [label, expected, actual])