|
29 | 29 | %% Edge cases |
30 | 30 | test_empty_args/1, |
31 | 31 | test_large_result/1, |
32 | | - test_nested_data/1 |
| 32 | + test_nested_data/1, |
| 33 | + %% Thread-local context tests |
| 34 | + test_thread_local_event_loop/1 |
33 | 35 | ]). |
34 | 36 |
|
35 | 37 | all() -> |
@@ -58,82 +60,16 @@ all() -> |
58 | 60 | %% Edge cases |
59 | 61 | test_empty_args, |
60 | 62 | test_large_result, |
61 | | - test_nested_data |
| 63 | + test_nested_data, |
| 64 | + %% Thread-local context tests |
| 65 | + test_thread_local_event_loop |
62 | 66 | ]. |
63 | 67 |
|
64 | 68 | groups() -> []. |
65 | 69 |
|
66 | 70 | init_per_suite(Config) -> |
67 | 71 | application:ensure_all_started(erlang_python), |
68 | 72 | timer:sleep(500), % Allow event loop to initialize |
69 | | - |
70 | | - %% Create test Python module with various test functions |
71 | | - TestModule = <<" |
72 | | -import asyncio |
73 | | -
|
74 | | -# Simple sync function |
75 | | -def sync_func(): |
76 | | - return 'sync_result' |
77 | | -
|
78 | | -def sync_add(x, y): |
79 | | - return x + y |
80 | | -
|
81 | | -def sync_multiply(x, y): |
82 | | - return x * y |
83 | | -
|
84 | | -# Async coroutines |
85 | | -async def simple_async(): |
86 | | - await asyncio.sleep(0.001) |
87 | | - return 'async_result' |
88 | | -
|
89 | | -async def add_async(x, y): |
90 | | - await asyncio.sleep(0.001) |
91 | | - return x + y |
92 | | -
|
93 | | -async def multiply_async(x, y): |
94 | | - await asyncio.sleep(0.001) |
95 | | - return x * y |
96 | | -
|
97 | | -async def sleep_and_return(seconds, value): |
98 | | - await asyncio.sleep(seconds) |
99 | | - return value |
100 | | -
|
101 | | -# Error cases |
102 | | -async def failing_async(): |
103 | | - await asyncio.sleep(0.001) |
104 | | - raise ValueError('test_error') |
105 | | -
|
106 | | -def sync_error(): |
107 | | - raise RuntimeError('sync_error') |
108 | | -
|
109 | | -# Edge cases |
110 | | -def return_none(): |
111 | | - return None |
112 | | -
|
113 | | -def return_empty_list(): |
114 | | - return [] |
115 | | -
|
116 | | -def return_empty_dict(): |
117 | | - return {} |
118 | | -
|
119 | | -def return_large_list(n): |
120 | | - return list(range(n)) |
121 | | -
|
122 | | -def return_nested(): |
123 | | - return {'a': [1, 2, {'b': 3}], 'c': (4, 5)} |
124 | | -
|
125 | | -def echo(*args, **kwargs): |
126 | | - return {'args': args, 'kwargs': kwargs} |
127 | | -
|
128 | | -# Slow function for timeout tests |
129 | | -async def slow_async(seconds): |
130 | | - await asyncio.sleep(seconds) |
131 | | - return 'completed' |
132 | | -">>, |
133 | | - |
134 | | - %% Execute test module to define functions |
135 | | - ok = py:exec(TestModule), |
136 | | - |
137 | 73 | Config. |
138 | 74 |
|
139 | 75 | end_per_suite(_Config) -> |
@@ -233,10 +169,10 @@ test_async_sleep(_Config) -> |
233 | 169 | %% ============================================================================ |
234 | 170 |
|
235 | 171 | test_async_error(_Config) -> |
236 | | - %% Test error from async coroutine |
237 | | - Ref = py_event_loop:create_task('__main__', failing_async, []), |
| 172 | + %% Test error handling - math.sqrt(-1) raises ValueError |
| 173 | + Ref = py_event_loop:create_task(math, sqrt, [-1.0]), |
238 | 174 | Result = py_event_loop:await(Ref, 5000), |
239 | | - ct:log("failing_async() = ~p", [Result]), |
| 175 | + ct:log("math.sqrt(-1) = ~p", [Result]), |
240 | 176 | case Result of |
241 | 177 | {error, _} -> ok; |
242 | 178 | {ok, _} -> ct:fail("Expected error but got success") |
@@ -265,10 +201,11 @@ test_invalid_function(_Config) -> |
265 | 201 | end. |
266 | 202 |
|
267 | 203 | test_timeout(_Config) -> |
268 | | - %% Test timeout handling |
269 | | - Ref = py_event_loop:create_task('__main__', slow_async, [10.0]), |
270 | | - Result = py_event_loop:await(Ref, 100), % 100ms timeout, but sleep is 10s |
271 | | - ct:log("slow_async with short timeout: ~p", [Result]), |
| 204 | + %% Test timeout handling - we just verify await timeout works |
| 205 | + %% Use a short sleep (0.5s) but even shorter timeout (50ms) |
| 206 | + Ref = py_event_loop:create_task(time, sleep, [0.5]), |
| 207 | + Result = py_event_loop:await(Ref, 50), |
| 208 | + ct:log("time.sleep(0.5) with 50ms timeout: ~p", [Result]), |
272 | 209 | {error, timeout} = Result. |
273 | 210 |
|
274 | 211 | %% ============================================================================ |
@@ -372,3 +309,35 @@ test_nested_data(_Config) -> |
372 | 309 | #{<<"a">> := AVal, <<"b">> := BVal} = Result, |
373 | 310 | [1, 2, 3] = AVal, |
374 | 311 | #{<<"c">> := 4} = BVal. |
| 312 | + |
| 313 | +%% ============================================================================ |
| 314 | +%% Thread-local context tests |
| 315 | +%% ============================================================================ |
| 316 | + |
| 317 | +test_thread_local_event_loop(_Config) -> |
| 318 | + %% Test that the event loop thread-local context is properly set. |
| 319 | + %% |
| 320 | + %% This verifies the fix for the thread-local event loop context issue. |
| 321 | + %% process_ready_tasks runs on dirty NIF scheduler threads (named 'Dummy-X'), |
| 322 | + %% not the main thread. Without the fix, asyncio.get_running_loop() would |
| 323 | + %% raise RuntimeError: "There is no current event loop in thread 'Dummy-1'." |
| 324 | + %% |
| 325 | + %% The fix sets events._set_running_loop() before processing tasks. |
| 326 | + %% |
| 327 | + %% We verify this by running multiple concurrent async tasks - if the |
| 328 | + %% running loop context weren't set, task creation would fail. |
| 329 | + NumTasks = 20, |
| 330 | + Refs = [py_event_loop:create_task(math, sqrt, [float(N * N)]) |
| 331 | + || N <- lists:seq(1, NumTasks)], |
| 332 | + |
| 333 | + %% Await all results - this exercises the event loop processing |
| 334 | + Results = [{N, py_event_loop:await(Ref, 5000)} |
| 335 | + || {N, Ref} <- lists:zip(lists:seq(1, NumTasks), Refs)], |
| 336 | + |
| 337 | + ct:log("Thread-local context test: ~p tasks completed", [length(Results)]), |
| 338 | + |
| 339 | + %% Verify all succeeded with correct results |
| 340 | + lists:foreach(fun({N, {ok, R}}) -> |
| 341 | + Expected = float(N), |
| 342 | + true = abs(R - Expected) < 0.0001 |
| 343 | + end, Results). |
0 commit comments