From b2270624b44cf714b81842d57c333e8d9391166b Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Fri, 20 Mar 2026 22:26:31 +0100 Subject: [PATCH 1/3] Add OWN_GIL tests for erlang.whereis, atom, Ref, Pid operations - Add erlang_api test group with 8 tests - Test whereis basic, nonexistent, with send, and parallel - Test atom equality and roundtrip (note: atoms become strings in OWN_GIL) - Test Ref type check and uniqueness - Test Pid equality, hashing, dict key, set membership --- test/py_owngil_features_SUITE.erl | 228 +++++++++++++++++++++++++++++- test/py_test_pid_send.py | 54 +++++++ 2 files changed, 281 insertions(+), 1 deletion(-) diff --git a/test/py_owngil_features_SUITE.erl b/test/py_owngil_features_SUITE.erl index d8476e2..f000233 100644 --- a/test/py_owngil_features_SUITE.erl +++ b/test/py_owngil_features_SUITE.erl @@ -96,6 +96,18 @@ owngil_local_env_call_test/1 ]). +%% Erlang API tests (whereis, atom, Ref, Pid) +-export([ + owngil_whereis_basic_test/1, + owngil_whereis_nonexistent_test/1, + owngil_whereis_and_send_test/1, + owngil_whereis_parallel_test/1, + owngil_atom_basic_test/1, + owngil_atom_roundtrip_test/1, + owngil_ref_roundtrip_test/1, + owngil_pid_operations_test/1 +]). + all() -> [{group, channels}, {group, buffers}, @@ -104,7 +116,8 @@ all() -> {group, reactor}, {group, async_task}, {group, asyncio}, - {group, local_env}]. + {group, local_env}, + {group, erlang_api}]. groups() -> [{channels, [sequence], [ @@ -166,6 +179,16 @@ groups() -> {local_env, [sequence], [ owngil_local_env_isolation_test, owngil_local_env_call_test + ]}, + {erlang_api, [sequence], [ + owngil_whereis_basic_test, + owngil_whereis_nonexistent_test, + owngil_whereis_and_send_test, + owngil_whereis_parallel_test, + owngil_atom_basic_test, + owngil_atom_roundtrip_test, + owngil_ref_roundtrip_test, + owngil_pid_operations_test ]}]. init_per_suite(Config) -> @@ -1403,6 +1426,18 @@ drain_tuple_messages(N) -> ok end. +collect_from_ctx_messages(0, Acc) -> + Acc; +collect_from_ctx_messages(N, Acc) -> + receive + %% Atom key (direct from Erlang) + {from_ctx, CtxNum} -> collect_from_ctx_messages(N - 1, [CtxNum | Acc]); + %% Binary key (roundtripped through Python) + {<<"from_ctx">>, CtxNum} -> collect_from_ctx_messages(N - 1, [CtxNum | Acc]) + after 5000 -> + ct:fail({timeout_collecting_messages, got, length(Acc), expected, N + length(Acc)}) + end. + create_socketpair() -> {ok, LSock} = gen_tcp:listen(0, [binary, {active, false}, {reuseaddr, true}]), {ok, Port} = inet:port(LSock), @@ -1478,3 +1513,194 @@ def greet(name): {ok, 2.0} = py_nif:context_call(CtxRef, <<"math">>, <<"sqrt">>, [4.0], #{}, Env), py_context:stop(Ctx). + +%%% ============================================================================ +%%% Erlang API Tests (whereis, atom, Ref, Pid) +%%% ============================================================================ + +%% @doc Basic whereis lookup in owngil context +owngil_whereis_basic_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + %% Register self under a name + true = register(owngil_whereis_test_proc, self()), + + %% Look up the process from Python + {ok, FoundPid} = py_context:call(Ctx, py_test_pid_send, whereis_basic, + [owngil_whereis_test_proc], #{}), + + %% Verify it matches + Self = self(), + Self = FoundPid, + + unregister(owngil_whereis_test_proc), + py_context:stop(Ctx). + +%% @doc Lookup non-existent name returns None in owngil context +owngil_whereis_nonexistent_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + %% Look up a name that doesn't exist + {ok, none} = py_context:call(Ctx, py_test_pid_send, whereis_basic, + [nonexistent_proc_name_xyz], #{}), + + py_context:stop(Ctx). + +%% @doc Combined whereis + send pattern in owngil context +owngil_whereis_and_send_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + %% Register self under a name + true = register(owngil_whereis_send_test, self()), + + %% Use whereis_and_send from Python + {ok, true} = py_context:call(Ctx, py_test_pid_send, whereis_and_send, + [owngil_whereis_send_test, <<"hello_from_whereis">>], #{}), + + %% Verify message received + receive <<"hello_from_whereis">> -> ok + after 5000 -> ct:fail(timeout) + end, + + unregister(owngil_whereis_send_test), + py_context:stop(Ctx). + +%% @doc Parallel whereis + send from multiple owngil contexts +owngil_whereis_parallel_test(Config) -> + NumContexts = 4, + TestDir = proplists:get_value(test_dir, Config), + + %% Register self + true = register(owngil_parallel_whereis_test, self()), + + Contexts = [begin + {ok, Ctx} = py_context:start_link(N, owngil), + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + Ctx + end || N <- lists:seq(1, NumContexts)], + + Parent = self(), + + %% Parallel whereis + send + [spawn_link(fun() -> + {ok, true} = py_context:call(Ctx, py_test_pid_send, whereis_and_send, + [owngil_parallel_whereis_test, {from_ctx, N}], #{}), + Parent ! {sender_done, N} + end) || {N, Ctx} <- lists:zip(lists:seq(1, NumContexts), Contexts)], + + %% Wait for senders to complete + [receive {sender_done, _} -> ok end || _ <- lists:seq(1, NumContexts)], + + %% Verify all messages received (order may vary) + Messages = collect_from_ctx_messages(NumContexts, []), + NumContexts = length(Messages), + + %% Verify we got all expected context numbers + Expected = lists:sort(lists:seq(1, NumContexts)), + Expected = lists:sort(Messages), + + unregister(owngil_parallel_whereis_test), + [py_context:stop(Ctx) || Ctx <- Contexts], + ok. + +%% @doc Basic atom operations in owngil context +owngil_atom_basic_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + %% Test atom type - get the actual type name for debugging + {ok, TypeName} = py_context:call(Ctx, py_test_pid_send, atom_type_check, + [test_atom], #{}), + ct:pal("Atom type name in OWN_GIL: ~p", [TypeName]), + + %% Test same atoms are equal + {ok, true} = py_context:call(Ctx, py_test_pid_send, atom_equality_test, + [hello, hello], #{}), + + %% Test different atoms are not equal + {ok, true} = py_context:call(Ctx, py_test_pid_send, atom_inequality_test, + [foo, bar], #{}), + + py_context:stop(Ctx). + +%% @doc Atom roundtrip through callback in owngil context +%% Note: In OWN_GIL mode, atoms are converted to Python strings. +%% On roundtrip, they return as binaries (Erlang strings). +owngil_atom_roundtrip_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + %% Pass atom to Python and get it back + TestAtom = test_atom_owngil, + {ok, ReturnedValue} = py_context:call(Ctx, py_test_pid_send, atom_roundtrip, + [TestAtom], #{}), + + %% In OWN_GIL mode, atoms become strings, so we get a binary back + ExpectedBinary = atom_to_binary(TestAtom), + ExpectedBinary = ReturnedValue, + + py_context:stop(Ctx). + +%% @doc Ref type check and uniqueness in owngil context +owngil_ref_roundtrip_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + %% Create refs + Ref1 = make_ref(), + Ref2 = make_ref(), + + %% Verify type check + {ok, true} = py_context:call(Ctx, py_test_pid_send, ref_type_check, [Ref1], #{}), + {ok, true} = py_context:call(Ctx, py_test_pid_send, ref_type_check, [Ref2], #{}), + + %% Verify refs are different + {ok, true} = py_context:call(Ctx, py_test_pid_send, ref_inequality_test, [Ref1, Ref2], #{}), + + py_context:stop(Ctx). + +%% @doc PID equality, hashing, and use as dict key in owngil context +owngil_pid_operations_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + Pid = self(), + + %% Test PID as dict key + {ok, true} = py_context:call(Ctx, py_test_pid_send, pid_as_dict_key, [Pid], #{}), + + %% Test PID in set + {ok, true} = py_context:call(Ctx, py_test_pid_send, pid_in_set, [Pid], #{}), + + %% Test PID equality (existing function) + {ok, true} = py_context:call(Ctx, py_test_pid_send, pid_equality, [Pid, Pid], #{}), + + %% Test PID hash equality (existing function) + {ok, true} = py_context:call(Ctx, py_test_pid_send, pid_hash_equal, [Pid, Pid], #{}), + + py_context:stop(Ctx). diff --git a/test/py_test_pid_send.py b/test/py_test_pid_send.py index c8e4604..ba14af2 100644 --- a/test/py_test_pid_send.py +++ b/test/py_test_pid_send.py @@ -195,3 +195,57 @@ def whereis_and_send(name, msg): erlang.send(pid, msg) return True return False + + +# OWN_GIL test helpers for erlang.* functions + +def whereis_basic(name): + """Look up a registered process by name.""" + return erlang.whereis(name) + + +def atom_equality_test(atom1, atom2): + """Test that two atoms are equal.""" + return atom1 == atom2 + + +def atom_inequality_test(atom1, atom2): + """Test that two different atoms are not equal.""" + return atom1 != atom2 + + +def atom_roundtrip(atom): + """Receive an atom and return it unchanged.""" + return atom + + +def atom_type_check(atom): + """Verify a value is an erlang.Atom type.""" + return type(atom).__name__ + + +def atom_has_value_attr(atom): + """Check if atom has a value attribute.""" + return hasattr(atom, 'value') + + +def ref_type_check(ref): + """Verify a value is an erlang.Ref type.""" + return isinstance(ref, erlang.Ref) + + +def ref_inequality_test(ref1, ref2): + """Test that two different refs are not equal.""" + return ref1 != ref2 + + +def pid_as_dict_key(pid): + """Test using PID as dict key.""" + d = {pid: 'value'} + return d.get(pid) == 'value' + + +def pid_in_set(pid): + """Test using PID in a set.""" + s = {pid} + return pid in s From c758da1acc25ed14d23891158a0567ce311b1082 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Fri, 20 Mar 2026 22:28:22 +0100 Subject: [PATCH 2/3] Make erlang.atom() available in OWN_GIL subinterpreters Add atom() wrapper with caching directly in C module creation, so it's available in OWN_GIL contexts where _erlang_impl package isn't imported. - Add atom_wrapper_code in create_erlang_module() with: - _atom_cache dict for memoization - _MAX_USER_ATOMS limit (10000) to prevent unbounded growth - atom() function that wraps erlang._atom() with caching - Add 3 new tests: atom_create, atom_create_different, atom_cache - Add Python helpers for testing erlang.atom() in OWN_GIL mode --- c_src/py_callback.c | 57 +++++++++++++++++++++++++++++++ test/py_owngil_features_SUITE.erl | 49 ++++++++++++++++++++++++++ test/py_test_pid_send.py | 33 ++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/c_src/py_callback.c b/c_src/py_callback.c index b818348..89b8b6d 100644 --- a/c_src/py_callback.c +++ b/c_src/py_callback.c @@ -3958,6 +3958,63 @@ static int create_erlang_module(void) { Py_DECREF(ext_globals); } + /* Add atom() wrapper with caching for OWN_GIL subinterpreters. + * In OWN_GIL mode, the Python package (_erlang_impl) is not imported, + * so erlang.atom() isn't available. This adds it directly to the C module. + */ + const char *atom_wrapper_code = + "_atom_cache = {}\n" + "_MAX_USER_ATOMS = 10000\n" + "def atom(name):\n" + " '''Create or retrieve a cached atom.\n" + " \n" + " Args:\n" + " name: String name for the atom\n" + " \n" + " Returns:\n" + " An erlang.Atom object\n" + " \n" + " Raises:\n" + " RuntimeError: If atom limit (10000) is reached\n" + " '''\n" + " if name in _atom_cache:\n" + " return _atom_cache[name]\n" + " if len(_atom_cache) >= _MAX_USER_ATOMS:\n" + " raise RuntimeError('Atom limit reached')\n" + " import erlang\n" + " result = erlang._atom(name)\n" + " _atom_cache[name] = result\n" + " return result\n" + "\n" + "import erlang\n" + "erlang.atom = atom\n" + "erlang._atom_cache = _atom_cache\n"; + + PyObject *atom_globals = PyDict_New(); + if (atom_globals != NULL) { + PyObject *builtins = PyEval_GetBuiltins(); + PyDict_SetItemString(atom_globals, "__builtins__", builtins); + + /* Import erlang module into globals so the code can reference it */ + PyObject *sys_modules = PySys_GetObject("modules"); + if (sys_modules != NULL) { + PyObject *erlang_mod = PyDict_GetItemString(sys_modules, "erlang"); + if (erlang_mod != NULL) { + PyDict_SetItemString(atom_globals, "erlang", erlang_mod); + } + } + + PyObject *result = PyRun_String(atom_wrapper_code, Py_file_input, atom_globals, atom_globals); + if (result == NULL) { + /* Non-fatal - atom() just won't be available */ + PyErr_Print(); + PyErr_Clear(); + } else { + Py_DECREF(result); + } + Py_DECREF(atom_globals); + } + return 0; } diff --git a/test/py_owngil_features_SUITE.erl b/test/py_owngil_features_SUITE.erl index f000233..74b8672 100644 --- a/test/py_owngil_features_SUITE.erl +++ b/test/py_owngil_features_SUITE.erl @@ -104,6 +104,9 @@ owngil_whereis_parallel_test/1, owngil_atom_basic_test/1, owngil_atom_roundtrip_test/1, + owngil_atom_create_test/1, + owngil_atom_create_different_test/1, + owngil_atom_cache_test/1, owngil_ref_roundtrip_test/1, owngil_pid_operations_test/1 ]). @@ -187,6 +190,9 @@ groups() -> owngil_whereis_parallel_test, owngil_atom_basic_test, owngil_atom_roundtrip_test, + owngil_atom_create_test, + owngil_atom_create_different_test, + owngil_atom_cache_test, owngil_ref_roundtrip_test, owngil_pid_operations_test ]}]. @@ -1660,6 +1666,49 @@ owngil_atom_roundtrip_test(Config) -> py_context:stop(Ctx). +%% @doc Test erlang.atom() creates equal atoms in owngil context +owngil_atom_create_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + %% Test that erlang.atom() creates equal atoms for same name + {ok, true} = py_context:call(Ctx, py_test_pid_send, atom_create_test, [], #{}), + + py_context:stop(Ctx). + +%% @doc Test erlang.atom() creates unequal atoms for different names +owngil_atom_create_different_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + %% Test that erlang.atom() creates different atoms for different names + {ok, true} = py_context:call(Ctx, py_test_pid_send, atom_create_different_test, [], #{}), + + py_context:stop(Ctx). + +%% @doc Test erlang.atom() caching - same name returns same object +owngil_atom_cache_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + %% Test atom caching - same object returned for same name + {ok, true} = py_context:call(Ctx, py_test_pid_send, atom_cache_test, [], #{}), + + %% Also verify we can get the type name + {ok, TypeName} = py_context:call(Ctx, py_test_pid_send, atom_type_name, [], #{}), + ct:pal("Atom type name from erlang.atom(): ~p", [TypeName]), + + py_context:stop(Ctx). + %% @doc Ref type check and uniqueness in owngil context owngil_ref_roundtrip_test(Config) -> {ok, Ctx} = py_context:start_link(1, owngil), diff --git a/test/py_test_pid_send.py b/test/py_test_pid_send.py index ba14af2..0a39bf3 100644 --- a/test/py_test_pid_send.py +++ b/test/py_test_pid_send.py @@ -249,3 +249,36 @@ def pid_in_set(pid): """Test using PID in a set.""" s = {pid} return pid in s + + +# OWN_GIL test helpers for erlang.atom() creation + +def atom_create_test(): + """Create atom using erlang.atom() in OWN_GIL and test equality.""" + import erlang + a1 = erlang.atom('test_owngil_atom') + a2 = erlang.atom('test_owngil_atom') + return a1 == a2 + + +def atom_create_different_test(): + """Test different atoms created with erlang.atom() are not equal.""" + import erlang + a1 = erlang.atom('atom_x') + a2 = erlang.atom('atom_y') + return a1 != a2 + + +def atom_cache_test(): + """Test atom caching - same name returns same object.""" + import erlang + a1 = erlang.atom('cached_atom_owngil') + a2 = erlang.atom('cached_atom_owngil') + return a1 is a2 # Should be same object due to caching + + +def atom_type_name(): + """Get type name of atom created with erlang.atom().""" + import erlang + a = erlang.atom('type_check_atom') + return type(a).__name__ From 81890acb71887c71d8101acecec6e0ddf88cbf47 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Fri, 20 Mar 2026 22:38:07 +0100 Subject: [PATCH 3/3] Add OWN_GIL tests for Channel, ByteChannel, and Buffer operations - Channel class: receive, iteration, context manager - ByteChannel: send_bytes, try_receive_bytes, iteration - Buffer: read methods, at_eof detection --- test/py_owngil_features_SUITE.erl | 209 +++++++++++++++++++++++++++++- test/py_test_pid_send.py | 70 ++++++++++ 2 files changed, 277 insertions(+), 2 deletions(-) diff --git a/test/py_owngil_features_SUITE.erl b/test/py_owngil_features_SUITE.erl index 74b8672..82545b5 100644 --- a/test/py_owngil_features_SUITE.erl +++ b/test/py_owngil_features_SUITE.erl @@ -108,7 +108,18 @@ owngil_atom_create_different_test/1, owngil_atom_cache_test/1, owngil_ref_roundtrip_test/1, - owngil_pid_operations_test/1 + owngil_pid_operations_test/1, + %% Channel class tests + owngil_channel_class_test/1, + owngil_channel_iteration_test/1, + owngil_channel_context_manager_test/1, + %% ByteChannel tests + owngil_bytechannel_send_receive_test/1, + owngil_bytechannel_try_receive_test/1, + owngil_bytechannel_iteration_test/1, + %% Buffer tests + owngil_buffer_read_methods_test/1, + owngil_buffer_at_eof_test/1 ]). all() -> @@ -194,7 +205,18 @@ groups() -> owngil_atom_create_different_test, owngil_atom_cache_test, owngil_ref_roundtrip_test, - owngil_pid_operations_test + owngil_pid_operations_test, + %% Channel tests + owngil_channel_class_test, + owngil_channel_iteration_test, + owngil_channel_context_manager_test, + %% ByteChannel tests + owngil_bytechannel_send_receive_test, + owngil_bytechannel_try_receive_test, + owngil_bytechannel_iteration_test, + %% Buffer tests + owngil_buffer_read_methods_test, + owngil_buffer_at_eof_test ]}]. init_per_suite(Config) -> @@ -1753,3 +1775,186 @@ owngil_pid_operations_test(Config) -> {ok, true} = py_context:call(Ctx, py_test_pid_send, pid_hash_equal, [Pid, Pid], #{}), py_context:stop(Ctx). + +%%% ============================================================================ +%%% Channel Class Tests (erlang_api group) +%%% ============================================================================ + +%% @doc Test Channel class receive/try_receive in OWN_GIL +owngil_channel_class_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + {ok, Ch} = py_channel:new(), + + %% Send message from Erlang + ok = py_channel:send(Ch, <<"test_channel_class">>), + + %% Receive via Channel class in Python + {ok, <<"test_channel_class">>} = py_context:call(Ctx, py_test_pid_send, + channel_receive_test, [Ch], #{}), + + py_channel:close(Ch), + py_context:stop(Ctx). + +%% @doc Test Channel iteration in OWN_GIL +owngil_channel_iteration_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + {ok, Ch} = py_channel:new(), + + %% Send multiple messages + ok = py_channel:send(Ch, <<"msg1">>), + ok = py_channel:send(Ch, <<"msg2">>), + ok = py_channel:send(Ch, <<"msg3">>), + + %% Iterate in Python + {ok, 3} = py_context:call(Ctx, py_test_pid_send, + channel_iteration_test, [Ch, 3], #{}), + + py_channel:close(Ch), + py_context:stop(Ctx). + +%% @doc Test Channel as context manager in OWN_GIL +owngil_channel_context_manager_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + {ok, Ch} = py_channel:new(), + + %% Send a message for the context manager test to try_receive + ok = py_channel:send(Ch, <<"context_msg">>), + + %% Test context manager usage + {ok, true} = py_context:call(Ctx, py_test_pid_send, + channel_context_manager_test, [Ch], #{}), + + py_channel:close(Ch), + py_context:stop(Ctx). + +%%% ============================================================================ +%%% ByteChannel Tests (erlang_api group) +%%% ============================================================================ + +%% @doc Test ByteChannel send_bytes/receive_bytes in OWN_GIL +owngil_bytechannel_send_receive_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + {ok, Ch} = py_byte_channel:new(), + + %% Send bytes from Python via ByteChannel + {ok, true} = py_context:call(Ctx, py_test_pid_send, + bytechannel_send_receive_test, [Ch], #{}), + + %% Receive raw bytes from Erlang using byte channel API + {ok, <<"hello_owngil">>} = py_byte_channel:try_receive(Ch), + + py_byte_channel:close(Ch), + py_context:stop(Ctx). + +%% @doc Test ByteChannel non-blocking try_receive_bytes in OWN_GIL +owngil_bytechannel_try_receive_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + {ok, Ch} = py_byte_channel:new(), + + %% Send raw bytes from Erlang using byte channel API + ok = py_byte_channel:send(Ch, <<"bytes_data">>), + + %% Receive via ByteChannel in Python + {ok, <<"bytes_data">>} = py_context:call(Ctx, py_test_pid_send, + bytechannel_try_receive_test, [Ch], #{}), + + py_byte_channel:close(Ch), + py_context:stop(Ctx). + +%% @doc Test ByteChannel iteration in OWN_GIL +owngil_bytechannel_iteration_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + {ok, Ch} = py_byte_channel:new(), + + %% Send multiple byte chunks using byte channel API + ok = py_byte_channel:send(Ch, <<"chunk1">>), + ok = py_byte_channel:send(Ch, <<"chunk2">>), + + %% Iterate in Python + {ok, 2} = py_context:call(Ctx, py_test_pid_send, + bytechannel_iteration_test, [Ch, 2], #{}), + + py_byte_channel:close(Ch), + py_context:stop(Ctx). + +%%% ============================================================================ +%%% Buffer Tests (erlang_api group) +%%% ============================================================================ + +%% @doc Test buffer read/read_nonblock/readable_amount in OWN_GIL +owngil_buffer_read_methods_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + {ok, Buf} = py_buffer:new(), + + %% Write data to buffer + ok = py_buffer:write(Buf, <<"test_data">>), + + %% Test read methods from Python + {ok, Result} = py_context:call(Ctx, py_test_pid_send, + buffer_read_methods_test, [Buf], #{}), + + %% Verify readable amount and data + #{<<"readable">> := Readable, <<"data">> := Data} = Result, + true = Readable > 0, + <<"test_data">> = Data, + + py_buffer:close(Buf), + py_context:stop(Ctx). + +%% @doc Test buffer at_eof detection in OWN_GIL +owngil_buffer_at_eof_test(Config) -> + {ok, Ctx} = py_context:start_link(1, owngil), + TestDir = proplists:get_value(test_dir, Config), + + ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format( + "import sys; sys.path.insert(0, '~s')", [TestDir]))), + + {ok, Buf} = py_buffer:new(), + + %% Buffer not yet at EOF (not closed) + {ok, false} = py_context:call(Ctx, py_test_pid_send, + buffer_at_eof_test, [Buf], #{}), + + %% Close buffer + ok = py_buffer:close(Buf), + + %% Now at EOF + {ok, true} = py_context:call(Ctx, py_test_pid_send, + buffer_at_eof_test, [Buf], #{}), + + py_context:stop(Ctx). diff --git a/test/py_test_pid_send.py b/test/py_test_pid_send.py index 0a39bf3..49d6c87 100644 --- a/test/py_test_pid_send.py +++ b/test/py_test_pid_send.py @@ -282,3 +282,73 @@ def atom_type_name(): import erlang a = erlang.atom('type_check_atom') return type(a).__name__ + + +# OWN_GIL test helpers for Channel, ByteChannel, and Buffer operations + +def channel_receive_test(channel_ref): + """Test Channel.try_receive() in OWN_GIL.""" + from erlang import Channel + ch = Channel(channel_ref) + msg = ch.try_receive() + return msg + + +def channel_iteration_test(channel_ref, expected_count): + """Test Channel iteration.""" + from erlang import Channel + ch = Channel(channel_ref) + messages = [] + for msg in ch: + messages.append(msg) + if len(messages) >= expected_count: + break + return len(messages) + + +def channel_context_manager_test(channel_ref): + """Test Channel as context manager.""" + from erlang import Channel + with Channel(channel_ref) as ch: + msg = ch.try_receive() + return msg is not None or True # Just verifies context manager works + + +def bytechannel_send_receive_test(channel_ref): + """Test ByteChannel send_bytes/try_receive_bytes.""" + from erlang import ByteChannel + ch = ByteChannel(channel_ref) + ch.send_bytes(b"hello_owngil") + return True + + +def bytechannel_try_receive_test(channel_ref): + """Test ByteChannel try_receive_bytes.""" + from erlang import ByteChannel + ch = ByteChannel(channel_ref) + data = ch.try_receive_bytes() + return data + + +def bytechannel_iteration_test(channel_ref, expected_count): + """Test ByteChannel iteration.""" + from erlang import ByteChannel + ch = ByteChannel(channel_ref) + chunks = [] + for chunk in ch: + chunks.append(chunk) + if len(chunks) >= expected_count: + break + return len(chunks) + + +def buffer_read_methods_test(buf): + """Test buffer read methods.""" + readable = buf.readable_amount() + data = buf.read_nonblock(readable) if readable > 0 else b'' + return {'readable': readable, 'data': data} + + +def buffer_at_eof_test(buf): + """Test buffer at_eof detection.""" + return buf.at_eof()