Skip to content

Commit

Permalink
Add initial ModemManager mock
Browse files Browse the repository at this point in the history
We mock the daemon and allow to add a modem (which also adds a SIM)
and a minimal Voice interface.

This is enough for `mmcli -m any` and `mmcli -i any` to work, for shells
like phosh to display mobile broadband information and for calls thinking
it's seeing a voice capable modem (thus enabling call functionality).
  • Loading branch information
agx authored and martinpitt committed May 24, 2024
1 parent e1e9118 commit d416d2e
Show file tree
Hide file tree
Showing 2 changed files with 353 additions and 0 deletions.
209 changes: 209 additions & 0 deletions dbusmock/templates/modemmanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""ModemManager mock template
This creates the expected methods and properties of the main
ModemManager object, but no devices. You can specify any property
such as DaemonVersion in "parameters".
"""

# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option) any
# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text
# of the license.

__author__ = "Guido Günther"
__copyright__ = "2024 The Phosh Developers"

import dbus

from dbusmock import MOCK_IFACE, OBJECT_MANAGER_IFACE, mockobject

BUS_NAME = "org.freedesktop.ModemManager1"
MAIN_OBJ = "/org/freedesktop/ModemManager1"
MAIN_IFACE = "org.freedesktop.ModemManager1"
SYSTEM_BUS = True
IS_OBJECT_MANAGER = True
MODEM_IFACE = "org.freedesktop.ModemManager1.Modem"
MODEM_3GPP_IFACE = "org.freedesktop.ModemManager1.Modem.Modem3gpp"
MODEM_VOICE_IFACE = "org.freedesktop.ModemManager1.Modem.Voice"
SIM_IFACE = "org.freedesktop.ModemManager1.Sim"


class MMModemMode:
"""
See
https://www.freedesktop.org/software/ModemManager/doc/latest/ModemManager/ModemManager-Flags-and-Enumerations.html#MMModemMode
"""

MODE_NONE = 0
MODE_CS = 1 << 0
MODE_2G = 1 << 1
MODE_3G = 1 << 2
MODE_4G = 1 << 3
MODE_5G = 1 << 4


class MMModemState:
"""
See
https://www.freedesktop.org/software/ModemManager/doc/latest/ModemManager/ModemManager-Flags-and-Enumerations.html#MMModemState
"""

STATE_FAILED = -1
STATE_UNKNOWN = 0
STATE_INITIALIZING = 1
STATE_LOCKED = 2
STATE_DISABLED = 3
STATE_DISABLING = 4
STATE_ENABLING = 5
STATE_ENABLED = 6
STATE_SEARCHING = 7
STATE_REGISTERED = 8
STATE_DISCONNECTING = 9
STATE_CONNECTING = 10
STATE_CONNECTED = 11


class MMModemPowerState:
"""
See
https://www.freedesktop.org/software/ModemManager/doc/latest/ModemManager/ModemManager-Flags-and-Enumerations.html#MMModemPowerState
"""

POWER_STATE_UNKNOWN = 0
POWER_STATE_OFF = 1
POWER_STATE_LOW = 2
POWER_STATE_ON = 3


class MMModemAccesssTechnology:
"""
See
https://www.freedesktop.org/software/ModemManager/doc/latest/ModemManager/ModemManager-Flags-and-Enumerations.html#MMModemAccessTechnology
"""

ACCESS_TECHNOLOGY_UNKNOWN = 0
ACCESS_TECHNOLOGY_POTS = 1 << 0
ACCESS_TECHNOLOGY_GSM = 1 << 1
ACCESS_TECHNOLOGY_GSM_COMPACT = 1 << 2
ACCESS_TECHNOLOGY_GPRS = 1 << 3
ACCESS_TECHNOLOGY_EDGE = 1 << 4
ACCESS_TECHNOLOGY_UMTS = 1 << 5
ACCESS_TECHNOLOGY_HSDPA = 1 << 6
ACCESS_TECHNOLOGY_HSUPA = 1 << 7
ACCESS_TECHNOLOGY_HSPA = 1 << 8
ACCESS_TECHNOLOGY_HSPA_PLUS = 1 << 9
ACCESS_TECHNOLOGY_1XRTT = 1 << 10
ACCESS_TECHNOLOGY_EVDO0 = 1 << 11
ACCESS_TECHNOLOGY_EVDOA = 1 << 12
ACCESS_TECHNOLOGY_EVDOB = 1 << 13
ACCESS_TECHNOLOGY_LTE = 1 << 14
ACCESS_TECHNOLOGY_5GNR = 1 << 15
ACCESS_TECHNOLOGY_LTE_CAT_M = 1 << 16
ACCESS_TECHNOLOGY_LTE_NB_IOT = 1 << 17


def load(mock, parameters):
methods = [
("ScanDevices", "", "", ""),
]

props = dbus.Dictionary(
{
"Version": parameters.get("DaemonVersion", "1.22"),
},
signature="sv",
)

mock.AddMethods(MAIN_IFACE, methods)
mock.AddProperties(MAIN_IFACE, props)


@dbus.service.method(MOCK_IFACE, in_signature="", out_signature="ss")
def AddSimpleModem(self):
"""Convenience method to add a simple Modem object
Please note that this does not set any global properties.
Returns the new object path.
"""
modem_path = "/org/freedesktop/ModemManager1/Modems/8"
sim_path = "/org/freedesktop/ModemManager1/SIM/2"
manager = mockobject.objects[MAIN_OBJ]

modem_props = {
"State": dbus.Int32(MMModemState.STATE_ENABLED),
"Model": dbus.String("E1750"),
"Revision": dbus.String("11.126.08.01.00"),
"AccessTechnologies": dbus.UInt32(MMModemAccesssTechnology.ACCESS_TECHNOLOGY_LTE),
"PowerState": dbus.UInt32(MMModemPowerState.POWER_STATE_ON),
"UnlockRequired": dbus.UInt32(0),
"UnlockRetries": dbus.Dictionary([], signature="uu"),
"CurrentModes": dbus.Struct(
(dbus.UInt32(MMModemMode.MODE_4G), dbus.UInt32(MMModemMode.MODE_4G)), signature="(uu)"
),
"SignalQuality": dbus.Struct(
(dbus.UInt32(70), dbus.Boolean(True)),
),
"Sim": dbus.ObjectPath(sim_path),
"SupportedModes": [
(dbus.UInt32(MMModemMode.MODE_4G), dbus.UInt32(MMModemMode.MODE_4G)),
(dbus.UInt32(MMModemMode.MODE_3G | MMModemMode.MODE_2G), dbus.UInt32(MMModemMode.MODE_3G)),
],
"SupportedBands": [dbus.UInt32(0)],
}
self.AddObject(modem_path, MODEM_IFACE, modem_props, [])

modem_3gpp_props = {
"Imei": dbus.String("doesnotmatter", variant_level=1),
"OperatorName": dbus.String("TheOperator"),
"Pco": dbus.Array([], signature="(ubay)"),
}
modem = mockobject.objects[modem_path]
modem.AddProperties(MODEM_3GPP_IFACE, modem_3gpp_props)

modem_voice_props = {
"Calls": dbus.Array([], signature="o"),
"EmergencyOnly": False,
}

modem.call_waiting = False
modem_voice_methods = [
("CallWaitingQuery", "", "b", "ret = self.call_waiting"),
("CallWaitingSetup", "b", "", "self.call_waiting = args[0]"),
]
modem = mockobject.objects[modem_path]
modem.AddProperties(MODEM_VOICE_IFACE, modem_voice_props)
modem.AddMethods(MODEM_VOICE_IFACE, modem_voice_methods)

manager.EmitSignal(
OBJECT_MANAGER_IFACE,
"InterfacesAdded",
"oa{sa{sv}}",
[
dbus.ObjectPath(modem_path),
{
MODEM_IFACE: modem_props,
MODEM_3GPP_IFACE: modem_3gpp_props,
MODEM_VOICE_IFACE: modem_voice_props,
},
],
)

sim_props = {
"Active": dbus.Boolean(True),
"Imsi": dbus.String("doesnotmatter"),
"PreferredNetworks": dbus.Array([], signature="(su)"),
}
self.AddObject(sim_path, SIM_IFACE, sim_props, [])
manager.EmitSignal(
OBJECT_MANAGER_IFACE,
"InterfacesAdded",
"oa{sa{sv}}",
[
dbus.ObjectPath(sim_path),
{SIM_IFACE: sim_props},
],
)

return (modem_path, sim_path)
144 changes: 144 additions & 0 deletions tests/test_modemmanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
""" Tests for accounts service """

# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option) any
# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text
# of the license.

__author__ = "Guido Günther"
__copyright__ = """
(c) 2024 The Phosh Developers
"""

import shutil
import subprocess
import sys
import unittest

import dbus
import dbus.mainloop.glib

import dbusmock

dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

have_mmcli = shutil.which("mmcli")


class TestModemManagerBase(dbusmock.DBusTestCase):
"""Test mocking ModemManager"""

dbus_interface = ""

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.start_system_bus()
cls.dbus_con = cls.get_dbus(True)

def setUp(self):
super().setUp()
(self.p_mock, self.p_obj) = self.spawn_server_template("modemmanager", {}, stdout=subprocess.PIPE)

def tearDown(self):
if self.p_mock:
self.p_mock.stdout.close()
self.p_mock.terminate()
self.p_mock.wait()

super().tearDown()

def get_property(self, name):
return self.p_obj.Get(self.dbus_interface, name, dbus_interface=dbus.PROPERTIES_IFACE)


@unittest.skipUnless(have_mmcli, "mmcli utility not available")
class TestModemManagerMmcliBase(TestModemManagerBase):
"""Base ModemManager interface tests using mmcli"""

ret = None

def run_mmcli(self, args):
self.assertIsNone(self.ret)
self.ret = subprocess.run( # pylint: disable=subprocess-run-check
["mmcli", *args], capture_output=True, text=True
)

def assertOutputEquals(self, expected_lines):
self.assertIsNotNone(self.ret)
lines = self.ret.stdout.split("\n")
self.assertEqual(len(lines), len(expected_lines))
for expected, line in zip(expected_lines, lines):
self.assertEqual(expected, line)

def assertOutputContainsLine(self, expected_line, ret=0):
self.assertEqual(self.ret.returncode, ret)
self.assertIn(expected_line, self.ret.stdout)


class TestModemManagerModemMmcli(TestModemManagerMmcliBase):
"""main ModemManager interface tests using mmcli"""

def test_no_modems(self):
self.run_mmcli(["-m", "any"])
self.assertEqual(self.ret.returncode, 1)
self.assertIn("error: couldn't find modem", self.ret.stderr)

def test_modem(self):
self.p_obj.AddSimpleModem()
self.run_mmcli(["-m", "any"])
self.assertOutputEquals(
[
" -----------------------------",
" General | path: /org/freedesktop/ModemManager1/Modems/8",
" -----------------------------",
" Hardware | model: E1750",
" | firmware revision: 11.126.08.01.00",
" -----------------------------",
" Status | state: enabled",
" | power state: on",
" | access tech: lte",
" | signal quality: 70% (recent)",
" -----------------------------",
" Modes | supported: allowed: 4g; preferred: 4g",
" | allowed: 2g, 3g; preferred: 3g",
" | current: allowed: 4g; preferred: 4g",
" -----------------------------",
" 3GPP | imei: doesnotmatter",
" | operator name: TheOperator",
" | registration: idle",
" -----------------------------",
" SIM | primary sim path: /org/freedesktop/ModemManager1/SIM/2",
"",
]
)

def test_sim(self):
self.p_obj.AddSimpleModem()
self.run_mmcli(["-i", "any"])
self.assertOutputEquals(
[
" --------------------",
" General | path: /org/freedesktop/ModemManager1/SIM/2",
" --------------------",
" Properties | active: yes",
" | imsi: doesnotmatter",
"",
]
)

def test_voice_call_list(self):
self.p_obj.AddSimpleModem()
self.run_mmcli(["-m", "any", "--voice-list-calls"])
self.assertOutputContainsLine("No calls were found\n")

def test_voice_status(self):
self.p_obj.AddSimpleModem()
self.run_mmcli(["-m", "any", "--voice-status"])
self.assertOutputContainsLine("emergency only: no\n")


if __name__ == "__main__":
# avoid writing to stderr
unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout))

0 comments on commit d416d2e

Please sign in to comment.