from __future__ import annotations import asyncio import os import sys import unittest SRC = os.path.join(os.path.dirname(__file__), "..", "src") if SRC not in sys.path: sys.path.insert(0, SRC) import remote from remote import RemoteExecutionError, RemoteHandle, RemoteKernelHandle class RemoteKernelHandleTests(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): env = { **os.environ, "PYTHONPATH": SRC + os.pathsep + os.environ.get("PYTHONPATH", ""), } self.proc = await asyncio.create_subprocess_exec( sys.executable, "-m", "rlm.repl", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env, ) assert self.proc.stdout is not None ready_line = await self.proc.stdout.readline() self.assertTrue(ready_line.startswith(b'{"event":"ready"')) self.handle = RemoteKernelHandle("localhost", self.proc, sys.executable, "/tmp") async def asyncTearDown(self): await self.handle.close() async def test_imports_and_call_lambda(self): await self.handle.imports("math") res = await self.handle.call(lambda: math.sqrt(144)) self.assertEqual(res, 12.0) async def test_chained_imports(self): res = await (await self.handle.imports("platform")).call(lambda: bool(platform.system())) self.assertTrue(res) async def test_repeated_imports_and_reload(self): await self.handle.imports("math") self.assertIn("math", self.handle._imported_modules) # Second import is a client-side no-op await self.handle.imports("math") res = await self.handle.call(lambda: math.pi > 3.0) self.assertTrue(res) # Explicit reload forces re-execution await self.handle.imports("math", reload=True) res2 = await self.handle.call(lambda: math.pi > 3.0) self.assertTrue(res2) async def test_call_with_args_and_kwargs(self): def multiply_and_add(x, y, offset=0): return x * y + offset res = await self.handle.call(multiply_and_add, 6, 7, offset=5) self.assertEqual(res, 47) async def test_call_async_function(self): async def async_sum(a, b): await asyncio.sleep(0.01) return a + b res = await self.handle.call(async_sum, 15, 27) self.assertEqual(res, 42) async def test_call_exception_handling(self): with self.assertRaises(RemoteExecutionError) as ctx: await self.handle.call(lambda: 1 / 0) self.assertIn("ZeroDivisionError", str(ctx.exception)) async def test_handle_liveness_and_poll(self): async def slow_work(): await asyncio.sleep(0.05) return "done!" h = self.handle.call(slow_work) self.assertIsInstance(h, RemoteHandle) self.assertTrue(h.running) self.assertIsNone(h.poll()) res = await h self.assertEqual(res, "done!") self.assertFalse(h.running) self.assertEqual(h.poll(), "done!") async def test_handle_output_streaming(self): def print_and_return(): print("step 1: started") print("step 2: finished") return 123 h = self.handle.call(print_and_return) res = await h self.assertEqual(res, 123) self.assertIn("step 1: started", h.text()) self.assertIn("step 2: finished", h.text()) self.assertIn("step 1: started", h.text()) self.assertIn("step 2", h.tail(1)) async def test_first_class_remote_bash_handle(self): h = self.handle.bash("echo 'first class remote bash'") self.assertIsInstance(h, RemoteHandle) res = await h self.assertEqual(res.exit_code, 0) self.assertIn("first class remote bash", res.text()) async def test_first_class_remote_bash_handle_with_input(self): h = self.handle.bash("cat", input="remote stdin string") self.assertIsInstance(h, RemoteHandle) res = await h self.assertEqual(res.exit_code, 0) self.assertEqual(res.text(), "remote stdin string") async def test_first_class_remote_bash_handle_with_bash_handle_input(self): from rlm.bash import bash as local_bash local_h = await local_bash("echo 'piped from local handle'") h = self.handle.bash("cat", input=local_h) res = await h self.assertEqual(res.exit_code, 0) self.assertIn("piped from local handle", res.text()) async def test_async_context_manager(self): async with self.handle as rem: await rem.imports("sys") res = await rem.call(lambda: sys.platform) self.assertIsInstance(res, str) self.assertTrue(self.handle.closed) if __name__ == "__main__": unittest.main()