Skip to content

Commit 3c9f55d

Browse files
committed
Fix intermittent test failures on free-threaded Python
- Add startup synchronization in init_per_suite to wait for event loop to be fully initialized before running tests - Add proper error handling for application startup failures - Increase timer delays (10ms -> 20ms minimum) for CI reliability - Add wait_for_event_loop/1 helper that verifies loop is functional The race condition occurred because free-threaded Python allows multiple threads to run simultaneously, and tests could start before the event loop policy was fully installed.
1 parent 1d7c962 commit 3c9f55d

File tree

1 file changed

+44
-9
lines changed

1 file changed

+44
-9
lines changed

test/py_event_loop_SUITE.erl

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,41 @@ all() ->
6666
].
6767

6868
init_per_suite(Config) ->
69-
{ok, _} = application:ensure_all_started(erlang_python),
70-
Config.
69+
case application:ensure_all_started(erlang_python) of
70+
{ok, _} ->
71+
%% Wait for event loop to be fully initialized
72+
%% This is important for free-threaded Python where initialization
73+
%% can race with test execution
74+
case wait_for_event_loop(5000) of
75+
ok ->
76+
Config;
77+
{error, Reason} ->
78+
ct:fail({event_loop_not_ready, Reason})
79+
end;
80+
{error, {App, Reason}} ->
81+
ct:fail({failed_to_start, App, Reason})
82+
end.
83+
84+
%% Wait for the event loop to be fully initialized
85+
wait_for_event_loop(Timeout) when Timeout =< 0 ->
86+
{error, timeout};
87+
wait_for_event_loop(Timeout) ->
88+
case py_event_loop:get_loop() of
89+
{ok, LoopRef} when is_reference(LoopRef) ->
90+
%% Verify the event loop is actually functional by checking
91+
%% we can create and immediately cancel a timer
92+
case py_nif:event_loop_new() of
93+
{ok, TestLoop} ->
94+
py_nif:event_loop_destroy(TestLoop),
95+
ok;
96+
_ ->
97+
timer:sleep(100),
98+
wait_for_event_loop(Timeout - 100)
99+
end;
100+
_ ->
101+
timer:sleep(100),
102+
wait_for_event_loop(Timeout - 100)
103+
end.
71104

72105
end_per_suite(_Config) ->
73106
ok = application:stop(erlang_python),
@@ -257,12 +290,13 @@ test_multiple_timers(_Config) ->
257290
ok = py_nif:event_loop_set_router(LoopRef, RouterPid),
258291

259292
%% Create timers with different delays
260-
{ok, _} = py_nif:call_later(LoopRef, 10, 1),
261-
{ok, _} = py_nif:call_later(LoopRef, 30, 2),
262-
{ok, _} = py_nif:call_later(LoopRef, 50, 3),
293+
%% Use larger delays for CI reliability (especially on free-threaded Python)
294+
{ok, _} = py_nif:call_later(LoopRef, 20, 1),
295+
{ok, _} = py_nif:call_later(LoopRef, 50, 2),
296+
{ok, _} = py_nif:call_later(LoopRef, 80, 3),
263297

264-
%% Wait for all
265-
timer:sleep(150),
298+
%% Wait for all with margin
299+
timer:sleep(200),
266300

267301
Pending = py_nif:get_pending(LoopRef),
268302

@@ -337,8 +371,9 @@ test_get_pending_clears_queue(_Config) ->
337371
ok = py_nif:event_loop_set_router(LoopRef, RouterPid),
338372

339373
%% Create a timer that fires quickly
340-
{ok, _} = py_nif:call_later(LoopRef, 10, 999),
341-
timer:sleep(50),
374+
%% Use 20ms minimum for CI reliability
375+
{ok, _} = py_nif:call_later(LoopRef, 20, 999),
376+
timer:sleep(100),
342377

343378
%% First get_pending should return the event
344379
Pending1 = py_nif:get_pending(LoopRef),

0 commit comments

Comments
 (0)