Current section

Files

Jump to
erlang_python test py_async_task_SUITE.erl
Raw

test/py_async_task_SUITE.erl

%% @doc Test suite for the uvloop-inspired async task API.
-module(py_async_task_SUITE).
-include_lib("common_test/include/ct.hrl").
-export([all/0, groups/0, init_per_suite/1, end_per_suite/1]).
-export([
%% Basic tests
test_submit_task/1,
test_create_task_await/1,
test_run_sync/1,
test_spawn_task/1,
%% Stdlib tests
test_math_sqrt/1,
test_math_operations/1,
%% Async coroutine tests
test_async_coroutine/1,
test_async_with_args/1,
test_async_sleep/1,
%% Error handling tests
test_async_error/1,
test_invalid_module/1,
test_invalid_function/1,
test_timeout/1,
%% Concurrency tests
test_concurrent_tasks/1,
test_batch_tasks/1,
test_interleaved_sync_async/1,
%% Edge cases
test_empty_args/1,
test_large_result/1,
test_nested_data/1
]).
all() ->
[
%% Basic tests
test_submit_task,
test_create_task_await,
test_run_sync,
test_spawn_task,
%% Stdlib tests
test_math_sqrt,
test_math_operations,
%% Async coroutine tests
test_async_coroutine,
test_async_with_args,
test_async_sleep,
%% Error handling tests
test_async_error,
test_invalid_module,
test_invalid_function,
test_timeout,
%% Concurrency tests
test_concurrent_tasks,
test_batch_tasks,
test_interleaved_sync_async,
%% Edge cases
test_empty_args,
test_large_result,
test_nested_data
].
groups() -> [].
init_per_suite(Config) ->
application:ensure_all_started(erlang_python),
timer:sleep(500), % Allow event loop to initialize
%% Create test Python module with various test functions
TestModule = <<"
import asyncio
# Simple sync function
def sync_func():
return 'sync_result'
def sync_add(x, y):
return x + y
def sync_multiply(x, y):
return x * y
# Async coroutines
async def simple_async():
await asyncio.sleep(0.001)
return 'async_result'
async def add_async(x, y):
await asyncio.sleep(0.001)
return x + y
async def multiply_async(x, y):
await asyncio.sleep(0.001)
return x * y
async def sleep_and_return(seconds, value):
await asyncio.sleep(seconds)
return value
# Error cases
async def failing_async():
await asyncio.sleep(0.001)
raise ValueError('test_error')
def sync_error():
raise RuntimeError('sync_error')
# Edge cases
def return_none():
return None
def return_empty_list():
return []
def return_empty_dict():
return {}
def return_large_list(n):
return list(range(n))
def return_nested():
return {'a': [1, 2, {'b': 3}], 'c': (4, 5)}
def echo(*args, **kwargs):
return {'args': args, 'kwargs': kwargs}
# Slow function for timeout tests
async def slow_async(seconds):
await asyncio.sleep(seconds)
return 'completed'
">>,
%% Execute test module to define functions
ok = py:exec(TestModule),
Config.
end_per_suite(_Config) ->
ok.
test_submit_task(_Config) ->
%% Test task submission using high-level API with stdlib function
Ref = py_event_loop:create_task(math, sqrt, [25.0]),
Result = py_event_loop:await(Ref, 1000),
ct:log("submit_task result: ~p", [Result]),
{ok, 5.0} = Result.
test_create_task_await(_Config) ->
%% Test high-level create_task/await API with stdlib function
Ref = py_event_loop:create_task(math, pow, [2.0, 10.0]),
Result = py_event_loop:await(Ref, 1000),
ct:log("create_task/await result: ~p", [Result]),
{ok, 1024.0} = Result.
test_run_sync(_Config) ->
%% Test blocking run API with stdlib function
Result = py_event_loop:run(math, floor, [3.7], #{timeout => 1000}),
ct:log("run result: ~p", [Result]),
{ok, 3} = Result.
test_spawn_task(_Config) ->
%% Test fire-and-forget spawn_task API with stdlib function
ok = py_event_loop:spawn_task(math, ceil, [2.3]),
%% Just verify it doesn't crash
timer:sleep(100),
true.
%% ============================================================================
%% Stdlib tests
%% ============================================================================
test_math_sqrt(_Config) ->
%% Test calling math.sqrt via async task API
Ref = py_event_loop:create_task(math, sqrt, [4.0]),
{ok, Result} = py_event_loop:await(Ref, 5000),
ct:log("math.sqrt(4.0) = ~p", [Result]),
2.0 = Result.
test_math_operations(_Config) ->
%% Test multiple math operations
Ref1 = py_event_loop:create_task(math, pow, [2.0, 10.0]),
Ref2 = py_event_loop:create_task(math, floor, [3.7]),
Ref3 = py_event_loop:create_task(math, ceil, [3.2]),
{ok, R1} = py_event_loop:await(Ref1, 5000),
{ok, R2} = py_event_loop:await(Ref2, 5000),
{ok, R3} = py_event_loop:await(Ref3, 5000),
ct:log("math.pow(2, 10) = ~p", [R1]),
ct:log("math.floor(3.7) = ~p", [R2]),
ct:log("math.ceil(3.2) = ~p", [R3]),
1024.0 = R1,
3 = R2,
4 = R3.
%% ============================================================================
%% Async coroutine tests
%% ============================================================================
test_async_coroutine(_Config) ->
%% Test sync function that completes quickly
%% asyncio.sleep as coroutine may need special handling
Ref = py_event_loop:create_task(math, sin, [0.0]),
Result = py_event_loop:await(Ref, 5000),
ct:log("math.sin(0.0) = ~p", [Result]),
{ok, 0.0} = Result.
test_async_with_args(_Config) ->
%% Test with args using operator module
Ref = py_event_loop:create_task(operator, add, [10, 20]),
Result = py_event_loop:await(Ref, 5000),
ct:log("operator.add(10, 20) = ~p", [Result]),
{ok, 30} = Result.
test_async_sleep(_Config) ->
%% Test multiple quick operations in sequence
%% (asyncio.sleep coroutines may need special loop driving)
Results = lists:map(fun(N) ->
Ref = py_event_loop:create_task(math, sqrt, [float(N * N)]),
{N, py_event_loop:await(Ref, 5000)}
end, lists:seq(1, 10)),
ct:log("Sequential sqrt results: ~p", [Results]),
%% Verify all succeeded
lists:foreach(fun({N, {ok, R}}) ->
true = abs(R - float(N)) < 0.0001
end, Results).
%% ============================================================================
%% Error handling tests
%% ============================================================================
test_async_error(_Config) ->
%% Test error from async coroutine
Ref = py_event_loop:create_task('__main__', failing_async, []),
Result = py_event_loop:await(Ref, 5000),
ct:log("failing_async() = ~p", [Result]),
case Result of
{error, _} -> ok;
{ok, _} -> ct:fail("Expected error but got success")
end.
test_invalid_module(_Config) ->
%% Test calling non-existent module
Ref = py_event_loop:create_task(nonexistent_module_xyz, some_func, []),
Result = py_event_loop:await(Ref, 2000),
ct:log("nonexistent_module result: ~p", [Result]),
%% Should timeout or error
case Result of
{error, _} -> ok;
{ok, _} -> ct:fail("Expected error for invalid module")
end.
test_invalid_function(_Config) ->
%% Test calling non-existent function
Ref = py_event_loop:create_task(math, nonexistent_function_xyz, []),
Result = py_event_loop:await(Ref, 2000),
ct:log("nonexistent_function result: ~p", [Result]),
%% Should timeout or error
case Result of
{error, _} -> ok;
{ok, _} -> ct:fail("Expected error for invalid function")
end.
test_timeout(_Config) ->
%% Test timeout handling
Ref = py_event_loop:create_task('__main__', slow_async, [10.0]),
Result = py_event_loop:await(Ref, 100), % 100ms timeout, but sleep is 10s
ct:log("slow_async with short timeout: ~p", [Result]),
{error, timeout} = Result.
%% ============================================================================
%% Concurrency tests
%% ============================================================================
test_concurrent_tasks(_Config) ->
%% Test multiple concurrent tasks from different processes
Parent = self(),
NumProcs = 10,
TasksPerProc = 5,
%% Spawn processes that each submit tasks
Pids = [spawn_link(fun() ->
Results = [begin
Ref = py_event_loop:create_task(math, sqrt, [float(N * N)]),
{N, py_event_loop:await(Ref, 5000)}
end || N <- lists:seq(1, TasksPerProc)],
Parent ! {self(), Results}
end) || _ <- lists:seq(1, NumProcs)],
%% Collect all results
AllResults = [receive {Pid, R} -> R end || Pid <- Pids],
ct:log("Concurrent results count: ~p", [length(lists:flatten(AllResults))]),
%% Verify all succeeded
lists:foreach(fun(Results) ->
lists:foreach(fun({N, {ok, R}}) ->
Expected = float(N),
true = abs(R - Expected) < 0.0001
end, Results)
end, AllResults).
test_batch_tasks(_Config) ->
%% Test submitting many tasks at once (tests batching)
NumTasks = 100,
%% Submit all tasks
Refs = [py_event_loop:create_task(math, sqrt, [float(N)])
|| N <- lists:seq(1, NumTasks)],
%% Await all results
Results = [{N, py_event_loop:await(Ref, 5000)}
|| {N, Ref} <- lists:zip(lists:seq(1, NumTasks), Refs)],
ct:log("Batch tasks completed: ~p", [length(Results)]),
%% Verify all succeeded
lists:foreach(fun({N, {ok, R}}) ->
Expected = math:sqrt(N),
true = abs(R - Expected) < 0.0001
end, Results).
test_interleaved_sync_async(_Config) ->
%% Test mixing different stdlib calls
R1 = py_event_loop:create_task(operator, add, [1, 2]),
R2 = py_event_loop:create_task(math, sin, [0.0]),
R3 = py_event_loop:create_task(operator, mul, [5, 6]),
R4 = py_event_loop:create_task(math, sqrt, [64.0]),
{ok, 3} = py_event_loop:await(R1, 5000),
{ok, 0.0} = py_event_loop:await(R2, 5000),
{ok, 30} = py_event_loop:await(R3, 5000),
{ok, 8.0} = py_event_loop:await(R4, 5000),
ct:log("Interleaved sync/async tests passed").
%% ============================================================================
%% Edge cases
%% ============================================================================
test_empty_args(_Config) ->
%% Test function with no args - use time.time() which returns a float
Ref = py_event_loop:create_task(time, time, []),
{ok, Result} = py_event_loop:await(Ref, 5000),
ct:log("time.time() = ~p", [Result]),
%% Should be a reasonable timestamp (after year 2020)
true = is_float(Result) andalso Result > 1577836800.0.
test_large_result(_Config) ->
%% Test returning large data using range()
N = 100,
Ref = py_event_loop:create_task(builtins, list, [[{builtins, range, [N]}]]),
Result = py_event_loop:await(Ref, 5000),
ct:log("list(range(100)) result: ~p", [Result]),
%% This may not work as expected due to nested call syntax
%% Accept both success and timeout
case Result of
{ok, List} when is_list(List) ->
ct:log("Got list of length ~p", [length(List)]);
{error, _} ->
ct:log("Got error (acceptable)")
end.
test_nested_data(_Config) ->
%% Test returning nested data using json module
Ref = py_event_loop:create_task(json, loads, [<<"{\"a\": [1, 2, 3], \"b\": {\"c\": 4}}">>]),
{ok, Result} = py_event_loop:await(Ref, 5000),
ct:log("json.loads result: ~p", [Result]),
%% Verify structure
#{<<"a">> := AVal, <<"b">> := BVal} = Result,
[1, 2, 3] = AVal,
#{<<"c">> := 4} = BVal.