tests: lwm2m: Information Reporting Interface [300-399]

Implement testcases for Information Reporting Interface [300-399]:

* LightweightM2M-1.1-int-301 - Observation and Notification of parameter
  values
* LightweightM2M-1.1-int-302 - Cancel Observations using Reset
* LightweightM2M-1.1-int-304 - Observe-Composite Operation
* LightweightM2M-1.1-int-306 – Send Operation
* LightweightM2M-1.1-int-307 – Muting Send
* LightweightM2M-1.1-int-308 - Observe-Composite and Creating
  Object Instance
* LightweightM2M-1.1-int-309 - Observe-Composite and Deleting
  Object Instance
* LightweightM2M-1.1-int-310 - Observe-Composite and modification of
  parameter values
* LightweightM2M-1.1-int-311 - Send command

303 and 305 cannot be implemented using Leshan as it only support
passive cancelling of observation.

Signed-off-by: Seppo Takalo <seppo.takalo@nordicsemi.no>
This commit is contained in:
Seppo Takalo 2023-10-24 17:01:55 +03:00 committed by Carles Cufí
commit 8608b2dc45
5 changed files with 267 additions and 20 deletions

View file

@ -55,6 +55,11 @@ LOG_MODULE_REGISTER(LOG_MODULE_NAME);
"PATH is LwM2M path\n" \ "PATH is LwM2M path\n" \
"NUM how many elements to cache\n" \ "NUM how many elements to cache\n" \
static void send_cb(enum lwm2m_send_status status)
{
LOG_INF("SEND status: %d\n", status);
}
static int cmd_send(const struct shell *sh, size_t argc, char **argv) static int cmd_send(const struct shell *sh, size_t argc, char **argv)
{ {
int ret = 0; int ret = 0;
@ -86,7 +91,7 @@ static int cmd_send(const struct shell *sh, size_t argc, char **argv)
} }
} }
ret = lwm2m_send_cb(ctx, lwm2m_path_list, path_cnt, NULL); ret = lwm2m_send_cb(ctx, lwm2m_path_list, path_cnt, send_cb);
if (ret < 0) { if (ret < 0) {
shell_error(sh, "can't do send operation, request failed (%d)\n", ret); shell_error(sh, "can't do send operation, request failed (%d)\n", ret);

View file

@ -170,6 +170,17 @@ Tests are written from test spec;
|LightweightM2M-1.1-int-261 - Write-Attribute Operation on a multiple resource|:large_orange_diamond:|Leshan don't allow writing attributes to resource instance| |LightweightM2M-1.1-int-261 - Write-Attribute Operation on a multiple resource|:large_orange_diamond:|Leshan don't allow writing attributes to resource instance|
|LightweightM2M-1.1-int-280 - Successful Read-Composite Operation|:white_check_mark:| | |LightweightM2M-1.1-int-280 - Successful Read-Composite Operation|:white_check_mark:| |
|LightweightM2M-1.1-int-281 - Partially Successful Read-Composite Operation|:white_check_mark:| | |LightweightM2M-1.1-int-281 - Partially Successful Read-Composite Operation|:white_check_mark:| |
|LightweightM2M-1.1-int-301 - Observation and Notification of parameter values|:white_check_mark:| |
|LightweightM2M-1.1-int-302 - Cancel Observations using Reset Operation|:white_check_mark:| |
|LightweightM2M-1.1-int-303 - Cancel observations using Observe with Cancel parameter|:large_orange_diamond:|Leshan only supports passive cancelling|
|LightweightM2M-1.1-int-304 - Observe-Composite Operation|:white_check_mark:| |
|LightweightM2M-1.1-int-305 - Cancel Observation-Composite Operation|:large_orange_diamond:|Leshan only supports passive cancelling|
|LightweightM2M-1.1-int-306 Send Operation|:white_check_mark:|[~~#64290~~](https://github.com/zephyrproject-rtos/zephyr/issues/64290)|
|LightweightM2M-1.1-int-307 Muting Send|:white_check_mark:| |
|LightweightM2M-1.1-int-308 - Observe-Composite and Creating Object Instance|:white_check_mark:|[~~#64634~~](https://github.com/zephyrproject-rtos/zephyr/issues/64634)|
|LightweightM2M-1.1-int-309 - Observe-Composite and Deleting Object Instance|:white_check_mark:|[~~#64634~~](https://github.com/zephyrproject-rtos/zephyr/issues/64634)|
|LightweightM2M-1.1-int-310 - Observe-Composite and modification of parameter values|:white_check_mark:| |
|LightweightM2M-1.1-int-311 - Send command|:white_check_mark:| |
|LightweightM2M-1.1-int-401 - UDP Channel Security - PSK Mode |:white_check_mark:| | |LightweightM2M-1.1-int-401 - UDP Channel Security - PSK Mode |:white_check_mark:| |
* :white_check_mark: Working OK. * :white_check_mark: Working OK.

View file

@ -12,10 +12,10 @@ from __future__ import annotations
import json import json
import binascii import binascii
import requests
from datetime import datetime
import time import time
from datetime import datetime
from contextlib import contextmanager from contextlib import contextmanager
import requests
class Leshan: class Leshan:
"""This class represents a Leshan client that interacts with demo server's REAT API""" """This class represents a Leshan client that interacts with demo server's REAT API"""
@ -86,11 +86,15 @@ class Leshan:
resp = self._s.post(f'{self.api_url}{path}' + uri_options, data=data, headers=headers, timeout=self.timeout) resp = self._s.post(f'{self.api_url}{path}' + uri_options, data=data, headers=headers, timeout=self.timeout)
return Leshan.handle_response(resp) return Leshan.handle_response(resp)
def delete(self, path: str): def delete_raw(self, path: str):
"""Send HTTP DELETE query""" """Send HTTP DELETE query"""
resp = self._s.delete(f'{self.api_url}{path}', timeout=self.timeout) resp = self._s.delete(f'{self.api_url}{path}', timeout=self.timeout)
return Leshan.handle_response(resp) return Leshan.handle_response(resp)
def delete(self, endpoint: str, path: str):
"""Send LwM2M DELETE command"""
return self.delete_raw(f'/clients/{endpoint}/{path}')
def execute(self, endpoint: str, path: str): def execute(self, endpoint: str, path: str):
"""Send LwM2M EXECUTE command""" """Send LwM2M EXECUTE command"""
return self.post(f'/clients/{endpoint}/{path}') return self.post(f'/clients/{endpoint}/{path}')
@ -247,6 +251,10 @@ class Leshan:
def parse_composite(cls, payload: dict): def parse_composite(cls, payload: dict):
"""Decode the Leshan's response to composite query back to a Python dictionary""" """Decode the Leshan's response to composite query back to a Python dictionary"""
data = {} data = {}
if 'status' in payload:
if payload['status'] != 'CONTENT(205)' or 'content' not in payload:
raise RuntimeError(f'No content received')
payload = payload['content']
for path, content in payload.items(): for path, content in payload.items():
keys = [int(key) for key in path.lstrip("/").split('/')] keys = [int(key) for key in path.lstrip("/").split('/')]
if len(keys) == 1: if len(keys) == 1:
@ -291,9 +299,7 @@ class Leshan:
parameters = self._composite_params(paths) parameters = self._composite_params(paths)
resp = self._s.get(f'{self.api_url}/clients/{endpoint}/composite', params=parameters, timeout=self.timeout) resp = self._s.get(f'{self.api_url}/clients/{endpoint}/composite', params=parameters, timeout=self.timeout)
payload = Leshan.handle_response(resp) payload = Leshan.handle_response(resp)
if not payload['status'] == 'CONTENT(205)': return self.parse_composite(payload)
raise RuntimeError(f'No content received')
return self.parse_composite(payload['content'])
def composite_write(self, endpoint: str, resources: dict): def composite_write(self, endpoint: str, resources: dict):
""" """
@ -314,11 +320,7 @@ class Leshan:
Objects or object instances cannot be targeted. Objects or object instances cannot be targeted.
""" """
data = { } data = { }
parameters = { parameters = self._composite_params()
'pathformat': self.format,
'nodeformat': self.format,
'timeout': self.timeout
}
for path, value in resources.items(): for path, value in resources.items():
path = path if path.startswith('/') else '/' + path path = path if path.startswith('/') else '/' + path
level = len(path.split('/')) - 1 level = len(path.split('/')) - 1
@ -349,7 +351,7 @@ class Leshan:
self.put('/security/clients/', f'{{"endpoint":"{endpoint}","tls":{{"mode":"psk","details":{{"identity":"{endpoint}","key":"{psk}"}} }} }}') self.put('/security/clients/', f'{{"endpoint":"{endpoint}","tls":{{"mode":"psk","details":{{"identity":"{endpoint}","key":"{psk}"}} }} }}')
def delete_device(self, endpoint: str): def delete_device(self, endpoint: str):
self.delete(f'/security/clients/{endpoint}') self.delete_raw(f'/security/clients/{endpoint}')
def create_bs_device(self, endpoint: str, server_uri: str, bs_passwd: str, passwd: str): def create_bs_device(self, endpoint: str, server_uri: str, bs_passwd: str, passwd: str):
psk = binascii.b2a_hex(bs_passwd.encode()).decode() psk = binascii.b2a_hex(bs_passwd.encode()).decode()
@ -361,11 +363,27 @@ class Leshan:
self.post(f'/bootstrap/{endpoint}', content) self.post(f'/bootstrap/{endpoint}', content)
def delete_bs_device(self, endpoint: str): def delete_bs_device(self, endpoint: str):
self.delete(f'/security/clients/{endpoint}') self.delete_raw(f'/security/clients/{endpoint}')
self.delete(f'/bootstrap/{endpoint}') self.delete_raw(f'/bootstrap/{endpoint}')
def observe(self, endpoint: str, path: str):
return self.post(f'/clients/{endpoint}/{path}/observe', data="")
def cancel_observe(self, endpoint: str, path: str):
return self.delete_raw(f'/clients/{endpoint}/{path}/observe')
def composite_observe(self, endpoint: str, paths: list[str]):
parameters = self._composite_params(paths)
resp = self._s.post(f'{self.api_url}/clients/{endpoint}/composite/observe', params=parameters, timeout=self.timeout)
payload = Leshan.handle_response(resp)
return self.parse_composite(payload)
def cancel_composite_observe(self, endpoint: str, paths: list[str]):
paths = [path if path.startswith('/') else '/' + path for path in paths]
return self.delete_raw(f'/clients/{endpoint}/composite/observe?paths=' + ','.join(paths))
@contextmanager @contextmanager
def get_event_stream(self, endpoint: str): def get_event_stream(self, endpoint: str, timeout: int = None):
""" """
Get stream of events regarding the given endpoint. Get stream of events regarding the given endpoint.
@ -377,11 +395,13 @@ class Leshan:
If timeout happens, the event streams returns None. If timeout happens, the event streams returns None.
""" """
r = self._s.get(f'{self.api_url}/event?{endpoint}', stream=True, headers={'Accept': 'text/event-stream'}, timeout=self.timeout) if timeout is None:
timeout = self.timeout
r = requests.get(f'{self.api_url}/event?{endpoint}', stream=True, headers={'Accept': 'text/event-stream'}, timeout=timeout)
if r.encoding is None: if r.encoding is None:
r.encoding = 'utf-8' r.encoding = 'utf-8'
try: try:
yield LeshanEventsIterator(r, self.timeout) yield LeshanEventsIterator(r, timeout)
finally: finally:
r.close() r.close()
@ -406,8 +426,11 @@ class LeshanEventsIterator:
if not line.startswith('data: '): if not line.startswith('data: '):
continue continue
data = json.loads(line.lstrip('data: ')) data = json.loads(line.lstrip('data: '))
if event == 'SEND': if event == 'SEND' or (event == 'NOTIFICATION' and data['kind'] == 'composite'):
return Leshan.parse_composite(data['val']) return Leshan.parse_composite(data['val'])
if event == 'NOTIFICATION':
d = {data['res']: data['val']}
return Leshan.parse_composite(d)
return data return data
if time.time() > timeout: if time.time() > timeout:
return None return None

View file

@ -495,3 +495,210 @@ def test_LightweightM2M_1_1_int_281(shell: Shell, leshan: Leshan, endpoint: str)
assert len(resp[1][0]) == 2 # /1/0/8 should not be there assert len(resp[1][0]) == 2 # /1/0/8 should not be there
assert resp[1][0][1] == 86400 assert resp[1][0][1] == 86400
assert resp[1][0][7] == 'U' assert resp[1][0][7] == 'U'
#
# Information Reporting Interface [300-399]
#
def test_LightweightM2M_1_1_int_301(shell: Shell, leshan: Leshan, endpoint: str):
"""LightweightM2M-1.1-int-301 - Observation and Notification of parameter values"""
pwr_src = leshan.read(endpoint, '3/0/6')
logger.debug(pwr_src)
assert pwr_src[6][0] == 1
assert pwr_src[6][1] == 5
assert leshan.put_raw(f'/clients/{endpoint}/3/0/7/attributes?pmin=5')['status'] == 'CHANGED(204)'
assert leshan.put_raw(f'/clients/{endpoint}/3/0/7/attributes?pmax=10')['status'] == 'CHANGED(204)'
leshan.observe(endpoint, '3/0/7')
with leshan.get_event_stream(endpoint, timeout=30) as events:
shell.exec_command('lwm2m write /3/0/7/0 -u32 3000')
data = events.next_event('NOTIFICATION')
assert data is not None
assert data[3][0][7][0] == 3000
# Ensure that we don't get new data before pMin
start = time.time()
shell.exec_command('lwm2m write /3/0/7/0 -u32 3500')
data = events.next_event('NOTIFICATION')
assert data[3][0][7][0] == 3500
assert (start + 5) < time.time() + 0.5 # Allow 0.5 second diff
assert (start + 5) > time.time() - 0.5
# Ensure that we get update when pMax expires
data = events.next_event('NOTIFICATION')
assert data[3][0][7][0] == 3500
assert (start + 15) <= time.time() + 1 # Allow 1 second slack. (pMinx + pMax=15)
leshan.cancel_observe(endpoint, '3/0/7')
def test_LightweightM2M_1_1_int_302(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str):
"""LightweightM2M-1.1-int-302 - Cancel Observations using Reset Operation"""
leshan.observe(endpoint, '3/0/7')
leshan.observe(endpoint, '3/0/8')
with leshan.get_event_stream(endpoint) as events:
shell.exec_command('lwm2m write /3/0/7/0 -u32 4000')
data = events.next_event('NOTIFICATION')
assert data[3][0][7][0] == 4000
leshan.cancel_observe(endpoint, '3/0/7')
shell.exec_command('lwm2m write /3/0/7/0 -u32 3000')
dut.readlines_until(regex=r'.*Observer removed for 3/0/7')
with leshan.get_event_stream(endpoint) as events:
shell.exec_command('lwm2m write /3/0/8/0 -u32 100')
data = events.next_event('NOTIFICATION')
assert data[3][0][8][0] == 100
leshan.cancel_observe(endpoint, '3/0/8')
shell.exec_command('lwm2m write /3/0/8/0 -u32 50')
dut.readlines_until(regex=r'.*Observer removed for 3/0/8')
def test_LightweightM2M_1_1_int_304(shell: Shell, leshan: Leshan, endpoint: str):
"""LightweightM2M-1.1-int-304 - Observe-Composite Operation"""
assert leshan.put_raw(f'/clients/{endpoint}/1/0/1/attributes?pmin=30')['status'] == 'CHANGED(204)'
assert leshan.put_raw(f'/clients/{endpoint}/1/0/1/attributes?pmax=45')['status'] == 'CHANGED(204)'
data = leshan.composite_observe(endpoint, ['/1/0/1', '/3/0/11/0', '/3/0/16'])
assert data[1][0][1] is not None
assert data[3][0][11][0] is not None
assert data[3][0][16] == 'U'
assert len(data) == 2
assert len(data[1]) == 1
assert len(data[3][0]) == 2
start = time.time()
with leshan.get_event_stream(endpoint, timeout=50) as events:
data = events.next_event('NOTIFICATION')
logger.debug(data)
assert data[1][0][1] is not None
assert data[3][0][11][0] is not None
assert data[3][0][16] == 'U'
assert len(data) == 2
assert len(data[1]) == 1
assert len(data[3][0]) == 2
assert (start + 30) < time.time()
assert (start + 45) > time.time() - 1
leshan.cancel_composite_observe(endpoint, ['/1/0/1', '/3/0/11/0', '/3/0/16'])
def test_LightweightM2M_1_1_int_306(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str):
"""LightweightM2M-1.1-int-306 - Send Operation"""
with leshan.get_event_stream(endpoint) as events:
shell.exec_command('lwm2m send /1 /3')
dut.readlines_until(regex=r'.*SEND status: 0', timeout=5.0)
data = events.next_event('SEND')
assert data is not None
verify_server_object(data[1])
verify_device_object(data[3])
def test_LightweightM2M_1_1_int_307(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str):
"""LightweightM2M-1.1-int-307 - Muting Send"""
leshan.write(endpoint, '1/0/23', True)
lines = shell.get_filtered_output(shell.exec_command('lwm2m send /3/0'))
assert any("can't do send operation" in line for line in lines)
leshan.write(endpoint, '1/0/23', False)
shell.exec_command('lwm2m send /3/0')
dut.readlines_until(regex=r'.*SEND status: 0', timeout=5.0)
def test_LightweightM2M_1_1_int_308(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str):
"""LightweightM2M-1.1-int-308 - Observe-Composite and Creating Object Instance"""
shell.exec_command('lwm2m delete /16/0')
shell.exec_command('lwm2m delete /16/1')
# Need to use Configuration C.1
shell.exec_command('lwm2m write 1/0/2 -u32 0')
shell.exec_command('lwm2m write 1/0/3 -u32 0')
resources_a = {
0: {0: 'aa',
1: 'bb',
2: 'cc',
3: 'dd'}
}
content_one = {16: {0: resources_a}}
resources_b = {
0: {0: '11',
1: '22',
2: '33',
3: '44'}
}
content_both = {16: {0: resources_a, 1: resources_b}}
assert leshan.create_obj_instance(endpoint, '16/0', resources_a)['status'] == 'CREATED(201)'
dut.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0)
assert leshan.put_raw(f'/clients/{endpoint}/16/0/attributes?pmin=30')['status'] == 'CHANGED(204)'
assert leshan.put_raw(f'/clients/{endpoint}/16/0/attributes?pmax=45')['status'] == 'CHANGED(204)'
data = leshan.composite_observe(endpoint, ['/16/0', '/16/1'])
assert data == content_one
with leshan.get_event_stream(endpoint, timeout=50) as events:
data = events.next_event('NOTIFICATION')
start = time.time()
assert data == content_one
assert leshan.create_obj_instance(endpoint, '16/1', resources_b)['status'] == 'CREATED(201)'
data = events.next_event('NOTIFICATION')
assert (start + 30) < time.time() + 2
assert (start + 45) > time.time() - 2
assert data == content_both
leshan.cancel_composite_observe(endpoint, ['/16/0', '/16/1'])
# Restore configuration C.3
shell.exec_command('lwm2m write 1/0/2 -u32 1')
shell.exec_command('lwm2m write 1/0/3 -u32 10')
def test_LightweightM2M_1_1_int_309(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str):
"""LightweightM2M-1.1-int-309 - Observe-Composite and Deleting Object Instance"""
shell.exec_command('lwm2m delete /16/0')
shell.exec_command('lwm2m delete /16/1')
# Need to use Configuration C.1
shell.exec_command('lwm2m write 1/0/2 -u32 0')
shell.exec_command('lwm2m write 1/0/3 -u32 0')
resources_a = {
0: {0: 'aa',
1: 'bb',
2: 'cc',
3: 'dd'}
}
content_one = {16: {0: resources_a}}
resources_b = {
0: {0: '11',
1: '22',
2: '33',
3: '44'}
}
content_both = {16: {0: resources_a, 1: resources_b}}
assert leshan.create_obj_instance(endpoint, '16/0', resources_a)['status'] == 'CREATED(201)'
assert leshan.create_obj_instance(endpoint, '16/1', resources_b)['status'] == 'CREATED(201)'
dut.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0)
assert leshan.put_raw(f'/clients/{endpoint}/16/0/attributes?pmin=30')['status'] == 'CHANGED(204)'
assert leshan.put_raw(f'/clients/{endpoint}/16/0/attributes?pmax=45')['status'] == 'CHANGED(204)'
data = leshan.composite_observe(endpoint, ['/16/0', '/16/1'])
assert data == content_both
with leshan.get_event_stream(endpoint, timeout=50) as events:
data = events.next_event('NOTIFICATION')
start = time.time()
assert data == content_both
assert leshan.delete(endpoint, '16/1')['status'] == 'DELETED(202)'
data = events.next_event('NOTIFICATION')
assert (start + 30) < time.time() + 2
assert (start + 45) > time.time() - 2
assert data == content_one
leshan.cancel_composite_observe(endpoint, ['/16/0', '/16/1'])
# Restore configuration C.3
shell.exec_command('lwm2m write 1/0/2 -u32 1')
shell.exec_command('lwm2m write 1/0/3 -u32 10')
def test_LightweightM2M_1_1_int_310(shell: Shell, leshan: Leshan, endpoint: str):
"""LightweightM2M-1.1-int-310 - Observe-Composite and modification of parameter values"""
# Need to use Configuration C.1
shell.exec_command('lwm2m write 1/0/2 -u32 0')
shell.exec_command('lwm2m write 1/0/3 -u32 0')
# Ensure that our previous attributes are not conflicting
assert leshan.put_raw(f'/clients/{endpoint}/3/attributes?pmin=0')['status'] == 'CHANGED(204)'
leshan.composite_observe(endpoint, ['/1/0/1', '/3/0'])
with leshan.get_event_stream(endpoint, timeout=50) as events:
assert leshan.put_raw(f'/clients/{endpoint}/3/attributes?pmax=5')['status'] == 'CHANGED(204)'
start = time.time()
data = events.next_event('NOTIFICATION')
assert data[3][0][0] == 'Zephyr'
assert data[1] == {0: {1: 86400}}
assert (start + 5) > time.time() - 1
start = time.time()
data = events.next_event('NOTIFICATION')
assert (start + 5) > time.time() - 1
leshan.cancel_composite_observe(endpoint, ['/1/0/1', '/3/0'])
# Restore configuration C.3
shell.exec_command('lwm2m write 1/0/2 -u32 1')
shell.exec_command('lwm2m write 1/0/3 -u32 10')
def test_LightweightM2M_1_1_int_311(shell: Shell, leshan: Leshan, endpoint: str):
"""LightweightM2M-1.1-int-311 - Send command"""
with leshan.get_event_stream(endpoint, timeout=50) as events:
shell.exec_command('lwm2m send /1/0/1 /3/0/11')
data = events.next_event('SEND')
assert data == {3: {0: {11: {0: 0}}}, 1: {0: {1: 86400}}}

View file

@ -1,10 +1,11 @@
tests: tests:
net.lwm2m.interop: net.lwm2m.interop:
harness: pytest harness: pytest
timeout: 300 timeout: 600
slow: true slow: true
harness_config: harness_config:
pytest_dut_scope: module pytest_dut_scope: module
pytest_args: []
integration_platforms: integration_platforms:
- native_posix - native_posix
platform_allow: platform_allow: