fixeria has uploaded this change for review. ( 
https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/42495?usp=email )


Change subject: s1gw: add more PFCP Heartbeat test cases
......................................................................

s1gw: add more PFCP Heartbeat test cases

TC_pfcp_heartbeat_periodic: verify that the IUT sends periodic PFCP
Heartbeat Requests and that the heartbeat timer re-arms correctly after
each successful response (at least 2 periodic HBs must be observed).

TC_pfcp_heartbeat_miss_threshold: verify that the IUT resets the PFCP
association after mp_pfcp_heartbeat_miss_count consecutive unanswered
Heartbeat Requests (a new Association Setup Request is expected).

TC_pfcp_heartbeat_miss_reset: verify that a successful Heartbeat
Response resets the miss counter; run 2 cycles of missing
(miss_count - 1) HBs then responding to one, expecting no association
reset throughout.

TC_pfcp_heartbeat_rts_mismatch: verify that the IUT detects a UPF
restart when a Heartbeat Response carries a different Recovery Timestamp
than the one observed during Association Setup, and that it promptly
resets and re-establishes the PFCP association.

Also add module parameters for the heartbeat configuration
(mp_pfcp_heartbeat_interval, mp_pfcp_heartbeat_req_timeout,
mp_pfcp_heartbeat_miss_count) and update osmo-s1gw.config accordingly.

Change-Id: Ie5ac25b1ca4bb11e61bff220449397c271b11464
Related: osmo-s1gw.git Iba954746fe20e6b9eeaec3196e1f83e3fc3e7fc2
Related: osmo-s1gw.git I306324f8eca325202a3fa23125854db9d5eaab38
Related: osmo-s1gw.git I00a8384db1ea9c38d0ad9ff90b3d45ad86c3a020
---
M s1gw/S1GW_ConnHdlr.ttcn
M s1gw/S1GW_Tests.ttcn
M s1gw/expected-results.xml
M s1gw/osmo-s1gw.config
4 files changed, 278 insertions(+), 8 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/osmo-ttcn3-hacks 
refs/changes/95/42495/1

diff --git a/s1gw/S1GW_ConnHdlr.ttcn b/s1gw/S1GW_ConnHdlr.ttcn
index 297eccd..aeda4c5 100644
--- a/s1gw/S1GW_ConnHdlr.ttcn
+++ b/s1gw/S1GW_ConnHdlr.ttcn
@@ -398,6 +398,15 @@
        Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
 }

+function f_PFCPEM_tx_assoc_setup_resp(LIN3_BO_LAST seq_nr)
+runs on PFCP_ConnHdlr
+{
+       PFCP.send(ts_PFCP_Assoc_Setup_Resp(seq_nr,
+                                          
ts_PFCP_Node_ID_fqdn("\07osmocom\03org"),
+                                          ts_PFCP_Cause(REQUEST_ACCEPTED),
+                                          f_PFCPEM_get_recovery_timestamp()));
+}
+
 private function f_ConnHdlr_pfcp_assoc_setup()
 runs on ConnHdlr
 {
@@ -406,10 +415,7 @@
        f_PFCPEM_subscribe_bcast(); /* ask PFCPEM to route all PDUs to us */
        rx := f_ConnHdlr_pfcp_expect(tr_PFCP_Assoc_Setup_Req, Tval := 10.0);
        f_PFCPEM_unsubscribe_bcast(); /* ask PFCPEM to *not* route all PDUs to 
us */
-       PFCP.send(ts_PFCP_Assoc_Setup_Resp(rx.sequence_number,
-                                          
ts_PFCP_Node_ID_fqdn("\07osmocom\03org"),
-                                          ts_PFCP_Cause(REQUEST_ACCEPTED),
-                                          f_PFCPEM_get_recovery_timestamp()));
+       f_PFCPEM_tx_assoc_setup_resp(rx.sequence_number);
 }

 function f_ConnHdlr_pfcp_expect(template (present) PDU_PFCP exp_rx := ?,
diff --git a/s1gw/S1GW_Tests.ttcn b/s1gw/S1GW_Tests.ttcn
index 38a5e7d..198bc7b 100644
--- a/s1gw/S1GW_Tests.ttcn
+++ b/s1gw/S1GW_Tests.ttcn
@@ -68,6 +68,11 @@

        charstring mp_rest_host := "127.0.0.1";
        integer mp_rest_port := 8080;
+
+       /* PFCP heartbeat test parameters (must match IUT's pfcp_peer 
configuration) */
+       float mp_pfcp_heartbeat_interval := 4.0;
+       float mp_pfcp_heartbeat_req_timeout := 2.0;
+       integer mp_pfcp_heartbeat_miss_count := 3;
 }

 type component test_CT extends StatsD_Checker_CT, http_CT {
@@ -87,6 +92,7 @@
 function f_init(boolean s1apsrv_start := true,
                integer num_mmes := 1,
                boolean upf_start := true,
+               boolean pfcp_answer_heartbeat := true,
                float Tval := 20.0) runs on test_CT {
        g_Tguard.start(Tval);
        activate(as_Tguard());
@@ -100,7 +106,7 @@
        }
        f_init_rest();
        if (upf_start) {
-               f_init_pfcp();
+               f_init_pfcp(pfcp_answer_heartbeat);
                f_pfcp_assoc();
        }
 }
@@ -121,14 +127,14 @@
        }
 }

-function f_init_pfcp() runs on test_CT {
+function f_init_pfcp(boolean answer_heartbeat_req := true) runs on test_CT {
        var PFCP_Emulation_Cfg pfcp_cfg := {
                pfcp_bind_ip := mp_upf_bind_ip,
                pfcp_bind_port := PFCP_PORT,
                pfcp_remote_ip := mp_s1gw_upf_ip,
                pfcp_remote_port := PFCP_PORT,
                role := UPF,
-               answer_heartbeat_req := true
+               answer_heartbeat_req := answer_heartbeat_req
        };

        vc_PFCP := PFCP_Emulation_CT.create("PFCPEM-" & testcasename()) alive;
@@ -1028,6 +1034,29 @@
        f_TC_exec(refers(f_TC_handover_res_alloc_fail), 1, 6);
 }

+/* Like f_TC_exec() but without S1AP server and with answer_heartbeat_req := 
false,
+ * allowing test functions to observe and control IUT-initiated HB requests 
via bcast.
+ * The PFCP association is established via f_pfcp_assoc() before the ConnHdlr 
starts. */
+private function f_TC_exec_pfcp_hb(void_fn fn, float Tval := 60.0) runs on 
test_CT {
+       var ConnHdlr vc_conn;
+
+       f_init(s1apsrv_start := false, pfcp_answer_heartbeat := false, Tval := 
Tval);
+       vc_conn := f_ConnHdlr_spawn(fn, valueof(t_ConnHdlrPars));
+       vc_conn.done;
+}
+
+/* Send a PFCP Heartbeat Response with an explicit RTS value */
+private function f_pfcp_send_hb_resp_rts(PDU_PFCP req, integer rts) runs on 
ConnHdlr {
+       var PDU_PFCP resp := valueof(ts_PFCP_Heartbeat_Resp(rts));
+       resp.sequence_number := req.sequence_number;
+       PFCP.send(resp);
+}
+
+/* Send a PFCP Heartbeat Response with the PFCPEM's own recovery timestamp */
+private function f_pfcp_send_hb_resp(PDU_PFCP req) runs on ConnHdlr {
+       f_pfcp_send_hb_resp_rts(req, f_PFCPEM_get_recovery_timestamp());
+}
+
 function f_TC_pfcp_heartbeat(charstring id) runs on ConnHdlr {
        var integer rts := f_PFCPEM_get_recovery_timestamp();
        var PfcpAssocInfo assoc_info;
@@ -1062,6 +1091,225 @@
        f_TC_exec(refers(f_TC_pfcp_heartbeat));
 }

+/* Verify that the IUT sends periodic PFCP Heartbeat Requests when 
heartbeat_interval > 0.
+ * The test subscribes to PFCPEM broadcast, responds to each HB request, and 
verifies
+ * that at least 2 periodic HBs are received (proving the timer re-arms 
correctly).
+ * The PFCP association is assumed to be already established (done by 
f_TC_exec_pfcp_hb). */
+function f_TC_pfcp_heartbeat_periodic(charstring id) runs on ConnHdlr {
+       var PDU_PFCP pfcp_pdu;
+       timer T;
+
+       if (mp_pfcp_heartbeat_interval == 0.0) {
+               setverdict(inconc, "periodic HB is disabled, skipped");
+               return;
+       }
+
+       /* Subscribe to bcast so we receive IUT-initiated HB requests */
+       f_PFCPEM_subscribe_bcast();
+
+       /* Expect at least 2 periodic HB requests, responding to each */
+       for (var integer i := 0; i < 2; i := i + 1) {
+               T.start(mp_pfcp_heartbeat_interval +
+                       mp_pfcp_heartbeat_req_timeout + 2.0);
+               alt {
+               [] PFCP.receive(tr_PFCP_Heartbeat_Req) -> value pfcp_pdu {
+                       f_pfcp_send_hb_resp(pfcp_pdu);
+                       T.stop;
+                       }
+               [] T.timeout {
+                       setverdict(fail, "Timeout waiting for periodic PFCP 
Heartbeat Request");
+                       Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
+                       }
+               }
+       }
+
+       f_PFCPEM_unsubscribe_bcast();
+       setverdict(pass);
+}
+testcase TC_pfcp_heartbeat_periodic() runs on test_CT {
+       const float Tval := (mp_pfcp_heartbeat_interval + 
mp_pfcp_heartbeat_req_timeout) * 3.0 + 10.0;
+       f_TC_exec_pfcp_hb(refers(f_TC_pfcp_heartbeat_periodic), Tval := Tval);
+}
+
+/* Verify that the IUT resets the PFCP association after miss_count 
consecutive unanswered
+ * Heartbeat Requests.  The test ignores all HB requests and waits for the IUT 
to initiate
+ * a new PFCP Association Setup (proving the association was reset).
+ * The PFCP association is assumed to be already established (done by 
f_TC_exec_pfcp_hb). */
+function f_TC_pfcp_heartbeat_miss_threshold(charstring id) runs on ConnHdlr {
+       var PDU_PFCP pfcp_pdu;
+       var float Tval := (mp_pfcp_heartbeat_interval + 
mp_pfcp_heartbeat_req_timeout)
+                       * int2float(mp_pfcp_heartbeat_miss_count) + 10.0;
+       timer T;
+
+       if (mp_pfcp_heartbeat_interval == 0.0) {
+               setverdict(inconc, "periodic HB is disabled, skipped");
+               return;
+       }
+
+       /* Subscribe to bcast: we'll receive both HB requests and the new assoc 
setup req */
+       f_PFCPEM_subscribe_bcast();
+
+       /* Do NOT respond to any HB requests; wait for the IUT to reset and 
re-associate */
+       T.start(Tval);
+       alt {
+       [] PFCP.receive(tr_PFCP_Assoc_Setup_Req) -> value pfcp_pdu {
+               /* IUT reset the association and started a new one - success */
+               f_PFCPEM_tx_assoc_setup_resp(pfcp_pdu.sequence_number);
+               }
+       [] PFCP.receive(tr_PFCP_Heartbeat_Req) {
+               /* Discard unanswered HB requests and keep waiting */
+               repeat;
+               }
+       [] T.timeout {
+               setverdict(fail, "Timeout: IUT did not reset PFCP association 
after ",
+                          mp_pfcp_heartbeat_miss_count, " missed heartbeats");
+               Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
+               }
+       }
+
+       f_PFCPEM_unsubscribe_bcast();
+       setverdict(pass);
+}
+testcase TC_pfcp_heartbeat_miss_threshold() runs on test_CT {
+       var float Tval := (mp_pfcp_heartbeat_interval + 
mp_pfcp_heartbeat_req_timeout)
+                       * int2float(mp_pfcp_heartbeat_miss_count + 1) + 15.0;
+       f_TC_exec_pfcp_hb(refers(f_TC_pfcp_heartbeat_miss_threshold), Tval := 
Tval);
+}
+
+/* Verify that a successful HB response resets the miss counter.  The test 
misses
+ * (miss_count - 1) HBs, then responds to one, then misses (miss_count - 1) 
again
+ * and responds to one.  If the miss counter is properly reset on success, the 
IUT
+ * should NOT reset the association during this sequence.
+ * The PFCP association is assumed to be already established (done by 
f_TC_exec_pfcp_hb). */
+function f_TC_pfcp_heartbeat_miss_reset(charstring id) runs on ConnHdlr {
+       var PDU_PFCP pfcp_pdu;
+       var float Tval := mp_pfcp_heartbeat_interval + 
mp_pfcp_heartbeat_req_timeout + 2.0;
+       timer T;
+
+       if (mp_pfcp_heartbeat_interval == 0.0) {
+               setverdict(inconc, "periodic HB is disabled, skipped");
+               return;
+       }
+
+       /* Subscribe to bcast: we'll receive HB requests (and assoc setup if 
reset occurs) */
+       f_PFCPEM_subscribe_bcast();
+
+       /* Run 2 full miss-then-recover cycles */
+       for (var integer i := 0; i < 2; i := i + 1) {
+               /* Miss (mp_pfcp_heartbeat_miss_count - 1) heartbeats */
+               for (var integer j := 0; j < mp_pfcp_heartbeat_miss_count - 1; 
j := j + 1) {
+                       T.start(Tval);
+                       alt {
+                       [] PFCP.receive(tr_PFCP_Heartbeat_Req) {
+                               /* Discard without responding */
+                               T.stop;
+                               }
+                       [] PFCP.receive(tr_PFCP_Assoc_Setup_Req) {
+                               setverdict(fail, "IUT reset association 
unexpectedly");
+                               Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
+                               }
+                       [] T.timeout {
+                               setverdict(fail, "Timeout waiting for periodic 
PFCP Heartbeat Request");
+                               Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
+                               }
+                       }
+               }
+
+               /* Respond to the next HB to reset the miss counter */
+               T.start(Tval);
+               alt {
+               [] PFCP.receive(tr_PFCP_Heartbeat_Req) -> value pfcp_pdu {
+                       T.stop;
+                       f_pfcp_send_hb_resp(pfcp_pdu);
+                       }
+               [] PFCP.receive(tr_PFCP_Assoc_Setup_Req) {
+                       setverdict(fail, "IUT reset association unexpectedly 
(miss counter not reset?)");
+                       Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
+                       }
+               [] T.timeout {
+                       setverdict(fail, "Timeout waiting for periodic PFCP 
Heartbeat Request");
+                       Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
+                       }
+               }
+       }
+
+       f_PFCPEM_unsubscribe_bcast();
+       setverdict(pass);
+}
+testcase TC_pfcp_heartbeat_miss_reset() runs on test_CT {
+       var float Tval := (mp_pfcp_heartbeat_interval + 
mp_pfcp_heartbeat_req_timeout)
+                       * int2float(mp_pfcp_heartbeat_miss_count) * 2.0 + 15.0;
+       f_TC_exec_pfcp_hb(refers(f_TC_pfcp_heartbeat_miss_reset), Tval := Tval);
+}
+
+/* Verify that the IUT detects a UPF restart when the Recovery Timestamp in a 
Heartbeat
+ * Response differs from the value stored during Association Setup, and resets 
the
+ * PFCP association as a result.
+ * The PFCP association is assumed to be already established (done by 
f_TC_exec_pfcp_hb). */
+function f_TC_pfcp_heartbeat_rts_mismatch(charstring id) runs on ConnHdlr {
+       var PDU_PFCP pfcp_pdu;
+       timer T;
+
+       if (mp_pfcp_heartbeat_interval == 0.0) {
+               setverdict(inconc, "periodic HB is disabled, skipped");
+               return;
+       }
+
+       /* Subscribe to bcast so we receive IUT-initiated HB requests */
+       f_PFCPEM_subscribe_bcast();
+
+       /* Wait for the first periodic HB request */
+       T.start(mp_pfcp_heartbeat_interval + mp_pfcp_heartbeat_req_timeout + 
2.0);
+       alt {
+       [] PFCP.receive(tr_PFCP_Heartbeat_Req) -> value pfcp_pdu {
+               T.stop;
+               /* Respond with a mismatched RTS to simulate a UPF restart */
+               f_pfcp_send_hb_resp_rts(pfcp_pdu, 
f_PFCPEM_get_recovery_timestamp() + 1);
+               }
+       [] T.timeout {
+               setverdict(fail, "Timeout waiting for periodic PFCP Heartbeat 
Request");
+               Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
+               }
+       }
+
+       /* IUT should detect the RTS mismatch and reset the association 
immediately */
+       T.start(2.0);
+       alt {
+       [] PFCP.receive(tr_PFCP_Assoc_Setup_Req) -> value pfcp_pdu {
+               T.stop;
+               /* Verify via REST that the IUT is in connecting state 
(association was reset) */
+               var PfcpAssocInfo assoc := f_REST_PfcpAssocState();
+               if (assoc.state != connecting) {
+                       setverdict(fail, "Unexpected PFCP assoc state ", 
assoc.state);
+                       Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
+               }
+               /* Complete the new association setup */
+               f_PFCPEM_tx_assoc_setup_resp(pfcp_pdu.sequence_number);
+               /* Allow the IUT to process the response, then verify it 
re-associated */
+               f_sleep(0.5);
+               assoc := f_REST_PfcpAssocState();
+               if (assoc.state != connected) {
+                       setverdict(fail, "Unexpected PFCP assoc state ", 
assoc.state);
+                       Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
+               }
+               }
+       [] PFCP.receive(tr_PFCP_Heartbeat_Req) {
+               repeat; /* discard any further HBs while waiting for the assoc 
reset */
+               }
+       [] T.timeout {
+               setverdict(fail, "Timeout: IUT did not reset PFCP association 
after RTS mismatch");
+               Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
+               }
+       }
+
+       f_PFCPEM_unsubscribe_bcast();
+       setverdict(pass);
+}
+testcase TC_pfcp_heartbeat_rts_mismatch() runs on test_CT {
+       var float Tval := mp_pfcp_heartbeat_interval + 
mp_pfcp_heartbeat_req_timeout + 20.0;
+       f_TC_exec_pfcp_hb(refers(f_TC_pfcp_heartbeat_rts_mismatch), Tval := 
Tval);
+}
+
 /* MME pool test: eNB connects, the first MME rejects, the second one accepts 
*/
 function f_TC_mme_pool_reject_fallback(charstring id) runs on ConnHdlr {
        f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
@@ -1410,6 +1658,10 @@
        execute( TC_handover_res_alloc() );
        execute( TC_handover_res_alloc_fail() );
        execute( TC_pfcp_heartbeat() );
+       execute( TC_pfcp_heartbeat_periodic() );
+       execute( TC_pfcp_heartbeat_miss_threshold() );
+       execute( TC_pfcp_heartbeat_miss_reset() );
+       execute( TC_pfcp_heartbeat_rts_mismatch() );
        execute( TC_mme_pool_reject_fallback() );
        execute( TC_mme_pool_timeout_fallback() );
        execute( TC_mme_pool_all_reject() );
diff --git a/s1gw/expected-results.xml b/s1gw/expected-results.xml
index b2b6cdb..d5fa997 100644
--- a/s1gw/expected-results.xml
+++ b/s1gw/expected-results.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0"?>
-<testsuite name='S1GW_Tests' tests='41' failures='0' errors='0' skipped='0' 
inconc='0' time='MASKED'>
+<testsuite name='S1GW_Tests' tests='45' failures='0' errors='0' skipped='0' 
inconc='0' time='MASKED'>
   <testcase classname='S1GW_Tests' name='TC_setup' time='MASKED'/>
   <testcase classname='S1GW_Tests' name='TC_setup_multi' time='MASKED'/>
   <testcase classname='S1GW_Tests' name='TC_conn_term_by_mme' time='MASKED'/>
@@ -35,6 +35,10 @@
   <testcase classname='S1GW_Tests' name='TC_handover_res_alloc' time='MASKED'/>
   <testcase classname='S1GW_Tests' name='TC_handover_res_alloc_fail' 
time='MASKED'/>
   <testcase classname='S1GW_Tests' name='TC_pfcp_heartbeat' time='MASKED'/>
+  <testcase classname='S1GW_Tests' name='TC_pfcp_heartbeat_periodic' 
time='MASKED'/>
+  <testcase classname='S1GW_Tests' name='TC_pfcp_heartbeat_miss_threshold' 
time='MASKED'/>
+  <testcase classname='S1GW_Tests' name='TC_pfcp_heartbeat_miss_reset' 
time='MASKED'/>
+  <testcase classname='S1GW_Tests' name='TC_pfcp_heartbeat_rts_mismatch' 
time='MASKED'/>
   <testcase classname='S1GW_Tests' name='TC_mme_pool_reject_fallback' 
time='MASKED'/>
   <testcase classname='S1GW_Tests' name='TC_mme_pool_timeout_fallback' 
time='MASKED'/>
   <testcase classname='S1GW_Tests' name='TC_mme_pool_all_reject' 
time='MASKED'/>
diff --git a/s1gw/osmo-s1gw.config b/s1gw/osmo-s1gw.config
index 5788446..f7fad64 100644
--- a/s1gw/osmo-s1gw.config
+++ b/s1gw/osmo-s1gw.config
@@ -30,6 +30,14 @@
       #{name => "mme1", laddr => "127.0.2.1", raddr => "127.0.2.11"},
       #{name => "mme2", laddr => "127.0.2.1", raddr => "127.0.2.12"}
    ]},
+   {pfcp_peer, #{
+      laddr => "127.0.3.1",
+      raddr => "127.0.3.10",
+      heartbeat_interval => 4_000,
+      heartbeat_req_timeout => 2_000,
+      heartbeat_miss_count => 3
+   }},
+   %% XXX: pfcp_{loc,rem}_addr kept for compatibility with the -latest (<= 
0.4.0)
    {pfcp_loc_addr, "127.0.3.1"}, %% local address for incoming PFCP PDUs from 
the UPF
    {pfcp_rem_addr, "127.0.3.10"} %% remote address for outgoing PFCP PDUs to 
the UPF
  ]},

--
To view, visit https://gerrit.osmocom.org/c/osmo-ttcn3-hacks/+/42495?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.osmocom.org/settings?usp=email

Gerrit-MessageType: newchange
Gerrit-Project: osmo-ttcn3-hacks
Gerrit-Branch: master
Gerrit-Change-Id: Ie5ac25b1ca4bb11e61bff220449397c271b11464
Gerrit-Change-Number: 42495
Gerrit-PatchSet: 1
Gerrit-Owner: fixeria <[email protected]>

Reply via email to