From 8608b2dc450df10c5144149fd5e321903e9fb731 Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Tue, 24 Oct 2023 17:01:55 +0300 Subject: [PATCH] tests: lwm2m: Information Reporting Interface [300-399] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- subsys/net/lib/lwm2m/lwm2m_shell.c | 7 +- tests/net/lib/lwm2m/interop/README.md | 11 + tests/net/lib/lwm2m/interop/pytest/leshan.py | 59 +++-- .../lib/lwm2m/interop/pytest/test_lwm2m.py | 207 ++++++++++++++++++ tests/net/lib/lwm2m/interop/testcase.yaml | 3 +- 5 files changed, 267 insertions(+), 20 deletions(-) diff --git a/subsys/net/lib/lwm2m/lwm2m_shell.c b/subsys/net/lib/lwm2m/lwm2m_shell.c index 26526611efa51e..a4807d517f2baa 100644 --- a/subsys/net/lib/lwm2m/lwm2m_shell.c +++ b/subsys/net/lib/lwm2m/lwm2m_shell.c @@ -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); diff --git a/tests/net/lib/lwm2m/interop/README.md b/tests/net/lib/lwm2m/interop/README.md index 72eb430f8e4d6d..bda38924d266e6 100644 --- a/tests/net/lib/lwm2m/interop/README.md +++ b/tests/net/lib/lwm2m/interop/README.md @@ -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. diff --git a/tests/net/lib/lwm2m/interop/pytest/leshan.py b/tests/net/lib/lwm2m/interop/pytest/leshan.py index 88030139d97564..7240aae2baf873 100644 --- a/tests/net/lib/lwm2m/interop/pytest/leshan.py +++ b/tests/net/lib/lwm2m/interop/pytest/leshan.py @@ -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""" @@ -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}') @@ -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: @@ -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): """ @@ -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 @@ -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() @@ -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. @@ -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() @@ -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 diff --git a/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py b/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py index bdeb87838474d9..8f3acd69c7e343 100644 --- a/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py +++ b/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py @@ -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}}} diff --git a/tests/net/lib/lwm2m/interop/testcase.yaml b/tests/net/lib/lwm2m/interop/testcase.yaml index 1b9818e777e4b6..42bd53814bf70d 100644 --- a/tests/net/lib/lwm2m/interop/testcase.yaml +++ b/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: