Skip to content

Commit

Permalink
Automate WWAN 3gpp scan test (New) (#1323)
Browse files Browse the repository at this point in the history
- Add a `3gpp-scan` subcommand to the wwan_tests.py script
- Create a new `wwan/3gpp-scan-{manufacturer}-{model}-{hw_id}-auto` template job to automatically scan available GSM connections using the new subcommand

---------

Co-authored-by: Pierre Equoy <pierre.equoy@canonical.com>
  • Loading branch information
stanley31huang and pieqq authored Jul 23, 2024
1 parent d45f528 commit 5508989
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 23 deletions.
73 changes: 73 additions & 0 deletions providers/base/bin/wwan_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,16 @@ def _wwan_radio_off():
print()


def _wwan_radio_status():
print_head("Get radio status")
cmd = ["nmcli", "r", "wwan"]
ret_sp = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True
)
print(ret_sp.stdout.decode("utf-8"))
return ret_sp.stdout.decode("utf-8").strip()


def _destroy_3gpp_connection():
print_head("Destroy 3GPP Connection")
cmd = ["nmcli", "c", "delete", GSM_CON_ID]
Expand Down Expand Up @@ -362,6 +372,68 @@ def invoked(self):
sys.exit(ret_code)


class WWANTestCtx:

def __init__(self, hardware_id, use_mmcli=True, need_enable=False):
self.mm_obj = MMCLI() if use_mmcli else MMDbus()
self.need_enable = need_enable
self.original_status = ""
self.modem_idx = self.mm_obj.equipment_id_to_mm_id(hardware_id)

def __enter__(self):
self.original_status = _wwan_radio_status()
if self.need_enable and self.original_status == "disabled":
_wwan_radio_on()
return self

def __exit__(self, type, value, traceback):
if self.need_enable and self.original_status == "disabled":
_wwan_radio_off()


class ThreeGppScanTest:

def register_argument(self):

parser = argparse.ArgumentParser()
parser.add_argument(
"hw_id", type=str, help="The hardware ID of the modem"
)
parser.add_argument(
"--timeout",
type=int,
default=300,
help="timeout for 3gpp-scan",
)
return parser.parse_args(sys.argv[2:])

def invoked(self):

args = self.register_argument()
ret_code = 1
try:
with WWANTestCtx(args.hw_id, True, True) as ctx:
cmd = [
"mmcli",
"-m",
str(ctx.modem_idx),
"--3gpp-scan",
"--timeout",
str(args.timeout),
]
print_head("Scanning 3GPP available network")
print("+ {}".format(" ".join(cmd)))
ret_sp = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
print(ret_sp.stdout.decode("utf-8"))
ret_code = ret_sp.returncode
except subprocess.CalledProcessError:
pass

sys.exit(ret_code)


class CountModems:

def invoked(self):
Expand Down Expand Up @@ -462,6 +534,7 @@ def main(self):
"count": CountModems,
"resources": Resources,
"3gpp-connection": ThreeGppConnection,
"3gpp-scan": ThreeGppScanTest,
"sim-present": SimPresent,
"sim-info": SimInfo,
}
Expand Down
157 changes: 156 additions & 1 deletion providers/base/tests/test_wwan_tests.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import unittest
import sys
from unittest.mock import patch, call, Mock, MagicMock
from unittest.mock import patch, Mock, MagicMock
from io import StringIO
from contextlib import redirect_stdout
import argparse
import subprocess

# Mock the dbus module due to is is not available on CI testing environment
sys.modules["dbus"] = MagicMock()
Expand Down Expand Up @@ -109,3 +111,156 @@ def test_invoked_with_mmdbus(self, mock_mmdbus):
self.assertTrue(mmdbus_instance.get_model_name.called)
self.assertTrue(mmdbus_instance.get_firmware_revision.called)
self.assertTrue(mmdbus_instance.get_hardware_revision.called)


class TestCommonFunctions(unittest.TestCase):

@patch("subprocess.run")
def test_wwan_radio_status(self, mock_run):
mock_run.return_value = subprocess.CompletedProcess(
args=[], returncode=0, stdout=b"disabled", stderr=b""
)
with redirect_stdout(StringIO()):
status = wwan_tests._wwan_radio_status()
self.assertEqual(status, "disabled")

@patch("subprocess.run")
def test_wwan_radio_status_exception(self, mock_run):
mock_run.side_effect = subprocess.CalledProcessError(1, "cmd")
with redirect_stdout(StringIO()):
with self.assertRaises(subprocess.CalledProcessError):
wwan_tests._wwan_radio_status()


class TestWWANTestCtx(unittest.TestCase):

@patch("wwan_tests._wwan_radio_off")
@patch("wwan_tests._wwan_radio_on")
@patch("wwan_tests._wwan_radio_status")
@patch("wwan_tests.MMDbus")
def test_wwan_ctx_init_with_dbus(
self, mock_mmdbus, mock_r_status, mock_r_on, mock_r_off
):
mmdbus_instance = Mock()
mmdbus_instance.equipment_id_to_mm_id.return_value = "0"
mock_mmdbus.return_value = mmdbus_instance
mock_r_status.return_value = "enabled"

with wwan_tests.WWANTestCtx("test_id", False):
pass

mmdbus_instance.equipment_id_to_mm_id.assert_called_with("test_id")
self.assertTrue(mock_mmdbus.called)
self.assertFalse(mock_r_on.called)
self.assertFalse(mock_r_off.called)
self.assertTrue(mock_r_status.called)

@patch("wwan_tests._wwan_radio_off")
@patch("wwan_tests._wwan_radio_on")
@patch("wwan_tests._wwan_radio_status")
@patch("wwan_tests.MMCLI")
def test_wwan_ctx_init_with_mmcli(
self, mock_mmcli, mock_r_status, mock_r_on, mock_r_off
):
mmcli_instance = Mock()
mmcli_instance.equipment_id_to_mm_id.return_value = "0"
mock_mmcli.return_value = mmcli_instance
mock_r_status.return_value = "disabled"

with wwan_tests.WWANTestCtx("test_id", True, True):
pass

mmcli_instance.equipment_id_to_mm_id.assert_called_with("test_id")
self.assertTrue(mock_mmcli.called)
self.assertTrue(mock_r_on.called)
self.assertTrue(mock_r_off.called)
self.assertTrue(mock_r_status.called)


class TestThreeGppScanTest(unittest.TestCase):

def test_register_argument(self):

sys.argv = ["wwan_tests.py", "3gpp-scan", "2", "--timeout", "600"]
obj_3gppscan = wwan_tests.ThreeGppScanTest()
ret_args = obj_3gppscan.register_argument()
self.assertEqual(ret_args.hw_id, "2")
self.assertEqual(ret_args.timeout, 600)

@patch("subprocess.run")
@patch("wwan_tests.WWANTestCtx")
@patch("wwan_tests.ThreeGppScanTest.register_argument")
def test_invoked_successfully(self, mock_arg, mock_mmctx, mock_run):
mock_arg.return_value = argparse.Namespace(hw_id="2", timeout=200)
mmcli_instance = Mock()
mmcli_instance.modem_idx = "0"
mock_mmctx.return_value.__enter__.return_value = mmcli_instance
mock_run.return_value = subprocess.CompletedProcess(
args=[], returncode=0, stdout=b"output", stderr=b""
)

with redirect_stdout(StringIO()):
with self.assertRaises(SystemExit) as context:
obj_3gppscan = wwan_tests.ThreeGppScanTest()
obj_3gppscan.invoked()

self.assertEqual(context.exception.code, 0)
mock_mmctx.assert_called_with("2", True, True)
mock_run.assert_called_with(
["mmcli", "-m", "0", "--3gpp-scan", "--timeout", "200"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)

@patch("subprocess.run")
@patch("wwan_tests.WWANTestCtx")
@patch("wwan_tests.ThreeGppScanTest.register_argument")
def test_invoked_failed_exit_code(self, mock_arg, mock_mmctx, mock_run):
mock_arg.return_value = argparse.Namespace(hw_id="2", timeout=200)
mmcli_instance = Mock()
mmcli_instance.modem_idx = "0"
mock_mmctx.return_value.__enter__.return_value = mmcli_instance
mock_run.return_value = subprocess.CompletedProcess(
args=[], returncode=1, stdout=b"output", stderr=b""
)

with redirect_stdout(StringIO()):
with self.assertRaises(SystemExit) as context:
obj_3gppscan = wwan_tests.ThreeGppScanTest()
obj_3gppscan.invoked()

self.assertEqual(context.exception.code, 1)
mock_mmctx.assert_called_with("2", True, True)
mock_run.assert_called_with(
["mmcli", "-m", "0", "--3gpp-scan", "--timeout", "200"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)

@patch("subprocess.run")
@patch("wwan_tests.WWANTestCtx")
@patch("wwan_tests.ThreeGppScanTest.register_argument")
def test_invoked_call_error(self, mock_arg, mock_mmctx, mock_run):
mock_arg.return_value = argparse.Namespace(hw_id="2", timeout=200)
mmcli_instance = Mock()
mmcli_instance.modem_idx = "0"
mock_mmctx.return_value.__enter__.return_value = mmcli_instance
mock_run.return_value = subprocess.CompletedProcess(
args=[],
returncode=1,
stdout=b"output",
stderr=b"",
)

with redirect_stdout(StringIO()):
with self.assertRaises(SystemExit) as context:
obj_3gppscan = wwan_tests.ThreeGppScanTest()
obj_3gppscan.invoked()

self.assertEqual(context.exception.code, 1)
mock_mmctx.assert_called_with("2", True, True)
mock_run.assert_called_with(
["mmcli", "-m", "0", "--3gpp-scan", "--timeout", "200"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
43 changes: 23 additions & 20 deletions providers/base/units/wwan/jobs.pxu
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ estimated_duration: 10.0
category_id: wwan
flags: preserve-locale also-after-suspend preserve-cwd
imports: from com.canonical.plainbox import manifest
depends: wwan/check-sim-present-{manufacturer}-{model}-{hw_id}-auto
requires:
manifest.has_wwan_module == 'True'
snap.name == 'modem-manager' or package.name == 'modemmanager'
Expand Down Expand Up @@ -101,6 +102,28 @@ requires:
manifest.has_wwan_module == 'True'
snap.name == 'modem-manager' or package.name == 'modemmanager'

unit: template
template-resource: wwan_resource
template-unit: job
id: wwan/3gpp-scan-{manufacturer}-{model}-{hw_id}-auto
template-id: wwan/3gpp-scan-manufacturer-model-hw_id-auto
_summary: Scan for available 3GPP networks with the modem
_template-summary: Scan for available 3GPP networks with the {model} modem
_description:
Scan for available 3GPP networks with the target modem
plugin: shell
command: wwan_tests.py 3gpp-scan {hw_id}
environ: LD_LIBRARY_PATH
user: root
estimated_duration: 10.0
category_id: wwan
flags: preserve-locale also-after-suspend
depends: wwan/check-sim-present-{manufacturer}-{model}-{hw_id}-auto
imports: from com.canonical.plainbox import manifest
requires:
manifest.has_wwan_module == 'True'
snap.name == 'modem-manager' or package.name == 'modemmanager'

id: wwan/detect-manual
plugin: manual
_summary: Check if WWAN module is available
Expand Down Expand Up @@ -171,26 +194,6 @@ requires:
depends:
wwan/check-sim-present-manual

id: wwan/scan-networks-manual
plugin: manual
_summary: Verify that modem can scan for available networks
_purpose:
Ensure that the modem can scan/find available networks
_steps:
1. Open another terminal on SUT (or press ctrl+z to suspend Checkbox)
2. Run `sudo mmcli -m 0 --3gpp-scan --timeout=300`
3. Run `fg` to jump back to checkbox (if you're running in the same terminal)
_verification:
Were available networks listed?
estimated_duration: 120s
flags: also-after-suspend
category_id: wwan
imports: from com.canonical.plainbox import manifest
requires:
manifest.has_wwan_module == 'True'
depends:
wwan/check-sim-present-manual

id: wwan/gsm-connection-interrupted-manual
plugin: manual
template-engine: jinja2
Expand Down
5 changes: 3 additions & 2 deletions providers/base/units/wwan/test-plan.pxu
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ _description: Automated wwan tests for Snappy Ubuntu Core devices
include:
# Note these tests require snap calling snap support
wwan/detect
wwan/3gpp-scan-manufacturer-model-hw_id-auto
wwan/gsm-connection-.*-auto
wwan/check-sim-present-.*-auto
bootstrap_include:
Expand All @@ -36,6 +37,8 @@ _name: Automated wwan tests (after suspend)
_description: Automated wwan tests for Snappy Ubuntu Core devices
include:
after-suspend-wwan/detect
after-suspend-wwan/check-sim-present-manufacturer-model-hw_id-auto
after-suspend-wwan/3gpp-scan-manufacturer-model-hw_id-auto
after-suspend-wwan/gsm-connection-.*-auto
bootstrap_include:
wwan_resource
Expand All @@ -46,7 +49,6 @@ _name: Manual wwan tests
_description: Manual wwan tests for Snappy Ubuntu Core devices
include:
wwan/detect-manual
wwan/scan-networks-manual
wwan/check-sim-present-manual
wwan/gsm-connection-interrupted-manual

Expand All @@ -56,6 +58,5 @@ _name: Manual wwan tests (after suspend)
_description: Manual wwan tests for Snappy Ubuntu Core devices
include:
after-suspend-wwan/detect-manual
after-suspend-wwan/scan-networks-manual
after-suspend-wwan/check-sim-present-manual
after-suspend-wwan/gsm-connection-interrupted-manual

0 comments on commit 5508989

Please sign in to comment.