293 lines
15 KiB
GDScript
293 lines
15 KiB
GDScript
extends RefCounted
|
|
# Isolated protocol/marshalling QA only. No production PathService or performance claim.
|
|
|
|
class EdgeProvider extends RefCounted:
|
|
var rows: Array = []
|
|
var calls: Array[int] = []
|
|
func edges(cell: Vector3i) -> Array:
|
|
calls.append(cell.x)
|
|
return rows[cell.x]
|
|
|
|
class ShrinkingProvider extends RefCounted:
|
|
var cells: Array[Vector3i] = []
|
|
var calls: Array[Vector3i] = []
|
|
func edges(cell: Vector3i) -> Array:
|
|
calls.append(cell)
|
|
cells.resize(2)
|
|
return [[Vector3i(1, 0, 0), 7]]
|
|
|
|
var failures: Array[String] = []
|
|
var checks: int = 0
|
|
var cases: Array = []
|
|
var cached_cases: Array = []
|
|
|
|
|
|
func _check(condition: bool, label: String) -> void:
|
|
checks += 1
|
|
if not condition:
|
|
failures.append(label)
|
|
|
|
func _ints(text: String) -> Array[int]:
|
|
var result: Array[int] = []
|
|
for token in text.replace("\n", " ").split(" ", false):
|
|
_check(token.is_valid_int() and str(token.to_int()) == token, "canonical signed integer " + token)
|
|
result.append(token.to_int())
|
|
return result
|
|
|
|
func _line(values) -> String:
|
|
var text: PackedStringArray = []
|
|
for value in values:
|
|
text.append(str(value))
|
|
return " ".join(text) + "\n"
|
|
|
|
func _serialized(output: Dictionary) -> String:
|
|
var status = output.get(&"status", "missing")
|
|
_check(typeof(status) == TYPE_STRING, "status String")
|
|
var text: String = str(status) + "\n"
|
|
for key in output:
|
|
_check(typeof(key) == TYPE_STRING_NAME, "output StringName key")
|
|
if status != "usable":
|
|
_check(output.size() == 1, "unusable/wrapped status only")
|
|
return text
|
|
_check(output.size() == 6, "usable six output fields")
|
|
_check(typeof(output.get(&"cost_values")) == TYPE_PACKED_INT64_ARRAY, "cost PackedInt64Array")
|
|
_check(typeof(output.get(&"parent_indices")) == TYPE_PACKED_INT64_ARRAY, "parents PackedInt64Array")
|
|
for name in [&"discovered", &"parent_discovered"]:
|
|
var values = output.get(name)
|
|
_check(typeof(values) == TYPE_ARRAY and values.is_typed() and values.get_typed_builtin() == TYPE_INT, "typed Array[int] " + str(name))
|
|
for name in [&"cost_values", &"parent_indices", &"discovered", &"parent_discovered"]:
|
|
text += _line(output.get(name, []))
|
|
var settled: Dictionary = output.get(&"settled", {})
|
|
var indices: Array[int] = []
|
|
for cell in settled:
|
|
_check(typeof(cell) == TYPE_VECTOR3I and typeof(settled[cell]) == TYPE_BOOL and settled[cell], "settled Vector3i -> true")
|
|
indices.append(cell.x)
|
|
text += _line(indices)
|
|
return text
|
|
|
|
func _case(core, row: Dictionary) -> void:
|
|
var tokens := _ints(FileAccess.get_file_as_string(row["input_path"]))
|
|
var count: int = tokens[0]
|
|
var start: int = tokens[1]
|
|
var goal_count: int = tokens[2]
|
|
var cursor: int = 3
|
|
var cells: Array[Vector3i] = []
|
|
var indices: Dictionary = {}
|
|
for i in count:
|
|
var cell := Vector3i(i, 0, 0)
|
|
cells.append(cell)
|
|
indices[cell] = i
|
|
var goals: Dictionary = {}
|
|
for i in goal_count:
|
|
goals[Vector3i(tokens[cursor], 0, 0)] = true
|
|
cursor += 1
|
|
var provider := EdgeProvider.new()
|
|
for i in count:
|
|
var edge_count: int = tokens[cursor]
|
|
cursor += 1
|
|
var edges: Array = []
|
|
for j in edge_count:
|
|
edges.append([Vector3i(tokens[cursor], 0, 0), tokens[cursor + 1]])
|
|
cursor += 2
|
|
provider.rows.append(edges)
|
|
_check(cursor == tokens.size(), "complete input consumption")
|
|
var before := var_to_bytes([cells, indices, goals, provider.rows])
|
|
var output = core.call("search", cells, indices, start, goals, Callable(provider, "edges"))
|
|
_check(typeof(output) == TYPE_DICTIONARY, "result Dictionary")
|
|
var actual := _serialized(output)
|
|
var expected := FileAccess.get_file_as_string(row["expected_path"])
|
|
_check(actual == expected, "whole protocol bytes: " + str(row["group"]) + " / " + str(row["case"]))
|
|
_check(var_to_bytes([cells, indices, goals, provider.rows]) == before, "caller input ownership")
|
|
cases.append({"group": row["group"], "case": row["case"], "actual": actual, "expected_sha256": row["expected_sha256"], "equal": actual == expected, "provider_calls": provider.calls.duplicate()})
|
|
# Separate cached API: one immutable caller graph until explicit clear.
|
|
core.call("clear_edge_cache")
|
|
provider.calls.clear()
|
|
var cold: Dictionary = core.call("search_cached", cells, indices, start, goals, Callable(provider, "edges"))
|
|
var cold_bytes := var_to_bytes(cold)
|
|
var cold_calls := provider.calls.duplicate()
|
|
_check(_serialized(cold) == expected and cold_bytes == var_to_bytes(output), "cached cold full typed output equals original")
|
|
provider.calls.clear()
|
|
var warm: Dictionary = core.call("search_cached", cells, indices, start, goals, Callable(provider, "edges"))
|
|
_check(_serialized(warm) == expected and var_to_bytes(warm) == cold_bytes, "cached warm full typed output equals original")
|
|
var warm_calls := provider.calls.duplicate()
|
|
_check(provider.calls.is_empty(), "warm search makes no already-cached provider calls")
|
|
# The original API remains uncached even on an instance with a warm cache.
|
|
provider.calls.clear()
|
|
var uncached: Dictionary = core.call("search", cells, indices, start, goals, Callable(provider, "edges"))
|
|
_check(var_to_bytes(uncached) == cold_bytes and provider.calls == cold_calls, "original API ignores warm native cache")
|
|
if cold.get(&"status") == "usable":
|
|
cold[&"cost_values"][0] = 12345
|
|
cold[&"parent_indices"][0] = 12345
|
|
cold[&"discovered"].clear()
|
|
cold[&"parent_discovered"].clear()
|
|
cold[&"settled"].clear()
|
|
var owned: Dictionary = core.call("search_cached", cells, indices, start, goals, Callable(provider, "edges"))
|
|
_check(var_to_bytes(owned) == cold_bytes, "external result mutations do not alter cached edge vectors or fresh output")
|
|
core.call("clear_edge_cache")
|
|
provider.calls.clear()
|
|
var cleared: Dictionary = core.call("search_cached", cells, indices, start, goals, Callable(provider, "edges"))
|
|
_check(var_to_bytes(cleared) == cold_bytes and provider.calls == cold_calls, "clear repeats original lazy calls and full output")
|
|
_check(var_to_bytes([cells, indices, goals, provider.rows]) == before, "cached APIs preserve all caller inputs")
|
|
cached_cases.append({"group":row["group"], "case":row["case"], "cold":_serialized(cleared), "warm":_serialized(warm), "expected_sha256":row["expected_sha256"], "cold_calls":cold_calls, "warm_calls":warm_calls, "equal":var_to_bytes(cleared)==cold_bytes and var_to_bytes(warm)==cold_bytes})
|
|
var provider_weak: WeakRef = weakref(provider)
|
|
provider = null
|
|
_check(provider_weak.get_ref() == null, "native cache does not retain the provider Callable")
|
|
output = null
|
|
cold = {}; warm = {}; uncached = {}; cleared = {}
|
|
|
|
func _prefix_wrap(core, malformed) -> void:
|
|
var cells: Array[Vector3i] = [Vector3i.ZERO, Vector3i(1, 0, 0), Vector3i(2, 0, 0)]
|
|
var indices: Dictionary = {cells[0]: 0, cells[1]: 1, cells[2]: 2}
|
|
var provider := EdgeProvider.new()
|
|
provider.rows = [[[cells[1], 9223372036854775802]], [[cells[2], 10], malformed], []]
|
|
var goals: Dictionary = {cells[2]: true}
|
|
var before := var_to_bytes([cells, indices, goals, provider.rows])
|
|
var output = core.call("search", cells, indices, 0, goals, Callable(provider, "edges"))
|
|
_check(_serialized(output) == "wrapped\n", "wrap before later malformed row")
|
|
_check(var_to_bytes([cells, indices, goals, provider.rows]) == before, "prefix-wrap caller ownership")
|
|
provider = null
|
|
output = null
|
|
|
|
func _shrink_transfer(core) -> void:
|
|
var cells: Array[Vector3i] = [Vector3i.ZERO, Vector3i(1, 0, 0), Vector3i(2, 0, 0)]
|
|
var indices: Dictionary = {cells[0]: 0, cells[1]: 1, cells[2]: 2}
|
|
var goals: Dictionary = {cells[1]: true}
|
|
var fixed_before := var_to_bytes([indices, goals])
|
|
var provider := ShrinkingProvider.new()
|
|
provider.cells = cells
|
|
var output = core.call("search", cells, indices, 0, goals, Callable(provider, "edges"))
|
|
# Old bridge transfers only the post-search prefix: the allocated third slots stay zero.
|
|
var expected := "usable\n0 7 0\n-1 0 0\n0 1\n1\n0 1\n"
|
|
_check(_serialized(output) == expected, "provider shrink preserves original transferred prefix and zero tail")
|
|
_check(cells.size() == 2 and provider.calls == [Vector3i.ZERO], "single lazy callback shrinks shared cells")
|
|
_check(var_to_bytes([indices, goals]) == fixed_before, "shrink fixture maps unchanged")
|
|
var costs: PackedInt64Array = output[&"cost_values"].duplicate()
|
|
var parents: PackedInt64Array = output[&"parent_indices"].duplicate()
|
|
costs[0] = 12345
|
|
parents[0] = 12345
|
|
_check(_serialized(output) == expected, "PackedInt64Array explicit duplicates do not mutate retained result")
|
|
var shared_costs: PackedInt64Array = output[&"cost_values"]
|
|
var shared_parents: PackedInt64Array = output[&"parent_indices"]
|
|
shared_costs[0] = 12345
|
|
shared_parents[0] = 12345
|
|
_check(_serialized(output) == "usable\n12345 7 0\n12345 0 0\n0 1\n1\n0 1\n", "PackedInt64Array aliases retain documented shared mutation")
|
|
# A new search allocates new outputs; external result mutations cannot poison it.
|
|
output[&"cost_values"] = PackedInt64Array([12345])
|
|
output[&"parent_indices"] = PackedInt64Array([12345])
|
|
cells.append(Vector3i(2, 0, 0))
|
|
provider.calls.clear()
|
|
var repeated = core.call("search", cells, indices, 0, goals, Callable(provider, "edges"))
|
|
_check(_serialized(repeated) == expected, "fresh result ownership after caller mutation")
|
|
_check(provider.calls == [Vector3i.ZERO] and cells.size() == 2, "repeat shrink lazy callback")
|
|
provider = null
|
|
output = null
|
|
repeated = null
|
|
|
|
func _cached_goals_and_reset(core) -> void:
|
|
core.call("clear_edge_cache")
|
|
var cells: Array[Vector3i] = [Vector3i.ZERO, Vector3i(1, 0, 0), Vector3i(2, 0, 0)]
|
|
var indices: Dictionary = {cells[0]:0, cells[1]:1, cells[2]:2}
|
|
var provider := EdgeProvider.new()
|
|
provider.rows = [[[cells[1],7],[cells[2],20]], [[cells[2],3]], []]
|
|
var goals: Dictionary = {cells[1]:true}
|
|
var first: Dictionary = core.call("search_cached", cells, indices, 0, goals, Callable(provider,"edges"))
|
|
_check(first[&"status"] == "usable" and first[&"cost_values"][1] == 7 and provider.calls == [0], "first goal stops before requesting goal edges or unseen node")
|
|
provider.calls.clear(); goals = {cells[2]:true}
|
|
var later: Dictionary = core.call("search_cached", cells, indices, 0, goals, Callable(provider,"edges"))
|
|
_check(provider.calls == [1], "changed goals lazily request only the newly expanded node")
|
|
var oracle: Dictionary = core.call("search", cells, indices, 0, goals, Callable(provider,"edges"))
|
|
_check(var_to_bytes(later)==var_to_bytes(oracle), "changed goals full typed equivalence")
|
|
provider.calls.clear()
|
|
var shifted: Dictionary = core.call("search_cached", cells, indices, 1, goals, Callable(provider,"edges"))
|
|
_check(provider.calls.is_empty(), "changed start reuses previously expanded node only")
|
|
oracle = core.call("search", cells, indices, 1, goals, Callable(provider,"edges"))
|
|
_check(var_to_bytes(shifted)==var_to_bytes(oracle), "changed start full typed equivalence")
|
|
# Graph mutation obeys the explicit API contract: clear before reuse.
|
|
provider.rows[0][0][1] = 2
|
|
core.call("clear_edge_cache"); provider.calls.clear()
|
|
var changed: Dictionary = core.call("search_cached", cells, indices, 0, goals, Callable(provider,"edges"))
|
|
_check(provider.calls == [0,1], "clear after edge mutation repopulates exact lazy nodes")
|
|
oracle = core.call("search", cells, indices, 0, goals, Callable(provider,"edges"))
|
|
_check(var_to_bytes(changed)==var_to_bytes(oracle) and changed[&"cost_values"][2]==5, "mutated graph after clear has complete correct output")
|
|
core.call("clear_edge_cache")
|
|
provider = null
|
|
|
|
func _cached_prefix_wrap(core, malformed) -> void:
|
|
core.call("clear_edge_cache")
|
|
var cells: Array[Vector3i] = [Vector3i.ZERO,Vector3i(1,0,0),Vector3i(2,0,0)]
|
|
var indices: Dictionary = {cells[0]:0,cells[1]:1,cells[2]:2}
|
|
var goals: Dictionary = {cells[2]:true}
|
|
var provider := EdgeProvider.new()
|
|
provider.rows = [[[cells[1],9223372036854775802]], [[cells[2],10],malformed], []]
|
|
var before := var_to_bytes([cells,indices,goals,provider.rows])
|
|
var cold: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges"))
|
|
_check(_serialized(cold)=="wrapped\n" and provider.calls==[0,1], "cached prefix real wrap before later malformed row")
|
|
provider.calls.clear()
|
|
var warm: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges"))
|
|
_check(var_to_bytes(warm)==var_to_bytes(cold) and provider.calls.is_empty(), "cached invalid-prefix wrap repeat unchanged")
|
|
_check(var_to_bytes([cells,indices,goals,provider.rows])==before, "cached prefix all caller inputs unchanged")
|
|
core.call("clear_edge_cache"); provider = null
|
|
|
|
func _warm_for_teardown(core) -> void:
|
|
core.call("clear_edge_cache")
|
|
var cells: Array[Vector3i] = [Vector3i.ZERO,Vector3i(1,0,0)]
|
|
var indices: Dictionary = {cells[0]:0,cells[1]:1}
|
|
var goals: Dictionary = {cells[1]:true}
|
|
var provider := EdgeProvider.new()
|
|
provider.rows = [[[cells[1],7]], []]
|
|
var output: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges"))
|
|
_check(output[&"status"]=="usable" and output[&"cost_values"][1]==7 and provider.calls==[0],"populate native cache before actual core teardown")
|
|
var provider_weak: WeakRef = weakref(provider)
|
|
provider = null
|
|
_check(provider_weak.get_ref()==null,"warm native cache retains no Callable or caller objects")
|
|
|
|
class ReentrantProvider extends RefCounted:
|
|
var core: Object
|
|
var cells: Array[Vector3i]
|
|
var indices: Dictionary
|
|
var goals: Dictionary
|
|
var entered := false
|
|
var inner_result: Dictionary
|
|
func edges(cell: Vector3i) -> Array:
|
|
if cell.x != 0: return []
|
|
if not entered:
|
|
entered = true
|
|
core.call("clear_edge_cache")
|
|
var inner := EdgeProvider.new()
|
|
inner.rows = [[[cells[1],9]], []]
|
|
inner_result = core.call("search_cached",cells,indices,0,goals,Callable(inner,"edges"))
|
|
return [[cells[1],7]]
|
|
|
|
func _reentrant_collision(core) -> Dictionary:
|
|
core.call("clear_edge_cache")
|
|
var cells: Array[Vector3i] = [Vector3i.ZERO,Vector3i(1,0,0)]
|
|
var indices: Dictionary = {cells[0]:0,cells[1]:1}
|
|
var goals: Dictionary = {cells[1]:true}
|
|
var provider := ReentrantProvider.new()
|
|
provider.core=core;provider.cells=cells;provider.indices=indices;provider.goals=goals
|
|
var outer: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges"))
|
|
_check(outer[&"status"] == "usable" and outer[&"cost_values"][1] == 7, "reentrant collision returns original converted outer row")
|
|
_check(provider.inner_result[&"status"] == "usable" and provider.inner_result[&"cost_values"][1] == 9, "inner populated same cache key before outer insertion")
|
|
var warm: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges"))
|
|
_check(warm[&"status"] == "usable" and warm[&"cost_values"][1] == 9, "warm preserves inner first cache publication")
|
|
var result := {"outer":outer,"inner":provider.inner_result,"warm":warm}
|
|
var ref: WeakRef = weakref(provider)
|
|
provider = null
|
|
_check(ref.get_ref() == null, "borrowed native cache retains no reentrant provider")
|
|
core.call("clear_edge_cache")
|
|
return result
|
|
|
|
func run_frozen(core, manifest_path: String) -> Dictionary:
|
|
var manifest: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(manifest_path))
|
|
_check(manifest["cases"].size() == 50, "same frozen fifty-input scope")
|
|
for row: Dictionary in manifest["cases"]: _case(core,row)
|
|
_prefix_wrap(core,true)
|
|
_prefix_wrap(core,[Vector3i(2,0,0),false])
|
|
_shrink_transfer(core)
|
|
_cached_goals_and_reset(core)
|
|
_cached_prefix_wrap(core,true)
|
|
_cached_prefix_wrap(core,[Vector3i(2,0,0),false])
|
|
var collision := _reentrant_collision(core)
|
|
_warm_for_teardown(core)
|
|
return {"passed":failures.is_empty(),"checks":checks,"failures":failures,"cases":cases,"cached_cases":cached_cases,"reentrant_collision":collision,"unit_accepted":false,"performance_accepted":false}
|