okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
31ff834
main
1- module ( albedo_runtime_test_support ). 2- export ([ temporary_database / 0 , cleanup / 1 , temporary_workspace / 0 , cleanup_workspace / 1 , 3kernel_memory / 1 , catalog_does_not_block / 3 , catalog_forget_while_blocked / 3 , 4hold_preparation / 2 , capture_during_preparation / 3 ]). 5temporary_workspace () -> 6Path = filename : join ( os : getenv ( "TMPDIR" , "/tmp" ), 7"runtime-workspace-" ++ binary_to_list ( albedo_native : new_id ())), 8ok = file : make_dir ( Path ), 9unicode : characters_to_binary ( Path ). 10cleanup_workspace ( Path ) -> 11_ = file : del_dir_r ( Path ), 12nil . 13temporary_database () -> 14unicode : characters_to_binary ( filename : join ( os : getenv ( "TMPDIR" , "/tmp" ), "albedo-" ++ binary_to_list ( albedo_native : new_id ()) ++ ".sqlite" )). 15cleanup ( Path ) -> 16lists : foreach ( fun ( Suffix ) -> file : delete (<< Path / binary , Suffix / binary >>) end , 17[<<>>, << "-wal" >>, << "-shm" >>]), 18_ = file : del_dir_r (<< Path / binary , ".backup" >>), 19nil . 20 21%% A barrier followed by GC measures retained ownership, not transient boot garbage. 22kernel_memory ({ session , _ , _ , Kernel , _ , _ , _ , _ , _ }) -> 23_ = albedo_python : events ( Kernel ), 24true = erlang : garbage_collect ( Kernel ), 25{ memory , Bytes } = erlang : process_info ( Kernel , memory ), 26Bytes . 27 28%% A catalog worker waiting on the real ledger must not own the runtime loop. 29catalog_does_not_block ( Runtime , Home , Id ) -> 30Store = 'albedo@harness@runtime' : ledger ( Runtime ), 31Owner = 'albedo@daemon@store' : owner ( Store ), 32erlang : suspend_process ( Owner ), 33Parent = self (), 34Worker = spawn_link ( fun () -> 35Result = 'albedo@harness@runtime' : observe_catalog ( Runtime , Home , Id ), 36Parent ! { catalog_complete , self (), Result } 37end ), 38try 39await_store_query ( Owner , erlang : monotonic_time ( millisecond ) + 3000 ), 40Reader = spawn_link ( fun () -> 41Loaded = 'albedo@harness@runtime' : loaded_sessions ( Runtime ), 42Parent ! { loaded_complete , self (), Loaded } 43end ), 44Responsive = receive 45{ loaded_complete , Reader , _ } -> true 46after 1000 -> false 47end , 48erlang : resume_process ( Owner ), 49Completed = receive 50{ catalog_complete , Worker , { ok , _ }} -> true ; 51{ catalog_complete , Worker , { error , _ }} -> false 52after 10000 -> false 53end , 54{ Responsive , Completed } 55after 56resume ( Owner ) 57end . 58 59%% The catalog began against a prepared composition that is removed while its 60%% discovery is held. Its eventual response must observe that newer ownership. 61catalog_forget_while_blocked ( Runtime , Home , Id ) -> 62Store = 'albedo@harness@runtime' : ledger ( Runtime ), 63Owner = 'albedo@daemon@store' : owner ( Store ), 64erlang : suspend_process ( Owner ), 65Parent = self (), 66Worker = spawn_link ( fun () -> 67Result = 'albedo@harness@runtime' : observe_catalog ( Runtime , Home , Id ), 68Parent ! { catalog_complete , self (), Result } 69end ), 70try 71await_store_query ( Owner , erlang : monotonic_time ( millisecond ) + 3000 ), 72'albedo@harness@runtime' : forget_session ( Runtime , Id ), 73erlang : resume_process ( Owner ), 74receive 75{ catalog_complete , Worker , Result } -> Result 76after 10000 -> error ( catalog_did_not_complete ) 77end 78after 79resume ( Owner ) 80end . 81 82%% A body that already resumed Owner leaves nothing to resume. 83resume ( Owner ) -> 84try erlang : resume_process ( Owner ) catch error : badarg -> false end . 85 86await_store_query ( Owner , Deadline ) -> 87case process_info ( Owner , message_queue_len ) of 88{ message_queue_len , N } when N > 0 -> ok ; 89_ -> 90case erlang : monotonic_time ( millisecond ) < Deadline of 91true -> receive after 10 -> ok end , await_store_query ( Owner , Deadline ); 92false -> error ( catalog_did_not_query_store ) 93end 94end . 95 96%% The real composition worker calls this context loader. Only an explicit 97%% release lets it finish; no process state or runtime messages are injected. 98hold_preparation ( Parent , Workspace ) -> 99Parent ! { preparation_held , self (), Workspace }, 100receive 101release_preparation -> { ok , <<>>} 102after 20000 -> { error , << "preparation release was not received" >>} 103end . 104 105capture_during_preparation ( Runtime , Session , Requests ) -> 106Parent = self (), 107%% Fixture startup follows peek_commands' deadline, not a latency assertion. 108Deadline = erlang : monotonic_time ( millisecond ) + 15000 , 109%% Inspect only the public runtime handle's subject, never actor State. 110{ ok , Owner } = 'gleam@erlang@process' : subject_owner ( element ( 2 , Runtime )), 111Callers = [ spawn ( fun () -> 112erlang : trace ( self (), true , [ send , { tracer , Parent }]), 113Result = try 'albedo@harness@runtime' : peek_commands ( Runtime , Id , Workspace ) 114catch Class : Reason -> { error , { preparation_caller_failed , Class , Reason }} 115end , 116erlang : trace ( self (), false , [ send ]), 117Parent ! { preparation_complete , self (), Result } 118end ) || { Id , Workspace } <- Requests ], 119try 120await_preparation_requests ( Callers , Owner , Deadline ), 121%% All actual request sends precede this public owner roundtrip. 122_ = 'albedo@harness@runtime' : loaded_sessions ( Runtime ), 123await_preparation_hold ( Deadline ), 124Captured = 'albedo@daemon@session' : capture ( Session ), 125Completed = release_preparations ( Callers , true ), 126{ Captured , Completed } 127catch Class : Reason : Stack -> 128%% Failure during setup must also release callbacks and drain callers. 129_ = release_preparations ( Callers , true ), 130erlang : raise ( Class , Reason , Stack ) 131end . 132 133await_preparation_requests ([], _Owner , _Deadline ) -> ok ; 134await_preparation_requests ( Callers , Owner , Deadline ) -> 135receive 136{ trace , Caller , send , _ , Owner } -> 137await_preparation_requests ( lists : delete ( Caller , Callers ), Owner , Deadline ); 138{ preparation_complete , _ , _ } = Completion -> 139self () ! Completion , 140error ({ preparation_completed_before_admission , Completion }) 141after max ( 0 , Deadline - erlang : monotonic_time ( millisecond )) -> 142error ( preparation_request_was_not_sent ) 143end . 144 145await_preparation_hold ( Deadline ) -> 146receive 147{ preparation_held , _ , _ } = Held -> 148%% Retain the hold so cleanup releases it after synchronous capture. 149self () ! Held ; 150{ preparation_complete , _ , _ } = Completion -> 151self () ! Completion , 152error ({ preparation_completed_before_context_hold , Completion }) 153after max ( 0 , Deadline - erlang : monotonic_time ( millisecond )) -> 154error ( preparation_context_did_not_start_before_call_deadline ) 155end . 156 157release_preparations ([], Completed ) -> Completed ; 158release_preparations ( Callers , Completed ) -> 159receive 160{ preparation_held , Worker , _ } -> 161Worker ! release_preparation , 162release_preparations ( Callers , Completed ); 163{ preparation_complete , Caller , Result } -> 164Successful = case Result of { ok , _ } -> true ; _ -> false end , 165release_preparations ( lists : delete ( Caller , Callers ), Completed andalso Successful ); 166{ trace , _ , send , _ , _ } -> release_preparations ( Callers , Completed ) 167after 10000 -> false 168end .