336 lines
9.4 KiB
GDScript
336 lines
9.4 KiB
GDScript
extends SceneTree
|
|
|
|
## Asset-free lifecycle, ownership, dependency and timing regression for the
|
|
## lightweight-WMO render Resource cache state.
|
|
|
|
const CACHE_STATE_SCRIPT := preload(
|
|
"res://src/render/wmo/wmo_render_resource_cache_state.gd"
|
|
)
|
|
const CACHE_STATE_PATH := "res://src/render/wmo/wmo_render_resource_cache_state.gd"
|
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
|
|
|
|
|
func _initialize() -> void:
|
|
var failures: Array[String] = []
|
|
_verify_invalid_and_request_state(failures)
|
|
_verify_validated_resource_completion(failures)
|
|
_verify_missing_completion_and_exclusivity(failures)
|
|
_verify_transient_and_full_clear(failures)
|
|
_verify_detached_sorted_diagnostics(failures)
|
|
_verify_ownership_boundaries(failures)
|
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
|
if not failures.is_empty():
|
|
for failure in failures:
|
|
push_error("WMO_RENDER_RESOURCE_CACHE_STATE: %s" % failure)
|
|
quit(1)
|
|
return
|
|
print(
|
|
"WMO_RENDER_RESOURCE_CACHE_STATE PASS cases=10 iterations=100 elapsed_ms=%.3f"
|
|
% elapsed_milliseconds
|
|
)
|
|
quit(0)
|
|
|
|
|
|
func _verify_invalid_and_request_state(failures: Array[String]) -> void:
|
|
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
|
_expect_false(
|
|
cache_state.call("remember_request", "", "res://cache.res"),
|
|
"empty normalized path rejected",
|
|
failures
|
|
)
|
|
_expect_false(
|
|
cache_state.call("remember_request", "world/a.wmo", ""),
|
|
"empty cache path rejected",
|
|
failures
|
|
)
|
|
_expect_true(
|
|
cache_state.call("remember_request", "world/a.wmo", "res://cache/a.res"),
|
|
"valid request accepted",
|
|
failures
|
|
)
|
|
_expect_true(
|
|
cache_state.call("has_request", "world/a.wmo"),
|
|
"pending request observable",
|
|
failures
|
|
)
|
|
_expect_false(
|
|
cache_state.call("remember_request", "world/a.wmo", "res://cache/other.res"),
|
|
"duplicate request rejected",
|
|
failures
|
|
)
|
|
var request_snapshot: Dictionary = cache_state.call("request_paths_snapshot")
|
|
_expect_string_equal(
|
|
String(request_snapshot.get("world/a.wmo", "")),
|
|
"res://cache/a.res",
|
|
"request cache path preserved",
|
|
failures
|
|
)
|
|
request_snapshot.clear()
|
|
_expect_true(
|
|
cache_state.call("has_request", "world/a.wmo"),
|
|
"request snapshot detached",
|
|
failures
|
|
)
|
|
|
|
|
|
func _verify_validated_resource_completion(failures: Array[String]) -> void:
|
|
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
|
var validated_resource := Resource.new()
|
|
cache_state.call("remember_request", "world/b.wmo", "res://cache/b.res")
|
|
_expect_false(
|
|
cache_state.call("complete_request_with_resource", "world/b.wmo", null),
|
|
"null completion rejected",
|
|
failures
|
|
)
|
|
_expect_true(
|
|
cache_state.call(
|
|
"complete_request_with_resource",
|
|
"world/b.wmo",
|
|
validated_resource
|
|
),
|
|
"validated Resource completes request",
|
|
failures
|
|
)
|
|
_expect_false(
|
|
cache_state.call("has_request", "world/b.wmo"),
|
|
"successful completion removes request",
|
|
failures
|
|
)
|
|
_expect_true(
|
|
cache_state.call("contains_resource", "world/b.wmo"),
|
|
"successful completion cached",
|
|
failures
|
|
)
|
|
_expect_true(
|
|
cache_state.call("resource_for", "world/b.wmo") == validated_resource,
|
|
"cached Resource identity preserved",
|
|
failures
|
|
)
|
|
_expect_false(
|
|
cache_state.call("remember_request", "world/b.wmo", "res://cache/b2.res"),
|
|
"cached path rejects request",
|
|
failures
|
|
)
|
|
|
|
|
|
func _verify_missing_completion_and_exclusivity(failures: Array[String]) -> void:
|
|
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
|
_expect_false(
|
|
cache_state.call("complete_request_as_missing", "world/unknown.wmo"),
|
|
"unknown completion rejected",
|
|
failures
|
|
)
|
|
cache_state.call("remember_request", "world/missing.wmo", "res://cache/missing.res")
|
|
_expect_true(
|
|
cache_state.call("complete_request_as_missing", "world/missing.wmo"),
|
|
"failed request completes as missing",
|
|
failures
|
|
)
|
|
_expect_true(
|
|
cache_state.call("is_missing", "world/missing.wmo"),
|
|
"negative cache observable",
|
|
failures
|
|
)
|
|
_expect_false(
|
|
cache_state.call("has_request", "world/missing.wmo"),
|
|
"missing completion removes request",
|
|
failures
|
|
)
|
|
_expect_false(
|
|
cache_state.call(
|
|
"remember_request",
|
|
"world/missing.wmo",
|
|
"res://cache/retry.res"
|
|
),
|
|
"negative cache rejects request",
|
|
failures
|
|
)
|
|
|
|
|
|
func _verify_transient_and_full_clear(failures: Array[String]) -> void:
|
|
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
|
var retained_resource := Resource.new()
|
|
cache_state.call("remember_request", "world/retained.wmo", "res://cache/retained.res")
|
|
cache_state.call(
|
|
"complete_request_with_resource",
|
|
"world/retained.wmo",
|
|
retained_resource
|
|
)
|
|
cache_state.call("remember_request", "world/pending.wmo", "res://cache/pending.res")
|
|
cache_state.call("remember_request", "world/missing.wmo", "res://cache/missing.res")
|
|
cache_state.call("complete_request_as_missing", "world/missing.wmo")
|
|
cache_state.call("clear_transient_state")
|
|
_expect_true(
|
|
cache_state.call("resource_for", "world/retained.wmo") == retained_resource,
|
|
"transient clear retains validated Resource",
|
|
failures
|
|
)
|
|
_expect_equal(
|
|
int(cache_state.call("pending_request_count")),
|
|
0,
|
|
"transient clear removes requests",
|
|
failures
|
|
)
|
|
_expect_false(
|
|
cache_state.call("is_missing", "world/missing.wmo"),
|
|
"transient clear removes negative cache",
|
|
failures
|
|
)
|
|
cache_state.call("clear_all")
|
|
cache_state.call("clear_all")
|
|
_expect_false(
|
|
cache_state.call("contains_resource", "world/retained.wmo"),
|
|
"full clear idempotently releases Resource",
|
|
failures
|
|
)
|
|
|
|
|
|
func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
|
|
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
|
for normalized_path in ["world/z.wmo", "world/a.wmo"]:
|
|
cache_state.call("remember_request", normalized_path, "res://cache/%s.res" % normalized_path)
|
|
var snapshot: Dictionary = cache_state.call("diagnostic_snapshot")
|
|
_expect_string_array(
|
|
snapshot["request_paths"],
|
|
["world/a.wmo", "world/z.wmo"],
|
|
"request diagnostics sorted",
|
|
failures
|
|
)
|
|
(snapshot["request_paths"] as Array).clear()
|
|
var fresh_snapshot: Dictionary = cache_state.call("diagnostic_snapshot")
|
|
_expect_equal(
|
|
(fresh_snapshot["request_paths"] as Array).size(),
|
|
2,
|
|
"diagnostics detached",
|
|
failures
|
|
)
|
|
|
|
|
|
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
|
var loader_source := _read_text(LOADER_PATH, failures)
|
|
var cache_state_source := _read_text(CACHE_STATE_PATH, failures)
|
|
_expect_true(
|
|
loader_source.contains("WMO_RENDER_RESOURCE_CACHE_STATE_SCRIPT.new()"),
|
|
"loader composes cache state",
|
|
failures
|
|
)
|
|
for legacy_field in [
|
|
"var _wmo_render_cache:",
|
|
"var _wmo_render_missing_cache:",
|
|
"var _wmo_render_load_requests:",
|
|
]:
|
|
_expect_false(
|
|
loader_source.contains(legacy_field),
|
|
"loader removes %s" % legacy_field,
|
|
failures
|
|
)
|
|
for loader_owned_token in [
|
|
"ResourceLoader.",
|
|
"FileAccess.",
|
|
"WMO_STREAMING_SCRIPT",
|
|
"format_version",
|
|
"Node3D",
|
|
"RID",
|
|
"Thread",
|
|
"Mutex",
|
|
"Semaphore",
|
|
]:
|
|
_expect_false(
|
|
cache_state_source.contains(loader_owned_token),
|
|
"cache state omits %s dependency" % loader_owned_token,
|
|
failures
|
|
)
|
|
_expect_true(
|
|
loader_source.contains("resource.get_script() == WMO_STREAMING_SCRIPT"),
|
|
"loader retains script validation",
|
|
failures
|
|
)
|
|
_expect_true(
|
|
loader_source.contains("WMO_STREAMING_SCRIPT.FORMAT_VERSION"),
|
|
"loader retains cache format validation",
|
|
failures
|
|
)
|
|
|
|
|
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
|
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
|
var started_microseconds := Time.get_ticks_usec()
|
|
for iteration in range(100):
|
|
for resource_index in range(256):
|
|
var normalized_path := "world/object_%d.wmo" % resource_index
|
|
cache_state.call(
|
|
"remember_request",
|
|
normalized_path,
|
|
"res://cache/object_%d.res" % resource_index
|
|
)
|
|
if resource_index % 2 == 0:
|
|
cache_state.call(
|
|
"complete_request_with_resource",
|
|
normalized_path,
|
|
Resource.new()
|
|
)
|
|
else:
|
|
cache_state.call("complete_request_as_missing", normalized_path)
|
|
cache_state.call("clear_all")
|
|
_expect_equal(
|
|
int(cache_state.call("pending_request_count")),
|
|
0,
|
|
"timing cycle %d clears" % iteration,
|
|
failures
|
|
)
|
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
|
_expect_true(
|
|
elapsed_milliseconds < 1000.0,
|
|
"100 resource/request cycles remain bounded",
|
|
failures
|
|
)
|
|
return elapsed_milliseconds
|
|
|
|
|
|
func _read_text(path: String, failures: Array[String]) -> String:
|
|
var file := FileAccess.open(path, FileAccess.READ)
|
|
if file == null:
|
|
failures.append("cannot open %s" % path)
|
|
return ""
|
|
return file.get_as_text()
|
|
|
|
|
|
func _expect_equal(
|
|
actual_value: int,
|
|
expected_value: int,
|
|
label: String,
|
|
failures: Array[String]
|
|
) -> void:
|
|
if actual_value != expected_value:
|
|
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
|
|
|
|
|
func _expect_string_equal(
|
|
actual_value: String,
|
|
expected_value: String,
|
|
label: String,
|
|
failures: Array[String]
|
|
) -> void:
|
|
if actual_value != expected_value:
|
|
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
|
|
|
|
|
func _expect_string_array(
|
|
actual_values: Array,
|
|
expected_values: Array,
|
|
label: String,
|
|
failures: Array[String]
|
|
) -> void:
|
|
if actual_values != expected_values:
|
|
failures.append("%s expected %s, got %s" % [label, expected_values, actual_values])
|
|
|
|
|
|
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
|
if not actual_value:
|
|
failures.append("%s expected true" % label)
|
|
|
|
|
|
func _expect_false(actual_value: bool, label: String, failures: Array[String]) -> void:
|
|
if actual_value:
|
|
failures.append("%s expected false" % label)
|