Current section
Files
Jump to
Current section
Files
test/py_event_loop_pool_SUITE.erl
%%% @doc Common Test suite for Event Loop Worker Pool.
%%%
%%% Tests the pool of event loops with process affinity for ordered execution.
-module(py_event_loop_pool_SUITE).
-include_lib("common_test/include/ct.hrl").
-export([
all/0,
init_per_suite/1,
end_per_suite/1,
init_per_testcase/2,
end_per_testcase/2
]).
-export([
%% Pool tests
test_pool_starts/1,
test_pool_stats/1,
test_get_loop_returns_reference/1,
test_same_process_same_loop/1,
%% Task API tests
test_create_task_and_await/1,
test_run_blocking/1,
test_spawn_task/1,
test_concurrent_tasks/1,
%% Ordering test
test_tasks_execute_in_order/1,
%% Exec/Eval tests
test_exec_basic/1,
test_eval_basic/1,
test_exec_eval_namespace/1,
test_exec_define_function/1
]).
all() ->
[
test_pool_starts,
test_pool_stats,
test_get_loop_returns_reference,
test_same_process_same_loop,
test_create_task_and_await,
test_run_blocking,
test_spawn_task,
test_concurrent_tasks,
test_tasks_execute_in_order,
test_exec_basic,
test_eval_basic,
test_exec_eval_namespace,
test_exec_define_function
].
init_per_suite(Config) ->
case application:ensure_all_started(erlang_python) of
{ok, _} ->
timer:sleep(500),
case wait_for_pool(5000) of
ok -> Config;
{error, Reason} -> ct:fail({pool_not_ready, Reason})
end;
{error, {App, Reason}} ->
ct:fail({failed_to_start, App, Reason})
end.
wait_for_pool(Timeout) when Timeout =< 0 ->
{error, timeout};
wait_for_pool(Timeout) ->
case py_event_loop_pool:get_loop() of
{ok, LoopRef} when is_reference(LoopRef) -> ok;
_ ->
timer:sleep(100),
wait_for_pool(Timeout - 100)
end.
end_per_suite(_Config) ->
ok = application:stop(erlang_python),
ok.
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, _Config) ->
ok.
%% ============================================================================
%% Pool Tests
%% ============================================================================
test_pool_starts(_Config) ->
Stats = py_event_loop_pool:get_stats(),
NumLoops = maps:get(num_loops, Stats),
ct:log("Pool size: ~p", [NumLoops]),
true = NumLoops > 0,
ok.
test_pool_stats(_Config) ->
Stats = py_event_loop_pool:get_stats(),
ct:log("Pool stats: ~p", [Stats]),
true = is_map(Stats),
true = maps:is_key(num_loops, Stats),
true = maps:is_key(supported, Stats),
true = maps:get(supported, Stats),
ok.
test_get_loop_returns_reference(_Config) ->
{ok, LoopRef} = py_event_loop_pool:get_loop(),
ct:log("Got loop ref: ~p", [LoopRef]),
true = is_reference(LoopRef),
ok.
test_same_process_same_loop(_Config) ->
%% Same process always gets the same loop (process affinity)
{ok, Loop1} = py_event_loop_pool:get_loop(),
{ok, Loop2} = py_event_loop_pool:get_loop(),
{ok, Loop3} = py_event_loop_pool:get_loop(),
ct:log("Loops: ~p, ~p, ~p", [Loop1, Loop2, Loop3]),
Loop1 = Loop2,
Loop2 = Loop3,
ok.
%% ============================================================================
%% Task API Tests
%% ============================================================================
test_create_task_and_await(_Config) ->
Ref = py_event_loop_pool:create_task(math, sqrt, [25.0]),
ct:log("Created task ref: ~p", [Ref]),
true = is_reference(Ref),
{ok, 5.0} = py_event_loop_pool:await(Ref, 5000),
ok.
test_run_blocking(_Config) ->
{ok, 3} = py_event_loop_pool:run(math, floor, [3.7]),
ok.
test_spawn_task(_Config) ->
ok = py_event_loop_pool:spawn_task(math, ceil, [2.3]),
timer:sleep(100),
ok.
test_concurrent_tasks(_Config) ->
NumTasks = 50,
Refs = [py_event_loop_pool:create_task(math, sqrt, [float(I * I)])
|| I <- lists:seq(1, NumTasks)],
ct:log("Created ~p tasks", [length(Refs)]),
Results = [py_event_loop_pool:await(Ref, 5000) || Ref <- Refs],
OkResults = [{ok, V} || {ok, V} <- Results],
NumTasks = length(OkResults),
Values = lists:sort([round(V) || {ok, V} <- Results]),
Expected = lists:seq(1, NumTasks),
Values = Expected,
ok.
%% ============================================================================
%% Ordering Test
%% ============================================================================
test_tasks_execute_in_order(_Config) ->
%% All tasks from this process go to the same loop, so they execute in order
Refs = [py_event_loop_pool:create_task(math, sqrt, [float(I)])
|| I <- [1, 4, 9, 16, 25]],
Results = [py_event_loop_pool:await(Ref, 5000) || Ref <- Refs],
ct:log("Results: ~p", [Results]),
%% Results should be in submission order
[{ok, 1.0}, {ok, 2.0}, {ok, 3.0}, {ok, 4.0}, {ok, 5.0}] = Results,
ok.
%% ============================================================================
%% Exec/Eval Tests
%% ============================================================================
test_exec_basic(_Config) ->
%% Basic exec should succeed
ok = py_event_loop_pool:exec(<<"x = 42">>),
ok.
test_eval_basic(_Config) ->
%% Basic eval of Python expression
{ok, 6} = py_event_loop_pool:eval(<<"2 + 4">>),
{ok, 10} = py_event_loop_pool:eval(<<"5 * 2">>),
ok.
test_exec_eval_namespace(_Config) ->
%% Variables defined in exec should be available in eval
ok = py_event_loop_pool:exec(<<"pool_test_var = 100">>),
{ok, 100} = py_event_loop_pool:eval(<<"pool_test_var">>),
{ok, 200} = py_event_loop_pool:eval(<<"pool_test_var * 2">>),
ok.
test_exec_define_function(_Config) ->
%% Define a function via exec and call it via create_task
ok = py_event_loop_pool:exec(<<"
def pool_test_multiply(a, b):
return a * b
">>),
%% Call the function via create_task
Ref = py_event_loop_pool:create_task('__main__', pool_test_multiply, [7, 6]),
{ok, 42} = py_event_loop_pool:await(Ref, 5000),
ok.