laforge has submitted this change. ( https://gerrit.osmocom.org/c/erlang/osmo-s1gw/+/37317?usp=email )
Change subject: erab_fsm: E-RAB FSM implementation ......................................................................
erab_fsm: E-RAB FSM implementation
The E-RAB FSM will be used in follow-up patches.
This patch also adds 'meck' dependency to the test profile, which is needed to mock the pfcp_peer API in the unit tests.
Change-Id: Ic57e14675339b7cadb0cdd7cbc5d3a56288d7297 --- M contrib/generate_build_dep.sh M rebar.config A src/erab_fsm.erl A test/erab_fsm_test.erl A test/pfcp_mock.erl A test/pfcp_mock.hrl 6 files changed, 671 insertions(+), 1 deletion(-)
Approvals: Jenkins Builder: Verified laforge: Looks good to me, approved
diff --git a/contrib/generate_build_dep.sh b/contrib/generate_build_dep.sh index 3237ae3..08efac9 100755 --- a/contrib/generate_build_dep.sh +++ b/contrib/generate_build_dep.sh @@ -10,10 +10,12 @@
set -x rm -rf _checkouts _build -rebar3 get-deps +REBAR_PROFILE=default rebar3 get-deps +REBAR_PROFILE=test rebar3 get-deps mkdir _checkouts mv ./_build/default/lib/* _checkouts/ mv ./_build/default/plugins/* _checkouts/ +mv ./_build/test/lib/meck _checkouts/ # delete erlang bytecode find _checkouts/ -iname '*beam' -delete tar czf build_dep.tar.gz ./_checkouts diff --git a/rebar.config b/rebar.config index c3e68bb..91bd759 100644 --- a/rebar.config +++ b/rebar.config @@ -4,12 +4,21 @@
{minimum_otp_vsn, "25.2.3"}.
+%% run-time deps {deps, [{logger_color_formatter, {git, "https://github.com/rlipscombe/logger_color_formatter.git", {tag, "0.5.0"}}}, {pfcplib, {git, "https://github.com/travelping/pfcplib.git", {branch, "master"}}} ]}.
+%% test deps +{profiles, + [{test, [{deps, [{meck, + {git, "https://github.com/eproxus/meck.git", {tag, "0.9.2"}}} + ]} + ]} + ]}. + {plugins, [{provider_asn1, ".*", {git, "https://github.com/knusbaum/provider_asn1.git", {tag, "0.4.0"}}} ]}. diff --git a/src/erab_fsm.erl b/src/erab_fsm.erl new file mode 100644 index 0000000..5635086 --- /dev/null +++ b/src/erab_fsm.erl @@ -0,0 +1,424 @@ +%% Copyright (C) 2024 by sysmocom - s.f.m.c. GmbH info@sysmocom.de +%% Author: Vadim Yanitskiy vyanitskiy@sysmocom.de +%% +%% All Rights Reserved +%% +%% SPDX-License-Identifier: AGPL-3.0-or-later +%% +%% This program is free software; you can redistribute it and/or modify +%% it under the terms of the GNU Affero General Public License as +%% published by the Free Software Foundation; either version 3 of the +%% License, or (at your option) any later version. +%% +%% This program is distributed in the hope that it will be useful, +%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +%% GNU General Public License for more details. +%% +%% You should have received a copy of the GNU Affero General Public License +%% along with this program. If not, see https://www.gnu.org/licenses/. +%% +%% Additional Permission under GNU AGPL version 3 section 7: +%% +%% If you modify this Program, or any covered work, by linking or +%% combining it with runtime libraries of Erlang/OTP as released by +%% Ericsson on https://www.erlang.org (or a modified version of these +%% libraries), containing parts covered by the terms of the Erlang Public +%% License (https://www.erlang.org/EPLICENSE), the licensors of this +%% Program grant you additional permission to convey the resulting work +%% without the need to license the runtime libraries of Erlang/OTP under +%% the GNU Affero General Public License. Corresponding Source for a +%% non-source form of such a combination shall include the source code +%% for the parts of the runtime libraries of Erlang/OTP used as well as +%% that of the covered work. + +-module(erab_fsm). +-behaviour(gen_statem). + +-export([init/1, + callback_mode/0, + erab_wait_setup_req/3, + session_establish/3, + erab_wait_setup_rsp/3, + session_modify/3, + erab_setup/3, + session_delete/3, + erab_wait_release_rsp/3, + code_change/4, + terminate/3]). +-export([start_link/1, + erab_setup_req/2, + erab_setup_rsp/2, + erab_release_req/1, + erab_release_rsp/1, + shutdown/1]). + +-include_lib("kernel/include/logger.hrl"). +-include_lib("pfcplib/include/pfcp_packet.hrl"). + + +-define(ERAB_T_WAIT_SETUP_RSP, 5_000). +-define(ERAB_T_WAIT_RELEASE_RSP, 5_000). +-define(ERAB_T_SESSION_ESTABLISH, 2_000). +-define(ERAB_T_SESSION_MODIFY, 2_000). +-define(ERAB_T_SESSION_DELETE, 2_000). + +-type teid() :: 0..16#ffffffff. + +-record(erab_state, {from :: undefined | gen_statem:from(), %% destination to use when replying + teid_from_access :: undefined | teid(), %% TEID to be used in eNB -> UPF + teid_to_access :: undefined | teid(), %% TEID to be used in UPF -> eNB + teid_from_core :: undefined | teid(), %% TEID to be used in PGW -> UPF + teid_to_core :: undefined | teid(), %% TEID to be used in UPF -> PGW + seid_loc :: undefined | pfcp_peer:pfcp_seid(), %% local SEID, chosen by us + seid_rem :: undefined | pfcp_peer:pfcp_seid() %% remote SEID, chosen by the UPF + }). + +-type erab_state() :: #erab_state{}. + +%% ------------------------------------------------------------------ +%% public API +%% ------------------------------------------------------------------ + +-spec start_link(term()) -> gen_statem:start_ret(). +start_link(UID) -> + gen_statem:start_link(?MODULE, [UID], []). + + +%% @doc Indicate reception of E-RAB setup request (from the eNB). +%% @param Pid PID of an erab_fsm. +%% @param UID *unique* E-RAB identifier. +%% @param TEID TEID chosen by the eNB. +%% @returns TEID to be sent to the core; an error otherwise. +%% @end +-spec erab_setup_req(pid(), teid()) -> {ok, teid()} | + {error, term()}. +erab_setup_req(Pid, TEID) -> + gen_statem:call(Pid, {?FUNCTION_NAME, TEID}). + + +%% @doc Indicate reception of E-RAB setup response (from the core). +%% @param Pid PID of an erab_fsm. +%% @param TEID TEID chosen by the core. +%% @returns TEID to be sent to the eNB; an error otherwise. +%% @end +-spec erab_setup_rsp(pid(), teid()) -> {ok, teid()} | + {error, term()}. +erab_setup_rsp(Pid, TEID) -> + gen_statem:call(Pid, {?FUNCTION_NAME, TEID}). + + +-spec erab_release_req(pid()) -> ok. +erab_release_req(Pid) -> + gen_statem:call(Pid, ?FUNCTION_NAME). + + +-spec erab_release_rsp(pid()) -> ok. +erab_release_rsp(Pid) -> + gen_statem:cast(Pid, ?FUNCTION_NAME). + + +-spec shutdown(pid()) -> ok. +shutdown(Pid) -> + gen_statem:stop(Pid). + + +%% ------------------------------------------------------------------ +%% gen_statem API +%% ------------------------------------------------------------------ + +init([UID]) -> + set_logging_prefix(UID), + %% request a unieue SEID for this E-RAB FSM + {ok, SEID} = pfcp_peer:seid_alloc(), + {ok, erab_wait_setup_req, + #erab_state{seid_loc = SEID}}. + + +callback_mode() -> + [state_functions, state_enter]. + + +%% state WAIT_SETUP_REQ :: wait E-RAB SETUP Req from the Access +erab_wait_setup_req(enter, ?FUNCTION_NAME, S) -> + ?LOG_DEBUG("State enter: ~p", [?FUNCTION_NAME]), + {keep_state, S}; + +erab_wait_setup_req({call, From}, + {erab_setup_req, TEID_FromAcc}, + #erab_state{} = S) -> + ?LOG_DEBUG("Rx E-RAB SETUP Req (Access TEID ~p)", [TEID_FromAcc]), + {next_state, session_establish, + S#erab_state{from = From, + teid_from_access = TEID_FromAcc}}; + +erab_wait_setup_req(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +%% state SESSION_ESTABLISH :: PFCP session establishment +session_establish(enter, OldState, S) -> + ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), + ok = session_establish_req(S), + {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout + [{state_timeout, ?ERAB_T_SESSION_ESTABLISH, ?FUNCTION_NAME}]}; + +session_establish(state_timeout, _Timeout, + #erab_state{from = From}) -> + ?LOG_ERROR("PFCP session establishment timeout"), + {stop_and_reply, + {shutdown, timeout}, + {reply, From, {error, {timeout, ?FUNCTION_NAME}}}}; + +session_establish(info, #pfcp{} = PDU, + #erab_state{from = From, + seid_loc = SEID_Rsp} = S) -> + case PDU of + #pfcp{type = session_establishment_response, + seid = SEID_Rsp, %% matches F-SEID we sent in the ESTABLISH Req + ie = #{pfcp_cause := 'Request accepted', + f_seid := #f_seid{seid = F_SEID}, + created_pdr := [#{f_teid := #f_teid{teid = TEID_ToCore}}, + #{f_teid := #f_teid{teid = TEID_ToAcc}}]}} -> + ?LOG_DEBUG("PFCP session established"), + {next_state, erab_wait_setup_rsp, + S#erab_state{from = undefined, + seid_rem = F_SEID, %% SEID to be used in further requests from us + teid_to_core = TEID_ToCore, + teid_to_access = TEID_ToAcc}, + [{reply, From, {ok, TEID_ToCore}}]}; + _ -> + ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]), + {stop_and_reply, + {shutdown, unexp_pdu}, + {reply, From, {error, {unexp_pdu, ?FUNCTION_NAME}}}} + end; + +session_establish(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +%% state WAIT_SETUP_RSP :: wait E-RAB SETUP Rsp from the Core +erab_wait_setup_rsp(enter, OldState, S) -> + ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), + {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout + [{state_timeout, ?ERAB_T_WAIT_SETUP_RSP, ?FUNCTION_NAME}]}; + +erab_wait_setup_rsp(state_timeout, _Timeout, _S) -> + {stop, {shutdown, timeout}}; + +erab_wait_setup_rsp({call, From}, + {erab_setup_rsp, TEID_FromCore}, + #erab_state{} = S) -> + ?LOG_DEBUG("Rx E-RAB SETUP Rsp (Core TEID ~p)", [TEID_FromCore]), + {next_state, session_modify, + S#erab_state{from = From, + teid_from_core = TEID_FromCore}}; + +%% Catch-all handler for this state +erab_wait_setup_rsp(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +%% state SESSION_MODIFY :: PFCP session modification +session_modify(enter, OldState, S) -> + ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), + ok = session_modify_req(S), + {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout + [{state_timeout, ?ERAB_T_SESSION_MODIFY, ?FUNCTION_NAME}]}; + +session_modify(state_timeout, _Timeout, + #erab_state{from = From}) -> + ?LOG_ERROR("PFCP session modification timeout"), + {stop_and_reply, + {shutdown, timeout}, + {reply, From, {error, {timeout, ?FUNCTION_NAME}}}}; + +session_modify(info, #pfcp{} = PDU, + #erab_state{from = From, + seid_loc = SEID_Rsp, + teid_to_access = TEID_ToAcc} = S) -> + case PDU of + #pfcp{type = session_modification_response, + seid = SEID_Rsp, %% matches F-SEID we sent in the ESTABLISH Req + ie = #{pfcp_cause := 'Request accepted'}} -> + ?LOG_DEBUG("PFCP session modified"), + {next_state, erab_setup, + S#erab_state{from = undefined}, + [{reply, From, {ok, TEID_ToAcc}}]}; + _ -> + ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]), + {stop_and_reply, + {shutdown, unexp_pdu}, + {reply, From, {error, {unexp_pdu, ?FUNCTION_NAME}}}} + end; + +session_modify(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +%% state SETUP :: E-RAB is fully setup +erab_setup(enter, OldState, S) -> + ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), + {keep_state, S}; + +erab_setup({call, From}, + erab_release_req, + #erab_state{} = S) -> + ?LOG_DEBUG("Rx E-RAB RELEASE Req"), + {next_state, session_delete, + S#erab_state{from = From}}; + +%% Catch-all handler for this state +erab_setup(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +%% state SESSION_DELETE :: PFCP session deletion +session_delete(enter, OldState, + #erab_state{seid_rem = SEID_Req} = S) -> + ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), + ok = pfcp_peer:session_delete_req(SEID_Req), + {next_state, ?FUNCTION_NAME, %% loop transition to enable state_timeout + %% unset seid_rem to prevent terminate/3 from deleting the session again + S#erab_state{seid_rem = undefined}, + [{state_timeout, ?ERAB_T_SESSION_DELETE, ?FUNCTION_NAME}]}; + +session_delete(state_timeout, _Timeout, + #erab_state{from = From}) -> + ?LOG_ERROR("PFCP session modification timeout"), + {stop_and_reply, + {shutdown, timeout}, + {reply, From, {error, {timeout, ?FUNCTION_NAME}}}}; + +session_delete(info, #pfcp{} = PDU, + #erab_state{from = From, + seid_loc = SEID_Rsp} = S) -> + case PDU of + #pfcp{type = session_deletion_response, + seid = SEID_Rsp, %% matches F-SEID we sent in the ESTABLISH Req + ie = #{pfcp_cause := 'Request accepted'}} -> + ?LOG_DEBUG("PFCP session deleted"), + {next_state, erab_wait_release_rsp, + S#erab_state{from = undefined}, + [{reply, From, ok}]}; + _ -> + ?LOG_ERROR("Rx unexpected PFCP PDU: ~p", [PDU]), + {stop_and_reply, + {shutdown, unexp_pdu}, + {reply, From, {error, {unexp_pdu, ?FUNCTION_NAME}}}} + end; + +session_delete(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +%% state WAIT_RELEASE_RSP :: wait E-RAB RELEASE Rsp from the Core +erab_wait_release_rsp(enter, OldState, S) -> + ?LOG_DEBUG("State change: ~p -> ~p", [OldState, ?FUNCTION_NAME]), + {next_state, ?FUNCTION_NAME, S, %% loop transition to enable state_timeout + [{state_timeout, ?ERAB_T_WAIT_RELEASE_RSP, ?FUNCTION_NAME}]}; + +erab_wait_release_rsp(state_timeout, _Timeout, _S) -> + {stop, {shutdown, timeout}}; + +erab_wait_release_rsp(cast, erab_release_rsp, + #erab_state{}) -> + ?LOG_DEBUG("Rx E-RAB RELEASE Rsp, we're done"), + {stop, normal}; %% we're done! + +%% Catch-all handler for this state +erab_wait_release_rsp(Event, EventData, S) -> + ?LOG_ERROR("Unexpected event ~p: ~p", [Event, EventData]), + {keep_state, S}. + + +code_change(_Vsn, State, S, _Extra) -> + {ok, State, S}. + + +terminate(Reason, State, S) -> + ?LOG_NOTICE("Terminating in state ~p, reason ~p", [State, Reason]), + case S of + %% PFCP session is not established or was deleted + #erab_state{seid_rem = undefined} -> + ok; + %% PFCP session is established, so we terminate it + #erab_state{seid_rem = SEID_Req} -> + ?LOG_INFO("Sending Session Deletion Req"), + pfcp_peer:session_delete_req(SEID_Req) + end. + + +%% ------------------------------------------------------------------ +%% private API +%% ------------------------------------------------------------------ + +%% set process metadata for the logger +set_logging_prefix(UID) -> + Prefix = io_lib:format("E-RAB ~p", [UID]), + logger:set_process_metadata(#{prefix => Prefix}). + + +-spec session_establish_req(erab_state()) -> pfcp_peer:pfcp_session_rsp(). +session_establish_req(#erab_state{seid_loc = F_SEID, %% used as F-SEID + teid_from_access = TEID_FromAcc}) -> + %% Packet Detection Rules + OHR = #outer_header_removal{header = 'GTP-U/UDP/IPv4'}, + PDRs = [#{pdr_id => {pdr_id, 1}, %% -- for Core -> Access + far_id => {far_id, 1}, %% see FARs below + precedence => {precedence, 255}, + outer_header_removal => OHR, + pdi => #{f_teid => #f_teid{teid = choose, + ipv4 = choose}, + source_interface => {source_interface, 'Core'}}}, + #{pdr_id => {pdr_id, 2}, %% -- for Access -> Core + far_id => {far_id, 2}, %% see FARs below + precedence => {precedence, 255}, + outer_header_removal => OHR, + pdi => #{f_teid => #f_teid{teid = choose, + ipv4 = choose}, + source_interface => {source_interface, 'Access'}}}], + %% Forwarding Action Rules + OHC = #outer_header_creation{n6 = false, + n19 = false, + type = 'GTP-U', + teid = TEID_FromAcc, + ipv4 = <<127,0,0,1>>}, %% XXX: Core facing addr of the UPF + FARs = [#{far_id => {far_id, 1}, %% -- for Core -> Access + %% We don't know the Core side TEID / GTP-U address yet, so we set + %% this FAR to DROP and modify it when we get E-RAB SETUP RESPONSE. + apply_action => #{'DROP' => []}}, + #{far_id => {far_id, 2}, %% -- for Access -> Core + apply_action => #{'FORW' => []}, + forwarding_parameters => + #{outer_header_creation => OHC, + destination_interface => {destination_interface, 'Core'}}}], + pfcp_peer:session_establish_req(F_SEID, PDRs, FARs). + + +-spec session_modify_req(erab_state()) -> pfcp_peer:pfcp_session_rsp(). +session_modify_req(#erab_state{seid_rem = SEID, %% SEID allocated to us + teid_from_core = TEID_FromCore}) -> + %% Forwarding Action Rules + OHC = #outer_header_creation{n6 = false, + n19 = false, + type = 'GTP-U', + teid = TEID_FromCore, + ipv4 = <<127,0,0,1>>}, %% XXX: Access facing addr of the UPF + FARs = [#{far_id => {far_id, 1}, %% -- for Core -> Access + %% Now we know the Core side TEID / GTP-U address, so we modify + %% this FAR (which was previously set to DROP) to FORW. + apply_action => #{'FORW' => []}, + forwarding_parameters => + #{outer_header_creation => OHC, + destination_interface => {destination_interface, 'Access'}}}], + pfcp_peer:session_modify_req(SEID, [], FARs). + +%% vim:set ts=4 sw=4 et: diff --git a/test/erab_fsm_test.erl b/test/erab_fsm_test.erl new file mode 100644 index 0000000..fce03fd --- /dev/null +++ b/test/erab_fsm_test.erl @@ -0,0 +1,122 @@ +-module(erab_fsm_test). + +-include_lib("eunit/include/eunit.hrl"). +-include("pfcp_mock.hrl"). + +%% ------------------------------------------------------------------ +%% setup functions +%% ------------------------------------------------------------------ + +-define(TC(Fun), {setup, + fun start/0, + fun stop/1, + Fun}). + + +start() -> + %% mock the PFCP peer API + pfcp_mock:mock_all(), + %% start an E-RAB FSM + {ok, Pid} = erab_fsm:start_link(?MODULE), + Pid. + + +stop(_Pid) -> + pfcp_mock:unmock_all(). + + +%% ------------------------------------------------------------------ +%% testcase descriptions +%% ------------------------------------------------------------------ + +erab_setup_test_() -> + [{"E-RAB setup :: success", + ?TC(fun test_erab_setup_success/1)}, + {"E-RAB setup :: PFCP session establishment error", + ?TC(fun test_erab_setup_pfcp_establish_error/1)}, + {"E-RAB setup :: PFCP session modification error", + ?TC(fun test_erab_setup_pfcp_modify_error/1)}]. + + +erab_release_test_() -> + [{"E-RAB release :: success", + ?TC(fun test_erab_release_success/1)}, + {"E-RAB release :: PFCP session deletion error", + ?TC(fun test_erab_release_pfcp_delete_error/1)}]. + + +erab_shutdown_test_() -> + [{"E-RAB shutdown (no session)", + ?TC(fun test_erab_shutdown_no_session/1)}, + {"E-RAB shutdown (expect session deletion)", + ?TC(fun test_erab_shutdown_session_del/1)}]. + + +%% ------------------------------------------------------------------ +%% actual testcases +%% ------------------------------------------------------------------ + +test_erab_setup_success(Pid) -> + [?_assertEqual({ok, ?TEID_ToCore}, erab_fsm:erab_setup_req(Pid, ?TEID_FromAcc)), + ?_assertEqual({ok, ?TEID_ToAcc}, erab_fsm:erab_setup_rsp(Pid, ?TEID_FromCore)), + ?_assertEqual(ok, erab_fsm:shutdown(Pid)), + ?_assertNot(erlang:is_process_alive(Pid))]. + + +test_erab_setup_pfcp_establish_error(Pid) -> + %% pfcp_peer:session_delete_req/1 shall not be called + ok = pfcp_mock:unmock_req(session_delete_req, 1), + %% pfcp_peer:session_establish_req/3 responds with a reject + PDU = pfcp_mock:pdu_rsp_reject(session_establishment_response, ?SEID_Loc), + pfcp_mock:mock_req(session_establish_req, PDU), + unlink(Pid), %% we expect the FSM to terminate abnormally + Error = {unexp_pdu, session_establish}, + [?_assertEqual({error, Error}, erab_fsm:erab_setup_req(Pid, ?TEID_FromAcc)), + ?_assertNot(erlang:is_process_alive(Pid))]. + + +test_erab_setup_pfcp_modify_error(Pid) -> + %% pfcp_peer:session_modify_req/3 responds with a reject + PDU = pfcp_mock:pdu_rsp_reject(session_modification_response, ?SEID_Loc), + pfcp_mock:mock_req(session_modify_req, PDU), + unlink(Pid), %% we expect the FSM to terminate abnormally + Error = {unexp_pdu, session_modify}, + [?_assertEqual({ok, ?TEID_ToCore}, erab_fsm:erab_setup_req(Pid, ?TEID_FromAcc)), + ?_assertEqual({error, Error}, erab_fsm:erab_setup_rsp(Pid, ?TEID_FromCore)), + ?_assertNot(erlang:is_process_alive(Pid))]. + + +test_erab_release_success(Pid) -> + [?_assertEqual({ok, ?TEID_ToCore}, erab_fsm:erab_setup_req(Pid, ?TEID_FromAcc)), + ?_assertEqual({ok, ?TEID_ToAcc}, erab_fsm:erab_setup_rsp(Pid, ?TEID_FromCore)), + ?_assertEqual(ok, erab_fsm:erab_release_req(Pid)), + ?_assertEqual(ok, erab_fsm:erab_release_rsp(Pid)), + ?_assertNot(erlang:is_process_alive(Pid))]. + + +test_erab_release_pfcp_delete_error(Pid) -> + %% pfcp_peer:session_delete_req/1 responds with a reject + PDU = pfcp_mock:pdu_rsp_reject(session_deletion_response, ?SEID_Loc), + pfcp_mock:mock_req(session_delete_req, PDU), + unlink(Pid), %% we expect the FSM to terminate abnormally + Error = {unexp_pdu, session_delete}, + [?_assertEqual({ok, ?TEID_ToCore}, erab_fsm:erab_setup_req(Pid, ?TEID_FromAcc)), + ?_assertEqual({ok, ?TEID_ToAcc}, erab_fsm:erab_setup_rsp(Pid, ?TEID_FromCore)), + ?_assertEqual({error, Error}, erab_fsm:erab_release_req(Pid)), + ?_assertNot(erlang:is_process_alive(Pid))]. + + +test_erab_shutdown_no_session(Pid) -> + %% pfcp_peer:session_delete_req/1 shall not be called + ok = pfcp_mock:unmock_req(session_delete_req, 1), + [?_assertEqual(ok, erab_fsm:shutdown(Pid)), + ?_assertNot(erlang:is_process_alive(Pid))]. + + +test_erab_shutdown_session_del(Pid) -> + [?_assertEqual({ok, ?TEID_ToCore}, erab_fsm:erab_setup_req(Pid, ?TEID_FromAcc)), + ?_assertEqual({ok, ?TEID_ToAcc}, erab_fsm:erab_setup_rsp(Pid, ?TEID_FromCore)), + ?_assertEqual(ok, erab_fsm:shutdown(Pid)), + ?_assertNot(erlang:is_process_alive(Pid))]. + +%% vim:set ts=4 sw=4 et: diff --git a/test/pfcp_mock.erl b/test/pfcp_mock.erl new file mode 100644 index 0000000..c9f8297 --- /dev/null +++ b/test/pfcp_mock.erl @@ -0,0 +1,104 @@ +-module(pfcp_mock). + +-export([mock_all/0, + mock_req/2, + unmock_all/0, + unmock_req/2]). +-export([pdu_rsp/3, + pdu_rsp_accept/2, + pdu_rsp_reject/2, + pdu_session_establish_rsp/0, + pdu_session_modify_rsp/0, + pdu_session_delete_rsp/0]). + +-include_lib("stdlib/include/assert.hrl"). +-include_lib("pfcplib/include/pfcp_packet.hrl"). +-include("pfcp_mock.hrl"). + +%% ------------------------------------------------------------------ +%% public API +%% ------------------------------------------------------------------ + +%% mock all pfcp_peer module functions +mock_all() -> + meck:new(pfcp_peer), + meck:expect(pfcp_peer, seid_alloc, fun() -> {ok, ?SEID_Loc} end), + mock_req(session_establish_req, pdu_session_establish_rsp()), + mock_req(session_modify_req, pdu_session_modify_rsp()), + mock_req(session_delete_req, pdu_session_delete_rsp()). + + +%% unmock all pfcp_peer module functions +unmock_all() -> + true = meck:validate(pfcp_peer), + meck:unload(pfcp_peer). + + +pfcp_req_stub(Rsp) -> + %% this code is executed by E-RAB FSM itself, + %% thus self() evaluates to its own Pid + self() ! Rsp, + ok. + + +%% mock one of the pfcp_peer:*_req functions +mock_req(session_establish_req = Fun, Rsp) -> + FunBody = fun(F_SEID, _PDRs, _FARs) -> + %% expect the E-RAB FSM to pass its local SEID as F-SEID + ?assertEqual(F_SEID, ?SEID_Loc), + pfcp_req_stub(Rsp) + end, + meck:expect(pfcp_peer, Fun, FunBody); + +mock_req(session_modify_req = Fun, Rsp) -> + FunBody = fun(SEID, _PDRs, _FARs) -> + %% expect the E-RAB FSM to pass the remote SEID + ?assertEqual(SEID, ?SEID_Rem), + pfcp_req_stub(Rsp) + end, + meck:expect(pfcp_peer, Fun, FunBody); + +mock_req(session_delete_req = Fun, Rsp) -> + FunBody = fun(SEID) -> + %% expect the E-RAB FSM to pass the remote SEID + ?assertEqual(SEID, ?SEID_Rem), + pfcp_req_stub(Rsp) + end, + meck:expect(pfcp_peer, Fun, FunBody). + + +%% unmock one of the pfcp_peer:*_req functions +unmock_req(Fun, Arity) -> + meck:delete(pfcp_peer, Fun, Arity). + + +%% ------------------------------------------------------------------ +%% PFCP PDU templates +%% ------------------------------------------------------------------ + +pdu_rsp(MsgType, SEID, IEs) -> + #pfcp{type = MsgType, seid = SEID, ie = IEs}. + +pdu_rsp_accept(MsgType, SEID) -> + pdu_rsp(MsgType, SEID, #{pfcp_cause => 'Request accepted'}). + +pdu_rsp_reject(MsgType, SEID) -> + pdu_rsp(MsgType, SEID, #{pfcp_cause => 'Request rejected'}). + + +pdu_session_establish_rsp() -> + IEs = #{pfcp_cause => 'Request accepted', + f_seid => #f_seid{seid = ?SEID_Rem}, + created_pdr => [#{f_teid => #f_teid{teid = ?TEID_ToCore}}, + #{f_teid => #f_teid{teid = ?TEID_ToAcc}}]}, + pdu_rsp(session_establishment_response, ?SEID_Loc, IEs). + + +pdu_session_modify_rsp() -> + pdu_rsp_accept(session_modification_response, ?SEID_Loc). + + +pdu_session_delete_rsp() -> + pdu_rsp_accept(session_deletion_response, ?SEID_Loc). + +%% vim:set ts=4 sw=4 et: diff --git a/test/pfcp_mock.hrl b/test/pfcp_mock.hrl new file mode 100644 index 0000000..06b5c5a --- /dev/null +++ b/test/pfcp_mock.hrl @@ -0,0 +1,9 @@ +-define(SEID_Loc, 16#dead). +-define(SEID_Rem, 16#beef). + +-define(TEID_FromAcc, 16#0001). +-define(TEID_FromCore, 16#0002). +-define(TEID_ToAcc, 16#0003). +-define(TEID_ToCore, 16#0004). + +%% vim:set ts=4 sw=4 et: