Skip to content

Commit

Permalink
tests: lwm2m: Information Reporting Interface [300-399]
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
SeppoTakalo authored and carlescufi committed Nov 7, 2023
1 parent 86efc9f commit 8608b2d
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 20 deletions.
7 changes: 6 additions & 1 deletion subsys/net/lib/lwm2m/lwm2m_shell.c
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions tests/net/lib/lwm2m/interop/README.md
Expand Up @@ -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.
Expand Down
59 changes: 41 additions & 18 deletions tests/net/lib/lwm2m/interop/pytest/leshan.py
Expand Up @@ -12,10 +12,10 @@

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"""
Expand Down Expand Up @@ -86,11 +86,15 @@ def post(self, path: str, data: str | dict | None = None):
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}')
Expand Down Expand Up @@ -247,6 +251,10 @@ def read(self, endpoint: str, path: str):
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:
Expand Down Expand Up @@ -291,9 +299,7 @@ def composite_read(self, endpoint: str, paths: list[str]):
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):
"""
Expand All @@ -314,11 +320,7 @@ def composite_write(self, endpoint: str, resources: dict):
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
Expand Down Expand Up @@ -349,7 +351,7 @@ def create_psk_device(self, endpoint: str, passwd: str):
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()
Expand All @@ -361,11 +363,27 @@ def create_bs_device(self, endpoint: str, server_uri: str, bs_passwd: str, passw
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.
Expand All @@ -377,11 +395,13 @@ def get_event_stream(self, endpoint: str):
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()

Expand All @@ -406,8 +426,11 @@ def next_event(self, event: str):
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
Expand Down
207 changes: 207 additions & 0 deletions tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py
Expand Up @@ -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}}}
3 changes: 2 additions & 1 deletion tests/net/lib/lwm2m/interop/testcase.yaml
@@ -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:
Expand Down

0 comments on commit 8608b2d

Please sign in to comment.