Skip to content

Commit b227062

Browse files
committed
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
1 parent efd4400 commit b227062

2 files changed

Lines changed: 281 additions & 1 deletion

File tree

test/py_owngil_features_SUITE.erl

Lines changed: 227 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@
9696
owngil_local_env_call_test/1
9797
]).
9898

99+
%% Erlang API tests (whereis, atom, Ref, Pid)
100+
-export([
101+
owngil_whereis_basic_test/1,
102+
owngil_whereis_nonexistent_test/1,
103+
owngil_whereis_and_send_test/1,
104+
owngil_whereis_parallel_test/1,
105+
owngil_atom_basic_test/1,
106+
owngil_atom_roundtrip_test/1,
107+
owngil_ref_roundtrip_test/1,
108+
owngil_pid_operations_test/1
109+
]).
110+
99111
all() ->
100112
[{group, channels},
101113
{group, buffers},
@@ -104,7 +116,8 @@ all() ->
104116
{group, reactor},
105117
{group, async_task},
106118
{group, asyncio},
107-
{group, local_env}].
119+
{group, local_env},
120+
{group, erlang_api}].
108121

109122
groups() ->
110123
[{channels, [sequence], [
@@ -166,6 +179,16 @@ groups() ->
166179
{local_env, [sequence], [
167180
owngil_local_env_isolation_test,
168181
owngil_local_env_call_test
182+
]},
183+
{erlang_api, [sequence], [
184+
owngil_whereis_basic_test,
185+
owngil_whereis_nonexistent_test,
186+
owngil_whereis_and_send_test,
187+
owngil_whereis_parallel_test,
188+
owngil_atom_basic_test,
189+
owngil_atom_roundtrip_test,
190+
owngil_ref_roundtrip_test,
191+
owngil_pid_operations_test
169192
]}].
170193

171194
init_per_suite(Config) ->
@@ -1403,6 +1426,18 @@ drain_tuple_messages(N) ->
14031426
ok
14041427
end.
14051428

1429+
collect_from_ctx_messages(0, Acc) ->
1430+
Acc;
1431+
collect_from_ctx_messages(N, Acc) ->
1432+
receive
1433+
%% Atom key (direct from Erlang)
1434+
{from_ctx, CtxNum} -> collect_from_ctx_messages(N - 1, [CtxNum | Acc]);
1435+
%% Binary key (roundtripped through Python)
1436+
{<<"from_ctx">>, CtxNum} -> collect_from_ctx_messages(N - 1, [CtxNum | Acc])
1437+
after 5000 ->
1438+
ct:fail({timeout_collecting_messages, got, length(Acc), expected, N + length(Acc)})
1439+
end.
1440+
14061441
create_socketpair() ->
14071442
{ok, LSock} = gen_tcp:listen(0, [binary, {active, false}, {reuseaddr, true}]),
14081443
{ok, Port} = inet:port(LSock),
@@ -1478,3 +1513,194 @@ def greet(name):
14781513
{ok, 2.0} = py_nif:context_call(CtxRef, <<"math">>, <<"sqrt">>, [4.0], #{}, Env),
14791514

14801515
py_context:stop(Ctx).
1516+
1517+
%%% ============================================================================
1518+
%%% Erlang API Tests (whereis, atom, Ref, Pid)
1519+
%%% ============================================================================
1520+
1521+
%% @doc Basic whereis lookup in owngil context
1522+
owngil_whereis_basic_test(Config) ->
1523+
{ok, Ctx} = py_context:start_link(1, owngil),
1524+
TestDir = proplists:get_value(test_dir, Config),
1525+
1526+
ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format(
1527+
"import sys; sys.path.insert(0, '~s')", [TestDir]))),
1528+
1529+
%% Register self under a name
1530+
true = register(owngil_whereis_test_proc, self()),
1531+
1532+
%% Look up the process from Python
1533+
{ok, FoundPid} = py_context:call(Ctx, py_test_pid_send, whereis_basic,
1534+
[owngil_whereis_test_proc], #{}),
1535+
1536+
%% Verify it matches
1537+
Self = self(),
1538+
Self = FoundPid,
1539+
1540+
unregister(owngil_whereis_test_proc),
1541+
py_context:stop(Ctx).
1542+
1543+
%% @doc Lookup non-existent name returns None in owngil context
1544+
owngil_whereis_nonexistent_test(Config) ->
1545+
{ok, Ctx} = py_context:start_link(1, owngil),
1546+
TestDir = proplists:get_value(test_dir, Config),
1547+
1548+
ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format(
1549+
"import sys; sys.path.insert(0, '~s')", [TestDir]))),
1550+
1551+
%% Look up a name that doesn't exist
1552+
{ok, none} = py_context:call(Ctx, py_test_pid_send, whereis_basic,
1553+
[nonexistent_proc_name_xyz], #{}),
1554+
1555+
py_context:stop(Ctx).
1556+
1557+
%% @doc Combined whereis + send pattern in owngil context
1558+
owngil_whereis_and_send_test(Config) ->
1559+
{ok, Ctx} = py_context:start_link(1, owngil),
1560+
TestDir = proplists:get_value(test_dir, Config),
1561+
1562+
ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format(
1563+
"import sys; sys.path.insert(0, '~s')", [TestDir]))),
1564+
1565+
%% Register self under a name
1566+
true = register(owngil_whereis_send_test, self()),
1567+
1568+
%% Use whereis_and_send from Python
1569+
{ok, true} = py_context:call(Ctx, py_test_pid_send, whereis_and_send,
1570+
[owngil_whereis_send_test, <<"hello_from_whereis">>], #{}),
1571+
1572+
%% Verify message received
1573+
receive <<"hello_from_whereis">> -> ok
1574+
after 5000 -> ct:fail(timeout)
1575+
end,
1576+
1577+
unregister(owngil_whereis_send_test),
1578+
py_context:stop(Ctx).
1579+
1580+
%% @doc Parallel whereis + send from multiple owngil contexts
1581+
owngil_whereis_parallel_test(Config) ->
1582+
NumContexts = 4,
1583+
TestDir = proplists:get_value(test_dir, Config),
1584+
1585+
%% Register self
1586+
true = register(owngil_parallel_whereis_test, self()),
1587+
1588+
Contexts = [begin
1589+
{ok, Ctx} = py_context:start_link(N, owngil),
1590+
ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format(
1591+
"import sys; sys.path.insert(0, '~s')", [TestDir]))),
1592+
Ctx
1593+
end || N <- lists:seq(1, NumContexts)],
1594+
1595+
Parent = self(),
1596+
1597+
%% Parallel whereis + send
1598+
[spawn_link(fun() ->
1599+
{ok, true} = py_context:call(Ctx, py_test_pid_send, whereis_and_send,
1600+
[owngil_parallel_whereis_test, {from_ctx, N}], #{}),
1601+
Parent ! {sender_done, N}
1602+
end) || {N, Ctx} <- lists:zip(lists:seq(1, NumContexts), Contexts)],
1603+
1604+
%% Wait for senders to complete
1605+
[receive {sender_done, _} -> ok end || _ <- lists:seq(1, NumContexts)],
1606+
1607+
%% Verify all messages received (order may vary)
1608+
Messages = collect_from_ctx_messages(NumContexts, []),
1609+
NumContexts = length(Messages),
1610+
1611+
%% Verify we got all expected context numbers
1612+
Expected = lists:sort(lists:seq(1, NumContexts)),
1613+
Expected = lists:sort(Messages),
1614+
1615+
unregister(owngil_parallel_whereis_test),
1616+
[py_context:stop(Ctx) || Ctx <- Contexts],
1617+
ok.
1618+
1619+
%% @doc Basic atom operations in owngil context
1620+
owngil_atom_basic_test(Config) ->
1621+
{ok, Ctx} = py_context:start_link(1, owngil),
1622+
TestDir = proplists:get_value(test_dir, Config),
1623+
1624+
ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format(
1625+
"import sys; sys.path.insert(0, '~s')", [TestDir]))),
1626+
1627+
%% Test atom type - get the actual type name for debugging
1628+
{ok, TypeName} = py_context:call(Ctx, py_test_pid_send, atom_type_check,
1629+
[test_atom], #{}),
1630+
ct:pal("Atom type name in OWN_GIL: ~p", [TypeName]),
1631+
1632+
%% Test same atoms are equal
1633+
{ok, true} = py_context:call(Ctx, py_test_pid_send, atom_equality_test,
1634+
[hello, hello], #{}),
1635+
1636+
%% Test different atoms are not equal
1637+
{ok, true} = py_context:call(Ctx, py_test_pid_send, atom_inequality_test,
1638+
[foo, bar], #{}),
1639+
1640+
py_context:stop(Ctx).
1641+
1642+
%% @doc Atom roundtrip through callback in owngil context
1643+
%% Note: In OWN_GIL mode, atoms are converted to Python strings.
1644+
%% On roundtrip, they return as binaries (Erlang strings).
1645+
owngil_atom_roundtrip_test(Config) ->
1646+
{ok, Ctx} = py_context:start_link(1, owngil),
1647+
TestDir = proplists:get_value(test_dir, Config),
1648+
1649+
ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format(
1650+
"import sys; sys.path.insert(0, '~s')", [TestDir]))),
1651+
1652+
%% Pass atom to Python and get it back
1653+
TestAtom = test_atom_owngil,
1654+
{ok, ReturnedValue} = py_context:call(Ctx, py_test_pid_send, atom_roundtrip,
1655+
[TestAtom], #{}),
1656+
1657+
%% In OWN_GIL mode, atoms become strings, so we get a binary back
1658+
ExpectedBinary = atom_to_binary(TestAtom),
1659+
ExpectedBinary = ReturnedValue,
1660+
1661+
py_context:stop(Ctx).
1662+
1663+
%% @doc Ref type check and uniqueness in owngil context
1664+
owngil_ref_roundtrip_test(Config) ->
1665+
{ok, Ctx} = py_context:start_link(1, owngil),
1666+
TestDir = proplists:get_value(test_dir, Config),
1667+
1668+
ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format(
1669+
"import sys; sys.path.insert(0, '~s')", [TestDir]))),
1670+
1671+
%% Create refs
1672+
Ref1 = make_ref(),
1673+
Ref2 = make_ref(),
1674+
1675+
%% Verify type check
1676+
{ok, true} = py_context:call(Ctx, py_test_pid_send, ref_type_check, [Ref1], #{}),
1677+
{ok, true} = py_context:call(Ctx, py_test_pid_send, ref_type_check, [Ref2], #{}),
1678+
1679+
%% Verify refs are different
1680+
{ok, true} = py_context:call(Ctx, py_test_pid_send, ref_inequality_test, [Ref1, Ref2], #{}),
1681+
1682+
py_context:stop(Ctx).
1683+
1684+
%% @doc PID equality, hashing, and use as dict key in owngil context
1685+
owngil_pid_operations_test(Config) ->
1686+
{ok, Ctx} = py_context:start_link(1, owngil),
1687+
TestDir = proplists:get_value(test_dir, Config),
1688+
1689+
ok = py_context:exec(Ctx, iolist_to_binary(io_lib:format(
1690+
"import sys; sys.path.insert(0, '~s')", [TestDir]))),
1691+
1692+
Pid = self(),
1693+
1694+
%% Test PID as dict key
1695+
{ok, true} = py_context:call(Ctx, py_test_pid_send, pid_as_dict_key, [Pid], #{}),
1696+
1697+
%% Test PID in set
1698+
{ok, true} = py_context:call(Ctx, py_test_pid_send, pid_in_set, [Pid], #{}),
1699+
1700+
%% Test PID equality (existing function)
1701+
{ok, true} = py_context:call(Ctx, py_test_pid_send, pid_equality, [Pid, Pid], #{}),
1702+
1703+
%% Test PID hash equality (existing function)
1704+
{ok, true} = py_context:call(Ctx, py_test_pid_send, pid_hash_equal, [Pid, Pid], #{}),
1705+
1706+
py_context:stop(Ctx).

test/py_test_pid_send.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,57 @@ def whereis_and_send(name, msg):
195195
erlang.send(pid, msg)
196196
return True
197197
return False
198+
199+
200+
# OWN_GIL test helpers for erlang.* functions
201+
202+
def whereis_basic(name):
203+
"""Look up a registered process by name."""
204+
return erlang.whereis(name)
205+
206+
207+
def atom_equality_test(atom1, atom2):
208+
"""Test that two atoms are equal."""
209+
return atom1 == atom2
210+
211+
212+
def atom_inequality_test(atom1, atom2):
213+
"""Test that two different atoms are not equal."""
214+
return atom1 != atom2
215+
216+
217+
def atom_roundtrip(atom):
218+
"""Receive an atom and return it unchanged."""
219+
return atom
220+
221+
222+
def atom_type_check(atom):
223+
"""Verify a value is an erlang.Atom type."""
224+
return type(atom).__name__
225+
226+
227+
def atom_has_value_attr(atom):
228+
"""Check if atom has a value attribute."""
229+
return hasattr(atom, 'value')
230+
231+
232+
def ref_type_check(ref):
233+
"""Verify a value is an erlang.Ref type."""
234+
return isinstance(ref, erlang.Ref)
235+
236+
237+
def ref_inequality_test(ref1, ref2):
238+
"""Test that two different refs are not equal."""
239+
return ref1 != ref2
240+
241+
242+
def pid_as_dict_key(pid):
243+
"""Test using PID as dict key."""
244+
d = {pid: 'value'}
245+
return d.get(pid) == 'value'
246+
247+
248+
def pid_in_set(pid):
249+
"""Test using PID in a set."""
250+
s = {pid}
251+
return pid in s

0 commit comments

Comments
 (0)