Add business pricing and closure controls with compatible digest optimization
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
extends RefCounted
|
||||
|
||||
## Frozen v1 encoder from commit 627b200, kept only as the byte-compatibility
|
||||
## oracle and unoptimized benchmark baseline. Do not update with production changes.
|
||||
##
|
||||
## Dictionary insertion order never affects the result. Type tags and byte
|
||||
## lengths make the canonical form unambiguous, and Array order remains
|
||||
## significant. Runtime Objects and other process-local values are rejected;
|
||||
## callers must first export them to Dictionaries, Arrays and value types.
|
||||
|
||||
const FORMAT_VERSION := 1
|
||||
const MAX_NESTING_DEPTH := 128
|
||||
|
||||
|
||||
static func compute(value: Variant) -> String:
|
||||
var canonical := canonical_bytes(value)
|
||||
if canonical.is_empty():
|
||||
return ""
|
||||
var context := HashingContext.new()
|
||||
if context.start(HashingContext.HASH_SHA256) != OK:
|
||||
push_error("Could not initialize the simulation digest hash")
|
||||
return ""
|
||||
context.update(canonical)
|
||||
return context.finish().hex_encode()
|
||||
|
||||
|
||||
static func canonical_bytes(value: Variant) -> PackedByteArray:
|
||||
var output := PackedByteArray()
|
||||
_append_ascii(output, "the-tree:simulation-digest:v%d;" % FORMAT_VERSION)
|
||||
if not _encode_value(value, output, 0):
|
||||
return PackedByteArray()
|
||||
return output
|
||||
|
||||
|
||||
static func _encode_value(value: Variant, output: PackedByteArray, depth: int) -> bool:
|
||||
if depth > MAX_NESTING_DEPTH:
|
||||
push_error("SimulationDigest data exceeds the maximum nesting depth")
|
||||
return false
|
||||
|
||||
match typeof(value):
|
||||
TYPE_NIL:
|
||||
_append_ascii(output, "n")
|
||||
TYPE_BOOL:
|
||||
_append_ascii(output, "b1" if bool(value) else "b0")
|
||||
TYPE_INT:
|
||||
_append_ascii(output, "i%d;" % int(value))
|
||||
TYPE_FLOAT:
|
||||
_append_ascii(output, "f")
|
||||
return _append_float64(output, float(value))
|
||||
TYPE_STRING:
|
||||
_append_sized_bytes(output, "s", String(value).to_utf8_buffer())
|
||||
TYPE_STRING_NAME:
|
||||
_append_sized_bytes(output, "q", String(value).to_utf8_buffer())
|
||||
TYPE_VECTOR2:
|
||||
var vector: Vector2 = value
|
||||
_append_ascii(output, "v2")
|
||||
return _append_float64(output, vector.x) and _append_float64(output, vector.y)
|
||||
TYPE_VECTOR2I:
|
||||
var vector: Vector2i = value
|
||||
_append_ascii(output, "w2")
|
||||
_append_ascii(output, "i%d;i%d;" % [vector.x, vector.y])
|
||||
TYPE_RECT2:
|
||||
var rect: Rect2 = value
|
||||
_append_ascii(output, "r2")
|
||||
return (
|
||||
_append_float64(output, rect.position.x)
|
||||
and _append_float64(output, rect.position.y)
|
||||
and _append_float64(output, rect.size.x)
|
||||
and _append_float64(output, rect.size.y)
|
||||
)
|
||||
TYPE_RECT2I:
|
||||
var rect: Rect2i = value
|
||||
_append_ascii(output, "x2")
|
||||
_append_ascii(output, "i%d;i%d;i%d;i%d;" % [rect.position.x, rect.position.y, rect.size.x, rect.size.y])
|
||||
TYPE_VECTOR3:
|
||||
var vector: Vector3 = value
|
||||
_append_ascii(output, "v3")
|
||||
return (
|
||||
_append_float64(output, vector.x)
|
||||
and _append_float64(output, vector.y)
|
||||
and _append_float64(output, vector.z)
|
||||
)
|
||||
TYPE_VECTOR3I:
|
||||
var vector: Vector3i = value
|
||||
_append_ascii(output, "w3")
|
||||
_append_ascii(output, "i%d;i%d;i%d;" % [vector.x, vector.y, vector.z])
|
||||
TYPE_VECTOR4:
|
||||
var vector: Vector4 = value
|
||||
_append_ascii(output, "v4")
|
||||
return (
|
||||
_append_float64(output, vector.x)
|
||||
and _append_float64(output, vector.y)
|
||||
and _append_float64(output, vector.z)
|
||||
and _append_float64(output, vector.w)
|
||||
)
|
||||
TYPE_VECTOR4I:
|
||||
var vector: Vector4i = value
|
||||
_append_ascii(output, "w4")
|
||||
_append_ascii(output, "i%d;i%d;i%d;i%d;" % [vector.x, vector.y, vector.z, vector.w])
|
||||
TYPE_COLOR:
|
||||
var color: Color = value
|
||||
_append_ascii(output, "c")
|
||||
return (
|
||||
_append_float64(output, color.r)
|
||||
and _append_float64(output, color.g)
|
||||
and _append_float64(output, color.b)
|
||||
and _append_float64(output, color.a)
|
||||
)
|
||||
TYPE_ARRAY:
|
||||
return _encode_array(value, output, depth)
|
||||
TYPE_DICTIONARY:
|
||||
return _encode_dictionary(value, output, depth)
|
||||
TYPE_PACKED_BYTE_ARRAY:
|
||||
_append_sized_bytes(output, "pb", value)
|
||||
TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY:
|
||||
return _encode_packed_int_array(value, output)
|
||||
TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY:
|
||||
return _encode_packed_float_array(value, output)
|
||||
TYPE_PACKED_STRING_ARRAY:
|
||||
return _encode_packed_string_array(value, output)
|
||||
TYPE_PACKED_VECTOR2_ARRAY:
|
||||
return _encode_packed_vector2_array(value, output)
|
||||
TYPE_PACKED_VECTOR3_ARRAY:
|
||||
return _encode_packed_vector3_array(value, output)
|
||||
TYPE_PACKED_COLOR_ARRAY:
|
||||
return _encode_packed_color_array(value, output)
|
||||
_:
|
||||
push_error("SimulationDigest does not support Variant type %s" % type_string(typeof(value)))
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _encode_array(value: Array, output: PackedByteArray, depth: int) -> bool:
|
||||
_append_ascii(output, "a%d:" % value.size())
|
||||
for item in value:
|
||||
if not _encode_value(item, output, depth + 1):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _encode_dictionary(value: Dictionary, output: PackedByteArray, depth: int) -> bool:
|
||||
var entries: Array[Dictionary] = []
|
||||
for key in value:
|
||||
var encoded_key := PackedByteArray()
|
||||
if not _encode_value(key, encoded_key, depth + 1):
|
||||
return false
|
||||
entries.append({
|
||||
"sort_key": encoded_key.hex_encode(),
|
||||
"encoded_key": encoded_key,
|
||||
"value": value[key],
|
||||
})
|
||||
entries.sort_custom(func(left: Dictionary, right: Dictionary) -> bool:
|
||||
return String(left["sort_key"]) < String(right["sort_key"])
|
||||
)
|
||||
|
||||
_append_ascii(output, "d%d:" % entries.size())
|
||||
for entry in entries:
|
||||
var encoded_key: PackedByteArray = entry["encoded_key"]
|
||||
output.append_array(encoded_key)
|
||||
if not _encode_value(entry["value"], output, depth + 1):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _encode_packed_int_array(value: Variant, output: PackedByteArray) -> bool:
|
||||
var tag := "pi32" if typeof(value) == TYPE_PACKED_INT32_ARRAY else "pi64"
|
||||
_append_ascii(output, "%s%d:" % [tag, value.size()])
|
||||
for item in value:
|
||||
_append_ascii(output, "i%d;" % int(item))
|
||||
return true
|
||||
|
||||
|
||||
static func _encode_packed_float_array(value: Variant, output: PackedByteArray) -> bool:
|
||||
var tag := "pf32" if typeof(value) == TYPE_PACKED_FLOAT32_ARRAY else "pf64"
|
||||
_append_ascii(output, "%s%d:" % [tag, value.size()])
|
||||
for item in value:
|
||||
if not _append_float64(output, float(item)):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _encode_packed_string_array(value: PackedStringArray, output: PackedByteArray) -> bool:
|
||||
_append_ascii(output, "ps%d:" % value.size())
|
||||
for item in value:
|
||||
_append_sized_bytes(output, "s", item.to_utf8_buffer())
|
||||
return true
|
||||
|
||||
|
||||
static func _encode_packed_vector2_array(value: PackedVector2Array, output: PackedByteArray) -> bool:
|
||||
_append_ascii(output, "pv2%d:" % value.size())
|
||||
for item in value:
|
||||
if not _append_float64(output, item.x) or not _append_float64(output, item.y):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _encode_packed_vector3_array(value: PackedVector3Array, output: PackedByteArray) -> bool:
|
||||
_append_ascii(output, "pv3%d:" % value.size())
|
||||
for item in value:
|
||||
if (
|
||||
not _append_float64(output, item.x)
|
||||
or not _append_float64(output, item.y)
|
||||
or not _append_float64(output, item.z)
|
||||
):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _encode_packed_color_array(value: PackedColorArray, output: PackedByteArray) -> bool:
|
||||
_append_ascii(output, "pc%d:" % value.size())
|
||||
for item in value:
|
||||
if (
|
||||
not _append_float64(output, item.r)
|
||||
or not _append_float64(output, item.g)
|
||||
or not _append_float64(output, item.b)
|
||||
or not _append_float64(output, item.a)
|
||||
):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _append_float64(output: PackedByteArray, value: float) -> bool:
|
||||
if is_nan(value) or is_inf(value):
|
||||
push_error("SimulationDigest rejects NaN and infinite float values")
|
||||
return false
|
||||
var normalized := 0.0 if value == 0.0 else value
|
||||
var encoded := PackedByteArray()
|
||||
encoded.resize(8)
|
||||
encoded.encode_double(0, normalized)
|
||||
output.append_array(encoded)
|
||||
return true
|
||||
|
||||
|
||||
static func _append_sized_bytes(output: PackedByteArray, tag: String, value: PackedByteArray) -> void:
|
||||
_append_ascii(output, "%s%d:" % [tag, value.size()])
|
||||
output.append_array(value)
|
||||
|
||||
|
||||
static func _append_ascii(output: PackedByteArray, value: String) -> void:
|
||||
output.append_array(value.to_utf8_buffer())
|
||||
@@ -0,0 +1 @@
|
||||
uid://cyhefo00b7wkr
|
||||
@@ -0,0 +1,222 @@
|
||||
extends SceneTree
|
||||
|
||||
const Digest = preload("res://scripts/core/simulation_digest.gd")
|
||||
const Reference = preload("res://tests/performance/fixtures/simulation_digest_v1_reference.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("Simulation digest: v1 byte compatibility and growing-history benchmark")
|
||||
_test_value_types()
|
||||
_test_dictionary_layouts()
|
||||
_test_mutable_values()
|
||||
_test_limits_and_rejections()
|
||||
_test_growing_histories()
|
||||
print("Digest performance: %d checks, %d failures" % [_checks, _failures])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _test_value_types() -> void:
|
||||
var values: Array = [
|
||||
null, false, true, -9223372036854775807, 9223372036854775807,
|
||||
0, 0.0, -0.0, -0.125, 1.25e100, "", "厨房\u0000🌳", &"", &"運搬",
|
||||
Vector2(-1.5, 2.25), Vector2i(-3, 7), Rect2(-1.5, 2.25, 3.5, 4.75),
|
||||
Rect2i(-3, 7, 2, 8), Vector3(-1.5, 2.25, 3.5), Vector3i(-3, 7, 2),
|
||||
Vector4(-1.5, 2.25, 3.5, 4.75), Vector4i(-3, 7, 2, 8), Color(0.25, 0.5, 0.75, 1),
|
||||
[], {}, PackedByteArray([0, 127, 255]), PackedInt32Array([-3, 0, 7]),
|
||||
PackedInt64Array([-9223372036854775807, 9223372036854775807]),
|
||||
PackedFloat32Array([-1.25, 0.0, 2.5]), PackedFloat64Array([-1.25, -0.0, 1.25e100]),
|
||||
PackedStringArray(["", "日本語", "🌳"]), PackedVector2Array([Vector2(-1, 2)]),
|
||||
PackedVector3Array([Vector3(-1, 2, 3)]), PackedColorArray([Color(0.25, 0.5, 0.75, 1)]),
|
||||
]
|
||||
var strings: Array[String] = ["", "a", "厨房"]
|
||||
var names: Array[StringName] = [&"", &"a", &"厨房"]
|
||||
values.append(strings)
|
||||
values.append(names)
|
||||
for index in values.size():
|
||||
_expect_bytes(values[index], "canonical bytes for value type %d, sample %d" % [typeof(values[index]), index])
|
||||
_expect_bytes({"values": values}, "nested complete supported value matrix")
|
||||
_expect(Digest.compute([1, 2, 3]) == "4e32bb2921e1b879d5b8778789d3bc7e46559d4448e95f9c2fed427d7e5c7b93", "published v1 reference digest remains unchanged")
|
||||
|
||||
|
||||
func _test_dictionary_layouts() -> void:
|
||||
var rows: Array = []
|
||||
# Native Variant equality can equate String and StringName; cache buckets must
|
||||
# keep their type tags apart even when the textual schemas are identical.
|
||||
for index in 60:
|
||||
var row := {}
|
||||
var keys: Array = ["z", "aa", "日本語", "a", "\u0000", "", "001"]
|
||||
if index % 2 == 1:
|
||||
keys.reverse()
|
||||
for key: String in keys:
|
||||
row[StringName(key) if index % 3 == 0 else key] = {"index": index, "key": key}
|
||||
rows.append(row)
|
||||
_expect_bytes(rows, "cached String and StringName schemas preserve tags and byte order")
|
||||
var mixed := {}
|
||||
mixed["text"] = "String"
|
||||
mixed[&"name"] = "StringName"
|
||||
mixed[7] = "integer"
|
||||
mixed[1.25] = "float"
|
||||
mixed[Vector2i(2, 3)] = "vector"
|
||||
mixed[["a", 1]] = "array key"
|
||||
mixed[{"inner": "key"}] = "dictionary key"
|
||||
_expect_bytes([mixed, mixed.duplicate(true)], "mixed and mutable dictionary keys use the uncached encoder")
|
||||
var forward := {"a": 1, "b": 2, "ccc": 3}
|
||||
var reverse := {"ccc": 3, "b": 2, "a": 1}
|
||||
_expect(Digest.canonical_bytes(forward) == Digest.canonical_bytes(reverse), "canonical order ignores dictionary insertion order")
|
||||
# Exceed each bound to check graceful uncached fallback, without relying on a
|
||||
# process-global cache or on the hash of an incomplete key signature.
|
||||
var many_schemas: Array = []
|
||||
for index in 600:
|
||||
many_schemas.append({"schema_%d" % index: index, "nested": {"value": index}})
|
||||
_expect_bytes(many_schemas, "more than the bounded layout capacity remains byte-identical")
|
||||
# These two complete key arrays collide under Godot 4.7's native hash. The
|
||||
# cache must still compare complete contents, never index by hash alone.
|
||||
var collision_left := {"schema.96ac180f": 1, "value": 2}
|
||||
var collision_right := {"schema.c5015a12": 3, "value": 4}
|
||||
if hash(collision_left.keys()) == hash(collision_right.keys()):
|
||||
print(" Native layout hash-collision fixture exercised")
|
||||
_expect_bytes([collision_left, collision_right, collision_left], "different key layouts with colliding native hashes remain distinct")
|
||||
var many_keys := {}
|
||||
for index in 80:
|
||||
many_keys["key_%d" % index] = index
|
||||
_expect_bytes(many_keys, "large dictionaries remain byte-identical")
|
||||
_expect_bytes({"長".repeat(200): 1}, "long keys bypass the cache")
|
||||
|
||||
|
||||
func _test_mutable_values() -> void:
|
||||
var nested := {"history": [{"minute": 1, "amount": 3}], "queue": [1, 2]}
|
||||
var before := Digest.compute(nested)
|
||||
nested["history"][0]["amount"] = 4
|
||||
nested["queue"].append(3)
|
||||
_expect_bytes(nested, "nested value mutations cannot reuse stale encoded values")
|
||||
_expect(Digest.compute(nested) != before, "nested mutation changes the hash")
|
||||
nested.erase("queue")
|
||||
nested[&"queue"] = [1, 2]
|
||||
_expect_bytes(nested, "key type mutation between digest calls is observed")
|
||||
var left := {"key": {"value": 1}}
|
||||
var right := {"key": {"value": 2}}
|
||||
_expect_bytes([left, right], "reused schema reads each dictionary's current values")
|
||||
var canonical := Digest.canonical_bytes(nested)
|
||||
canonical.fill(0)
|
||||
_expect_bytes(nested, "caller mutation of previous output cannot corrupt another call")
|
||||
|
||||
|
||||
func _test_limits_and_rejections() -> void:
|
||||
var typed_leaf: Array[String] = ["leaf"]
|
||||
_expect_bytes(_nested(typed_leaf, 127), "typed string item at exact nesting limit is valid")
|
||||
_expect_bytes(_nested({"leaf": 1}, 127), "cached dictionary key at exact nesting limit is valid")
|
||||
_expect_bytes(_nested([], 128), "empty container at nesting limit is valid")
|
||||
var rejected: Array = [_nested(typed_leaf, 128), _nested({"leaf": 1}, 128), _nested(null, 129), NAN, INF, -INF, Vector2(NAN, 1), PackedFloat64Array([INF]), {"nested": NAN}, NodePath("runtime/object")]
|
||||
var object := RefCounted.new()
|
||||
rejected.append(object)
|
||||
var errors_enabled := Engine.print_error_messages
|
||||
Engine.print_error_messages = false
|
||||
for index in rejected.size():
|
||||
var legacy: PackedByteArray = Reference.canonical_bytes(rejected[index])
|
||||
var current: PackedByteArray = Digest.canonical_bytes(rejected[index])
|
||||
_expect(legacy.is_empty() and current.is_empty(), "unsupported or over-depth sample %d still rejects" % index)
|
||||
Engine.print_error_messages = errors_enabled
|
||||
|
||||
|
||||
func _test_growing_histories() -> void:
|
||||
var reports: Array[Dictionary] = []
|
||||
for history_count in [100, 1000, 5000]:
|
||||
var snapshot := _history_snapshot(history_count)
|
||||
_expect_bytes(snapshot, "growing-history snapshot with %d records" % history_count)
|
||||
_expect(Digest.compute(snapshot) == Reference.compute(snapshot), "growing-history SHA-256 with %d records" % history_count)
|
||||
# Warm both encoders, then alternate execution order. Timing is reported,
|
||||
# never asserted: CPU load and editor/preview activity vary across machines.
|
||||
Reference.compute(snapshot)
|
||||
Digest.compute(snapshot)
|
||||
var reference_samples: Array[float] = []
|
||||
var optimized_samples: Array[float] = []
|
||||
for sample in 5:
|
||||
if sample % 2 == 0:
|
||||
reference_samples.append(_measure(Reference.compute, snapshot))
|
||||
optimized_samples.append(_measure(Digest.compute, snapshot))
|
||||
else:
|
||||
optimized_samples.append(_measure(Digest.compute, snapshot))
|
||||
reference_samples.append(_measure(Reference.compute, snapshot))
|
||||
reference_samples.sort()
|
||||
optimized_samples.sort()
|
||||
var reference_ms: float = reference_samples[2]
|
||||
var optimized_ms: float = optimized_samples[2]
|
||||
var report := {
|
||||
"history_records": history_count,
|
||||
"canonical_bytes": Digest.canonical_bytes(snapshot).size(),
|
||||
"baseline_median_ms": snappedf(reference_ms, 0.001),
|
||||
"optimized_median_ms": snappedf(optimized_ms, 0.001),
|
||||
"speedup": snappedf(reference_ms / optimized_ms, 0.001),
|
||||
}
|
||||
reports.append(report)
|
||||
print("DIGEST_BENCHMARK %s" % JSON.stringify(report))
|
||||
print("DIGEST_PERFORMANCE_REPORT %s" % JSON.stringify({"samples_per_encoder": 5, "metric": "median wall time including SHA-256", "fixture": "synthetic event, ledger, and service histories; complete histories retained", "results": reports}))
|
||||
var ids: Array[String] = []
|
||||
for index in 10000:
|
||||
ids.append("facility.operation.finished:%08d:reception.1:guest.%08d" % [index, index / 4])
|
||||
var id_snapshot := {"version": 1, "published_ids": ids, "recent_events": []}
|
||||
_expect_bytes(id_snapshot, "append-only published event ID history remains complete")
|
||||
var id_reference_samples: Array[float] = []
|
||||
var id_optimized_samples: Array[float] = []
|
||||
for sample in 5:
|
||||
id_reference_samples.append(_measure(Reference.compute, id_snapshot))
|
||||
id_optimized_samples.append(_measure(Digest.compute, id_snapshot))
|
||||
id_reference_samples.sort()
|
||||
id_optimized_samples.sort()
|
||||
print("DIGEST_EVENT_HISTORY_BENCHMARK %s" % JSON.stringify({"published_ids": ids.size(), "canonical_bytes": Digest.canonical_bytes(id_snapshot).size(), "baseline_median_ms": snappedf(id_reference_samples[2], 0.001), "optimized_median_ms": snappedf(id_optimized_samples[2], 0.001), "speedup": snappedf(id_reference_samples[2] / id_optimized_samples[2], 0.001)}))
|
||||
|
||||
|
||||
func _history_snapshot(count: int) -> Dictionary:
|
||||
var published_ids: Array[String] = []
|
||||
var recent_events: Array[Dictionary] = []
|
||||
var entries: Array[Dictionary] = []
|
||||
var receipts: Array[Dictionary] = []
|
||||
for index in count:
|
||||
var event_id := "event.economy.ledger.%08d" % index
|
||||
published_ids.append(event_id)
|
||||
entries.append({
|
||||
"entry_id": "entry.%08d" % index, "absolute_minute": index * 3,
|
||||
"entry_type": "operating_expense" if index % 3 == 0 else "service_revenue",
|
||||
"amount": -12 if index % 3 == 0 else 35, "account_id": "cash",
|
||||
"counterparty_id": "guest.%05d" % (index % 250), "reference_id": event_id,
|
||||
"metadata": {"facility_id": "cafeteria.1", "quantity": 1, "description": "基本の食事"},
|
||||
})
|
||||
receipts.append({"receipt_id": "receipt.%08d" % index, "minute": index * 3, "guest_id": "guest.%05d" % (index % 250), "facility_id": "cafeteria.1", "satisfied": true, "price": 35})
|
||||
if index >= maxi(count - 128, 0):
|
||||
recent_events.append({"event_id": event_id, "event_type": "ledger.posted", "absolute_minute": index * 3, "source_service": "economy", "entity_ids": ["cafeteria.1"], "payload": {"amount": 35, "quantity": 1}, "causation_command_id": index})
|
||||
return {"version": 7, "minute": count * 3, "domain_events": {"version": 1, "published_ids": published_ids, "recent_events": recent_events}, "economy": {"version": 1, "balance": 5000 + count * 12, "entries": entries}, "operations": {"version": 1, "receipts": receipts}}
|
||||
|
||||
|
||||
func _nested(leaf: Variant, levels: int) -> Variant:
|
||||
var result: Variant = leaf
|
||||
for index in levels:
|
||||
result = [result]
|
||||
return result
|
||||
|
||||
|
||||
func _measure(encoder: Callable, snapshot: Dictionary) -> float:
|
||||
var started := Time.get_ticks_usec()
|
||||
encoder.call(snapshot)
|
||||
return float(Time.get_ticks_usec() - started) / 1000.0
|
||||
|
||||
|
||||
func _expect_bytes(value: Variant, label: String) -> void:
|
||||
var expected: PackedByteArray = Reference.canonical_bytes(value)
|
||||
var actual: PackedByteArray = Digest.canonical_bytes(value)
|
||||
_expect(not expected.is_empty() and actual == expected, label)
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print(" PASS: %s" % label)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" FAIL: %s" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c78f41p838v73
|
||||
Reference in New Issue
Block a user