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í
parent 86efc9f1c3
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" \
"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)
{
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) {
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-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-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:| |
* :white_check_mark: Working OK.

View file

@ -12,10 +12,10 @@ from __future__ import annotations
import json
import binascii
import requests
from datetime import datetime
import time
from datetime import datetime
from contextlib import contextmanager
import requests
class Leshan:
"""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)
return Leshan.handle_response(resp)
def delete(self, path: str):
def delete_raw(self, path: str):
"""Send HTTP DELETE query"""
resp = self._s.delete(f'{self.api_url}{path}', timeout=self.timeout)
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):
"""Send LwM2M EXECUTE command"""
return self.post(f'/clients/{endpoint}/{path}')
@ -247,6 +251,10 @@ class Leshan:
def parse_composite(cls, payload: dict):
"""Decode the Leshan's response to composite query back to a Python dictionary"""
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():
keys = [int(key) for key in path.lstrip("/").split('/')]
if len(keys) == 1:
@ -291,9 +299,7 @@ class Leshan:
parameters = self._composite_params(paths)
resp = self._s.get(f'{self.api_url}/clients/{endpoint}/composite', params=parameters, timeout=self.timeout)
payload = Leshan.handle_response(resp)
if not payload['status'] == 'CONTENT(205)':
raise RuntimeError(f'No content received')
return self.parse_composite(payload['content'])
return self.parse_composite(payload)
def composite_write(self, endpoint: str, resources: dict):
"""
@ -314,11 +320,7 @@ class Leshan:
Objects or object instances cannot be targeted.
"""
data = { }
parameters = {
'pathformat': self.format,
'nodeformat': self.format,
'timeout': self.timeout
}
parameters = self._composite_params()
for path, value in resources.items():
path = path if path.startswith('/') else '/' + path
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}"}} }} }}')
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):
psk = binascii.b2a_hex(bs_passwd.encode()).decode()
@ -361,11 +363,27 @@ class Leshan:
self.post(f'/bootstrap/{endpoint}', content)
def delete_bs_device(self, endpoint: str):
self.delete(f'/security/clients/{endpoint}')
self.delete(f'/bootstrap/{endpoint}')
self.delete_raw(f'/security/clients/{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
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.
@ -377,11 +395,13 @@ class Leshan:
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:
r.encoding = 'utf-8'
try:
yield LeshanEventsIterator(r, self.timeout)
yield LeshanEventsIterator(r, timeout)
finally:
r.close()
@ -406,8 +426,11 @@ class LeshanEventsIterator:
if not line.startswith('data: '):
continue
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'])
if event == 'NOTIFICATION':
d = {data['res']: data['val']}
return Leshan.parse_composite(d)
return data
if time.time() > timeout:
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 resp[1][0][1] == 86400
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:
net.lwm2m.interop:
harness: pytest
timeout: 300
timeout: 600
slow: true
harness_config:
pytest_dut_scope: module
pytest_args: []
integration_platforms:
- native_posix
platform_allow: