347 lines
11 KiB
GDScript
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])
|