extends SceneTree var failures: int = 0 var checks: int = 0 var completions: Array = [] func _initialize() -> void: call_deferred("run") func check(value: bool, message: String) -> void: checks += 1 if not value: failures += 1 push_error(message) func run() -> void: var manager = load("res://autoload/save_manager.gd").new() var directory: String = "/tmp/owniverse-async-%s-%s" % [OS.get_process_id(), Time.get_ticks_usec()] DirAccess.make_dir_recursive_absolute(directory) manager.save_directory = directory root.add_child(manager) manager.save_completed.connect(func(filename, success, error): completions.append([filename, success, error])) var world = load("res://world/owniverse/owniverse.gd").new() var data: Dictionary = {"state": world.state, "ui": {"current_page": 1}} var global: Node = root.get_node("Global") var old_plaintext: bool = global.save_plaintext_copy global.save_plaintext_copy = true check(manager.request_save("snapshot", "test-password", data), "Snapshot queued") data.state.energy = 99.0 data.ui.current_page = 2 var frames: int = 0 var deadline: int = Time.get_ticks_msec() + 10000 while manager.is_saving() and Time.get_ticks_msec() < deadline: frames += 1 await process_frame check(not manager.is_saving() and frames > 0, "Frames continue while save is pending") var saved: Dictionary = manager.load_data("snapshot", "test-password") check(saved.state.energy == 1e14 and saved.ui.current_page == 1, "Worker owns a deep snapshot") var plain: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("snapshot.adventure.json"))) check(plain.state.energy == 1e14, "Diagnostic JSON uses same snapshot") check(completions.size() == 1 and completions[0][1], "Completion reports successful write") completions.clear() data.state.energy = 1.0 manager.request_save("latest", "test-password", data) data.state.energy = 2.0 manager.request_save("latest", "test-password", data) data.state.energy = 3.0 manager.request_save("latest", "test-password", data) data.state.energy = 4.0 manager.flush_saves() check(completions.size() == 2, "Repeated requests coalesce behind active write") check(manager.load_data("latest", "test-password").state.energy == 3.0, "Last queued snapshot wins") var incompatible_path: String = directory.path_join("incompatible.adventure") var incompatible = FileAccess.open_encrypted_with_pass(incompatible_path, FileAccess.WRITE, "test-password") incompatible.store_var({"format_version": -1, "state": {"unknown": true}}, false) incompatible.close() var original: PackedByteArray = FileAccess.get_file_as_bytes(incompatible_path) var previous_archive = FileAccess.open(incompatible_path + ".incompatible.bak", FileAccess.WRITE) previous_archive.store_string("keep previous backup") previous_archive.close() manager.blocked_saves.incompatible = true check(manager.load_data("incompatible", "test-password").is_empty() and manager.last_error == "", "Unsupported format permits a fresh session") check(not manager.blocked_saves.has("incompatible") and not FileAccess.file_exists(incompatible_path), "Incompatible file no longer blocks saving") check(FileAccess.get_file_as_bytes(incompatible_path + ".incompatible.bak.1") == original, "Unsupported save archived without losing bytes") check(FileAccess.get_file_as_string(incompatible_path + ".incompatible.bak") == "keep previous backup", "Earlier archive preserved") check(manager.request_save("incompatible", "test-password", data), "New session can queue a save") manager.flush_saves() check(manager.load_data("incompatible", "test-password").state == data.state, "New format saves and reloads after incompatible file") # Expected failures below must leave the last good encrypted file intact. data.state.energy = -1.0 manager.request_save("latest", "test-password", data) manager.flush_saves() check(not completions.back()[1] and manager.last_error.contains("Invalid save"), "Worker validation error reaches main thread") check(manager.load_data("latest", "test-password").state.energy == 3.0, "Invalid snapshot preserves previous save") manager.blocked_saves.latest = true check(not manager.request_save("latest", "test-password", data), "Unreadable save protection also applies to queue") # A valid snapshot exercises an actual I/O failure, not validation. data.state.energy = 5.0 manager.request_save("missing/failure", "test-password", data) manager.flush_saves() check(manager.last_error.contains("Cannot write"), "I/O error reported without hanging queue") manager.request_save("shutdown", "test-password", data) data.state.energy = 6.0 manager.request_save("shutdown", "test-password", data) manager.free() var reader = load("res://autoload/save_manager.gd").new() reader.save_directory = directory check(reader.load_data("shutdown", "test-password").state.energy == 6.0, "Teardown drains final pending snapshot") reader.free() world.free() # Exercise the stage entry point too: manager tests alone do not catch a # regression back to the synchronous API in stage.gd. var shared_manager: Node = root.get_node("SaveManager") var original_directory: String = shared_manager.save_directory shared_manager.save_directory = directory var stage = load("res://stage/stage.gd").new() stage.pending_page = 2 stage.owniverse.state.energy = 42.0 stage.save_data() check(shared_manager.is_saving(), "Stage queues a background save instead of waiting for disk") stage.owniverse.state.energy = 43.0 shared_manager.flush_saves() check(shared_manager.load_data("owniverse", "qwerty").state.energy == 42.0, "Stage save writes an independent snapshot") stage.owniverse.free() stage.free() shared_manager.save_directory = original_directory global.save_plaintext_copy = old_plaintext for filename in DirAccess.get_files_at(directory): DirAccess.remove_absolute(directory.path_join(filename)) DirAccess.remove_absolute(directory) print("Async save: %d checks, %d failures (validation/block/I/O errors above are intentional)" % [checks, failures]) quit(0 if failures == 0 else 1)