diff --git a/source/loaders/py_loader/include/py_loader/py_loader_impl.h b/source/loaders/py_loader/include/py_loader/py_loader_impl.h index a33f1a4d1b..d1cabd9985 100644 --- a/source/loaders/py_loader/include/py_loader/py_loader_impl.h +++ b/source/loaders/py_loader/include/py_loader/py_loader_impl.h @@ -62,6 +62,10 @@ PY_LOADER_NO_EXPORT PyObject *py_loader_impl_capsule_new_null(void); PY_LOADER_NO_EXPORT int py_loader_impl_initialize_asyncio_module(loader_impl_py py_impl, const int host); +PY_LOADER_NO_EXPORT PyObject *py_loader_impl_get_asyncio_loop(loader_impl_py py_impl); + +PY_LOADER_NO_EXPORT PyObject *py_loader_impl_get_thread_background_module(loader_impl_py py_impl); + #ifdef __cplusplus } #endif diff --git a/source/loaders/py_loader/include/py_loader/py_loader_symbol_fallback.h b/source/loaders/py_loader/include/py_loader/py_loader_symbol_fallback.h index cc6d005757..291490f1cf 100644 --- a/source/loaders/py_loader/include/py_loader/py_loader_symbol_fallback.h +++ b/source/loaders/py_loader/include/py_loader/py_loader_symbol_fallback.h @@ -69,6 +69,7 @@ PY_LOADER_NO_EXPORT PyObject *PyExc_FileNotFoundErrorPtr(void); PY_LOADER_NO_EXPORT PyObject *PyExc_TypeErrorPtr(void); PY_LOADER_NO_EXPORT PyObject *PyExc_ValueErrorPtr(void); PY_LOADER_NO_EXPORT PyObject *PyExc_RuntimeErrorPtr(void); +PY_LOADER_NO_EXPORT PyObject *PyExc_MemoryErrorPtr(void); PY_LOADER_NO_EXPORT PyObject *Py_ReturnNone(void); PY_LOADER_NO_EXPORT PyObject *Py_ReturnFalse(void); PY_LOADER_NO_EXPORT PyObject *Py_ReturnTrue(void); diff --git a/source/loaders/py_loader/source/py_loader_impl.c b/source/loaders/py_loader/source/py_loader_impl.c index 143abaa49c..9b46effc59 100644 --- a/source/loaders/py_loader/source/py_loader_impl.c +++ b/source/loaders/py_loader/source/py_loader_impl.c @@ -1989,6 +1989,16 @@ int py_loader_impl_initialize_asyncio_module(loader_impl_py py_impl, const int h return 1; } +PyObject *py_loader_impl_get_asyncio_loop(loader_impl_py py_impl) +{ + return py_impl->asyncio_loop; +} + +PyObject *py_loader_impl_get_thread_background_module(loader_impl_py py_impl) +{ + return py_impl->thread_background_module; +} + int py_loader_impl_initialize_traceback(loader_impl impl, loader_impl_py py_impl) { (void)impl; diff --git a/source/loaders/py_loader/source/py_loader_port.c b/source/loaders/py_loader/source/py_loader_port.c index 189be21dab..ba42c20f31 100644 --- a/source/loaders/py_loader/source/py_loader_port.c +++ b/source/loaders/py_loader/source/py_loader_port.c @@ -562,8 +562,119 @@ static PyObject *py_loader_port_invoke(PyObject *self, PyObject *var_args) return result; } -// TODO -#if 0 +/* Context passed to resolve/reject callbacks for await */ +typedef struct py_loader_port_await_context_type +{ + loader_impl_py py_impl; + PyObject *future; +} py_loader_port_await_context; + +static void *py_loader_port_await_resolve(void *result, void *data) +{ + py_loader_port_await_context *ctx = (py_loader_port_await_context *)data; + loader_impl impl = loader_get_impl(py_loader_tag); + + py_loader_thread_acquire(); + + /* Convert metacall value to Python object */ + PyObject *py_result = NULL; + + if (result != NULL) + { + py_result = py_loader_impl_value_to_capi(impl, value_type_id(result), result); + } + + if (py_result == NULL) + { + Py_IncRef(Py_None); + py_result = Py_None; + } + + /* Get thread background module and asyncio loop */ + PyObject *thread_bg_module = py_loader_impl_get_thread_background_module(ctx->py_impl); + PyObject *asyncio_loop = py_loader_impl_get_asyncio_loop(ctx->py_impl); + + /* Call future_resolve(tl, future, value) */ + PyObject *future_resolve_func = PyObject_GetAttrString(thread_bg_module, "future_resolve"); + + if (future_resolve_func != NULL && PyCallable_Check(future_resolve_func)) + { + PyObject *args = PyTuple_Pack(3, asyncio_loop, ctx->future, py_result); + PyObject *call_result = PyObject_Call(future_resolve_func, args, NULL); + + Py_DecRef(call_result); + Py_DecRef(args); + Py_DecRef(future_resolve_func); + } + + Py_DecRef(py_result); + Py_DecRef(ctx->future); + + py_loader_thread_release(); + + free(ctx); + + return NULL; +} + +static void *py_loader_port_await_reject(void *result, void *data) +{ + py_loader_port_await_context *ctx = (py_loader_port_await_context *)data; + loader_impl impl = loader_get_impl(py_loader_tag); + + py_loader_thread_acquire(); + + /* Convert to Python exception object */ + PyObject *py_exception = NULL; + + if (result != NULL) + { + py_exception = py_loader_impl_value_to_capi(impl, value_type_id(result), result); + } + + if (py_exception == NULL) + { + py_exception = PyExc_RuntimeErrorPtr(); + Py_IncRef(py_exception); + } + + /* Create an Exception instance if we got a string or other value */ + if (!PyExceptionInstance_Check(py_exception) && !PyExceptionClass_Check(py_exception)) + { + PyObject *exc_args = PyTuple_Pack(1, py_exception); + PyObject *new_exc = PyObject_Call(PyExc_RuntimeErrorPtr(), exc_args, NULL); + Py_DecRef(exc_args); + Py_DecRef(py_exception); + py_exception = new_exc; + } + + /* Get thread background module and asyncio loop */ + PyObject *thread_bg_module = py_loader_impl_get_thread_background_module(ctx->py_impl); + PyObject *asyncio_loop = py_loader_impl_get_asyncio_loop(ctx->py_impl); + + /* Call future_reject(tl, future, exception) */ + PyObject *future_reject_func = PyObject_GetAttrString(thread_bg_module, "future_reject"); + + if (future_reject_func != NULL && PyCallable_Check(future_reject_func)) + { + PyObject *args = PyTuple_Pack(3, asyncio_loop, ctx->future, py_exception); + PyObject *call_result = PyObject_Call(future_reject_func, args, NULL); + + Py_DecRef(call_result); + Py_DecRef(args); + Py_DecRef(future_reject_func); + } + + Py_DecRef(py_exception); + Py_DecRef(ctx->future); + + py_loader_thread_release(); + + free(ctx); + + return NULL; +} + static PyObject *py_loader_port_await(PyObject *self, PyObject *var_args) { PyObject *name, *result = NULL; @@ -573,38 +684,50 @@ static PyObject *py_loader_port_await(PyObject *self, PyObject *var_args) size_t args_size = 0, args_count; Py_ssize_t var_args_size; loader_impl impl; + loader_impl_py py_impl; (void)self; /* Obtain Python loader implementation */ impl = loader_get_impl(py_loader_tag); + py_impl = loader_impl_get(impl); + + /* Check if asyncio is initialized */ + PyObject *asyncio_loop = py_loader_impl_get_asyncio_loop(py_impl); + + if (asyncio_loop == NULL) + { + PyErr_SetString(PyExc_RuntimeErrorPtr(), "Asyncio loop not initialized. Cannot use metacall_await."); + return Py_ReturnNone(); + } var_args_size = PyTuple_Size(var_args); if (var_args_size == 0) { - PyErr_SetString(PyExc_TypeErrorPtr(), "Invalid number of arguments, use it like: metacall('function_name', 'asd', 123, [7, 4]);"); + PyErr_SetString(PyExc_TypeErrorPtr(), "Invalid number of arguments, use it like: metacall_await('function_name', arg1, arg2, ...);"); return Py_ReturnNone(); } + /* Get function name */ name = PyTuple_GetItem(var_args, 0); - #if PY_MAJOR_VERSION == 2 +#if PY_MAJOR_VERSION == 2 { if (!(PyString_Check(name) && PyString_AsStringAndSize(name, &name_str, &name_length) != -1)) { name_str = NULL; } } - #elif PY_MAJOR_VERSION == 3 +#elif PY_MAJOR_VERSION == 3 { name_str = PyUnicode_Check(name) ? (char *)PyUnicode_AsUTF8AndSize(name, &name_length) : NULL; } - #endif +#endif if (name_str == NULL) { - PyErr_SetString(PyExc_TypeErrorPtr(), "Invalid function name string conversion, first parameter must be a string"); + PyErr_SetString(PyExc_TypeErrorPtr(), "First parameter must be a string (function name)"); return Py_ReturnNone(); } @@ -618,7 +741,7 @@ static PyObject *py_loader_port_await(PyObject *self, PyObject *var_args) if (value_args == NULL) { - PyErr_SetString(PyExc_ValueErrorPtr(), "Invalid argument allocation"); + PyErr_SetString(PyExc_MemoryErrorPtr(), "Failed to allocate arguments"); return Py_ReturnNone(); } @@ -631,57 +754,103 @@ static PyObject *py_loader_port_await(PyObject *self, PyObject *var_args) } } - /* Execute the await */ + /* Create Python Future */ + PyObject *thread_bg_module = py_loader_impl_get_thread_background_module(py_impl); + PyObject *future_create_func = PyObject_GetAttrString(thread_bg_module, "future_create"); + + if (future_create_func == NULL || !PyCallable_Check(future_create_func)) { - void *ret; + PyErr_SetString(PyExc_RuntimeErrorPtr(), "Failed to get future_create function"); + Py_DecRef(future_create_func); + goto cleanup_args; + } - py_loader_thread_release(); + PyObject *future_args = PyTuple_Pack(1, asyncio_loop); + PyObject *future = PyObject_Call(future_create_func, future_args, NULL); + Py_DecRef(future_args); + Py_DecRef(future_create_func); - /* TODO: */ - /* - if (value_args != NULL) - { - ret = metacallv_s(name_str, value_args, args_size); - } - else + if (future == NULL) + { + if (PyErr_Occurred() == NULL) { - ret = metacallv_s(name_str, metacall_null_args, 0); + PyErr_SetString(PyExc_RuntimeErrorPtr(), "Failed to create Future"); } - */ + goto cleanup_args; + } - py_loader_thread_acquire(); + /* Create callback context */ + py_loader_port_await_context *ctx = (py_loader_port_await_context *)malloc(sizeof(py_loader_port_await_context)); - if (ret == NULL) - { - result = Py_ReturnNone(); - goto clear; - } + if (ctx == NULL) + { + Py_DecRef(future); + PyErr_SetString(PyExc_MemoryErrorPtr(), "Failed to allocate context"); + goto cleanup_args; + } - result = py_loader_impl_value_to_capi(impl, value_type_id(ret), ret); + ctx->py_impl = py_impl; + ctx->future = future; + Py_IncRef(future); /* Keep reference for callback */ - value_type_destroy(ret); + /* Execute the await call */ + py_loader_thread_release(); - if (result == NULL) + void *ret = metacall_await_s( + name_str, + value_args != NULL ? value_args : metacall_null_args, + args_size, + py_loader_port_await_resolve, + py_loader_port_await_reject, + ctx); + + py_loader_thread_acquire(); + + /* Check for immediate errors (e.g., function not found) */ + if (ret != NULL && value_type_id(ret) == TYPE_THROWABLE) + { + PyObject *error = py_loader_impl_value_to_capi(impl, TYPE_THROWABLE, ret); + if (error != NULL) { - result = Py_ReturnNone(); - goto clear; + PyErr_SetObject(PyExc_RuntimeErrorPtr(), error); + Py_DecRef(error); } + else + { + PyErr_SetString(PyExc_RuntimeErrorPtr(), "Async call failed"); + } + Py_DecRef(future); + Py_DecRef(ctx->future); + free(ctx); + value_type_destroy(ret); + result = Py_ReturnNone(); + goto cleanup_args; } -clear: + if (ret != NULL) + { + value_type_destroy(ret); + } + + result = future; + +cleanup_args: if (value_args != NULL) { + py_loader_thread_release(); + for (args_count = 0; args_count < args_size; ++args_count) { value_type_destroy(value_args[args_count]); } + py_loader_thread_acquire(); + free(value_args); } return result; } -#endif static PyObject *py_loader_port_inspect(PyObject *self, PyObject *args) { @@ -925,6 +1094,8 @@ static PyMethodDef metacall_methods[] = { "Get information about all loaded objects." }, { "metacall", py_loader_port_invoke, METH_VARARGS, "Call a function anonymously." }, + { "metacall_await", py_loader_port_await, METH_VARARGS, + "Call an async function and return a Future." }, { "metacall_value_create_ptr", py_loader_port_value_create_ptr, METH_VARARGS, "Create a new value of type Pointer." }, { "metacall_value_reference", py_loader_port_value_reference, METH_VARARGS, diff --git a/source/loaders/py_loader/source/py_loader_symbol_fallback.c b/source/loaders/py_loader/source/py_loader_symbol_fallback.c index 1f5e55ff9e..c2a9a8828f 100644 --- a/source/loaders/py_loader/source/py_loader_symbol_fallback.c +++ b/source/loaders/py_loader/source/py_loader_symbol_fallback.c @@ -41,6 +41,7 @@ static PyObject **PyExc_FileNotFoundErrorStructPtr = NULL; static PyObject **PyExc_TypeErrorStructPtr = NULL; static PyObject **PyExc_ValueErrorStructPtr = NULL; static PyObject **PyExc_RuntimeErrorStructPtr = NULL; +static PyObject **PyExc_MemoryErrorStructPtr = NULL; static PyObject *Py_FalseStructPtr = NULL; static PyObject *Py_TrueStructPtr = NULL; #endif @@ -183,6 +184,14 @@ int py_loader_symbol_fallback_initialize(dynlink py_library) dynlink_symbol_uncast_type(address, PyObject **, PyExc_RuntimeErrorStructPtr); + /* PyExc_MemoryError */ + if (dynlink_symbol(py_library, "PyExc_MemoryError", &address) != 0) + { + return 1; + } + + dynlink_symbol_uncast_type(address, PyObject **, PyExc_MemoryErrorStructPtr); + /* Py_False */ if (dynlink_symbol(py_library, "_Py_FalseStruct", &address) != 0) { @@ -337,6 +346,15 @@ PyObject *PyExc_RuntimeErrorPtr(void) #endif } +PyObject *PyExc_MemoryErrorPtr(void) +{ +#if defined(_WIN32) && defined(_MSC_VER) + return *PyExc_MemoryErrorStructPtr; +#else + return PyExc_MemoryError; +#endif +} + PyObject *Py_ReturnNone(void) { #if defined(_WIN32) && defined(_MSC_VER) diff --git a/source/ports/py_port/metacall/__init__.py b/source/ports/py_port/metacall/__init__.py index 412cd2dd18..d82f0838ca 100644 --- a/source/ports/py_port/metacall/__init__.py +++ b/source/ports/py_port/metacall/__init__.py @@ -17,4 +17,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from metacall.api import metacall, metacall_load_from_file, metacall_load_from_memory, metacall_load_from_package, metacall_inspect, metacall_value_create_ptr, metacall_value_reference, metacall_value_dereference +from metacall.api import ( + metacall, + metacall_load_from_file, + metacall_load_from_memory, + metacall_load_from_package, + metacall_inspect, + metacall_value_create_ptr, + metacall_value_reference, + metacall_value_dereference, + metacall_await, + MetaCallFunction +) diff --git a/source/ports/py_port/metacall/api.py b/source/ports/py_port/metacall/api.py index acd4aa87dd..5693152fb6 100644 --- a/source/ports/py_port/metacall/api.py +++ b/source/ports/py_port/metacall/api.py @@ -127,6 +127,124 @@ def metacall_load_from_memory(tag, buffer): def metacall(function_name, *args): return module.metacall(function_name, *args) +# Await invocation - returns a Future +def metacall_await(function_name, *args): + """ + Call an async function from another runtime and return a Future. + + The returned Future can be awaited in async context: + result = await metacall_await('async_func', arg1, arg2) + + Args: + function_name: Name of the async function to call + *args: Arguments to pass to the function + + Returns: + A Future that resolves to the function's return value + """ + return module.metacall_await(function_name, *args) + + +class MetaCallFunction: + """ + Wrapper for MetaCall functions that supports both sync and async calls. + + For synchronous functions: + func = MetaCallFunction('my_sync_func', is_async=False) + result = func(arg1, arg2) + + For asynchronous functions: + func = MetaCallFunction('my_async_func', is_async=True) + result = await func(arg1, arg2) + + The wrapper can also be called with explicit methods: + result = func.call(arg1, arg2) # Sync call + result = await func.async_call(arg1, arg2) # Async call + """ + + def __init__(self, name, is_async=False): + """ + Initialize a MetaCallFunction wrapper. + + Args: + name: The name of the function in MetaCall + is_async: Whether the function is asynchronous + """ + self._name = name + self._is_async = is_async + self.__name__ = name + self.__qualname__ = name + self.__doc__ = f"MetaCall function '{name}' ({'async' if is_async else 'sync'})" + + @property + def name(self): + """Get the function name.""" + return self._name + + @property + def is_async(self): + """Check if the function is asynchronous.""" + return self._is_async + + def __call__(self, *args): + """ + Call the function. + + For async functions, returns a coroutine that must be awaited. + For sync functions, returns the result directly. + """ + if self._is_async: + return self._async_call_impl(*args) + else: + return metacall(self._name, *args) + + def call(self, *args): + """ + Synchronous call to the function. + + For async functions, this will block until the result is available. + """ + if self._is_async: + import asyncio + future = module.metacall_await(self._name, *args) + # If we're in an event loop, wrap the future + try: + loop = asyncio.get_running_loop() + # We're in an async context, return awaitable + return asyncio.wrap_future(future, loop=loop) + except RuntimeError: + # No running loop, block on the future + return future.result() + else: + return metacall(self._name, *args) + + async def _async_call_impl(self, *args): + """Internal async implementation.""" + import asyncio + future = module.metacall_await(self._name, *args) + return await asyncio.wrap_future(future) + + async def async_call(self, *args): + """ + Asynchronous call to the function. + + Can be used for both sync and async functions. + For sync functions, the call is wrapped in an executor. + """ + if self._is_async: + return await self._async_call_impl(*args) + else: + import asyncio + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, lambda: metacall(self._name, *args)) + + def __repr__(self): + return f"MetaCallFunction('{self._name}', is_async={self._is_async})" + + def __str__(self): + return f"" + + # Wrap metacall inspect and transform the json string into a dict def metacall_inspect(): data = module.metacall_inspect() @@ -174,12 +292,19 @@ def metacall_value_dereference(ptr): def __metacall_import__(name, globals=None, locals=None, fromlist=(), level=0): def find_handle(handle_name): + """Find handle and create wrappers with async detection.""" metadata = metacall_inspect() for loader in metadata.keys(): for handle in metadata[loader]: if handle['name'] == handle_name: - return dict(functools.reduce(lambda symbols, func: {**symbols, func['name']: lambda *args: metacall(func['name'], *args) }, handle['scope']['funcs'], {})) + symbols = {} + for func in handle['scope']['funcs']: + func_name = func['name'] + # Check for async property in function metadata + is_async = func.get('async', False) + symbols[func_name] = MetaCallFunction(func_name, is_async=is_async) + return symbols return None diff --git a/source/ports/py_port/test/test_await.py b/source/ports/py_port/test/test_await.py new file mode 100644 index 0000000000..21c05c9b43 --- /dev/null +++ b/source/ports/py_port/test/test_await.py @@ -0,0 +1,572 @@ +#!/usr/bin/env python3 + +# MetaCall Python Port by Parra Studios +# A frontend for Python language bindings in MetaCall. +# +# Copyright (C) 2016 - 2025 Vicente Eduardo Ferrer Garcia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import unittest +import asyncio + +# Load metacall from Python Port path +abspath = os.path.dirname(os.path.abspath(__file__)) +relpath = '..' +path = os.path.normpath(os.path.join(abspath, relpath)) +sys.path.insert(0, path) + +from metacall import ( + metacall, + metacall_load_from_file, + metacall_load_from_memory, + metacall_inspect, + metacall_await, + MetaCallFunction +) + + +class TestMetaCallAwait(unittest.TestCase): + """Tests for metacall_await functionality.""" + + @classmethod + def setUpClass(cls): + """Load test scripts before running tests.""" + # Load Node.js async test script + node_script = ''' +async function async_add(a, b) { + return a + b; +} + +async function async_multiply(a, b) { + return new Promise(resolve => { + setTimeout(() => resolve(a * b), 10); + }); +} + +async function async_delayed(value, delay_ms) { + return new Promise(resolve => { + setTimeout(() => resolve(value), delay_ms); + }); +} + +async function async_return_object() { + return { name: 'test', value: 42 }; +} + +async function async_return_array() { + return [1, 2, 3, 4, 5]; +} + +async function async_return_null() { + return null; +} + +async function async_chain(value) { + const step1 = await async_add(value, 10); + const step2 = await async_multiply(step1, 2); + return step2; +} + +function sync_add(a, b) { + return a + b; +} + +module.exports = { + async_add, + async_multiply, + async_delayed, + async_return_object, + async_return_array, + async_return_null, + async_chain, + sync_add +}; +''' + result = metacall_load_from_memory('node', node_script) + if not result: + raise RuntimeError("Failed to load Node.js test script") + + def test_metacall_await_basic(self): + """Test basic async function call with await.""" + async def run_test(): + result = await metacall_await('async_add', 5, 3) + self.assertEqual(result, 8) + + asyncio.run(run_test()) + + def test_metacall_await_with_promise(self): + """Test async function that returns a Promise.""" + async def run_test(): + result = await metacall_await('async_multiply', 6, 7) + self.assertEqual(result, 42) + + asyncio.run(run_test()) + + def test_metacall_await_with_delay(self): + """Test async function with setTimeout delay.""" + async def run_test(): + result = await metacall_await('async_delayed', 'hello', 50) + self.assertEqual(result, 'hello') + + asyncio.run(run_test()) + + def test_metacall_await_returns_object(self): + """Test async function returning an object.""" + async def run_test(): + result = await metacall_await('async_return_object') + self.assertIsInstance(result, dict) + self.assertEqual(result['name'], 'test') + self.assertEqual(result['value'], 42) + + asyncio.run(run_test()) + + def test_metacall_await_returns_array(self): + """Test async function returning an array.""" + async def run_test(): + result = await metacall_await('async_return_array') + self.assertIsInstance(result, list) + self.assertEqual(result, [1, 2, 3, 4, 5]) + + asyncio.run(run_test()) + + def test_metacall_await_returns_null(self): + """Test async function returning null.""" + async def run_test(): + result = await metacall_await('async_return_null') + self.assertIsNone(result) + + asyncio.run(run_test()) + + def test_metacall_await_chained_calls(self): + """Test async function that chains other async calls.""" + async def run_test(): + # async_chain(5) -> async_add(5, 10) = 15 -> async_multiply(15, 2) = 30 + result = await metacall_await('async_chain', 5) + self.assertEqual(result, 30) + + asyncio.run(run_test()) + + def test_metacall_await_multiple_concurrent(self): + """Test multiple concurrent async calls.""" + async def run_test(): + results = await asyncio.gather( + metacall_await('async_add', 1, 2), + metacall_await('async_add', 3, 4), + metacall_await('async_add', 5, 6) + ) + self.assertEqual(results, [3, 7, 11]) + + asyncio.run(run_test()) + + def test_metacall_await_with_various_types(self): + """Test async calls with various argument types.""" + async def run_test(): + # Integer arguments + result1 = await metacall_await('async_add', 100, 200) + self.assertEqual(result1, 300) + + # Float arguments + result2 = await metacall_await('async_add', 1.5, 2.5) + self.assertEqual(result2, 4.0) + + # Negative numbers + result3 = await metacall_await('async_add', -10, 5) + self.assertEqual(result3, -5) + + asyncio.run(run_test()) + + +class TestMetaCallFunction(unittest.TestCase): + """Tests for MetaCallFunction class.""" + + @classmethod + def setUpClass(cls): + """Load test scripts before running tests.""" + node_script = ''' +async function wrapper_async_func(x) { + return x * 2; +} + +function wrapper_sync_func(x) { + return x + 10; +} + +async function wrapper_async_sum(a, b, c) { + return a + b + c; +} + +module.exports = { + wrapper_async_func, + wrapper_sync_func, + wrapper_async_sum +}; +''' + metacall_load_from_memory('node', node_script) + + def test_metacall_function_creation_sync(self): + """Test creating a sync MetaCallFunction.""" + func = MetaCallFunction('wrapper_sync_func', is_async=False) + + self.assertEqual(func.name, 'wrapper_sync_func') + self.assertFalse(func.is_async) + self.assertEqual(func.__name__, 'wrapper_sync_func') + + def test_metacall_function_creation_async(self): + """Test creating an async MetaCallFunction.""" + func = MetaCallFunction('wrapper_async_func', is_async=True) + + self.assertEqual(func.name, 'wrapper_async_func') + self.assertTrue(func.is_async) + self.assertEqual(func.__name__, 'wrapper_async_func') + + def test_metacall_function_sync_call(self): + """Test calling a sync function through MetaCallFunction.""" + func = MetaCallFunction('wrapper_sync_func', is_async=False) + result = func(5) + self.assertEqual(result, 15) # 5 + 10 + + def test_metacall_function_async_call(self): + """Test calling an async function through MetaCallFunction.""" + func = MetaCallFunction('wrapper_async_func', is_async=True) + + async def run_test(): + result = await func(7) + self.assertEqual(result, 14) # 7 * 2 + + asyncio.run(run_test()) + + def test_metacall_function_async_call_method(self): + """Test using async_call method on async function.""" + func = MetaCallFunction('wrapper_async_func', is_async=True) + + async def run_test(): + result = await func.async_call(10) + self.assertEqual(result, 20) # 10 * 2 + + asyncio.run(run_test()) + + def test_metacall_function_async_call_on_sync(self): + """Test using async_call method on sync function.""" + func = MetaCallFunction('wrapper_sync_func', is_async=False) + + async def run_test(): + result = await func.async_call(5) + self.assertEqual(result, 15) # 5 + 10 + + asyncio.run(run_test()) + + def test_metacall_function_repr(self): + """Test string representation of MetaCallFunction.""" + sync_func = MetaCallFunction('test_sync', is_async=False) + async_func = MetaCallFunction('test_async', is_async=True) + + self.assertIn('test_sync', repr(sync_func)) + self.assertIn('is_async=False', repr(sync_func)) + self.assertIn('test_async', repr(async_func)) + self.assertIn('is_async=True', repr(async_func)) + + def test_metacall_function_str(self): + """Test str() of MetaCallFunction.""" + sync_func = MetaCallFunction('test_sync', is_async=False) + async_func = MetaCallFunction('test_async', is_async=True) + + self.assertIn('sync', str(sync_func)) + self.assertIn('async', str(async_func)) + + def test_metacall_function_with_multiple_args(self): + """Test MetaCallFunction with multiple arguments.""" + func = MetaCallFunction('wrapper_async_sum', is_async=True) + + async def run_test(): + result = await func(1, 2, 3) + self.assertEqual(result, 6) + + asyncio.run(run_test()) + + +class TestAsyncImportDetection(unittest.TestCase): + """Tests for automatic async function detection during import.""" + + @classmethod + def setUpClass(cls): + """Load test script with both sync and async functions.""" + node_script = ''' +async function import_async_double(x) { + return x * 2; +} + +function import_sync_triple(x) { + return x * 3; +} + +async function import_async_greet(name) { + return 'Hello, ' + name + '!'; +} + +module.exports = { + import_async_double, + import_sync_triple, + import_async_greet +}; +''' + metacall_load_from_memory('node', node_script) + + def test_inspect_shows_async_property(self): + """Test that inspect returns async property for functions.""" + metadata = metacall_inspect() + + # Find our test functions in the metadata + found_async = False + found_sync = False + + for loader_key in metadata: + for handle in metadata[loader_key]: + for func in handle.get('scope', {}).get('funcs', []): + if func['name'] == 'import_async_double': + self.assertTrue(func.get('async', False), + "import_async_double should be marked as async") + found_async = True + elif func['name'] == 'import_sync_triple': + self.assertFalse(func.get('async', False), + "import_sync_triple should not be marked as async") + found_sync = True + + self.assertTrue(found_async, "Did not find import_async_double in metadata") + self.assertTrue(found_sync, "Did not find import_sync_triple in metadata") + + +class TestAwaitEdgeCases(unittest.TestCase): + """Tests for edge cases in await functionality.""" + + @classmethod + def setUpClass(cls): + """Load test script with edge case functions.""" + node_script = ''' +async function edge_no_args() { + return 'no args result'; +} + +async function edge_many_args(a, b, c, d, e) { + return a + b + c + d + e; +} + +async function edge_long_string() { + return 'a'.repeat(10000); +} + +async function edge_large_array() { + return Array.from({length: 1000}, (_, i) => i); +} + +async function edge_deep_object() { + return { + level1: { + level2: { + level3: { + value: 'deep' + } + } + } + }; +} + +module.exports = { + edge_no_args, + edge_many_args, + edge_long_string, + edge_large_array, + edge_deep_object +}; +''' + metacall_load_from_memory('node', node_script) + + def test_await_no_args(self): + """Test async function with no arguments.""" + async def run_test(): + result = await metacall_await('edge_no_args') + self.assertEqual(result, 'no args result') + + asyncio.run(run_test()) + + def test_await_many_args(self): + """Test async function with many arguments.""" + async def run_test(): + result = await metacall_await('edge_many_args', 1, 2, 3, 4, 5) + self.assertEqual(result, 15) + + asyncio.run(run_test()) + + def test_await_long_string(self): + """Test async function returning long string.""" + async def run_test(): + result = await metacall_await('edge_long_string') + self.assertEqual(len(result), 10000) + self.assertTrue(all(c == 'a' for c in result)) + + asyncio.run(run_test()) + + def test_await_large_array(self): + """Test async function returning large array.""" + async def run_test(): + result = await metacall_await('edge_large_array') + self.assertEqual(len(result), 1000) + self.assertEqual(result[0], 0) + self.assertEqual(result[999], 999) + + asyncio.run(run_test()) + + def test_await_deep_object(self): + """Test async function returning deeply nested object.""" + async def run_test(): + result = await metacall_await('edge_deep_object') + self.assertEqual(result['level1']['level2']['level3']['value'], 'deep') + + asyncio.run(run_test()) + + +class TestAwaitConcurrency(unittest.TestCase): + """Tests for concurrent await operations.""" + + @classmethod + def setUpClass(cls): + """Load test script for concurrency tests.""" + node_script = ''' +let counter = 0; + +async function concurrent_increment() { + counter++; + await new Promise(r => setTimeout(r, 10)); + return counter; +} + +async function concurrent_get_counter() { + return counter; +} + +async function concurrent_reset() { + counter = 0; + return true; +} + +async function concurrent_slow(id, delay) { + await new Promise(r => setTimeout(r, delay)); + return id; +} + +module.exports = { + concurrent_increment, + concurrent_get_counter, + concurrent_reset, + concurrent_slow +}; +''' + metacall_load_from_memory('node', node_script) + + def test_sequential_awaits(self): + """Test multiple sequential await calls.""" + async def run_test(): + await metacall_await('concurrent_reset') + + result1 = await metacall_await('concurrent_increment') + result2 = await metacall_await('concurrent_increment') + result3 = await metacall_await('concurrent_increment') + + # Sequential calls should increment properly + self.assertEqual(result3, 3) + + asyncio.run(run_test()) + + def test_gather_multiple_awaits(self): + """Test concurrent awaits with asyncio.gather.""" + async def run_test(): + # Run multiple slow operations concurrently + results = await asyncio.gather( + metacall_await('concurrent_slow', 'a', 30), + metacall_await('concurrent_slow', 'b', 20), + metacall_await('concurrent_slow', 'c', 10) + ) + + # All results should complete + self.assertEqual(set(results), {'a', 'b', 'c'}) + + asyncio.run(run_test()) + + def test_task_creation(self): + """Test creating asyncio Tasks from await futures.""" + async def run_test(): + task1 = asyncio.create_task(metacall_await('concurrent_slow', 1, 20)) + task2 = asyncio.create_task(metacall_await('concurrent_slow', 2, 10)) + + result2 = await task2 + result1 = await task1 + + self.assertEqual(result1, 1) + self.assertEqual(result2, 2) + + asyncio.run(run_test()) + + +class TestMetaCallFunctionAdvanced(unittest.TestCase): + """Advanced tests for MetaCallFunction.""" + + @classmethod + def setUpClass(cls): + """Load test script.""" + node_script = ''' +async function adv_process(data) { + return { processed: true, input: data }; +} + +function adv_transform(value) { + return value.toUpperCase(); +} + +module.exports = { adv_process, adv_transform }; +''' + metacall_load_from_memory('node', node_script) + + def test_metacall_function_doc(self): + """Test MetaCallFunction has proper documentation.""" + func = MetaCallFunction('adv_process', is_async=True) + self.assertIn('adv_process', func.__doc__) + self.assertIn('async', func.__doc__) + + def test_metacall_function_callable(self): + """Test MetaCallFunction is callable.""" + func = MetaCallFunction('adv_transform', is_async=False) + self.assertTrue(callable(func)) + + def test_metacall_function_as_higher_order(self): + """Test using MetaCallFunction in higher-order scenarios.""" + sync_func = MetaCallFunction('adv_transform', is_async=False) + + # Use in map-like scenario + inputs = ['hello', 'world'] + results = [sync_func(x) for x in inputs] + + self.assertEqual(results, ['HELLO', 'WORLD']) + + def test_metacall_function_properties(self): + """Test MetaCallFunction property access.""" + func = MetaCallFunction('adv_process', is_async=True) + + # Properties should be accessible + self.assertEqual(func.name, 'adv_process') + self.assertTrue(func.is_async) + + +if __name__ == '__main__': + unittest.main() diff --git a/source/tests/CMakeLists.txt b/source/tests/CMakeLists.txt index 2f2e6bb890..0b31d5df47 100644 --- a/source/tests/CMakeLists.txt +++ b/source/tests/CMakeLists.txt @@ -182,9 +182,10 @@ add_subdirectory(metacall_python_relative_path_test) add_subdirectory(metacall_python_without_functions_test) add_subdirectory(metacall_python_builtins_test) add_subdirectory(metacall_python_async_test) -# TODO: add_subdirectory(metacall_python_await_test) # TODO: Implement metacall_await in Python Port +add_subdirectory(metacall_python_await_test) add_subdirectory(metacall_python_exception_test) -# TODO: add_subdirectory(metacall_python_node_await_test) # TODO: Implement metacall_await in Python Port +add_subdirectory(metacall_python_node_await_test) +add_subdirectory(metacall_python_port_await_test) add_subdirectory(metacall_python_without_env_vars_test) add_subdirectory(metacall_map_test) add_subdirectory(metacall_map_await_test) diff --git a/source/tests/metacall_python_await_test/source/metacall_python_await_test.cpp b/source/tests/metacall_python_await_test/source/metacall_python_await_test.cpp index 71ff8b6f50..dbc5c82d77 100644 --- a/source/tests/metacall_python_await_test/source/metacall_python_await_test.cpp +++ b/source/tests/metacall_python_await_test/source/metacall_python_await_test.cpp @@ -41,6 +41,7 @@ TEST_F(metacall_python_await_test, DefaultConstructor) static const char buffer[] = "import asyncio\n" "import sys\n" + "import threading\n" "sys.path.insert(0, '" METACALL_PYTHON_PORT_PATH "')\n" "from metacall import metacall_load_from_memory, metacall_await\n" "script = \"\"\"\n" diff --git a/source/tests/metacall_python_port_await_test/CMakeLists.txt b/source/tests/metacall_python_port_await_test/CMakeLists.txt new file mode 100644 index 0000000000..36cadb4c9d --- /dev/null +++ b/source/tests/metacall_python_port_await_test/CMakeLists.txt @@ -0,0 +1,170 @@ +# Check if required loaders are enabled +if(NOT OPTION_BUILD_LOADERS OR NOT OPTION_BUILD_LOADERS_NODE OR NOT OPTION_BUILD_LOADERS_PY OR NOT OPTION_BUILD_PORTS OR NOT OPTION_BUILD_PORTS_PY) + return() +endif() + +# +# Executable name and options +# + +# Target name +set(target metacall-python-port-await-test) +message(STATUS "Test ${target}") + +# +# Compiler warnings +# + +include(Warnings) + +# +# Compiler security +# + +include(SecurityFlags) + +# +# Sources +# + +set(include_path "${CMAKE_CURRENT_SOURCE_DIR}/include/${target}") +set(source_path "${CMAKE_CURRENT_SOURCE_DIR}/source") + +set(sources + ${source_path}/main.cpp + ${source_path}/metacall_python_port_await_test.cpp +) + +# Group source files +set(header_group "Header Files (API)") +set(source_group "Source Files") +source_group_by_path(${include_path} "\\\\.h$|\\\\.hpp$" + ${header_group} ${headers}) +source_group_by_path(${source_path} "\\\\.cpp$|\\\\.c$|\\\\.h$|\\\\.hpp$" + ${source_group} ${sources}) + +# +# Create executable +# + +# Build executable +add_executable(${target} + ${sources} +) + +# Create namespaced alias +add_executable(${META_PROJECT_NAME}::${target} ALIAS ${target}) + +# +# Project options +# + +set_target_properties(${target} + PROPERTIES + ${DEFAULT_PROJECT_OPTIONS} + FOLDER "${IDE_FOLDER}" +) + +# +# Include directories +# + +target_include_directories(${target} + PRIVATE + ${DEFAULT_INCLUDE_DIRECTORIES} + ${PROJECT_BINARY_DIR}/source/include +) + +# +# Libraries +# + +target_link_libraries(${target} + PRIVATE + ${DEFAULT_LIBRARIES} + + GTest + + ${META_PROJECT_NAME}::metacall +) + +# +# Compile definitions +# + +target_compile_definitions(${target} + PRIVATE + ${DEFAULT_COMPILE_DEFINITIONS} + + # Python Port path + METACALL_PYTHON_PORT_PATH="${CMAKE_SOURCE_DIR}/source/ports/py_port" +) + +# +# Compile options +# + +target_compile_options(${target} + PRIVATE + ${DEFAULT_COMPILE_OPTIONS} +) + +# +# Compile features +# + +target_compile_features(${target} + PRIVATE + cxx_std_17 +) + +# +# Linker options +# + +target_link_options(${target} + PRIVATE + ${DEFAULT_LINKER_OPTIONS} +) + +# +# Define test +# + +add_test(NAME ${target} + COMMAND $ +) + +# +# Define dependencies +# + +add_dependencies(${target} + node_loader + py_loader +) + +# +# Define test properties +# + +set_property(TEST ${target} + PROPERTY LABELS ${target} +) + +include(TestEnvironmentVariables) + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(PY_DEBUG_ENVIRONMENT_VARIABLES + PYTHONTHREADDEBUG=1 + PYTHONASYNCIODEBUG=1 + ) +else() + set(PY_DEBUG_ENVIRONMENT_VARIABLES) +endif() + +test_environment_variables(${target} + "" + ${TESTS_ENVIRONMENT_VARIABLES} + ${PY_DEBUG_ENVIRONMENT_VARIABLES} +) diff --git a/source/tests/metacall_python_port_await_test/source/main.cpp b/source/tests/metacall_python_port_await_test/source/main.cpp new file mode 100644 index 0000000000..23b78927ec --- /dev/null +++ b/source/tests/metacall_python_port_await_test/source/main.cpp @@ -0,0 +1,27 @@ +/* + * MetaCall Library by Parra Studios + * A library for providing a foreign function interface calls. + * + * Copyright (C) 2016 - 2025 Vicente Eduardo Ferrer Garcia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +int main(int argc, char *argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/source/tests/metacall_python_port_await_test/source/metacall_python_port_await_test.cpp b/source/tests/metacall_python_port_await_test/source/metacall_python_port_await_test.cpp new file mode 100644 index 0000000000..4941318925 --- /dev/null +++ b/source/tests/metacall_python_port_await_test/source/metacall_python_port_await_test.cpp @@ -0,0 +1,232 @@ +/* + * MetaCall Library by Parra Studios + * A library for providing a foreign function interface calls. + * + * Copyright (C) 2016 - 2025 Vicente Eduardo Ferrer Garcia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +#include +#include +#include + +class metacall_python_port_await_test : public testing::Test +{ +public: +}; + +TEST_F(metacall_python_port_await_test, BasicAwait) +{ + metacall_print_info(); + + ASSERT_EQ((int)0, (int)metacall_initialize()); + +#if defined(OPTION_BUILD_LOADERS_NODE) && defined(OPTION_BUILD_LOADERS_PY) + { + static const char buffer[] = + "import sys\n" + "sys.path.insert(0, '" METACALL_PYTHON_PORT_PATH "')\n" + "import asyncio\n" + "from metacall import metacall_load_from_memory, metacall_await\n" + "\n" + "# Load Node.js async function\n" + "node_script = '''\n" + "async function test_add(a, b) {\n" + " return a + b;\n" + "}\n" + "module.exports = { test_add };\n" + "'''\n" + "metacall_load_from_memory('node', node_script)\n" + "\n" + "# Test basic await\n" + "async def test():\n" + " result = await metacall_await('test_add', 5, 3)\n" + " if result != 8:\n" + " raise Exception(f'Expected 8, got {result}')\n" + "\n" + "asyncio.run(test())\n"; + + ASSERT_EQ((int)0, (int)metacall_load_from_memory("py", buffer, sizeof(buffer), NULL)); + } +#endif /* OPTION_BUILD_LOADERS_NODE && OPTION_BUILD_LOADERS_PY */ + + metacall_destroy(); +} + +TEST_F(metacall_python_port_await_test, MultipleAwaits) +{ + metacall_print_info(); + + ASSERT_EQ((int)0, (int)metacall_initialize()); + +#if defined(OPTION_BUILD_LOADERS_NODE) && defined(OPTION_BUILD_LOADERS_PY) + { + static const char buffer[] = + "import sys\n" + "sys.path.insert(0, '" METACALL_PYTHON_PORT_PATH "')\n" + "import asyncio\n" + "from metacall import metacall_load_from_memory, metacall_await\n" + "\n" + "node_script = '''\n" + "async function multiply(a, b) {\n" + " return a * b;\n" + "}\n" + "module.exports = { multiply };\n" + "'''\n" + "metacall_load_from_memory('node', node_script)\n" + "\n" + "async def test():\n" + " r1 = await metacall_await('multiply', 2, 3)\n" + " r2 = await metacall_await('multiply', 4, 5)\n" + " r3 = await metacall_await('multiply', 6, 7)\n" + " if r1 != 6 or r2 != 20 or r3 != 42:\n" + " raise Exception(f'Wrong results: {r1}, {r2}, {r3}')\n" + "\n" + "asyncio.run(test())\n"; + + ASSERT_EQ((int)0, (int)metacall_load_from_memory("py", buffer, sizeof(buffer), NULL)); + } +#endif /* OPTION_BUILD_LOADERS_NODE && OPTION_BUILD_LOADERS_PY */ + + metacall_destroy(); +} + +TEST_F(metacall_python_port_await_test, ConcurrentAwaits) +{ + metacall_print_info(); + + ASSERT_EQ((int)0, (int)metacall_initialize()); + +#if defined(OPTION_BUILD_LOADERS_NODE) && defined(OPTION_BUILD_LOADERS_PY) + { + static const char buffer[] = + "import sys\n" + "sys.path.insert(0, '" METACALL_PYTHON_PORT_PATH "')\n" + "import asyncio\n" + "from metacall import metacall_load_from_memory, metacall_await\n" + "\n" + "node_script = '''\n" + "async function delayed(val, ms) {\n" + " return new Promise(r => setTimeout(() => r(val), ms));\n" + "}\n" + "module.exports = { delayed };\n" + "'''\n" + "metacall_load_from_memory('node', node_script)\n" + "\n" + "async def test():\n" + " results = await asyncio.gather(\n" + " metacall_await('delayed', 'a', 30),\n" + " metacall_await('delayed', 'b', 20),\n" + " metacall_await('delayed', 'c', 10)\n" + " )\n" + " if set(results) != {'a', 'b', 'c'}:\n" + " raise Exception(f'Wrong results: {results}')\n" + "\n" + "asyncio.run(test())\n"; + + ASSERT_EQ((int)0, (int)metacall_load_from_memory("py", buffer, sizeof(buffer), NULL)); + } +#endif /* OPTION_BUILD_LOADERS_NODE && OPTION_BUILD_LOADERS_PY */ + + metacall_destroy(); +} + +TEST_F(metacall_python_port_await_test, MetaCallFunctionClass) +{ + metacall_print_info(); + + ASSERT_EQ((int)0, (int)metacall_initialize()); + +#if defined(OPTION_BUILD_LOADERS_NODE) && defined(OPTION_BUILD_LOADERS_PY) + { + static const char buffer[] = + "import sys\n" + "sys.path.insert(0, '" METACALL_PYTHON_PORT_PATH "')\n" + "import asyncio\n" + "from metacall import metacall_load_from_memory, MetaCallFunction\n" + "\n" + "node_script = '''\n" + "async function async_square(x) { return x * x; }\n" + "function sync_double(x) { return x * 2; }\n" + "module.exports = { async_square, sync_double };\n" + "'''\n" + "metacall_load_from_memory('node', node_script)\n" + "\n" + "# Test sync function\n" + "sync_func = MetaCallFunction('sync_double', is_async=False)\n" + "result = sync_func(5)\n" + "if result != 10:\n" + " raise Exception(f'Expected 10, got {result}')\n" + "\n" + "# Test async function\n" + "async def test_async():\n" + " async_func = MetaCallFunction('async_square', is_async=True)\n" + " result = await async_func(4)\n" + " if result != 16:\n" + " raise Exception(f'Expected 16, got {result}')\n" + "\n" + "asyncio.run(test_async())\n"; + + ASSERT_EQ((int)0, (int)metacall_load_from_memory("py", buffer, sizeof(buffer), NULL)); + } +#endif /* OPTION_BUILD_LOADERS_NODE && OPTION_BUILD_LOADERS_PY */ + + metacall_destroy(); +} + +TEST_F(metacall_python_port_await_test, AwaitWithComplexTypes) +{ + metacall_print_info(); + + ASSERT_EQ((int)0, (int)metacall_initialize()); + +#if defined(OPTION_BUILD_LOADERS_NODE) && defined(OPTION_BUILD_LOADERS_PY) + { + static const char buffer[] = + "import sys\n" + "sys.path.insert(0, '" METACALL_PYTHON_PORT_PATH "')\n" + "import asyncio\n" + "from metacall import metacall_load_from_memory, metacall_await\n" + "\n" + "node_script = '''\n" + "async function return_object() {\n" + " return { name: 'test', values: [1, 2, 3] };\n" + "}\n" + "async function return_array() {\n" + " return [{ a: 1 }, { b: 2 }];\n" + "}\n" + "module.exports = { return_object, return_array };\n" + "'''\n" + "metacall_load_from_memory('node', node_script)\n" + "\n" + "async def test():\n" + " obj = await metacall_await('return_object')\n" + " if obj['name'] != 'test' or obj['values'] != [1, 2, 3]:\n" + " raise Exception(f'Wrong object: {obj}')\n" + " \n" + " arr = await metacall_await('return_array')\n" + " if arr[0]['a'] != 1 or arr[1]['b'] != 2:\n" + " raise Exception(f'Wrong array: {arr}')\n" + "\n" + "asyncio.run(test())\n"; + + ASSERT_EQ((int)0, (int)metacall_load_from_memory("py", buffer, sizeof(buffer), NULL)); + } +#endif /* OPTION_BUILD_LOADERS_NODE && OPTION_BUILD_LOADERS_PY */ + + metacall_destroy(); +}