Current section

Files

Jump to
erlang_python test py_api_SUITE.erl
Raw

test/py_api_SUITE.erl

%%% @doc Common Test suite for py module's new process-per-context API.
%%%
%%% Tests the new explicit context API where py:call/eval/exec can take
%%% a context pid as the first argument.
-module(py_api_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([
%% Existing API compatibility
test_call_basic/1,
test_call_with_kwargs/1,
test_eval_basic/1,
test_exec_basic/1,
%% New explicit context API
test_explicit_context_call/1,
test_explicit_context_eval/1,
test_explicit_context_exec/1,
test_explicit_context_isolation/1,
%% Context management API
test_context_management/1,
test_start_stop_contexts/1,
%% Mixed usage
test_mixed_api_usage/1,
%% Type conversion
test_explicit_bytes/1
]).
%% ============================================================================
%% Common Test callbacks
%% ============================================================================
all() ->
[
%% Existing API compatibility
test_call_basic,
test_call_with_kwargs,
test_eval_basic,
test_exec_basic,
%% New explicit context API
test_explicit_context_call,
test_explicit_context_eval,
test_explicit_context_exec,
test_explicit_context_isolation,
%% Context management API
test_context_management,
test_start_stop_contexts,
%% Mixed usage
test_mixed_api_usage,
%% Type conversion
test_explicit_bytes
].
init_per_suite(Config) ->
application:ensure_all_started(erlang_python),
Config.
end_per_suite(_Config) ->
ok.
init_per_testcase(_TestCase, Config) ->
%% Ensure fresh contexts are available for each test
catch py:stop_contexts(),
{ok, _} = py:start_contexts(),
Config.
end_per_testcase(_TestCase, _Config) ->
%% Clean up after each test
catch py:stop_contexts(),
ok.
%% ============================================================================
%% Existing API Compatibility Tests
%% ============================================================================
%% @doc Test that basic py:call still works (backwards compatibility).
test_call_basic(_Config) ->
{ok, 4.0} = py:call(math, sqrt, [16]),
{ok, 5.0} = py:call(math, sqrt, [25]),
{ok, 3} = py:call(builtins, len, [[1, 2, 3]]).
%% @doc Test that py:call with kwargs still works.
test_call_with_kwargs(_Config) ->
{ok, _} = py:call(json, dumps, [[{a, 1}]], #{indent => 2}),
{ok, _} = py:call(json, dumps, [#{a => 1, b => 2}], #{sort_keys => true}).
%% @doc Test that py:eval still works.
test_eval_basic(_Config) ->
{ok, 6} = py:eval(<<"2 + 4">>),
{ok, 15} = py:eval(<<"x * 3">>, #{x => 5}),
{ok, [1, 4, 9]} = py:eval(<<"[i*i for i in range(1, 4)]">>).
%% @doc Test that py:exec still works.
test_exec_basic(_Config) ->
ok = py:exec(<<"import math">>).
%% ============================================================================
%% New Explicit Context API Tests
%% ============================================================================
%% @doc Test py:call with explicit context pid.
test_explicit_context_call(_Config) ->
{ok, _} = py:start_contexts(#{contexts => 2}),
Ctx = py:context(),
true = is_pid(Ctx),
%% Call with context as first argument
{ok, 4.0} = py:call(Ctx, math, sqrt, [16]),
{ok, 5.0} = py:call(Ctx, math, sqrt, [25]),
%% Call with options
{ok, _} = py:call(Ctx, json, dumps, [[{a, 1}]], #{kwargs => #{indent => 2}}).
%% @doc Test py:eval with explicit context pid.
test_explicit_context_eval(_Config) ->
{ok, _} = py:start_contexts(#{contexts => 2}),
Ctx = py:context(),
%% Eval with context as first argument
{ok, 6} = py:eval(Ctx, <<"2 + 4">>),
%% Eval with context and locals
{ok, 15} = py:eval(Ctx, <<"x * 3">>, #{x => 5}).
%% @doc Test py:exec with explicit context pid.
test_explicit_context_exec(_Config) ->
{ok, _} = py:start_contexts(#{contexts => 2}),
Ctx = py:context(),
%% Exec with context as first argument
ok = py:exec(Ctx, <<"test_var = 42">>),
%% Verify the variable persists in that context
{ok, 42} = py:eval(Ctx, <<"test_var">>, #{}).
%% @doc Test that different contexts are isolated.
test_explicit_context_isolation(_Config) ->
{ok, _} = py:start_contexts(#{contexts => 2}),
Ctx1 = py:context(1),
Ctx2 = py:context(2),
%% Set different values in each context
ok = py:exec(Ctx1, <<"isolation_test = 'context1'">>),
ok = py:exec(Ctx2, <<"isolation_test = 'context2'">>),
%% Verify isolation
{ok, <<"context1">>} = py:eval(Ctx1, <<"isolation_test">>, #{}),
{ok, <<"context2">>} = py:eval(Ctx2, <<"isolation_test">>, #{}).
%% ============================================================================
%% Context Management API Tests
%% ============================================================================
%% @doc Test py:context/0 and py:context/1.
test_context_management(_Config) ->
%% Stop any existing contexts first, then start with specific count
py:stop_contexts(),
{ok, Contexts} = py:start_contexts(#{contexts => 4}),
4 = length(Contexts),
%% Get context for current scheduler
Ctx = py:context(),
true = is_pid(Ctx),
true = lists:member(Ctx, Contexts),
%% Get specific contexts by index
Ctx1 = py:context(1),
Ctx2 = py:context(2),
Ctx3 = py:context(3),
Ctx4 = py:context(4),
[Ctx1, Ctx2, Ctx3, Ctx4] = Contexts.
%% @doc Test py:start_contexts/0,1 and py:stop_contexts/0.
test_start_stop_contexts(_Config) ->
%% Start with default settings
{ok, Contexts1} = py:start_contexts(),
true = length(Contexts1) > 0,
%% All contexts should be alive
lists:foreach(fun(Ctx) ->
true = is_process_alive(Ctx)
end, Contexts1),
%% Stop
ok = py:stop_contexts(),
%% Wait for contexts to die
timer:sleep(100),
lists:foreach(fun(Ctx) ->
false = is_process_alive(Ctx)
end, Contexts1),
%% Start with custom settings
{ok, Contexts2} = py:start_contexts(#{contexts => 2}),
2 = length(Contexts2),
ok = py:stop_contexts().
%% ============================================================================
%% Mixed Usage Tests
%% ============================================================================
%% @doc Test implicit and explicit context API usage.
test_mixed_api_usage(_Config) ->
{ok, _} = py:start_contexts(#{contexts => 2}),
%% Use implicit API (auto-routes through py_context_router)
{ok, 4.0} = py:call(math, sqrt, [16]),
%% Use explicit API (direct context pid)
Ctx = py:context(),
{ok, 5.0} = py:call(Ctx, math, sqrt, [25]),
%% Both should work correctly
{ok, 6} = py:eval(<<"2 + 4">>),
{ok, 7} = py:eval(Ctx, <<"3 + 4">>, #{}).
%% ============================================================================
%% Type Conversion Tests
%% ============================================================================
%% @doc Test explicit bytes conversion using {bytes, Binary} tuple.
test_explicit_bytes(_Config) ->
Ctx = py:context(1),
%% Define test functions
ok = py:exec(Ctx, <<"
def check_type(val):
return type(val).__name__
def check_bytes_value(val):
return val == b'hello'
">>),
%% Regular binary -> str (default UTF-8 decoding)
{ok, <<"str">>} = py:eval(Ctx, <<"check_type(val)">>, #{<<"val">> => <<"hello">>}),
%% Explicit bytes tuple -> bytes
{ok, <<"bytes">>} = py:eval(Ctx, <<"check_type(val)">>, #{<<"val">> => {bytes, <<"hello">>}}),
%% Verify value is correct
{ok, true} = py:eval(Ctx, <<"check_bytes_value(val)">>, #{<<"val">> => {bytes, <<"hello">>}}),
%% Test with binary data (non-UTF8)
NonUtf8 = <<255, 254, 0, 1>>,
{ok, <<"bytes">>} = py:eval(Ctx, <<"check_type(val)">>, #{<<"val">> => {bytes, NonUtf8}}),
ok.