2222 test_callback_with_complex_types /1 ,
2323 test_multiple_sequential_callbacks /1 ,
2424 test_call_from_non_worker_thread /1 ,
25+ test_thread_callback_fd_above_fd_setsize /1 ,
2526 test_callback_with_try_except /1 ,
2627 test_async_call /1 ,
2728 test_callback_name_registry /1 ,
@@ -38,6 +39,7 @@ all() ->
3839 test_callback_with_complex_types ,
3940 test_multiple_sequential_callbacks ,
4041 test_call_from_non_worker_thread ,
42+ test_thread_callback_fd_above_fd_setsize ,
4143 test_callback_with_try_except ,
4244 test_async_call ,
4345 test_callback_name_registry ,
@@ -133,6 +135,12 @@ test_etf_decode_safe(_Config) ->
133135
134136 % % Negative: many DISTINCT brand-new atoms wrapped in marker-shaped binaries
135137 % % must all come back verbatim, never decoded into atoms.
138+ % % Warm up first so modules loaded on first use (base64, the callback
139+ % % path) do not count as atoms minted by the round trips below.
140+ Warm = etf_marker (novel_atom_etf (" zzqx_etf_safe_warmup" )),
141+ py :register_function (etf_probe_novel , fun (_ ) -> Warm end ),
142+ {ok , Warm } = py :eval (<<" __import__('erlang').call('etf_probe_novel', [])" >>),
143+ assert_atom_absent (" zzqx_etf_safe_warmup" ),
136144 Before = erlang :system_info (atom_count ),
137145 N = 50 ,
138146 lists :foreach (
@@ -349,6 +357,32 @@ test_call_from_non_worker_thread(_Config) ->
349357 py :unregister_function (simple_add ),
350358 ok .
351359
360+ % % @doc Thread callbacks must work when the response pipe lands on an fd
361+ % % above FD_SETSIZE: select() is undefined there and used to make the
362+ % % handler ready-wait time out after 10 s ("Failed to spawn thread handler").
363+ test_thread_callback_fd_above_fd_setsize (_Config ) ->
364+ py :register_function (high_fd_add , fun ([A , B ]) -> A + B end ),
365+ TestDir = filename :join (code :lib_dir (erlang_python ), " test" ),
366+ ok = py :exec (iolist_to_binary (io_lib :format (
367+ " import sys; sys.path.insert(0, '~s ')" , [TestDir ]))),
368+ try
369+ case py :call (py_test_high_fds , prepare , [1200 ]) of
370+ {ok , Last } when Last >= 1200 ->
371+ ct :log (" highest fd opened: ~p " , [Last ]),
372+ N = 8 ,
373+ {ok , Results } = py :call (py_test_high_fds , call_from_threads , [N ]),
374+ Expected = [I + 1 || I <- lists :seq (0 , N - 1 )],
375+ Expected = Results ;
376+ {ok , - 1 } ->
377+ {skip , " fd hard limit too low to open 1200 files" };
378+ Other ->
379+ ct :fail ({prepare_failed , Other })
380+ end
381+ after
382+ _ = py :call (py_test_high_fds , cleanup , []),
383+ py :unregister_function (high_fd_add )
384+ end .
385+
352386% % @doc Test that erlang.call() works even when wrapped in try/except blocks.
353387% % This simulates ASGI/WSGI middleware that catches all exceptions.
354388% % The flag-based detection should work even when the SuspensionRequired
0 commit comments