imsd/tests/Sdp/main.cpp
Jorijn van der Graaf 6e77823933 imsd 0.2.7 — initial public snapshot
Userspace VoLTE/IMS daemon for mainline Linux phones (developed on the
Fairphone 6): a GLib-free core (SIP, SDP, USIM AKA, IPsec SA setup, RTP
media, call engine) behind a GDBus control daemon, a standalone AMR-WB
media leg, and a Plasma Dialer backend. C++26 modules built with Crafter
Build; the tree is clean under the project's house-style linter
(crafter-build lint) across all three build products.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-22 23:04:21 +02:00

204 lines
8 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// lint-disable-file fixed-width-types
// Imsd:Sdp unit tests — offer building (byte-exact) and answer parsing
// (payload-type preference, octet-align, address family). The IPv6 answer
// mirrors what a live commercial IMS core actually returns (AMR-WB pt=97
// octet-aligned, IPv6 media gateway).
import std;
import Imsd;
using namespace imsd::sdp;
namespace {
int Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++Failures;
}
}
}
int main() {
// ---- BuildOffer, IPv6 + preconditions (the shape a VoLTE UE sends)
{
std::string sdp = BuildOffer({
.local = "2001:db8:29e9:a05f::1",
.rtpPort = 22222,
.sessionId = 1234567,
.precond = true,
});
std::string expected =
"v=0\r\n"
"o=- 1234567 1234567 IN IP6 2001:db8:29e9:a05f::1\r\n"
"s=-\r\n"
"c=IN IP6 2001:db8:29e9:a05f::1\r\n"
"t=0 0\r\n"
"m=audio 22222 RTP/AVP 97 98\r\n"
"b=AS:41\r\n"
"b=RS:512\r\n"
"b=RR:1536\r\n"
"a=rtpmap:97 AMR-WB/16000/1\r\n"
"a=fmtp:97 octet-align=1;mode-change-capability=2;max-red=0\r\n"
"a=rtpmap:98 telephone-event/16000\r\n"
"a=fmtp:98 0-15\r\n"
"a=ptime:20\r\n"
"a=maxptime:240\r\n"
"a=curr:qos local sendrecv\r\n"
"a=curr:qos remote none\r\n"
"a=des:qos mandatory local sendrecv\r\n"
"a=des:qos mandatory remote sendrecv\r\n"
"a=sendrecv\r\n";
Check(sdp == expected, "IPv6 precondition offer is byte-exact");
}
// ---- BuildOffer, IPv4 without preconditions
{
std::string sdp = BuildOffer({
.local = "10.0.0.2",
.rtpPort = 4000,
.sessionId = 42,
.precond = false,
});
Check(sdp.contains("o=- 42 42 IN IP4 10.0.0.2\r\n"), "IPv4 origin line");
Check(sdp.contains("c=IN IP4 10.0.0.2\r\n"), "IPv4 connection line");
Check(!sdp.contains("a=curr:qos"), "no precondition block");
Check(sdp.ends_with("a=sendrecv\r\n"), "trailing sendrecv + CRLF");
}
// ---- ParseAnswer, real-world IPv6 answer
{
Answer a = ParseAnswer(
"v=0\r\n"
"o=- 99 99 IN IP6 2001:db8::307\r\n"
"s=-\r\n"
"c=IN IP6 2001:db8::307\r\n"
"t=0 0\r\n"
"m=audio 35868 RTP/AVP 97 98\r\n"
"a=rtpmap:97 AMR-WB/16000/1\r\n"
"a=fmtp:97 octet-align=1;mode-change-capability=2\r\n"
"a=rtpmap:98 telephone-event/16000\r\n"
"a=ptime:20\r\n");
Check(a.ip == "2001:db8::307", "answer IP6");
Check(a.port == 35868, "answer port");
Check(a.payloadType == 97, "answer payload type");
Check(a.octetAlign, "answer octet-align");
Check(a.codec == "AMR-WB", "answer codec");
}
// ---- payload-type preference: AMR remembered, later AMR-WB overrides
{
Answer a = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 96 97\r\n" "a=rtpmap:96 AMR/8000/1\r\n" "a=rtpmap:97 AMR-WB/16000/1\r\n");
Check(a.payloadType == 97, "AMR-WB preferred over earlier AMR");
Check(a.codec == "AMR-WB", "codec follows the preferred rtpmap");
}
// ---- octet-aligned AMR-WB preferred over an earlier BE AMR-WB
// (the s56 MT-static shape: an offer listing bandwidth-efficient first)
{
Answer a = ParseAnswer(
"c=IN IP6 2001:db8::1\r\n"
"m=audio 5000 RTP/AVP 116 118 110\r\n"
"a=rtpmap:116 AMR-WB/16000/1\r\n"
"a=fmtp:116 mode-change-capability=2;max-red=0\r\n"
"a=rtpmap:118 AMR-WB/16000/1\r\n"
"a=fmtp:118 octet-align=1;mode-change-capability=2\r\n"
"a=rtpmap:110 telephone-event/16000\r\n");
Check(a.payloadType == 118, "octet-aligned AMR-WB wins over earlier BE");
Check(a.octetAlign, "octet-align follows the chosen pt");
Check(a.codec == "AMR-WB", "codec is AMR-WB");
}
// ---- BE-only AMR-WB offer stays BE at the first pt
{
Answer a = ParseAnswer(
"c=IN IP6 2001:db8::1\r\n"
"m=audio 5000 RTP/AVP 116 96\r\n"
"a=rtpmap:116 AMR-WB/16000/1\r\n"
"a=fmtp:116 mode-change-capability=2\r\n"
"a=rtpmap:96 AMR/8000/1\r\n"
"a=fmtp:96 octet-align=1\r\n");
Check(a.payloadType == 116, "BE-only AMR-WB beats octet-aligned AMR");
Check(!a.octetAlign, "BE-only offer negotiates bandwidth-efficient");
}
// ---- AMR-only answer
{
Answer a = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 96\r\n" "a=rtpmap:96 AMR/8000/1\r\n" "a=fmtp:96 octet-align=1\r\n");
Check(a.payloadType == 96 && a.codec == "AMR", "plain AMR fallback");
Check(a.octetAlign, "octet-align seen for AMR too");
}
// ---- no rtpmap at all -> first payload type, bandwidth-efficient
{
Answer a = ParseAnswer("c=IN IP4 192.0.2.1\r\n" "m=audio 5000 RTP/AVP 8 0\r\n");
Check(a.payloadType == 8, "no rtpmap -> first payload type");
Check(!a.octetAlign, "no fmtp -> bandwidth-efficient");
Check(a.codec.empty(), "no AMR rtpmap -> codec unknown, not assumed");
}
// ---- degenerate body
{
Answer a = ParseAnswer("v=0\r\n");
Check(a.ip.empty() && a.port == -1 && a.payloadType == -1, "empty answer parses to absent fields");
}
// ---- TelephoneEventPt
{
std::string_view offer =
"m=audio 27864 RTP/AVP 104 105\r\n"
"a=rtpmap:104 AMR-WB/16000/1\r\n"
"a=rtpmap:105 telephone-event/16000\r\n"
"a=rtpmap:106 telephone-event/8000\r\n";
Check(TelephoneEventPt(offer, 16000) == 105, "telephone-event pt by clock rate");
Check(TelephoneEventPt(offer, 8000) == 106, "narrowband telephone-event pt");
Check(!TelephoneEventPt(offer, 48000).has_value(), "absent clock rate -> nullopt");
Check(!TelephoneEventPt("m=audio 1 RTP/AVP 0\r\n", 16000).has_value(), "no rtpmap -> nullopt");
}
// ---- BuildAnswer: the offer's own payload types, octet-align mirrored
{
std::string sdp = BuildAnswer({
.local = "2001:db8:29e9:a05f::1",
.rtpPort = 50004,
.sessionId = 777,
.payloadType = 104,
.octetAlign = true,
.codec = "AMR-WB",
.dtmfPt = 105,
});
std::string expected =
"v=0\r\n"
"o=- 777 777 IN IP6 2001:db8:29e9:a05f::1\r\n"
"s=-\r\n"
"c=IN IP6 2001:db8:29e9:a05f::1\r\n"
"t=0 0\r\n"
"m=audio 50004 RTP/AVP 104 105\r\n"
"b=AS:41\r\n"
"b=RS:512\r\n"
"b=RR:1536\r\n"
"a=rtpmap:104 AMR-WB/16000/1\r\n"
"a=fmtp:104 octet-align=1\r\n"
"a=rtpmap:105 telephone-event/16000\r\n"
"a=fmtp:105 0-15\r\n"
"a=ptime:20\r\n"
"a=maxptime:240\r\n"
"a=sendrecv\r\n";
Check(sdp == expected, "AMR-WB answer is byte-exact");
}
// ---- BuildAnswer: bandwidth-efficient offer, no DTMF, narrowband
{
std::string sdp = BuildAnswer({
.local = "10.0.0.2",
.rtpPort = 4000,
.sessionId = 8,
.payloadType = 96,
.octetAlign = false,
.codec = "AMR",
});
Check(sdp.contains("m=audio 4000 RTP/AVP 96\r\n"), "single-pt m-line");
Check(sdp.contains("a=rtpmap:96 AMR/8000/1\r\n"), "narrowband rtpmap");
Check(!sdp.contains("octet-align"), "bandwidth-efficient stays unmarked");
Check(!sdp.contains("telephone-event"), "no DTMF when the offer had none");
Check(sdp.contains("b=AS:30\r\n"), "narrowband bandwidth");
}
if (Failures == 0) std::println("Sdp: all tests passed");
return Failures;
}