okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
6c003b5
main
1"""Watcher ownership and completion/read races need GC and busy-host control that daemon E2E cannot reliably provide.""" 2 3from pathlib import Path 4import sys 5import textwrap 6import unittest 7 8sys .path .insert (0 ,str (Path (__file__ ).resolve ().parent )) 9from job_wake_test import Owner # noqa: E402 10 11 12class JobWatchersTest (unittest .TestCase ): 13def setUp (self ): 14self .owner = Owner (["run" ,"files" ]) 15self .addCleanup (self .owner .close ) 16self .owner .wait_for (lambda frame :frame .get ("type" )== "ready" ) 17self .cells = 0 18self .cell ( 19"import __main__ as kernel, asyncio, gc, weakref\n" 20"from albedo_plugins import run as runner, files as file_plugin" 21 ) 22 23def cell (self ,code ): 24self .cells += 1 25cell_id = f"c { self . cells } " 26self .owner .send ( 27 {"type" :"execute" ,"id" :cell_id ,"code" :textwrap .dedent (code )} 28 ) 29done = self .owner .wait_for ( 30lambda frame :frame .get ("type" )== "done" and frame .get ("id" )== cell_id 31 ) 32self .assertEqual (done ["status" ],"ok" ,done ) 33return done 34 35def test_an_early_read_does_not_consume_the_completion_watcher (self ): 36self .cell (""" 37job = run('echo', 'result') 38output.read(job.id) 39await asyncio.shield(job.task) 40assert not job._read and not job._awaited 41assert output.read(job.id).strip() == 'result' 42assert job._read 43""" ) 44 45def test_forgotten_jobs_and_internal_file_commands_release_watchers (self ): 46self .cell (""" 47job = run('true') 48await job 49reference = weakref.ref(job) 50runner.forget(job) 51del job 52await file_plugin._run('printf internal') 53await asyncio.sleep(0) 54gc.collect() 55assert reference() is None 56assert not jobs and not kernel.READ_WATCHERS 57assert not [c for c in kernel.ARCHIVES.values() if c.kind == 'job'] 58""" ) 59 60def test_eviction_releases_jobs_but_preserves_held_handles (self ): 61self .cell (""" 62held = run('echo', 'held output') 63await held 64disposable = run('true') 65await disposable 66reference = weakref.ref(disposable) 67capture = disposable.capture 68del disposable 69for index in range(runner.RETAINED_LIMIT): 70# Keep this output past job eviction to catch strong watcher ownership. 71kernel.remember(capture) 72await run('true') 73await asyncio.sleep(0) 74gc.collect() 75assert reference() is None and capture.id in kernel.ARCHIVES 76assert capture.id not in kernel.READ_WATCHERS 77assert held.id not in jobs and held.id not in kernel.ARCHIVES 78assert held.id not in kernel.READ_WATCHERS 79assert held.tail().strip() == 'held output' and await held is held 80""" ) 81 82def test_other_callables_and_replacement_watchers_remain_usable (self ): 83self .cell (""" 84capture = kernel.background_capture('callables') 85reads = [] 86kernel.watch_output(capture.id, lambda: reads.append('function')) 87gc.collect() 88output.read(capture.id) 89class Callback: 90def __call__(self): 91reads.append('callable') 92callback = Callback() 93kernel.watch_output(capture.id, callback) 94output.read(capture.id) 95kernel.watch_output(capture.id, callback.__call__) 96kernel.watch_output(capture.id, lambda: reads.append('replacement')) 97del callback 98gc.collect() 99output.read(capture.id) 100assert reads == ['function', 'callable', 'replacement'] 101kernel.forget_output(capture.id) 102assert capture.id not in kernel.READ_WATCHERS 103""" ) 104 105 106if __name__ == "__main__" : 107unittest .main ()