Current section

Files

Jump to
erlang_python test py_state_SUITE.erl
Raw

test/py_state_SUITE.erl

%%% @doc Common Test suite for py_state shared-state store, focused on the
%%% optional entry cap and its accounting (memory-exhaustion resistance).
-module(py_state_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([
cap_disabled_test/1,
cap_enforced_test/1,
cap_accounting_test/1,
sentinel_protected_test/1
]).
-define(SIZE_KEY, '$py_state_size$').
all() ->
[cap_disabled_test, cap_enforced_test, cap_accounting_test, sentinel_protected_test].
init_per_suite(Config) ->
{ok, _} = application:ensure_all_started(erlang_python),
Config.
end_per_suite(_Config) ->
application:stop(erlang_python),
ok.
init_per_testcase(_TC, Config) ->
application:unset_env(erlang_python, max_state_entries),
py_state:clear(),
Config.
end_per_testcase(_TC, _Config) ->
application:unset_env(erlang_python, max_state_entries),
py_state:clear(),
ok.
%% @doc Default (infinity) preserves the previous unbounded behavior.
cap_disabled_test(_Config) ->
[ok = py_state:store(N, N) || N <- lists:seq(1, 1000)],
{ok, 500} = py_state:fetch(500),
ok.
%% @doc A finite cap rejects new keys beyond the limit; overwrites don't consume
%% capacity.
cap_enforced_test(_Config) ->
application:set_env(erlang_python, max_state_entries, 5),
[ok = py_state:store({k, N}, N) || N <- lists:seq(1, 5)],
{error, full} = py_state:store({k, 6}, 6),
%% Overwriting an existing key does NOT consume capacity.
ok = py_state:store({k, 3}, 30),
{ok, 30} = py_state:fetch({k, 3}),
{error, full} = py_state:store({k, 7}, 7),
ok.
%% @doc Removing frees capacity; removing a missing key must not drift the count
%% (which would let the cap be bypassed or never trip).
cap_accounting_test(_Config) ->
application:set_env(erlang_python, max_state_entries, 3),
ok = py_state:store(a, 1),
ok = py_state:store(b, 2),
ok = py_state:store(c, 3),
{error, full} = py_state:store(d, 4),
%% Remove a missing key repeatedly: no phantom capacity freed.
[py_state:remove(missing) || _ <- lists:seq(1, 10)],
{error, full} = py_state:store(d, 4),
%% Remove a real key: frees exactly one slot.
ok = py_state:remove(a),
ok = py_state:store(d, 4),
{error, full} = py_state:store(e, 5),
ok.
%% @doc The internal size sentinel can't be written or counter-corrupted by a
%% caller, and is hidden from keys/0 and fetch/1.
sentinel_protected_test(_Config) ->
application:set_env(erlang_python, max_state_entries, 2),
{error, reserved_key} = py_state:store(?SIZE_KEY, 9999),
{'EXIT', _} = (catch py_state:incr(?SIZE_KEY, 100)),
ok = py_state:store(x, 1),
ok = py_state:store(y, 2),
{error, full} = py_state:store(z, 3),
{error, not_found} = py_state:fetch(?SIZE_KEY),
false = lists:member(?SIZE_KEY, py_state:keys()),
ok.