2026-09-20 22:14:47 +03:00
extends SceneTree
2026-09-30 08:08:27 +03:00
const E = preload ( "res://world/owniverse/math/exact.gd" )
2026-09-20 22:14:47 +03:00
var failures : int = 0
var checks : int = 0
var completions : Array = []
func _initialize () -> void :
call_deferred ( "run" )
func check ( value : bool , message : String ) -> void :
2026-09-30 08:08:27 +03:00
checks = E . add ( checks , 1 )
if ( not value ):
failures = E . add ( failures , 1 )
2026-09-20 22:14:47 +03:00
push_error ( message )
func run () -> void :
var manager = load ( "res://autoload/save_manager.gd" ) . new ()
2026-09-30 08:08:27 +03:00
var directory : String = ( "/tmp/owniverse-async- %s - %s " % [ OS . get_process_id (), Time . get_ticks_usec ()])
2026-09-20 22:14:47 +03:00
DirAccess . make_dir_recursive_absolute ( directory )
manager . save_directory = directory
root . add_child ( manager )
2026-09-30 08:08:27 +03:00
manager . save_completed . connect ( func ( filename , success , error ) : completions . append ([ filename , success , error ]))
2026-09-20 22:14:47 +03:00
var world = load ( "res://world/owniverse/owniverse.gd" ) . new ()
2026-09-30 08:08:27 +03:00
world . state . energy = E . subtract ( world . state . energy , 1 )
world . state . time = preload ( "res://world/owniverse/simulation_time.gd" ) . advance ({ "datetime" : E . of ( "1e27" )}, 0.000000001 , 1 )
2026-09-20 22:14:47 +03:00
var data : Dictionary = { "state" : world . state , "ui" : { "current_page" : 1 }}
2026-09-30 08:08:27 +03:00
var initial_energy : Variant = world . state . energy
2026-09-20 22:14:47 +03:00
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" )
2026-09-30 08:08:27 +03:00
data . state . energy = 99
2026-09-20 22:14:47 +03:00
data . ui . current_page = 2
var frames : int = 0
2026-09-30 08:08:27 +03:00
var deadline : int = E . add ( Time . get_ticks_msec (), 10000 )
while ( manager . is_saving () and E . lt ( Time . get_ticks_msec (), deadline )):
frames = E . add ( frames , 1 )
2026-09-20 22:14:47 +03:00
await process_frame
2026-09-30 08:08:27 +03:00
check ((( not manager . is_saving ()) and E . gt ( frames , 0 )), "Frames continue while save is pending" )
2026-09-20 22:14:47 +03:00
var saved : Dictionary = manager . load_data ( "snapshot" , "test-password" )
2026-09-30 08:08:27 +03:00
check (( E . eq ( saved . state . energy , initial_energy ) and E . eq ( saved . ui . current_page , 1 )), "Worker owns a deep snapshot" )
check ( E . eq ( saved . state . time , world . state . time ), "Encrypted save preserves exact ticks at huge age" )
2026-09-20 22:14:47 +03:00
var plain : Dictionary = JSON . parse_string ( FileAccess . get_file_as_string ( directory . path_join ( "snapshot.adventure.json" )))
2026-09-30 08:08:27 +03:00
check ( E . eq ( E . decode ( plain . state . energy ), initial_energy ), "Diagnostic JSON uses same snapshot" )
check ( E . eq ( E . decode ( plain . state . time ), saved . state . time ), "Diagnostic JSON preserves fractional time" )
check (( E . eq ( completions . size (), 1 ) and completions [ 0 ][ 1 ]), "Completion reports successful write" )
2026-09-20 22:14:47 +03:00
completions . clear ()
2026-09-30 08:08:27 +03:00
data . state . energy = 1
2026-09-20 22:14:47 +03:00
manager . request_save ( "latest" , "test-password" , data )
2026-09-30 08:08:27 +03:00
data . state . energy = 2
2026-09-20 22:14:47 +03:00
manager . request_save ( "latest" , "test-password" , data )
2026-09-30 08:08:27 +03:00
data . state . energy = 3
2026-09-20 22:14:47 +03:00
manager . request_save ( "latest" , "test-password" , data )
2026-09-30 08:08:27 +03:00
data . state . energy = 4
2026-09-20 22:14:47 +03:00
manager . flush_saves ()
2026-09-30 08:08:27 +03:00
check ( E . eq ( completions . size (), 2 ), "Repeated requests coalesce behind active write" )
check ( E . eq ( manager . load_data ( "latest" , "test-password" ) . state . energy , 3 ), "Last queued snapshot wins" )
2026-09-20 22:14:47 +03:00
2026-10-01 23:43:34 +03:00
var pending_clock : Dictionary = preload ( "res://world/owniverse/simulation_time.gd" ) . advance ( world . state . time , 0.01 , 1 )
data . simulation = { "requested_clock" : pending_clock , "active_clock" : pending_clock , "speed" : 1 , "productions" : [{ "id" : "H" , "amount" : 7 , "resource" : true }]}
manager . request_save ( "checkpoint" , "test-password" , data )
manager . flush_saves ()
check ( E . eq ( manager . load_data ( "checkpoint" , "test-password" ) . simulation , data . simulation ), "Encrypted save preserves requested time and queued production" )
var malformed : Dictionary = manager . _snapshot ( "probe" , "test-password" , data ) . payload
malformed . simulation . productions [ 0 ] . id = "missing-entity"
check ( manager . validate_data ( malformed ) . contains ( "queued production" ), "Unknown queued production is rejected before resume" )
malformed = manager . _snapshot ( "probe" , "test-password" , data ) . payload
malformed . simulation . requested_clock . year = - 1
check ( manager . validate_data ( malformed ) . contains ( "simulation clock" ), "Invalid requested clock is rejected before resume" )
data . erase ( "simulation" )
2026-09-20 22:14:47 +03:00
var incompatible_path : String = directory . path_join ( "incompatible.adventure" )
var incompatible = FileAccess . open_encrypted_with_pass ( incompatible_path , FileAccess . WRITE , "test-password" )
2026-09-30 08:08:27 +03:00
incompatible . store_var ({ "format_version" : E . neg ( 1 ), "state" : { "unknown" : true }}, false )
2026-09-20 22:14:47 +03:00
incompatible . close ()
var original : PackedByteArray = FileAccess . get_file_as_bytes ( incompatible_path )
2026-09-30 08:08:27 +03:00
var previous_archive = FileAccess . open ( E . add ( incompatible_path , ".incompatible.bak" ), FileAccess . WRITE )
2026-09-20 22:14:47 +03:00
previous_archive . store_string ( "keep previous backup" )
previous_archive . close ()
manager . blocked_saves . incompatible = true
2026-09-30 08:08:27 +03:00
check (( manager . load_data ( "incompatible" , "test-password" ) . is_empty () and E . eq ( 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 ( E . eq ( FileAccess . get_file_as_bytes ( E . add ( incompatible_path , ".incompatible.bak.1" )), original ), "Unsupported save archived without losing bytes" )
check ( E . eq ( FileAccess . get_file_as_string ( E . add ( incompatible_path , ".incompatible.bak" )), "keep previous backup" ), "Earlier archive preserved" )
2026-09-20 22:14:47 +03:00
check ( manager . request_save ( "incompatible" , "test-password" , data ), "New session can queue a save" )
manager . flush_saves ()
2026-09-30 08:08:27 +03:00
check ( E . eq ( manager . load_data ( "incompatible" , "test-password" ) . state , data . state ), "New format saves and reloads after incompatible file" )
2026-09-20 22:14:47 +03:00
2026-10-03 11:06:52 +03:00
var readable_path : String = directory . path_join ( "readable-incompatible.adventure" )
var readable = FileAccess . open_encrypted_with_pass ( readable_path , FileAccess . WRITE , "test-password" )
readable . store_var ( manager . _snapshot ( "readable-incompatible" , "test-password" , data ) . payload , false )
readable . close ()
manager . blocked_saves [ "readable-incompatible" ] = true
check ( manager . archive_incompatible_save ( "readable-incompatible" ), "Readable incompatible save is archived" )
check (( not FileAccess . file_exists ( readable_path )) and FileAccess . file_exists ( E . add ( readable_path , ".incompatible.bak" )), "Archived save no longer occupies the active path" )
check ( not manager . blocked_saves . has ( "readable-incompatible" ), "Archiving an incompatible save permits a fresh save" )
2026-09-20 22:14:47 +03:00
# Expected failures below must leave the last good encrypted file intact.
2026-09-30 08:08:27 +03:00
data . state . energy = E . neg ( 1 )
2026-09-20 22:14:47 +03:00
manager . request_save ( "latest" , "test-password" , data )
manager . flush_saves ()
2026-09-30 08:08:27 +03:00
check ((( not completions . back ()[ 1 ]) and manager . last_error . contains ( "Invalid save" )), "Worker validation error reaches main thread" )
check ( E . eq ( manager . load_data ( "latest" , "test-password" ) . state . energy , 3 ), "Invalid snapshot preserves previous save" )
2026-09-20 22:14:47 +03:00
manager . blocked_saves . latest = true
2026-09-30 08:08:27 +03:00
check (( not manager . request_save ( "latest" , "test-password" , data )), "Unreadable save protection also applies to queue" )
2026-09-20 22:14:47 +03:00
# A valid snapshot exercises an actual I/O failure, not validation.
2026-09-30 08:08:27 +03:00
data . state . energy = 5
2026-09-20 22:14:47 +03:00
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 )
2026-09-30 08:08:27 +03:00
data . state . energy = 6
2026-09-20 22:14:47 +03:00
manager . request_save ( "shutdown" , "test-password" , data )
manager . free ()
var reader = load ( "res://autoload/save_manager.gd" ) . new ()
reader . save_directory = directory
2026-09-30 08:08:27 +03:00
check ( E . eq ( reader . load_data ( "shutdown" , "test-password" ) . state . energy , 6 ), "Teardown drains final pending snapshot" )
2026-09-20 22:14:47 +03:00
reader . free ()
world . free ()
2026-09-22 22:50:54 +03:00
# 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
2026-09-30 08:08:27 +03:00
stage . owniverse . state . energy = 42
2026-09-22 22:50:54 +03:00
stage . save_data ()
check ( shared_manager . is_saving (), "Stage queues a background save instead of waiting for disk" )
2026-09-30 08:08:27 +03:00
stage . owniverse . state . energy = 43
2026-09-22 22:50:54 +03:00
shared_manager . flush_saves ()
2026-09-30 08:08:27 +03:00
check ( E . eq ( shared_manager . load_data ( "owniverse" , "qwerty" ) . state . energy , 42 ), "Stage save writes an independent snapshot" )
2026-09-22 22:50:54 +03:00
stage . owniverse . free ()
stage . free ()
shared_manager . save_directory = original_directory
2026-09-20 22:14:47 +03:00
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 )
2026-09-30 08:08:27 +03:00
print (( "Async save: %d checks, %d failures (validation/block/I/O errors above are intentional)" % [ checks , failures ]))
quit (( 0 if E . eq ( failures , 0 ) else 1 ))