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(a)sysmocom.de>
+%% Author: Vadim Yanitskiy <vyanitskiy(a)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:
--
To view, visit
https://gerrit.osmocom.org/c/erlang/osmo-s1gw/+/37317?usp=email
To unsubscribe, or for help writing mail filters, visit
https://gerrit.osmocom.org/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: erlang/osmo-s1gw
Gerrit-Branch: master
Gerrit-Change-Id: Ic57e14675339b7cadb0cdd7cbc5d3a56288d7297
Gerrit-Change-Number: 37317
Gerrit-PatchSet: 12
Gerrit-Owner: fixeria <vyanitskiy(a)sysmocom.de>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge(a)osmocom.org>
Gerrit-Reviewer: osmith <osmith(a)sysmocom.de>
Gerrit-Reviewer: pespin <pespin(a)sysmocom.de>