146 lines
9.3 KiB
GDScript
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)
|