%% @doc Tests for erlang.schedule(), schedule_py(), and consume_time_slice(). %% %% Tests explicit scheduling API for cooperative dirty scheduler release. -module(py_schedule_SUITE). -include_lib("common_test/include/ct.hrl"). -export([all/0, init_per_suite/1, end_per_suite/1]). -export([ test_schedule_available/1, test_schedule_py_available/1, test_consume_time_slice_available/1, test_schedule_returns_marker/1, test_schedule_py_returns_marker/1, test_consume_time_slice_returns_bool/1, test_schedule_with_callback/1, test_schedule_py_basic/1, test_schedule_py_with_args/1, test_schedule_py_with_kwargs/1, test_call_is_blocking/1 ]). all() -> [ test_schedule_available, test_schedule_py_available, test_consume_time_slice_available, test_schedule_returns_marker, test_schedule_py_returns_marker, test_consume_time_slice_returns_bool, test_schedule_with_callback, test_schedule_py_basic, test_schedule_py_with_args, test_schedule_py_with_kwargs, test_call_is_blocking ]. init_per_suite(Config) -> {ok, _} = application:ensure_all_started(erlang_python), {ok, _} = py:start_contexts(), %% Register a test callback for schedule() tests py_callback:register(<<"_test_add">>, fun([A, B]) -> A + B end), py_callback:register(<<"_test_mul">>, fun([A, B]) -> A * B end), py_callback:register(<<"_test_echo">>, fun(Args) -> Args end), timer:sleep(500), Config. end_per_suite(_Config) -> py_callback:unregister(<<"_test_add">>), py_callback:unregister(<<"_test_mul">>), py_callback:unregister(<<"_test_echo">>), ok. %% Test that erlang.schedule is available test_schedule_available(_Config) -> ok = py:exec(<<" import erlang assert hasattr(erlang, 'schedule'), 'erlang.schedule not found' ">>), ct:pal("erlang.schedule is available"), ok. %% Test that erlang.schedule_py is available test_schedule_py_available(_Config) -> ok = py:exec(<<" import erlang assert hasattr(erlang, 'schedule_py'), 'erlang.schedule_py not found' ">>), ct:pal("erlang.schedule_py is available"), ok. %% Test that erlang.consume_time_slice is available test_consume_time_slice_available(_Config) -> ok = py:exec(<<" import erlang assert hasattr(erlang, 'consume_time_slice'), 'erlang.consume_time_slice not found' ">>), ct:pal("erlang.consume_time_slice is available"), ok. %% Test that schedule() returns a ScheduleMarker test_schedule_returns_marker(_Config) -> ok = py:exec(<<" import erlang marker = erlang.schedule('_test_add', 1, 2) assert isinstance(marker, erlang.ScheduleMarker), f'Expected ScheduleMarker, got {type(marker)}' ">>), ct:pal("schedule() returns ScheduleMarker"), ok. %% Test that schedule_py() returns a ScheduleMarker test_schedule_py_returns_marker(_Config) -> ok = py:exec(<<" import erlang marker = erlang.schedule_py('math', 'sqrt', [16.0]) assert isinstance(marker, erlang.ScheduleMarker), f'Expected ScheduleMarker, got {type(marker)}' ">>), ct:pal("schedule_py() returns ScheduleMarker"), ok. %% Test that consume_time_slice() returns bool test_consume_time_slice_returns_bool(_Config) -> ok = py:exec(<<" import erlang result = erlang.consume_time_slice(1) assert isinstance(result, bool), f'Expected bool, got {type(result)}' ">>), ct:pal("consume_time_slice() returns bool"), ok. %% Test schedule() with a registered Erlang callback test_schedule_with_callback(_Config) -> %% Define the function ok = py:exec(<<" def schedule_add(a, b): import erlang return erlang.schedule('_test_add', a, b) ">>), %% Call it - the schedule marker should be detected and callback executed {ok, Result} = py:eval(<<"schedule_add(5, 7)">>), ct:pal("schedule() result: ~p", [Result]), 12 = Result, ok. %% Test schedule_py() basic functionality test_schedule_py_basic(_Config) -> %% Define the target function in __main__ so it's accessible via py:call ok = py:exec(<<" import __main__ def double(x): return x * 2 # Add to __main__ so it's accessible from schedule_py callback __main__.double = double def schedule_double(x): import erlang return erlang.schedule_py('__main__', 'double', [x]) ">>), %% Call the scheduling function {ok, Result} = py:eval(<<"schedule_double(5)">>), ct:pal("schedule_py() result: ~p", [Result]), 10 = Result, ok. %% Test schedule_py() with multiple args test_schedule_py_with_args(_Config) -> ok = py:exec(<<" import __main__ def add_three(a, b, c): return a + b + c __main__.add_three = add_three def schedule_add_three(a, b, c): import erlang return erlang.schedule_py('__main__', 'add_three', [a, b, c]) ">>), {ok, Result} = py:eval(<<"schedule_add_three(1, 2, 3)">>), ct:pal("schedule_py() with args result: ~p", [Result]), 6 = Result, ok. %% Test schedule_py() with kwargs test_schedule_py_with_kwargs(_Config) -> ok = py:exec(<<" import __main__ def greet(name, prefix='Hello'): return f'{prefix}, {name}!' __main__.greet = greet def schedule_greet(name, prefix): import erlang return erlang.schedule_py('__main__', 'greet', [name], {'prefix': prefix}) ">>), {ok, Result} = py:eval(<<"schedule_greet('World', 'Hi')">>), ct:pal("schedule_py() with kwargs result: ~p", [Result]), <<"Hi, World!">> = Result, ok. %% Test that erlang.call() is now blocking (doesn't replay) test_call_is_blocking(_Config) -> %% The original bug was that erlang.call() used replay mechanism which %% caused double-execution of code. With blocking mode, the call should %% only execute once even with timing-sensitive code. ok = py:exec(<<" import erlang import time counter = [0] # Use list to avoid closure issues def test_call_once(): counter[0] += 1 erlang.call('_py_sleep', 0.05) # 50ms sleep return counter[0] result = test_call_once() assert result == 1, f'Expected 1, got {result} - call may have replayed' ">>), ct:pal("erlang.call() is blocking (no replay)"), ok.