Files
TheTree_mock/tests/s6/test_save_recovery_preservation.gd
T

146 lines
9.3 KiB
GDScript

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)