Files
gamedev-the-steward/tests/unit/test_regional_chunked_file_store.gd
2026-08-12 22:29:58 +02:00

407 lines
15 KiB
GDScript

extends GutTest
const WORLD_ID := &"regional_world"
const ORIGIN_LOCATION_ID := &"location_origin"
const DESTINATION_LOCATION_ID := &"location_destination"
const ORIGIN_SETTLEMENT_ID := &"settlement_origin"
const DESTINATION_SETTLEMENT_ID := &"settlement_destination"
const GROUP_ID := &"group_caravan"
const ROUTE_ID := &"route_trade"
const DEPARTURE_TICK := 10
var _test_directory := ""
func before_each() -> void:
var test_parent := (
ProjectSettings.globalize_path("user://regional_chunked_file_store_tests").simplify_path()
)
_remove_tree(test_parent)
_test_directory = (
"user://regional_chunked_file_store_tests/run_%d_%d"
% [Time.get_ticks_usec(), get_instance_id()]
)
func after_each() -> void:
var absolute_path := ProjectSettings.globalize_path(_test_directory).simplify_path()
var expected_parent := (
ProjectSettings.globalize_path("user://regional_chunked_file_store_tests").simplify_path()
)
if absolute_path.begins_with(expected_parent + "/"):
_remove_tree(absolute_path)
func test_atomic_generations_round_trip_exact_archive_and_report_telemetry() -> void:
var explicit_absolute_base := ProjectSettings.globalize_path(_test_directory)
var store := RegionalChunkedFileStore.new(explicit_absolute_base)
var first := _fixture_archive(false)
var second := _fixture_archive(true)
assert_eq(store.get_base_directory(), explicit_absolute_base)
assert_true(store.save(first))
var first_generation := store.get_generation_ids()[0]
assert_true(store.save(second))
var generation_ids := store.get_generation_ids()
assert_eq(generation_ids.size(), 2)
assert_eq(generation_ids[0], first_generation)
assert_true(
(
DirAccess.open(
ProjectSettings.globalize_path(store.get_generation_directory(first_generation))
)
!= null
)
)
var save_telemetry := store.get_last_telemetry()
assert_true(save_telemetry["succeeded"])
assert_gt(save_telemetry["serialized_bytes"], 0)
assert_gt(save_telemetry["bytes_written"], 0)
assert_gte(save_telemetry["write_usec"], 0)
assert_gte(save_telemetry["validation_usec"], 0)
var latest_generation := generation_ids[-1]
var generation_path := store.get_generation_directory(latest_generation)
var generation_files := DirAccess.get_files_at(generation_path)
assert_eq(generation_files.size(), second.get_chunk_ids().size() + 1)
assert_true(RegionalChunkedFileStore.MANIFEST_FILE_NAME in generation_files)
for file_name: String in generation_files:
assert_false("/" in file_name)
assert_false("\\" in file_name)
assert_lte(file_name.length(), 64)
var decoded: Variant = _read_variant(generation_path.path_join(file_name))
assert_true(_contains_only_primitive_values(decoded))
var restored := store.load_archive()
assert_not_null(restored)
assert_eq(restored.checksum(), second.checksum())
assert_eq(restored.restore_service().checksum(), second.restore_service().checksum())
var load_telemetry := store.get_last_telemetry()
assert_true(load_telemetry["succeeded"])
assert_eq(load_telemetry["generation_id"], latest_generation)
assert_false(load_telemetry["recovered_generation"])
assert_gt(load_telemetry["bytes_read"], 0)
assert_gte(load_telemetry["read_usec"], 0)
assert_gte(load_telemetry["validation_usec"], 0)
func test_interrupted_pointer_and_corrupt_latest_generation_recover_last_valid() -> void:
var store := RegionalChunkedFileStore.new(_test_directory)
var first := _fixture_archive(false)
var second := _fixture_archive(true)
assert_true(store.save(first))
var first_generation := store.get_generation_ids()[0]
var current_pointer := ProjectSettings.globalize_path(store.get_current_pointer_path())
var previous_pointer := ProjectSettings.globalize_path(
_test_directory.path_join(RegionalChunkedFileStore.PREVIOUS_POINTER_FILE_NAME)
)
assert_eq(DirAccess.rename_absolute(current_pointer, previous_pointer), OK)
var recovered_missing_pointer := store.load_archive()
assert_not_null(recovered_missing_pointer)
assert_eq(recovered_missing_pointer.checksum(), first.checksum())
assert_true(store.get_last_telemetry()["recovered_generation"])
assert_true(store.save(second))
var latest_generation := store.get_generation_ids()[-1]
var latest_path := store.get_generation_directory(latest_generation)
var manifest: Dictionary = _read_variant(
latest_path.path_join(RegionalChunkedFileStore.MANIFEST_FILE_NAME)
)
var first_chunk_file := String(manifest["chunk_files"][0]["file_name"])
_write_variant(latest_path.path_join(first_chunk_file), {"corrupt": true})
var interrupted_path := ProjectSettings.globalize_path(
store.get_generations_directory().path_join(
"temporary_generation_00000000000000000003_interrupted"
)
)
assert_eq(DirAccess.make_dir_recursive_absolute(interrupted_path), OK)
_write_variant(interrupted_path.path_join("partial.bin"), {"partial": true})
var recovered := store.load_archive()
assert_not_null(recovered)
assert_eq(recovered.checksum(), first.checksum())
assert_eq(store.get_last_telemetry()["generation_id"], first_generation)
assert_true(store.get_last_telemetry()["recovered_generation"])
assert_gte(store.get_last_telemetry()["candidates_validated"], 2)
assert_true(FileAccess.file_exists(latest_path.path_join(first_chunk_file)))
func test_partial_location_load_reads_only_selected_chunks_and_keeps_unloaded_authority() -> void:
var store := RegionalChunkedFileStore.new(_test_directory)
var archive := _fixture_archive(false)
assert_true(store.save(archive))
var saved_telemetry := store.get_last_telemetry()
var generation_id := store.get_generation_ids()[0]
var generation_path := store.get_generation_directory(generation_id)
var manifest: Dictionary = _read_variant(
generation_path.path_join(RegionalChunkedFileStore.MANIFEST_FILE_NAME)
)
var scheduler_entry := _find_chunk_file_entry(
manifest, RegionalChunkedPersistence.SCHEDULER_CHUNK_ID
)
var scheduler_path := generation_path.path_join(String(scheduler_entry["file_name"]))
_write_variant(scheduler_path, {"corrupt_unloaded_scheduler": true})
var partial := store.load_active_location_metadata(ORIGIN_LOCATION_ID)
assert_false(partial.is_empty())
assert_eq(partial["generation_id"], generation_id)
assert_eq(
partial["loaded_chunk_ids"],
[
RegionalChunkedPersistence.GLOBAL_CHUNK_ID,
"location:%s" % ORIGIN_LOCATION_ID,
]
)
assert_true(RegionalChunkedPersistence.SCHEDULER_CHUNK_ID in partial["unloaded_chunk_ids"])
assert_eq(partial["mobile_group_refs"].size(), 1)
assert_false(partial["full_generation_validated"])
assert_true(FileAccess.file_exists(scheduler_path))
var partial_telemetry := store.get_last_telemetry()
assert_true(partial_telemetry["partial_load"])
assert_eq(partial_telemetry["files_read"], 4)
assert_lt(partial_telemetry["bytes_read"], saved_telemetry["bytes_written"])
assert_null(store.load_archive())
func test_unsafe_base_and_traversal_pointer_cannot_escape_generation_root() -> void:
var archive := _fixture_archive(false)
var unsafe_relative := RegionalChunkedFileStore.new("../regional_escape")
assert_false(unsafe_relative.save(archive))
assert_true("unsafe" in unsafe_relative.get_last_error())
assert_false(RegionalChunkedFileStore.new("user://").save(archive))
assert_false(RegionalChunkedFileStore.new("res://").save(archive))
var store := RegionalChunkedFileStore.new(_test_directory)
assert_true(store.save(archive))
assert_eq(store.get_generation_directory("../outside"), "")
var malicious_pointer := {
"schema_version": RegionalChunkedFileStore.SCHEMA_VERSION,
"format_id": RegionalChunkedFileStore.FORMAT_ID,
"generation_id": "../outside",
"manifest_checksum": "0".repeat(64),
"archive_checksum": "0".repeat(64),
}
_write_variant(store.get_current_pointer_path(), malicious_pointer)
var recovered := store.load_archive()
assert_not_null(recovered)
assert_eq(recovered.checksum(), archive.checksum())
assert_true(store.get_last_telemetry()["recovered_generation"])
func test_explicit_conservative_prune_keeps_current_and_two_newest_valid_generations() -> void:
var store := RegionalChunkedFileStore.new(_test_directory)
var first := _fixture_archive(false)
var latest := _fixture_archive(true)
assert_true(store.save(first))
assert_true(store.save(latest))
assert_true(store.save(latest))
var before := store.get_generation_ids()
assert_eq(before.size(), 3)
assert_eq(store.prune_old_generations(2), 1)
assert_eq(store.get_generation_ids(), [before[1], before[2]])
assert_eq(store.get_last_telemetry()["operation"], "prune_old_generations")
assert_eq(store.get_last_telemetry()["generations_pruned"], 1)
assert_true(store.get_last_telemetry()["succeeded"])
assert_gt(store.get_last_telemetry()["files_deleted"], 0)
assert_eq(store.load_archive().checksum(), latest.checksum())
var retained := store.get_generation_ids()
assert_eq(store.prune_old_generations(1), 0)
assert_eq(store.get_generation_ids(), retained)
assert_true("at least two" in store.get_last_error())
func test_partial_load_does_not_resurrect_a_location_removed_by_newer_authority() -> void:
var store := RegionalChunkedFileStore.new(_test_directory)
var older_with_destination := _fixture_archive(false)
var latest_without_destination := _single_location_archive()
assert_true(store.save(older_with_destination))
assert_true(store.save(latest_without_destination))
assert_true(store.load_active_location_metadata(DESTINATION_LOCATION_ID).is_empty())
assert_true("does not contain" in store.get_last_error())
assert_eq(store.load_archive().checksum(), latest_without_destination.checksum())
func _fixture_archive(departed: bool) -> RegionalChunkedPersistence:
var origin_location := LocationStateRecord.create(
ORIGIN_LOCATION_ID, WORLD_ID, &"settlement", "Origin", _address(ORIGIN_LOCATION_ID)
)
var destination_location := LocationStateRecord.create(
DESTINATION_LOCATION_ID,
WORLD_ID,
&"settlement",
"Destination",
_address(DESTINATION_LOCATION_ID)
)
var origin := SettlementStateRecord.create(
ORIGIN_SETTLEMENT_ID,
ORIGIN_LOCATION_ID,
"Origin",
&"polity_origin",
1,
&"founders_origin",
0,
{&"food": 20.0}
)
var destination := SettlementStateRecord.create(
DESTINATION_SETTLEMENT_ID,
DESTINATION_LOCATION_ID,
"Destination",
&"polity_destination",
1,
&"founders_destination",
0,
{&"food": 4.0}
)
var route := RouteStateRecord.create(ROUTE_ID, ORIGIN_LOCATION_ID, DESTINATION_LOCATION_ID, 5)
var group := MobileGroupStateRecord.create_at_location(
GROUP_ID,
&"caravan",
"Caravan",
&"polity_origin",
_address(ORIGIN_LOCATION_ID),
[],
{},
{},
10.0
)
var origin_polity := PolityStateRecord.create(
&"polity_origin", "Origin polity", ORIGIN_LOCATION_ID, [ORIGIN_SETTLEMENT_ID]
)
var destination_polity := PolityStateRecord.create(
&"polity_destination",
"Destination polity",
DESTINATION_LOCATION_ID,
[DESTINATION_SETTLEMENT_ID]
)
var locations: Array[LocationStateRecord] = [destination_location, origin_location]
var settlements: Array[SettlementStateRecord] = [destination, origin]
var routes: Array[RouteStateRecord] = [route]
var groups: Array[MobileGroupStateRecord] = [group]
var persons: Array[PersonStateRecord] = []
var cohorts: Array[PopulationCohortRecord] = []
var polities: Array[PolityStateRecord] = [destination_polity, origin_polity]
var relations: Array[DiplomaticRelationRecord] = []
var world_state := RegionalWorldState.create(
WORLD_ID, locations, settlements, routes, groups, persons, cohorts, polities, relations
)
var service := RegionalCaravanService.create(
world_state, RegionalJobScheduler.new(), WorldEventStore.new()
)
if departed:
assert_true(
service.depart(
GROUP_ID,
ORIGIN_SETTLEMENT_ID,
DESTINATION_SETTLEMENT_ID,
ROUTE_ID,
{&"food": 6.0},
DEPARTURE_TICK
)
)
return RegionalChunkedPersistence.capture(service, 2)
func _single_location_archive() -> RegionalChunkedPersistence:
var location := LocationStateRecord.create(
ORIGIN_LOCATION_ID, WORLD_ID, &"settlement", "Origin", _address(ORIGIN_LOCATION_ID)
)
var settlement := SettlementStateRecord.create(
ORIGIN_SETTLEMENT_ID,
ORIGIN_LOCATION_ID,
"Origin",
&"polity_origin",
1,
&"founders_origin",
0,
{&"food": 20.0}
)
var polity := PolityStateRecord.create(
&"polity_origin", "Origin polity", ORIGIN_LOCATION_ID, [ORIGIN_SETTLEMENT_ID]
)
var locations: Array[LocationStateRecord] = [location]
var settlements: Array[SettlementStateRecord] = [settlement]
var routes: Array[RouteStateRecord] = []
var groups: Array[MobileGroupStateRecord] = []
var persons: Array[PersonStateRecord] = []
var cohorts: Array[PopulationCohortRecord] = []
var polities: Array[PolityStateRecord] = [polity]
var relations: Array[DiplomaticRelationRecord] = []
var world_state := RegionalWorldState.create(
WORLD_ID, locations, settlements, routes, groups, persons, cohorts, polities, relations
)
var service := RegionalCaravanService.create(
world_state, RegionalJobScheduler.new(), WorldEventStore.new()
)
return RegionalChunkedPersistence.capture(service, 2)
func _address(location_id: StringName) -> SpatialAddress:
return SpatialAddress.create(WORLD_ID, location_id, Vector3.ZERO)
func _find_chunk_file_entry(manifest: Dictionary, chunk_id: String) -> Dictionary:
for raw_entry: Variant in manifest["chunk_files"]:
if raw_entry is Dictionary and String(raw_entry.get("chunk_id", "")) == chunk_id:
return raw_entry
return {}
func _read_variant(path: String) -> Variant:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
return null
var bytes := file.get_buffer(file.get_length())
file.close()
return bytes_to_var(bytes) if not bytes.is_empty() else null
func _write_variant(path: String, value: Variant) -> void:
var file := FileAccess.open(path, FileAccess.WRITE)
assert_not_null(file)
if file == null:
return
file.store_buffer(var_to_bytes(value))
file.flush()
file.close()
func _contains_only_primitive_values(value: Variant) -> bool:
match typeof(value):
TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_STRING:
return true
TYPE_FLOAT:
return is_finite(float(value))
TYPE_ARRAY:
for item: Variant in value:
if not _contains_only_primitive_values(item):
return false
return true
TYPE_DICTIONARY:
for key: Variant in value:
if not key is String or not _contains_only_primitive_values(value[key]):
return false
return true
return false
func _remove_tree(absolute_path: String) -> void:
var directory := DirAccess.open(absolute_path)
if directory == null:
return
directory.include_hidden = true
directory.list_dir_begin()
var entry_name := directory.get_next()
while not entry_name.is_empty():
var entry_path := absolute_path.path_join(entry_name)
if directory.current_is_dir():
_remove_tree(entry_path)
else:
DirAccess.remove_absolute(entry_path)
entry_name = directory.get_next()
directory.list_dir_end()
DirAccess.remove_absolute(absolute_path)