okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
57a82be
main
1"""Exercise owner frames the daemon cannot generate, including malformed messages. 2 3The real kernel must clean up children on invalid input while preserving pending 4call races, live references and typed values without an SSH server. 5""" 6 7import json 8import select 9import struct 10import subprocess 11import sys 12import tempfile 13import time 14import unittest 15from pathlib import Path 16 17ROOT = Path (__file__ ).resolve ().parents [2 ] 18KERNEL = ROOT / "priv" / "python" / "albedo_kernel.py" 19 20sys .path .insert (0 ,str (ROOT / "test" )) 21import scratch # noqa: E402 22 23# Temporary workspaces and homes go under this run's scratch directory, which 24# is removed at exit. 25scratch .claim ("harness" ) 26MAX_FRAME = 8 * 1024 * 1024 27 28 29class OwnerChannel : 30"""A minimal owner: speak the kernel's control frames over stdio. 31 32Frames the owner is not looking for stay buffered, so looking for one kind 33never discards another (mirrors and job events interleave with replies). 34""" 35 36def __init__ (self ,modules ): 37self .workspace = tempfile .mkdtemp (prefix = "albedo-owner-" ) 38self .process = subprocess .Popen ( 39 [sys .executable ,"-u" ,str (KERNEL ),json .dumps (modules )], 40stdin = subprocess .PIPE , 41stdout = subprocess .PIPE , 42stderr = subprocess .PIPE , 43bufsize = 0 , 44cwd = self .workspace , 45 ) 46self .buffered = [] 47 48def close (self ): 49if self .process .poll ()is None : 50self .send ({"type" :"shutdown" }) 51self .process .wait (timeout = 10 ) 52assert self .process .stdin is not None 53assert self .process .stdout is not None 54assert self .process .stderr is not None 55self .process .stdin .close () 56self .process .stdout .close () 57self .process .stderr .close () 58self .buffered .clear () 59 60def send (self ,message ): 61data = json .dumps (message ).encode () 62assert self .process .stdin is not None 63self .process .stdin .write (struct .pack (">I" ,len (data ))+ data ) 64self .process .stdin .flush () 65 66def recv (self ): 67assert self .process .stdout is not None 68if not select .select ([self .process .stdout ], [], [],10 )[0 ]: 69raise AssertionError ("kernel did not send a frame within 10 seconds" ) 70header = self .read_exact (4 ) 71size = struct .unpack (">I" ,header )[0 ] 72assert size <= MAX_FRAME ,"control frame exceeds the ceiling" 73return json .loads (self .read_exact (size )) 74 75def read_exact (self ,size ): 76assert self .process .stdout is not None 77data = bytearray () 78while len (data )< size : 79chunk = self .process .stdout .read (size - len (data )) 80if not chunk : 81raise AssertionError ("kernel closed the channel during a frame" ) 82data .extend (chunk ) 83return bytes (data ) 84 85def wait_for (self ,predicate ,timeout = 10.0 ): 86deadline = time .monotonic ()+ timeout 87while time .monotonic ()< deadline : 88for index ,frame in enumerate (self .buffered ): 89if predicate (frame ): 90return self .buffered .pop (index ) 91self .buffered .append (self .recv ()) 92raise AssertionError ( 93f"no matching frame; buffered: { [ f . get ( 'type' ) for f in self . buffered ] } " 94 ) 95 96def recv_reply (self ,call_id ): 97return self .wait_for ( 98lambda f : ( 99f .get ("id" )== call_id and f ["type" ]in ("invoked" ,"introspected" ) 100 ) 101 ) 102 103def invoke (self ,call_id ,** frame ): 104self .send ({"type" :"invoke" ,"id" :call_id ,** frame }) 105return self .recv_reply (call_id ) 106 107 108def python (source ): 109"""run() arguments for a few steps of Python in a child interpreter.""" 110return [sys .executable ,"-c" ,source ] 111 112 113class KernelInvokeTest (unittest .TestCase ): 114def setUp (self ): 115self .channel = OwnerChannel (["run" ,"files" ]) 116self .addCleanup (self .channel .close ) 117ready = self .channel .recv () 118self .assertEqual (ready ["type" ],"ready" ) 119 120def test_malformed_frames_shutdown_and_clean_up_a_child (self ): 121frames = [ 122 [], 123None , 124 {}, 125 {"type" :"unknown" }, 126 {"type" :"execute" ,"id" :"x" }, 127 {"type" :"execute" ,"id" :"x" ,"code" :7 }, 128 {"type" :"execute" ,"id" :"x" ,"code" :"" ,"durable" :1 }, 129 {"type" :"execute" ,"id" :"x" ,"code" :"" ,"max_edge" :True }, 130 {"type" :"invoke" ,"id" : []}, 131 {"type" :"invoke" ,"id" :"x" ,"args" : {}}, 132 {"type" :"invoke" ,"id" :"x" ,"kwargs" : []}, 133 {"type" :"invoke" ,"id" :"x" ,"await" :1 }, 134 {"type" :"invoke" ,"id" :"x" ,"target" : {"handle" :"a" ,"pending" :"b" }}, 135 {"type" :"invoke" ,"id" :"x" ,"target" : {"handle" :1 }}, 136 {"type" :"reply" ,"id" :"x" ,"value" : []}, 137 {"type" :"reply" ,"id" :"x" ,"value" : {"ok" :1 ,"value" :None }}, 138 {"type" :"reply" ,"id" :"x" ,"value" : {"ok" :True }}, 139 {"type" :"reply" ,"id" :"x" ,"value" : {"ok" :False ,"code" :"bad" }}, 140 {"type" :"interrupt" ,"id" :"x" ,"reason" :"other" }, 141 {"type" :"restore" ,"id" :"x" ,"path" :None }, 142 {"type" :"release" ,"handle" : []}, 143 {"type" :"job_stop" ,"id" : []}, 144 ] 145payloads = [json .dumps (frame ).encode ()for frame in frames ]+ [b"{bad json" ] 146wire_frames = [struct .pack (">I" ,len (data ))+ data for data in payloads ] 147wire_frames .append (struct .pack (">I" ,MAX_FRAME + 1 )) 148for wire in wire_frames : 149with self .subTest (wire = wire [:160 ]): 150channel = OwnerChannel (["run" ]) 151try : 152self .assertEqual (channel .recv ()["type" ],"ready" ) 153channel .invoke ( 154"child" ,name = "run" ,args = python ("import time; time.sleep(60)" ) 155 ) 156started = channel .wait_for ( 157lambda frame :frame ["type" ]== "job_start" 158 ) 159assert channel .process .stdin is not None 160channel .process .stdin .write (wire ) 161channel .process .stdin .flush () 162channel .process .wait (timeout = 5 ) 163# Cleanup reaps the child before the kernel kills its own group. 164self .assertFalse (Path (f"/proc/ { started [ 'pgid' ] } " ).exists ()) 165finally : 166channel .close () 167 168def test_extra_fields_and_missing_methods_keep_the_channel_open (self ): 169reply = self .channel .invoke ( 170"extra" ,name = "files.read" ,args = ["missing" ],extra = True 171 ) 172self .assertFalse (reply ["ok" ]) 173self .assertFalse (self .channel .invoke ("missing" )["ok" ]) 174self .channel .send ({"type" :"introspect" ,"id" :"still-open" ,"extra" : []}) 175self .assertIn ("run" ,self .channel .recv_reply ("still-open" )["names" ]) 176 177def test_pending_targets_resolve_calls_raced_ahead_of_their_reply (self ): 178self .channel .send ( 179 { 180"type" :"invoke" , 181"id" :"r1" , 182"name" :"run" , 183"args" :python ( 184"import time; print('raced', flush=True); time.sleep(0.3); print('done')" 185 ), 186 } 187 ) 188# tail() is called before the run reply can exist; the pending target waits 189raced = self .channel .invoke ("r2" ,target = {"pending" :"r1" },name = "tail" ) 190self .assertTrue (raced ["ok" ]) 191self .assertEqual (raced ["value" ],"" ) 192reply = self .channel .recv_reply ("r1" ) 193self .assertTrue (reply ["ok" ]) 194done = self .channel .invoke ( 195"r3" ,target = {"handle" :reply ["handle" ]},** {"await" :True } 196 ) 197self .assertEqual (done ["state" ]["tail" ],"raced\ndone\n" ) 198 199def test_interrupt_cancels_an_await_without_losing_the_reference (self ): 200reply = self .channel .invoke ( 201"job" , 202name = "run" , 203args = python ("import time; time.sleep(1); print('finished')" ), 204 ) 205handle = reply ["handle" ] 206self .channel .send ( 207 { 208"type" :"invoke" , 209"id" :"wait" , 210"target" : {"handle" :handle }, 211"await" :True , 212 } 213 ) 214# A mirror arrives after the await task has had a loop turn to start. 215self .channel .wait_for (lambda frame :frame ["type" ]== "mirror" ) 216self .channel .send ({"type" :"interrupt" ,"id" :"wait" }) 217cancelled = self .channel .recv_reply ("wait" ) 218self .assertFalse (cancelled ["ok" ]) 219self .assertTrue (cancelled ["cancelled" ]) 220finished = self .channel .invoke ( 221"finish" ,target = {"handle" :handle },** {"await" :True } 222 ) 223self .assertTrue (finished ["ok" ]) 224self .assertEqual (finished ["state" ]["tail" ],"finished\n" ) 225 226def test_interrupt_for_a_queued_cell_lands_when_it_starts (self ): 227self .channel .send ( 228 {"type" :"execute" ,"id" :"busy" ,"code" :"import time; time.sleep(0.5)" } 229 ) 230self .channel .send ({"type" :"execute" ,"id" :"queued" ,"code" :"ran = True" }) 231self .channel .send ({"type" :"interrupt" ,"id" :"queued" ,"reason" :"deadline" }) 232busy = self .channel .wait_for (lambda f :f ["type" ]== "done" ) 233self .assertEqual ((busy ["id" ],busy ["status" ]), ("busy" ,"ok" )) 234queued = self .channel .wait_for (lambda f :f ["type" ]== "done" ) 235self .assertEqual ((queued ["id" ],queued ["status" ]), ("queued" ,"interrupted" )) 236self .assertIn ("deadline exceeded" ,queued ["output" ]) 237self .channel .send ({"type" :"execute" ,"id" :"check" ,"code" :"'ran' in dir()" }) 238check = self .channel .wait_for (lambda f :f ["type" ]== "done" ) 239self .assertEqual (check ["value" ],"False" ) 240 241def test_a_failing_pending_call_propagates_its_error (self ): 242self .channel .send ({"type" :"invoke" ,"id" :"f1" ,"name" :"nope" }) 243failure = self .channel .invoke ("f2" ,target = {"pending" :"f1" },name = "tail" ) 244self .assertFalse (failure ["ok" ]) 245self .assertEqual (failure ["error" ]["ename" ],"LookupError" ) 246 247def test_typed_composites_carry_a_reference_and_their_value (self ): 248Path (self .channel .workspace ,"note.txt" ).write_text ( 249"needle one\nplain\nneedle two\n" 250 ) 251reply = self .channel .invoke ("i1" ,name = "files.find" ,args = ["needle" ,"." ]) 252self .assertTrue (reply ["ok" ]) 253self .assertIn ("handle" ,reply ) 254value = reply ["value" ] 255self .assertEqual (value ["__list__" ],"albedo_plugins.files.Rows" ) 256self .assertEqual (value ["items" ][0 ]["__class__" ],"albedo_plugins.files.Match" ) 257self .assertEqual (value ["items" ][0 ]["fields" ]["line" ],1 ) 258self .assertEqual (len (value ["items" ]),2 ) 259 260def test_reference_arguments_resolve_back_to_live_objects (self ): 261reply = self .channel .invoke ("i1" ,name = "run" ,args = ["echo" ,"ref-roundtrip" ]) 262handle = reply ["handle" ] 263bad = self .channel .invoke ( 264"i2" ,name = "files.read" ,args = [{"__ref__" :"not-a-handle" }] 265 ) 266self .assertFalse (bad ["ok" ]) 267self .assertEqual (bad ["error" ]["ename" ],"LookupError" ) 268# A live reference resolves to the object itself; the failure that follows 269# is the call's own, proving the marker never leaked through as a dict. 270answered = self .channel .invoke ( 271"i3" ,name = "files.read" ,args = [{"__ref__" :handle }] 272 ) 273self .assertFalse (answered ["ok" ]) 274self .assertNotEqual (answered ["error" ]["ename" ],"LookupError" ) 275 276def test_release_forgets_the_reference (self ): 277reply = self .channel .invoke ("i1" ,name = "run" ,args = ["true" ]) 278handle = reply ["handle" ] 279self .channel .send ({"type" :"release" ,"handle" :handle }) 280gone = self .channel .invoke ("i2" ,target = {"handle" :handle },name = "poll" ) 281self .assertFalse (gone ["ok" ]) 282 283def test_job_stop_unknown_job_answers_not_found (self ): 284self .channel .send ({"type" :"job_stop" ,"id" :"unknown-job" }) 285reply = self .channel .wait_for (lambda f :f ["type" ]== "job_stop" ) 286self .assertEqual ( 287reply , 288 { 289"type" :"job_stop" , 290"id" :"unknown-job" , 291"ok" :False , 292"error" :"job not found" , 293 }, 294 ) 295 296def test_job_stop_running_job_stops_and_confirms (self ): 297self .channel .invoke ( 298"child" ,name = "run" ,args = python ("import time; time.sleep(60)" ) 299 ) 300started = self .channel .wait_for (lambda f :f ["type" ]== "job_start" ) 301job_id = started ["id" ] 302self .channel .send ({"type" :"job_stop" ,"id" :job_id }) 303reply = self .channel .wait_for (lambda f :f ["type" ]== "job_stop" ) 304self .assertEqual (reply , {"type" :"job_stop" ,"id" :job_id ,"ok" :True }) 305# The job also announces its exit 306ended = self .channel .wait_for ( 307lambda f :f ["type" ]== "job" and f ["id" ]== job_id 308 ) 309self .assertTrue (ended ["cleanup" ]["gone" ]) 310 311 312if __name__ == "__main__" : 313unittest .main ()