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+
99111all () ->
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
109122groups () ->
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
171194init_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+
14061441create_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 ).
0 commit comments