extends SceneTree # Candidate acceptance/reproduction driver. Disposable user:// fixtures only. # On the unpatched base, EXPECT failures expose the two preservation defects. var _checks := 0 var _failures := 0 var _source: Dictionary var _prefix := "" func _initialize() -> void: call_deferred("_run") func _run() -> void: _prefix = "user://tests/recovery_readonly_%d" % OS.get_process_id() var clock: Variant = root.get_node("GameClock") var state: Variant = root.get_node("GameState") clock.reset(1,6,0); clock.set_paused(true); state.reset() _source = SimulationSnapshot.capture(clock,state) _test_explicit_read_only() _test_identified_unknown_preservation() _test_current_corruption_recovery() _test_registered_legacy_migration() print("Recovery read-only candidate: %d checks, %d failures" % [_checks,_failures]) quit(_failures) func _test_explicit_read_only() -> void: for formal_present: bool in [true,false]: var path := _prefix + "/explicit_%s" % str(formal_present) _setup(path) if not formal_present: _remove_tree(ProjectSettings.globalize_path(path)) var before := _fingerprint(path) var result := SaveService.load_recovery_candidate(path,&"backup") _expect(result[&"ok"] and result[&"read_only"] and result[&"snapshot"] == _source,"explicit backup is readable with/without formal") _expect(_fingerprint(path)==before,"explicit backup selection never promotes, removes or migrates artifacts") var normal := SaveService.load_snapshot(path) _expect(normal[&"ok"],"ordinary load still reconciles stale marker or missing formal") _expect(not FileAccess.file_exists(ProjectSettings.globalize_path(path)+SaveService.SAVE_TRANSACTION_SUFFIX),"ordinary load owns marker cleanup") _cleanup(path) func _test_identified_unknown_preservation() -> void: var operations := ["load_snapshot","load_with_recovery","inspect_recovery_candidates","migrate_save","load_recovery_candidate","save_snapshot","replace_snapshot_with_backup","inspect_snapshot"] for variant: String in ["matching_future","corrupt_future","header_only_future","manifest_only_future","unsupported_legacy"]: for operation: String in operations: var path := _prefix + "/%s_%s" % [variant,operation] _setup(path) var version := 0 if variant=="unsupported_legacy" else 2 _set_version(path,version,variant!="header_only_future",variant!="manifest_only_future") if variant=="header_only_future": _write_text(ProjectSettings.globalize_path(path).path_join(SaveService.MANIFEST_FILE),"{bad json") if variant=="corrupt_future": var world_path := ProjectSettings.globalize_path(path).path_join(SaveService.WORLD_FILE) var bytes := FileAccess.get_file_as_bytes(world_path) bytes[bytes.size()-1] = bytes[bytes.size()-1] ^ 0xff _write_bytes(world_path,bytes) var before := _fingerprint(path) var result: Dictionary match operation: "load_snapshot": result=SaveService.load_snapshot(path) "load_with_recovery": result=SaveService.load_with_recovery(path) "inspect_recovery_candidates": result=SaveService.inspect_recovery_candidates(path) "migrate_save": result=SaveService.migrate_save(path,SaveMigrationRegistry.new()) "load_recovery_candidate": result=SaveService.load_recovery_candidate(path,&"backup") "save_snapshot": result=SaveService.save_snapshot(path,_source) "replace_snapshot_with_backup": var candidate := _source.duplicate(true) candidate[&"clock"][&"absolute_minute"] += 1 result=SaveService.replace_snapshot_with_backup(path,_source,candidate) "inspect_snapshot": result=SaveService.inspect_snapshot(path) _expect(not result[&"ok"],"%s rejects %s formal instead of silently loading old backup" % [operation,variant]) _expect(_fingerprint(path)==before,"%s preserves formal and every related artifact for %s" % [operation,variant]) _cleanup(path) func _test_current_corruption_recovery() -> void: var path := _prefix + "/current_corruption" _setup(path) var world_path := ProjectSettings.globalize_path(path).path_join(SaveService.WORLD_FILE) var bytes := FileAccess.get_file_as_bytes(world_path);bytes[bytes.size()-1] = bytes[bytes.size()-1] ^ 0xff _write_bytes(world_path,bytes) var result := SaveService.load_snapshot(path) _expect(result[&"ok"] and result[&"interrupted_save_reconciled"],"damaged recognized current formal retains authorized transaction recovery") _expect(result[&"snapshot"] == _source,"recognized current transaction recovers exact verified snapshot") _cleanup(path) func _test_registered_legacy_migration() -> void: var path := _prefix + "/registered_legacy" _setup(path);_set_version(path,0,true,true) var before_world := FileAccess.get_file_as_bytes(ProjectSettings.globalize_path(path).path_join(SaveService.WORLD_FILE)) var before_artifacts := _fingerprint(path) var registry := SaveMigrationRegistry.new() _expect(registry.register_step(0,&"registered_zero_to_one",Callable(self,"_migrate_legacy"))[&"ok"],"register explicit known legacy migration") var result := SaveService.migrate_save(path,registry) _expect(result[&"ok"] and result[&"backup_created"],"explicit known legacy chain remains supported") _expect(FileAccess.get_file_as_bytes(ProjectSettings.globalize_path(path+SaveService.MIGRATION_BACKUP_SUFFIX).path_join(SaveService.WORLD_FILE))==before_world,"legacy migration preserves exact original binary backup") _expect(SaveService.inspect_snapshot(path)[&"ok"],"migrated formal is current-loadable") var after_artifacts := _fingerprint(path) var owned_name := ProjectSettings.globalize_path(path).get_file() for suffix: String in [".tmp/",".bak/",SaveService.SAVE_TRANSACTION_SUFFIX]: _expect(after_artifacts.get(owned_name+suffix)==before_artifacts.get(owned_name+suffix),"registered legacy migration does not pre-clean stale normal artifact "+suffix) _cleanup(path) path = _prefix + "/registered_legacy_failure" _setup(path);_set_version(path,0,true,true) before_artifacts = _fingerprint(path) registry = SaveMigrationRegistry.new() registry.register_step(0,&"explicit_reject",Callable(self,"_reject_legacy")) result = SaveService.migrate_save(path,registry) _expect(not result[&"ok"] and result[&"code"]==&"FIXTURE_REJECTED","registered legacy callback failure is returned") _expect(_fingerprint(path)==before_artifacts,"failed legacy migration preserves source and stale normal artifacts") _cleanup(path) func _migrate_legacy(payload: Dictionary,_from: int,_to: int) -> Dictionary:return payload func _reject_legacy(_payload: Dictionary,_from: int,_to: int) -> Dictionary:return {&"ok":false,&"code":&"FIXTURE_REJECTED"} func _setup(path: String) -> void: _expect(SaveService.save_snapshot(path,_source)[&"ok"],"write disposable formal") _expect(SaveService.save_snapshot(path+".bak",_source)[&"ok"],"write disposable verified backup") _expect(SaveService.save_snapshot(path+".tmp",_source)[&"ok"],"write disposable verified candidate") _write_text(ProjectSettings.globalize_path(path)+SaveService.SAVE_TRANSACTION_SUFFIX,SaveService.SAVE_TRANSACTION_MARKER) func _set_version(path: String,version: int,manifest: bool,header: bool) -> void: var absolute := ProjectSettings.globalize_path(path) if manifest: var value: Dictionary=JSON.parse_string(FileAccess.get_file_as_string(absolute.path_join(SaveService.MANIFEST_FILE))) value["format_version"]=version;_write_text(absolute.path_join(SaveService.MANIFEST_FILE),JSON.stringify(value)) if header: var file:=FileAccess.open(absolute.path_join(SaveService.WORLD_FILE),FileAccess.READ_WRITE) file.seek(4);file.store_32(version) func _fingerprint(path: String) -> Dictionary: var absolute:=ProjectSettings.globalize_path(path);var parent:=DirAccess.open(absolute.get_base_dir());var result:={} for file: String in parent.get_files(): if file.begins_with(absolute.get_file()):result[file]=FileAccess.get_sha256(absolute.get_base_dir().path_join(file)) for directory: String in parent.get_directories(): if directory.begins_with(absolute.get_file()):result[directory+"/"]=_tree(absolute.get_base_dir().path_join(directory)) return result func _tree(path: String) -> Dictionary: var dir:=DirAccess.open(path);var result:={} for file: String in dir.get_files():result[file]=FileAccess.get_sha256(path.path_join(file)) for child: String in dir.get_directories():result[child+"/"]=_tree(path.path_join(child)) return result func _cleanup(path: String) -> void: var absolute:=ProjectSettings.globalize_path(path);var parent:=DirAccess.open(absolute.get_base_dir()) for file: String in parent.get_files(): if file.begins_with(absolute.get_file()):DirAccess.remove_absolute(absolute.get_base_dir().path_join(file)) for directory: String in parent.get_directories(): if directory.begins_with(absolute.get_file()):_remove_tree(absolute.get_base_dir().path_join(directory)) func _remove_tree(path: String) -> void: var dir:=DirAccess.open(path) if dir==null:return for file: String in dir.get_files():DirAccess.remove_absolute(path.path_join(file)) for child: String in dir.get_directories():_remove_tree(path.path_join(child)) DirAccess.remove_absolute(path) func _write_text(path: String,value: String) -> void: var file:=FileAccess.open(path,FileAccess.WRITE);file.store_string(value) func _write_bytes(path: String,value: PackedByteArray) -> void: var file:=FileAccess.open(path,FileAccess.WRITE);file.store_buffer(value) func _expect(ok: bool,label: String) -> void: _checks+=1 if not ok:_failures+=1;push_error(label)