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>
This commit is contained in:
commit
6e77823933
31 changed files with 8761 additions and 0 deletions
204
tests/Sdp/main.cpp
Normal file
204
tests/Sdp/main.cpp
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
// 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue