[codecs filter] msc_vlr_test_call: test codecs resolution

Show that the codecs filter now works as expected in call messaging.

Related: SYS#5066
Change-Id: I879ec61f523ad4ffc69a0b02810591f7c0261ff9
diff --git a/tests/msc_vlr/msc_vlr_test_call.c b/tests/msc_vlr/msc_vlr_test_call.c
index 07c6e13..2c67c1e 100644
--- a/tests/msc_vlr/msc_vlr_test_call.c
+++ b/tests/msc_vlr/msc_vlr_test_call.c
@@ -24,6 +24,7 @@
 #include "msc_vlr_tests.h"
 
 #include <osmocom/msc/gsm_04_08.h>
+#include <osmocom/msc/codec_mapping.h>
 
 #define mncc_sends_to_cc(MSG_TYPE, MNCC) do { \
 		(MNCC)->msg_type = MSG_TYPE; \
@@ -163,6 +164,44 @@
 	vlr_subscr_put(vsub, __func__);
 }
 
+static void lu_geran_noauth(void)
+{
+	rx_from_ran = OSMO_RAT_GERAN_A;
+	net->authentication_required = false;
+	net->vlr->cfg.assign_tmsi = false;
+
+	btw("Location Update request causes a GSUP LU request to HLR");
+	lu_result_sent = RES_NONE;
+	gsup_expect_tx("04010809710000000156f0280102" VLR_TO_HLR);
+	ms_sends_msg("0508" /* MM LU */
+		     "7" /* ciph key seq: no key available */
+		     "0" /* LU type: normal */
+		     "09f107" "0017" /* LAI, LAC */
+		     "57" /* classmark 1: R99, early classmark, no power lvl */
+		     "089910070000106005" /* IMSI */
+		     "3303575886" /* classmark 2 */
+		     );
+	OSMO_ASSERT(gsup_tx_confirmed);
+	VERBOSE_ASSERT(lu_result_sent, == RES_NONE, "%d");
+
+	btw("HLR sends _INSERT_DATA_REQUEST, VLR responds with _INSERT_DATA_RESULT");
+	gsup_rx("10010809710000000156f00804036470f1" HLR_TO_VLR,
+		"12010809710000000156f0" VLR_TO_HLR);
+	VERBOSE_ASSERT(lu_result_sent, == RES_NONE, "%d");
+
+	btw("HLR also sends GSUP _UPDATE_LOCATION_RESULT");
+	expect_bssap_clear();
+	gsup_rx("06010809710000000156f0" HLR_TO_VLR, NULL);
+
+	btw("LU was successful, and the conn has already been closed");
+	VERBOSE_ASSERT(lu_result_sent, == RES_ACCEPT, "%d");
+	VERBOSE_ASSERT(bssap_clear_sent, == true, "%d");
+
+	ran_sends_clear_complete();
+	EXPECT_CONN_COUNT(0);
+}
+
+
 static void test_call_mo()
 {
 	struct gsm_mncc mncc = {
@@ -719,6 +758,691 @@
 	comment_end();
 }
 
+#define LIST_END 0xffff
+
+struct codec_test {
+	const char *desc;
+
+	/* What to send during Complete Layer 3 as Codec List (BSS Supported). List ends with a LIST_END entry */
+	enum gsm0808_speech_codec_type mo_rx_compl_l3_codec_list_bss_supported[8];
+
+	/* What to send during CC Setup as MS Bearer Capability. List ends with a LIST_END entry */
+	enum gsm48_bcap_speech_ver mo_rx_ms_bcap[8];
+
+	/* What codecs should osmo-msc send in the MNCC_SETUP_IND message.
+	 * Just the SDP subtype names like "GSM", "GSM-EFR", "AMR", ..., list ends with NULL entry */
+	const char *mo_tx_sdp_mncc_setup_ind[8];
+
+	/* What codecs the remote call leg should send as SDP via MNCC during MNCC_RTP_CREATE (if any). */
+	const char *mo_rx_sdp_mncc_rtp_create[8];
+
+	/* What the MSC should send as Channel Type IE in the Assignment Command to the BSS. List ends with a
+	 * LIST_END entry */
+	enum gsm0808_permitted_speech mo_tx_assignment_perm_speech[8];
+
+	/* What codec to assign in the Assignment Complete's Codec (Chosen) IE. Just a subtype name. */
+	const char *mo_rx_assigned_codec;
+
+	/* MO acks the MNCC_RTP_CREATE with these codecs (if any). */
+	const char *mo_tx_sdp_mncc_rtp_create[8];
+
+	/* mt_rx_sdp_mncc_setup_req == mo_tx_sdp_mncc_rtp_create */
+#define mt_rx_sdp_mncc_setup_req  mo_tx_sdp_mncc_rtp_create
+
+	enum gsm0808_speech_codec_type mt_rx_compl_l3_codec_list_bss_supported[8];
+	bool expect_codec_mismatch_on_paging_response;
+	enum gsm48_bcap_speech_ver mt_tx_cc_setup_bcap[8];
+	enum gsm48_bcap_speech_ver mt_rx_ms_bcap[8];
+	bool expect_codec_mismatch_on_cc_call_conf;
+	const char *mt_tx_sdp_mncc_call_conf_ind[8];
+
+	enum gsm0808_permitted_speech mt_tx_assignment_perm_speech[8];
+	const char *mt_rx_assigned_codec;
+
+	const char *mt_rx_sdp_mncc_rtp_create[8];
+	const char *mt_tx_sdp_mncc_rtp_create[8];
+
+	const char *mt_tx_sdp_mncc_alert_ind[8];
+	const char *mt_tx_sdp_mncc_setup_cnf[8];
+	const char *mt_rx_sdp_mncc_setup_compl_req[8];
+
+	/* mo_rx_sdp_mncc_alert_req == mt_tx_sdp_mncc_alert_ind */
+#define mo_rx_sdp_mncc_alert_req  mt_tx_sdp_mncc_alert_ind
+#define mo_rx_sdp_mncc_setup_rsp  mt_tx_sdp_mncc_alert_ind
+
+	const char *mo_tx_sdp_mncc_setup_compl_ind[8];
+};
+
+#define CODEC_LIST_ALL_GSM { \
+			GSM0808_SCT_FR1, \
+			GSM0808_SCT_FR2, \
+			GSM0808_SCT_FR3, \
+			GSM0808_SCT_HR1, \
+			GSM0808_SCT_HR3, \
+			LIST_END \
+		}
+
+#define BCAP_ALL_GSM { \
+			GSM48_BCAP_SV_AMR_F, \
+			GSM48_BCAP_SV_AMR_H, \
+			GSM48_BCAP_SV_AMR_OH, \
+			GSM48_BCAP_SV_EFR, \
+			GSM48_BCAP_SV_FR, \
+			GSM48_BCAP_SV_HR, \
+			LIST_END \
+		}
+
+#define PERM_SPEECH_ALL_GSM { \
+			GSM0808_PERM_FR3, \
+			GSM0808_PERM_HR3, \
+			GSM0808_PERM_FR2, \
+			GSM0808_PERM_FR1, \
+			GSM0808_PERM_HR1, \
+			LIST_END \
+		}
+
+#define SDP_CODECS_ALL_GSM { \
+			"AMR", \
+			"GSM-EFR", \
+			"GSM", \
+			"GSM-HR-08", \
+		}
+
+static const struct codec_test codec_tests[] = {
+	{
+		.desc = "AMR picked by both MO and MT",
+		.mo_rx_compl_l3_codec_list_bss_supported = CODEC_LIST_ALL_GSM,
+		.mo_rx_ms_bcap = BCAP_ALL_GSM,
+		.mo_tx_sdp_mncc_setup_ind = SDP_CODECS_ALL_GSM,
+		.mo_rx_sdp_mncc_rtp_create = {},
+		.mo_tx_assignment_perm_speech = PERM_SPEECH_ALL_GSM,
+		.mo_rx_assigned_codec = "AMR",
+		.mo_tx_sdp_mncc_rtp_create = { "AMR" },
+		/* mt_rx_sdp_mncc_setup_req == mo_tx_sdp_mncc_rtp_create */
+		.mt_rx_compl_l3_codec_list_bss_supported = CODEC_LIST_ALL_GSM,
+		.mt_tx_cc_setup_bcap = {
+			GSM48_BCAP_SV_AMR_F,
+			GSM48_BCAP_SV_AMR_H,
+			GSM48_BCAP_SV_AMR_OH,
+			LIST_END
+		},
+		.mt_rx_ms_bcap = BCAP_ALL_GSM,
+		.mt_tx_sdp_mncc_call_conf_ind = {},
+		.mt_rx_sdp_mncc_rtp_create = {},
+		.mt_tx_assignment_perm_speech = { GSM0808_PERM_FR3, GSM0808_PERM_HR3, LIST_END },
+		.mt_rx_assigned_codec = "AMR",
+		.mt_tx_sdp_mncc_rtp_create = { "AMR" },
+		.mt_tx_sdp_mncc_alert_ind = { "AMR" },
+		.mt_tx_sdp_mncc_setup_cnf = { "AMR" },
+		.mo_tx_sdp_mncc_setup_compl_ind = {},
+	},
+
+	{
+		.desc = "FR1 picked by MO from Codec List (BSS Supported), MT hence also picks FR1",
+		.mo_rx_compl_l3_codec_list_bss_supported = { GSM0808_SCT_FR1, LIST_END },
+		.mo_rx_ms_bcap = BCAP_ALL_GSM,
+		.mo_tx_sdp_mncc_setup_ind = { "GSM" },
+		.mo_rx_sdp_mncc_rtp_create = {},
+		.mo_tx_assignment_perm_speech = { GSM0808_PERM_FR1, LIST_END },
+		.mo_rx_assigned_codec = "GSM",
+		.mo_tx_sdp_mncc_rtp_create = { "GSM" },
+		/* .mt_rx_sdp_mncc_setup_req == .mo_tx_sdp_mncc_setup_ind */
+		.mt_rx_compl_l3_codec_list_bss_supported = CODEC_LIST_ALL_GSM,
+		.mt_tx_cc_setup_bcap = { GSM48_BCAP_SV_FR, LIST_END },
+		.mt_rx_ms_bcap = BCAP_ALL_GSM,
+		.mt_tx_sdp_mncc_call_conf_ind = {},
+		.mt_rx_sdp_mncc_rtp_create = {},
+		.mt_tx_assignment_perm_speech = { GSM0808_PERM_FR1, LIST_END },
+		.mt_rx_assigned_codec = "GSM",
+		.mt_tx_sdp_mncc_rtp_create = { "GSM" },
+		.mt_tx_sdp_mncc_alert_ind = { "GSM" },
+		.mt_tx_sdp_mncc_setup_cnf = { "GSM" },
+		.mo_tx_sdp_mncc_setup_compl_ind = {},
+	},
+
+	{
+		.desc = "FR1 picked by MO from Bearer Cap, MT hence also picks FR1",
+		.mo_rx_compl_l3_codec_list_bss_supported = CODEC_LIST_ALL_GSM,
+		.mo_rx_ms_bcap = { GSM48_BCAP_SV_FR, LIST_END },
+		.mo_tx_sdp_mncc_setup_ind = { "GSM" },
+		.mo_rx_sdp_mncc_rtp_create = {},
+		.mo_tx_assignment_perm_speech = { GSM0808_PERM_FR1, LIST_END },
+		.mo_rx_assigned_codec = "GSM",
+		.mo_tx_sdp_mncc_rtp_create = { "GSM" },
+		/* .mt_rx_sdp_mncc_setup_req == .mo_tx_sdp_mncc_setup_ind */
+		.mt_rx_compl_l3_codec_list_bss_supported = CODEC_LIST_ALL_GSM,
+		.mt_tx_cc_setup_bcap = { GSM48_BCAP_SV_FR, LIST_END },
+		.mt_rx_ms_bcap = BCAP_ALL_GSM,
+		.mt_tx_sdp_mncc_call_conf_ind = {},
+		.mt_rx_sdp_mncc_rtp_create = {},
+		.mt_tx_assignment_perm_speech = { GSM0808_PERM_FR1, LIST_END },
+		.mt_rx_assigned_codec = "GSM",
+		.mt_tx_sdp_mncc_rtp_create = { "GSM" },
+		.mt_tx_sdp_mncc_alert_ind = { "GSM" },
+		.mt_tx_sdp_mncc_setup_cnf = { "GSM" },
+		.mo_tx_sdp_mncc_setup_compl_ind = {},
+	},
+
+	{
+		.desc = "FR1 picked by MT's Codec List (BSS Supported), hence MO also picks FR1 (EXPECTED FAILURE)",
+		/* Currently the MO Assignment happens before MT gets a chance to send its available codecs.
+		 * So even though the MO side would be able to assign FR1 and match MT, this is established too late
+		 * and MO mismatches MT. This can only be fixed by a) moving MO Assignment to after MT Assignment
+		 * or b) doing a Channel Mode Change or re-assignment after MT Assignment -- since re-assigning might
+		 * need an lchan type change and means more overhead, a) would be the best option. */
+		.mo_rx_compl_l3_codec_list_bss_supported = CODEC_LIST_ALL_GSM,
+		.mo_rx_ms_bcap = BCAP_ALL_GSM,
+		.mo_tx_sdp_mncc_setup_ind = SDP_CODECS_ALL_GSM,
+		.mo_rx_sdp_mncc_rtp_create = {},
+		.mo_tx_assignment_perm_speech = PERM_SPEECH_ALL_GSM,
+		.mo_rx_assigned_codec = "AMR", /* <- Early Assignment means codec mismatch */
+		.mo_tx_sdp_mncc_rtp_create = { "AMR" },
+
+		.mt_rx_compl_l3_codec_list_bss_supported = { GSM0808_SCT_FR1, LIST_END },
+		.expect_codec_mismatch_on_paging_response = true,
+		/* The mismatching codec AMR vs. GSM means the call fails (in the lack of transcoding) */
+	},
+
+	{
+		.desc = "FR1 picked by MT's MS Bearer Capability, hence MO also picks FR1 (EXPECTED FAILURE)",
+		/* Like above, MO Assignment happens too early to be able to match MT's codec availability. */
+		.mo_rx_compl_l3_codec_list_bss_supported = CODEC_LIST_ALL_GSM,
+		.mo_rx_ms_bcap = BCAP_ALL_GSM,
+		.mo_tx_sdp_mncc_setup_ind = SDP_CODECS_ALL_GSM,
+		.mo_rx_sdp_mncc_rtp_create = {},
+		.mo_tx_assignment_perm_speech = PERM_SPEECH_ALL_GSM,
+		.mo_rx_assigned_codec = "AMR", /* <- Early Assignment means codec mismatch */
+		.mo_tx_sdp_mncc_rtp_create = { "AMR" },
+
+		.mt_rx_compl_l3_codec_list_bss_supported = CODEC_LIST_ALL_GSM,
+		.mt_tx_cc_setup_bcap = {
+			GSM48_BCAP_SV_AMR_F,
+			GSM48_BCAP_SV_AMR_H,
+			GSM48_BCAP_SV_AMR_OH,
+			LIST_END
+		},
+		.mt_rx_ms_bcap = { GSM48_BCAP_SV_FR, LIST_END },
+		.mt_tx_sdp_mncc_call_conf_ind = {},
+		.mt_rx_sdp_mncc_rtp_create = {},
+		.mt_tx_assignment_perm_speech = { GSM0808_PERM_FR3, GSM0808_PERM_HR3, LIST_END },
+		.expect_codec_mismatch_on_cc_call_conf = true,
+		/* The mismatching codec AMR vs. GSM means the call fails (in the lack of transcoding) */
+	},
+
+};
+
+static char namebuf[4][1024];
+static int use_namebuf = 0;
+
+static const char *codec_list_name(const enum gsm0808_speech_codec_type compl_l3_codec_list_bss_supported[])
+{
+	struct osmo_strbuf sb = { .buf = namebuf[use_namebuf], .len = sizeof(namebuf[0]) };
+	use_namebuf = (use_namebuf + 1) % ARRAY_SIZE(namebuf);
+
+	const enum gsm0808_speech_codec_type *pos;
+	sb.buf[0] = '\0';
+	for (pos = compl_l3_codec_list_bss_supported; *pos != LIST_END; pos++)
+		OSMO_STRBUF_PRINTF(sb, " %s", gsm0808_speech_codec_type_name(*pos));
+	return sb.buf;
+}
+
+static const struct gsm0808_speech_codec_list *codec_list(const enum gsm0808_speech_codec_type compl_l3_codec_list_bss_supported[])
+{
+	static struct gsm0808_speech_codec_list scl;
+	scl = (struct gsm0808_speech_codec_list){};
+	const enum gsm0808_speech_codec_type *pos;
+	for (pos = compl_l3_codec_list_bss_supported; *pos != LIST_END; pos++) {
+		scl.codec[scl.len] = (struct gsm0808_speech_codec){
+			.fi = true,
+			.type = *pos,
+		};
+		scl.len++;
+	}
+	return &scl;
+}
+
+static const char *bcap_name(const enum gsm48_bcap_speech_ver ms_bcap[])
+{
+	struct osmo_strbuf sb = { .buf = namebuf[use_namebuf], .len = sizeof(namebuf[0]) };
+	use_namebuf = (use_namebuf + 1) % ARRAY_SIZE(namebuf);
+
+	const enum gsm48_bcap_speech_ver *pos;
+	sb.buf[0] = '\0';
+	for (pos = ms_bcap; *pos != LIST_END; pos++) {
+		const struct codec_mapping *m = codec_mapping_by_speech_ver(*pos);
+		OSMO_STRBUF_PRINTF(sb, " %s", m ? m->sdp.subtype_name : "NULL");
+	}
+	return sb.buf;
+}
+
+static const char *perm_speech_name(const enum gsm0808_permitted_speech perm_speech[])
+{
+	struct osmo_strbuf sb = { .buf = namebuf[use_namebuf], .len = sizeof(namebuf[0]) };
+	use_namebuf = (use_namebuf + 1) % ARRAY_SIZE(namebuf);
+
+	const enum gsm0808_permitted_speech *pos;
+	sb.buf[0] = '\0';
+	for (pos = perm_speech; *pos != LIST_END; pos++)
+		OSMO_STRBUF_PRINTF(sb, " %s", gsm0808_permitted_speech_name(*pos));
+	return sb.buf;
+}
+
+static const char *strlist_name(const char *const*strs)
+{
+	struct osmo_strbuf sb = { .buf = namebuf[use_namebuf], .len = sizeof(namebuf[0]) };
+	use_namebuf = (use_namebuf + 1) % ARRAY_SIZE(namebuf);
+
+	const char * const *pos;
+	sb.buf[0] = '\0';
+	for (pos = strs; *pos != NULL; pos++)
+		OSMO_STRBUF_PRINTF(sb, " %s", *pos);
+	return sb.buf;
+}
+
+static bool validate_sdp(const char *func, const char *desc,
+			 const char *sdp_str, const char * const expected_codecs[])
+{
+	const char * const *expect_pos;
+	struct sdp_audio_codec *codec;
+	struct sdp_msg sdp;
+	if (sdp_msg_from_sdp_str(&sdp, sdp_str)) {
+		BTW("%s: %s: ERROR: failed to parse SDP\n%s", func, desc, sdp_str);
+		return false;
+	}
+
+	expect_pos = expected_codecs;
+	foreach_sdp_audio_codec(codec, &sdp.audio_codecs) {
+		if (!*expect_pos) {
+			BTW("%s: %s: ERROR: did not expect %s", func, desc, codec->subtype_name);
+			return false;
+		}
+		if (strcmp(*expect_pos, codec->subtype_name)) {
+			BTW("%s: %s: ERROR: mismatch: in idx %d, expect %s, got %s", func, desc,
+			    (int)(expect_pos - expected_codecs), *expect_pos, codec->subtype_name);
+			return false;
+		}
+		expect_pos++;
+	}
+	if (*expect_pos) {
+		BTW("%s: %s: ERROR: mismatch: expected %s to be listed, but not found", func, desc, *expect_pos);
+		return false;
+	}
+	return true;
+}
+
+#define VALIDATE_SDP(GOT_SDP_STR, EXPECT_SDP_STR) do { \
+		if (validate_sdp(__func__, t->desc, GOT_SDP_STR, EXPECT_SDP_STR)) { \
+			btw("VALIDATE_SDP OK: " #GOT_SDP_STR " == " #EXPECT_SDP_STR " ==%s", strlist_name(EXPECT_SDP_STR)); \
+		} else { \
+			btw("Failed to validate SDP:\nexpected%s\ngot\n%s", \
+			    strlist_name(EXPECT_SDP_STR), GOT_SDP_STR); \
+			OSMO_ASSERT(false); \
+		} \
+	} while (0)
+
+static bool validate_perm_speech(const char *func, const char *desc,
+				 const struct gsm0808_channel_type *ct,
+				 const enum gsm0808_permitted_speech perm_speech[])
+{
+	const enum gsm0808_permitted_speech *pos;
+	const uint8_t *pos2 = ct->perm_spch;
+	for (pos = perm_speech; *pos != LIST_END; pos++, pos2++) {
+		if (pos2 - ct->perm_spch >= ct->perm_spch_len) {
+			BTW("%s: %s: ERROR: mismatch: expected %s to be listed, but not found", func, desc,
+			    gsm0808_permitted_speech_name(*pos));
+			return false;
+		}
+		if (*pos2 != *pos) {
+			BTW("%s: %s: ERROR: mismatch: in idx %d, expect %s", func, desc,
+			    (int)(pos - perm_speech), gsm0808_permitted_speech_name(*pos));
+			btw("in idx %d, got %s", (int)(pos - perm_speech), gsm0808_permitted_speech_name(*pos2));
+			return false;
+		}
+	}
+	if (pos2 - ct->perm_spch < ct->perm_spch_len) {
+		BTW("%s: %s: ERROR: did not expect %s", func, desc, gsm0808_permitted_speech_name(*pos2));
+		return false;
+	}
+	return true;
+}
+
+#define VALIDATE_PERM_SPEECH(GOT_PERM_SPEECH, EXPECT_PERM_SPEECH) do { \
+		if (validate_perm_speech(__func__, t->desc, GOT_PERM_SPEECH, EXPECT_PERM_SPEECH)) { \
+			btw("VALIDATE_PERM_SPEECH OK: " #GOT_PERM_SPEECH " == " #EXPECT_PERM_SPEECH " ==%s", \
+			    perm_speech_name(EXPECT_PERM_SPEECH)); \
+		} else { \
+			btw("Failed to validate Permitted Speech:\nexpected%s", \
+			    perm_speech_name(EXPECT_PERM_SPEECH)); \
+			btw("got:"); \
+			int i; \
+			for (i = 0; i < (GOT_PERM_SPEECH)->perm_spch_len; i++) { \
+				btw("%s", gsm0808_permitted_speech_name((GOT_PERM_SPEECH)->perm_spch[i])); \
+			} \
+			OSMO_ASSERT(false); \
+		} \
+	} while (0)
+
+static struct sdp_msg *sdp_from_subtype_names(const char *const *subtype_names)
+{
+	static struct sdp_msg sdp;
+	sdp = (struct sdp_msg){};
+	const char *const *subtype_name;
+	osmo_sockaddr_str_from_str(&sdp.rtp, "1.2.3.4", 56);
+	for (subtype_name = subtype_names; *subtype_name; subtype_name++) {
+		const struct codec_mapping *m = codec_mapping_by_subtype_name(*subtype_name);
+		if (!m) {
+			BTW("ERROR: unknown subtype_name: %s", *subtype_name);
+			abort();
+		}
+		sdp_audio_codecs_add_copy(&sdp.audio_codecs, &m->sdp);
+	}
+	return &sdp;
+}
+
+static int sdp_str_from_subtype_names(char *buf, size_t buflen, const char *const *subtype_names)
+{
+	if (!subtype_names[0]) {
+		buf[0] = '\0';
+		return 0;
+	}
+	return sdp_msg_to_sdp_str_buf(buf, buflen, sdp_from_subtype_names(subtype_names));
+}
+
+static const char *bcap_hexstr(const enum gsm48_bcap_speech_ver ms_bcap[])
+{
+	struct gsm_mncc_bearer_cap bcap = {
+		.transfer = GSM_MNCC_BCAP_SPEECH,
+		.speech_ver = { -1 },
+	};
+	const enum gsm48_bcap_speech_ver *pos;
+	for (pos = ms_bcap; *pos != LIST_END; pos++)
+		bearer_cap_add_speech_ver(&bcap, *pos);
+	bearer_cap_set_radio(&bcap);
+	struct msgb *msg = msgb_alloc(128, "bcap");
+	gsm48_encode_bearer_cap(msg, 0, &bcap);
+	char *ret = osmo_hexdump_nospc(msg->data, msg->len);
+	msgb_free(msg);
+	return ret;
+}
+
+static void test_codecs_mo(const struct codec_test *t)
+{
+	struct gsm_mncc mncc = {
+		.imsi = IMSI,
+	};
+
+	struct gsm_mncc_rtp *mncc_rtp = (void *)&mncc;
+
+	BTW("======================== MO call: %s", t->desc);
+	btw("CM Service Request with Codec List (BSS Supported) =%s",
+	    codec_list_name(t->mo_rx_compl_l3_codec_list_bss_supported));
+
+	cm_service_result_sent = RES_NONE;
+	ms_sends_compl_l3("052471"
+			  "03575886" /* classmark 2 */
+			  "089910070000106005" /* IMSI */,
+			  codec_list(t->mo_rx_compl_l3_codec_list_bss_supported));
+	VERBOSE_ASSERT(cm_service_result_sent, == RES_ACCEPT, "%d");
+	EXPECT_ACCEPTED(true);
+
+	btw("MS sends CC SETUP with Bearer Capability = %s",
+	    bcap_name(t->mo_rx_ms_bcap));
+	expect_crcx(RTP_TO_CN);
+	expect_crcx(RTP_TO_RAN);
+	ms_sends_msgf("0385" /* CC, seq = 2 -> 0x80 | CC Setup = 0x5 */
+		      "%s" /* Bearer Capability */
+		      "5e038121f3" /* Called Number BCD */
+		      "15020100" /* CC Capabilities */
+		      "4008" /* Supported Codec List */
+		      "04026000" /* UMTS: AMR 2 | AMR */
+		      "00021f00" /* GSM: HR AMR | FR AMR | GSM EFR | GSM HR | GSM FR */,
+		      bcap_hexstr(t->mo_rx_ms_bcap)
+		     );
+	OSMO_ASSERT(crcx_scheduled(RTP_TO_CN));
+	OSMO_ASSERT(crcx_scheduled(RTP_TO_RAN));
+
+	btw("As soon as the MGW port towards CN is created, MNCC_SETUP_IND is triggered");
+	cc_to_mncc_expect_tx(IMSI, MNCC_SETUP_IND);
+	crcx_ok(RTP_TO_CN);
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+	mncc.callref = cc_to_mncc_tx_got_callref;
+	VALIDATE_SDP(cc_to_mncc_tx_last_sdp, t->mo_tx_sdp_mncc_setup_ind);
+
+	btw("MNCC replies with MNCC_RTP_CREATE, causing MGW endpoint CRCX to RAN");
+	sdp_str_from_subtype_names(mncc_rtp->sdp, sizeof(mncc_rtp->sdp), t->mo_rx_sdp_mncc_rtp_create);
+	mncc_sends_to_cc(MNCC_RTP_CREATE, &mncc);
+
+	btw("MGW acknowledges the CRCX, triggering Assignment with%s", perm_speech_name(t->mo_tx_assignment_perm_speech));
+	expect_bssap_assignment();
+	crcx_ok(RTP_TO_RAN);
+	OSMO_ASSERT(bssap_assignment_sent);
+	VALIDATE_PERM_SPEECH(&bssap_assignment_command_last_channel_type, t->mo_tx_assignment_perm_speech);
+
+	btw("Assignment succeeds, triggering MNCC_RTP_CREATE ack to MNCC");
+	cc_to_mncc_expect_tx("", MNCC_RTP_CREATE);
+	ms_sends_assignment_complete(t->mo_rx_assigned_codec);
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+	VALIDATE_SDP(cc_to_mncc_tx_last_sdp, t->mo_tx_sdp_mncc_rtp_create);
+
+	btw("MNCC says that's fine");
+	dtap_expect_tx("8302" /* CC: Call Proceeding */);
+	mncc_sends_to_cc(MNCC_CALL_PROC_REQ, &mncc);
+	OSMO_ASSERT(dtap_tx_confirmed);
+
+	fake_time_passes(1, 23);
+
+	btw("The other call leg got established (not shown here), MNCC tells us so, with codecs {%s }",
+	    strlist_name(t->mo_rx_sdp_mncc_alert_req));
+	dtap_expect_tx("8301" /* CC: Call Alerting */);
+	sdp_str_from_subtype_names(mncc.sdp, sizeof(mncc.sdp), t->mo_rx_sdp_mncc_alert_req);
+	mncc_sends_to_cc(MNCC_ALERT_REQ, &mncc);
+	OSMO_ASSERT(dtap_tx_confirmed);
+
+	dtap_expect_tx("8307" /* CC: Connect */);
+	sdp_str_from_subtype_names(mncc.sdp, sizeof(mncc.sdp), t->mo_rx_sdp_mncc_setup_rsp);
+	mncc_sends_to_cc(MNCC_SETUP_RSP, &mncc);
+	OSMO_ASSERT(dtap_tx_confirmed);
+
+	fake_time_passes(1, 23);
+
+	cc_to_mncc_expect_tx("", MNCC_SETUP_COMPL_IND);
+	ms_sends_msg("03cf" /* CC: Connect Acknowledge */);
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+	VALIDATE_SDP(cc_to_mncc_tx_last_sdp, t->mo_tx_sdp_mncc_setup_compl_ind);
+
+	BTW("RTP stream goes ahead, not shown here.");
+	fake_time_passes(123, 45);
+
+	BTW("Call ends");
+	cc_to_mncc_expect_tx("", MNCC_DISC_IND);
+	ms_sends_msg("032502e090" /* CC: Disconnect, cause: Normal Call Clearing */);
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+
+	dtap_expect_tx("832d" /* CC: Release */);
+	mncc_sends_to_cc(MNCC_REL_REQ, &mncc);
+	OSMO_ASSERT(dtap_tx_confirmed);
+
+	cc_to_mncc_expect_tx("", MNCC_REL_CNF);
+	expect_bssap_clear();
+	ms_sends_msg("036a" /* CC: Release Complete */);
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+	OSMO_ASSERT(bssap_clear_sent);
+
+	ran_sends_clear_complete();
+	EXPECT_CONN_COUNT(0);
+	BTW("======================== SUCCESS: MO call: %s", t->desc);
+}
+
+static void test_codecs_mt(const struct codec_test *t)
+{
+	struct gsm_mncc mncc = {
+		.imsi = IMSI,
+		.callref = 0x423,
+		.fields = MNCC_F_BEARER_CAP,
+		.bearer_cap = {
+			.speech_ver = { GSM48_BCAP_SV_FR, -1, },
+		},
+	};
+	struct gsm_mncc_rtp *mncc_rtp = (void *)&mncc;
+
+	BTW("======================== MT call: %s", t->desc);
+
+	BTW("MNCC asks us to setup a call, causing Paging");
+
+	paging_expect_imsi(IMSI);
+	paging_sent = false;
+	sdp_str_from_subtype_names(mncc.sdp, sizeof(mncc.sdp), t->mt_rx_sdp_mncc_setup_req);
+	mncc_sends_to_cc(MNCC_SETUP_REQ, &mncc);
+	mncc.sdp[0] = '\0';
+
+	VERBOSE_ASSERT(paging_sent, == true, "%d");
+
+	btw("MS replies with Paging Response, with Codec List (BSS Supported) =%s",
+	    codec_list_name(t->mt_rx_compl_l3_codec_list_bss_supported));
+
+	if (t->expect_codec_mismatch_on_paging_response) {
+		btw("VLR accepts, but MSC notices a codec mismatch and aborts");
+		cc_to_mncc_expect_tx("", MNCC_REL_IND);
+		expect_bssap_clear();
+		ms_sends_compl_l3("062707"
+				  "03575886" /* classmark 2 */
+				  "089910070000106005" /* IMSI */,
+				  codec_list(t->mt_rx_compl_l3_codec_list_bss_supported));
+		OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+		OSMO_ASSERT(bssap_clear_sent);
+
+		ran_sends_clear_complete();
+		EXPECT_CONN_COUNT(0);
+
+		BTW("======================== SUCCESS: MT call: %s", t->desc);
+		return;
+	}
+
+	btw("VLR accepts, MSC sends CC Setup with Bearer Capability = %s",
+	    bcap_name(t->mt_tx_cc_setup_bcap));
+	char *cc_setup_bcap = talloc_asprintf(msc_vlr_tests_ctx, "0305%s",
+					      bcap_hexstr(t->mt_tx_cc_setup_bcap));
+	dtap_expect_tx(cc_setup_bcap);
+	ms_sends_compl_l3("062707"
+			  "03575886" /* classmark 2 */
+			  "089910070000106005" /* IMSI */,
+			  codec_list(t->mt_rx_compl_l3_codec_list_bss_supported));
+	OSMO_ASSERT(dtap_tx_confirmed);
+	talloc_free(cc_setup_bcap);
+
+	btw("MS confirms call, we create a RAN-side RTP and forward MNCC_CALL_CONF_IND");
+	expect_crcx(RTP_TO_CN);
+	expect_crcx(RTP_TO_RAN);
+	cc_to_mncc_expect_tx(IMSI, MNCC_CALL_CONF_IND);
+	ms_sends_msgf("8348" /* CC: Call Confirmed */
+		      "%s" /* Bearer Capability */
+		      "15020100" /* Call Control Capabilities */
+		      "40080402600400021f00" /* Supported Codec List */,
+		      bcap_hexstr(t->mt_rx_ms_bcap)
+		     );
+	OSMO_ASSERT(crcx_scheduled(RTP_TO_CN));
+	OSMO_ASSERT(crcx_scheduled(RTP_TO_RAN));
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+	VALIDATE_SDP(cc_to_mncc_tx_last_sdp, t->mt_tx_sdp_mncc_call_conf_ind);
+
+	btw("MGW acknowledges the CRCX to RAN, triggering Assignment with%s", perm_speech_name(t->mt_tx_assignment_perm_speech));
+
+	if (t->expect_codec_mismatch_on_cc_call_conf) {
+		btw("MS Bearer Capability leads to a codec mismatch, Assignment aborts");
+
+		dtap_expect_tx("032d0802e1af" /* CC Release */);
+		cc_to_mncc_expect_tx("", MNCC_REL_IND);
+		expect_bssap_clear();
+		crcx_ok(RTP_TO_RAN);
+
+		OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+		OSMO_ASSERT(bssap_clear_sent);
+
+		ran_sends_clear_complete();
+		EXPECT_CONN_COUNT(0);
+		BTW("======================== SUCCESS: MT call: %s", t->desc);
+		return;
+	}
+
+	expect_bssap_assignment();
+	crcx_ok(RTP_TO_RAN);
+	OSMO_ASSERT(bssap_assignment_sent);
+	VALIDATE_PERM_SPEECH(&bssap_assignment_command_last_channel_type, t->mt_tx_assignment_perm_speech);
+
+	btw("Assignment completes, triggering CRCX to CN");
+	ms_sends_assignment_complete(t->mt_rx_assigned_codec);
+
+	btw("MNCC sends MNCC_RTP_CREATE, which first waits for the CN side RTP");
+	sdp_str_from_subtype_names(mncc_rtp->sdp, sizeof(mncc_rtp->sdp), t->mt_rx_sdp_mncc_rtp_create);
+	mncc_sends_to_cc(MNCC_RTP_CREATE, &mncc);
+
+	btw("When the CN side RTP address is known, ack MNCC_RTP_CREATE");
+	cc_to_mncc_expect_tx("", MNCC_RTP_CREATE);
+	crcx_ok(RTP_TO_CN);
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+	VALIDATE_SDP(cc_to_mncc_tx_last_sdp, t->mt_tx_sdp_mncc_rtp_create);
+
+	fake_time_passes(1, 23);
+
+	cc_to_mncc_expect_tx("", MNCC_ALERT_IND);
+	ms_sends_msg("8381" /* CC: Alerting */);
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+	VALIDATE_SDP(cc_to_mncc_tx_last_sdp, t->mt_tx_sdp_mncc_alert_ind);
+
+	fake_time_passes(1, 23);
+
+	cc_to_mncc_expect_tx(IMSI, MNCC_SETUP_CNF);
+	ms_sends_msg("83c7" /* CC: Connect */);
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+	VALIDATE_SDP(cc_to_mncc_tx_last_sdp, t->mt_tx_sdp_mncc_setup_cnf);
+
+	dtap_expect_tx("030f" /* CC: Connect Acknowledge */);
+	sdp_str_from_subtype_names(mncc.sdp, sizeof(mncc.sdp), t->mt_rx_sdp_mncc_setup_compl_req);
+	mncc_sends_to_cc(MNCC_SETUP_COMPL_REQ, &mncc);
+
+	BTW("RTP stream goes ahead, not shown here.");
+	fake_time_passes(123, 45);
+
+	BTW("Call ends");
+	cc_to_mncc_expect_tx("", MNCC_DISC_IND);
+	ms_sends_msg("832502e090" /* CC: Disconnect, cause: Normal Call Clearing */);
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+
+	dtap_expect_tx("032d" /* CC: Release */);
+	mncc_sends_to_cc(MNCC_REL_REQ, &mncc);
+	OSMO_ASSERT(dtap_tx_confirmed);
+
+	cc_to_mncc_expect_tx("", MNCC_REL_CNF);
+	expect_bssap_clear();
+	ms_sends_msg("836a" /* CC: Release Complete */);
+	OSMO_ASSERT(cc_to_mncc_tx_confirmed);
+	OSMO_ASSERT(bssap_clear_sent);
+
+	ran_sends_clear_complete();
+	EXPECT_CONN_COUNT(0);
+	BTW("======================== SUCCESS: MT call: %s", t->desc);
+}
+
+static void test_codecs(void)
+{
+	const struct codec_test *t;
+	clear_vlr();
+
+	comment_start();
+
+	fake_time_start();
+
+	lu_geran_noauth();
+
+	for (t = codec_tests; t - codec_tests < ARRAY_SIZE(codec_tests); t++) {
+		test_codecs_mo(t);
+		test_codecs_mt(t);
+	}
+
+	EXPECT_CONN_COUNT(0);
+	clear_vlr();
+	comment_end();
+}
 
 msc_vlr_test_func_t msc_vlr_tests[] = {
 	test_call_mo,
@@ -726,5 +1450,6 @@
 	test_call_mt2,
 	test_call_mo_to_unknown,
 	test_call_mo_to_unknown_timeout,
+	test_codecs,
 	NULL
 };