From 1b33da409236ffcbd70d5bae50a60cb5621d8298 Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Fri, 20 Oct 2023 10:10:28 +0300 Subject: [PATCH 1/6] tests: lwm2m: Add Qemu X86 and fix Qemu Cortex-M3 * Add support for running interoperability tests with Qemu X86. * Remove some debugging messages to allow binary to fix Qemu Cortex-M3 board. * Tune buffer and stack sizes to fit all boards. Signed-off-by: Seppo Takalo --- samples/net/lwm2m_client/boards/qemu_x86.conf | 4 ++ .../lwm2m/interop/boards/native_posix.conf | 1 + .../lwm2m/interop/boards/qemu_cortex_m3.conf | 20 +++--- .../lib/lwm2m/interop/boards/qemu_x86.conf | 4 ++ tests/net/lib/lwm2m/interop/prj.conf | 69 +++++++++++-------- .../net/lib/lwm2m/interop/src/lwm2m-client.c | 39 ++++++++++- tests/net/lib/lwm2m/interop/testcase.yaml | 1 + 7 files changed, 96 insertions(+), 42 deletions(-) create mode 100644 tests/net/lib/lwm2m/interop/boards/qemu_x86.conf diff --git a/samples/net/lwm2m_client/boards/qemu_x86.conf b/samples/net/lwm2m_client/boards/qemu_x86.conf index c099c8e81dfb8a..5a1dc0925c3746 100644 --- a/samples/net/lwm2m_client/boards/qemu_x86.conf +++ b/samples/net/lwm2m_client/boards/qemu_x86.conf @@ -1 +1,5 @@ CONFIG_FPU=y +CONFIG_NET_L2_ETHERNET=y +CONFIG_NET_QEMU_ETHERNET=y + +CONFIG_PCIE=y diff --git a/tests/net/lib/lwm2m/interop/boards/native_posix.conf b/tests/net/lib/lwm2m/interop/boards/native_posix.conf index 44346db12edf68..422e2c1bde471f 100644 --- a/tests/net/lib/lwm2m/interop/boards/native_posix.conf +++ b/tests/net/lib/lwm2m/interop/boards/native_posix.conf @@ -5,3 +5,4 @@ CONFIG_LWM2M_DNS_SUPPORT=y CONFIG_NET_CONFIG_MY_IPV4_GW="192.0.2.2" CONFIG_NATIVE_POSIX_SLOWDOWN_TO_REAL_TIME=y CONFIG_NATIVE_UART_0_ON_STDINOUT=y +CONFIG_ASAN=y diff --git a/tests/net/lib/lwm2m/interop/boards/qemu_cortex_m3.conf b/tests/net/lib/lwm2m/interop/boards/qemu_cortex_m3.conf index 7a3fd344e50680..44263f885a4374 100644 --- a/tests/net/lib/lwm2m/interop/boards/qemu_cortex_m3.conf +++ b/tests/net/lib/lwm2m/interop/boards/qemu_cortex_m3.conf @@ -5,16 +5,14 @@ CONFIG_NET_QEMU_ETHERNET=y # RAM/ROM tuning CONFIG_IDLE_STACK_SIZE=128 -CONFIG_MBEDTLS_HEAP_SIZE=7000 CONFIG_ISR_STACK_SIZE=512 -CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=1024 -CONFIG_LWM2M_ENGINE_STACK_SIZE=2000 CONFIG_LWM2M_LOG_LEVEL_INF=y -CONFIG_LWM2M_ENGINE_MAX_MESSAGES=3 -CONFIG_LWM2M_ENGINE_VALIDATION_BUFFER_SIZE=0 -CONFIG_LWM2M_ENGINE_MAX_OBSERVER=5 -CONFIG_LWM2M_SECURITY_DTLS_TLS_CIPHERSUITE_MAX=3 -CONFIG_LWM2M_DEVICE_PWRSRC_MAX=2 -CONFIG_LWM2M_DEVICE_ERROR_CODE_MAX=5 -CONFIG_LWM2M_DEVICE_EXT_DEV_INFO_MAX=2 -CONFIG_LWM2M_NUM_ATTR=10 +CONFIG_LOG_BUFFER_SIZE=512 + +# qemu_cortex_m3 have smaller memory so simulate a small +# device and small network where max CoAP packet is 256+headers. +# This excercises the outgoing block-wise module intentionally. +CONFIG_LWM2M_COAP_MAX_MSG_SIZE=256 +CONFIG_LWM2M_COAP_BLOCK_SIZE=256 +CONFIG_LWM2M_COAP_BLOCK_TRANSFER=y +CONFIG_LWM2M_COAP_ENCODE_BUFFER_SIZE=2048 diff --git a/tests/net/lib/lwm2m/interop/boards/qemu_x86.conf b/tests/net/lib/lwm2m/interop/boards/qemu_x86.conf new file mode 100644 index 00000000000000..18e513dffd155d --- /dev/null +++ b/tests/net/lib/lwm2m/interop/boards/qemu_x86.conf @@ -0,0 +1,4 @@ +CONFIG_FPU=y +CONFIG_NET_L2_ETHERNET=y +CONFIG_NET_QEMU_ETHERNET=y +CONFIG_PCIE=y diff --git a/tests/net/lib/lwm2m/interop/prj.conf b/tests/net/lib/lwm2m/interop/prj.conf index 66d6334264f8a0..b393621b58147b 100644 --- a/tests/net/lib/lwm2m/interop/prj.conf +++ b/tests/net/lib/lwm2m/interop/prj.conf @@ -1,36 +1,22 @@ CONFIG_NETWORKING=y CONFIG_LOG=y -CONFIG_LWM2M_LOG_LEVEL_DBG=y CONFIG_TEST_RANDOM_GENERATOR=y -CONFIG_NET_IPV6=y -CONFIG_NET_IF_UNICAST_IPV6_ADDR_COUNT=3 -CONFIG_NET_IF_MCAST_IPV6_ADDR_COUNT=2 CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=n CONFIG_NET_DHCPV4=n -CONFIG_NET_IF_UNICAST_IPV4_ADDR_COUNT=3 -CONFIG_NET_IF_MCAST_IPV4_ADDR_COUNT=2 -CONFIG_PRINTK=y -CONFIG_NET_PKT_RX_COUNT=10 -CONFIG_NET_PKT_TX_COUNT=10 -CONFIG_NET_BUF_RX_COUNT=10 -CONFIG_NET_BUF_TX_COUNT=10 -CONFIG_NET_MAX_CONTEXTS=5 -CONFIG_NET_CONFIG_MY_IPV6_ADDR="2001:db8::1" -CONFIG_NET_CONFIG_PEER_IPV6_ADDR="2001:db8::2" +CONFIG_NET_PKT_RX_COUNT=4 +CONFIG_NET_PKT_TX_COUNT=4 +CONFIG_NET_BUF_RX_COUNT=8 +CONFIG_NET_BUF_TX_COUNT=8 +CONFIG_NET_MAX_CONTEXTS=4 CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.0.2.1" CONFIG_NET_CONFIG_MY_IPV4_GW="192.0.2.2" - -CONFIG_NET_LOG=y - -CONFIG_NET_CONFIG_NEED_IPV6=y CONFIG_NET_CONFIG_NEED_IPV4=y CONFIG_NET_CONFIG_SETTINGS=y CONFIG_LWM2M=y -CONFIG_LWM2M_COAP_BLOCK_SIZE=512 CONFIG_LWM2M_IPSO_SUPPORT=y CONFIG_LWM2M_SHELL=y -CONFIG_LWM2M_ACCESS_CONTROL_ENABLE=n #Enable Portfolio object CONFIG_LWM2M_PORTFOLIO_OBJ_SUPPORT=y @@ -47,7 +33,7 @@ CONFIG_LWM2M_RW_SENML_JSON_SUPPORT=y #Enable SenML CBOR content format CONFIG_LWM2M_RW_SENML_CBOR_SUPPORT=y -CONFIG_LWM2M_RW_SENML_CBOR_RECORDS=60 +CONFIG_LWM2M_RW_SENML_CBOR_RECORDS=40 CONFIG_ZCBOR_CANONICAL=y #Enable legacy content formats @@ -62,7 +48,6 @@ CONFIG_COAP_EXTENDED_OPTIONS_LEN_VALUE=40 CONFIG_LWM2M_QUEUE_MODE_ENABLED=y CONFIG_LWM2M_QUEUE_MODE_UPTIME=20 CONFIG_LWM2M_UPDATE_PERIOD=30 -CONFIG_LWM2M_SECONDS_TO_UPDATE_EARLY=10 # LwM2M configuration as OMA-ETS-LightweightM2M_INT-V1_1-20190912-D Configuration 3 CONFIG_LWM2M_ENGINE_DEFAULT_LIFETIME=30 @@ -74,19 +59,45 @@ CONFIG_MBEDTLS_TLS_VERSION_1_2=y # Special MbedTLS changes CONFIG_MBEDTLS_ENABLE_HEAP=y -CONFIG_MBEDTLS_HEAP_SIZE=8192 -CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=1500 +# MTU - IPv6 header - UDP header - DTLS header +# 1280 - 40 - 8 - 21 +CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=1211 +CONFIG_MBEDTLS_HEAP_SIZE=7168 CONFIG_MBEDTLS_CIPHER_CCM_ENABLED=y - # Disable RSA, we don't parse certs: saves flash/memory CONFIG_MBEDTLS_KEY_EXCHANGE_RSA_ENABLED=n # Enable PSK instead CONFIG_MBEDTLS_KEY_EXCHANGE_PSK_ENABLED=y +CONFIG_LWM2M_SECURITY_DTLS_TLS_CIPHERSUITE_MAX=3 CONFIG_NET_SOCKETS_SOCKOPT_TLS=y -CONFIG_NET_SOCKETS_TLS_MAX_CONTEXTS=4 +# For testing purposes, limit DTLS contexts to one, +# LwM2M engine should not use more than one on any given time. +CONFIG_NET_SOCKETS_TLS_MAX_CONTEXTS=1 CONFIG_NET_SOCKETS_ENABLE_DTLS=y -# MbedTLS needs a larger stack -CONFIG_MAIN_STACK_SIZE=2048 -CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +# Assume that IPv6 minimum MTU is accepted +# MTU - IPv6 header - UDP header - DTLS header - CoAP header room +# 1280 - 40 - 8 - 21 - 48 +CONFIG_LWM2M_COAP_MAX_MSG_SIZE=1163 +CONFIG_LWM2M_COAP_BLOCK_SIZE=1024 +CONFIG_LWM2M_COAP_BLOCK_TRANSFER=y +CONFIG_LWM2M_COAP_ENCODE_BUFFER_SIZE=4096 +CONFIG_LWM2M_NUM_OUTPUT_BLOCK_CONTEXT=1 +CONFIG_LWM2M_NUM_BLOCK1_CONTEXT=1 +CONFIG_SYS_HASH_FUNC32=y +CONFIG_LWM2M_ENGINE_VALIDATION_BUFFER_SIZE=0 +CONFIG_LWM2M_ENGINE_MAX_PENDING=2 +CONFIG_LWM2M_ENGINE_MAX_REPLIES=2 +CONFIG_LWM2M_ENGINE_MAX_MESSAGES=3 +CONFIG_LWM2M_ENGINE_MAX_OBSERVER=5 +CONFIG_LWM2M_DEVICE_PWRSRC_MAX=2 +CONFIG_LWM2M_DEVICE_ERROR_CODE_MAX=2 +CONFIG_LWM2M_DEVICE_EXT_DEV_INFO_MAX=2 +CONFIG_LWM2M_NUM_ATTR=20 + +# Configure stack sizes +CONFIG_MAIN_STACK_SIZE=1024 +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=512 +CONFIG_SHELL_STACK_SIZE=1536 +CONFIG_LWM2M_ENGINE_STACK_SIZE=2048 diff --git a/tests/net/lib/lwm2m/interop/src/lwm2m-client.c b/tests/net/lib/lwm2m/interop/src/lwm2m-client.c index 983fea79f3ecb6..9fe1f1dce7bb94 100644 --- a/tests/net/lib/lwm2m/interop/src/lwm2m-client.c +++ b/tests/net/lib/lwm2m/interop/src/lwm2m-client.c @@ -11,7 +11,6 @@ #include LOG_MODULE_REGISTER(LOG_MODULE_NAME); #include -#include #include #include #include @@ -28,15 +27,37 @@ LOG_MODULE_REGISTER(LOG_MODULE_NAME); #define VERSION "1.2.3" static struct lwm2m_ctx client; +static void rd_client_event(struct lwm2m_ctx *client, + enum lwm2m_rd_client_event client_event); +static void observe_cb(enum lwm2m_observe_event event, + struct lwm2m_obj_path *path, void *user_data); + +static uint8_t bat_idx = LWM2M_DEVICE_PWR_SRC_TYPE_BAT_INT; +static int bat_mv = 3800; +static int bat_ma = 125; +static uint8_t usb_idx = LWM2M_DEVICE_PWR_SRC_TYPE_USB; +static int usb_mv = 5000; +static int usb_ma = 900; + +static void reboot_handler(struct k_work *work) +{ + /* I cannot really restart the client, as we don't know + * the endpoint name. Testcase sets that on a command line. + * So we only stop. + */ + lwm2m_rd_client_stop(&client, rd_client_event, true); +} + +K_WORK_DEFINE(reboot_work, reboot_handler); static int device_reboot_cb(uint16_t obj_inst_id, uint8_t *args, uint16_t args_len) { LOG_INF("DEVICE: REBOOT"); + k_work_submit(&reboot_work); return 0; } - static int lwm2m_setup(void) { /* setup DEVICE object */ @@ -53,6 +74,20 @@ static int lwm2m_setup(void) lwm2m_set_res_buf(&LWM2M_OBJ(3, 0, 17), CONFIG_BOARD, sizeof(CONFIG_BOARD), sizeof(CONFIG_BOARD), LWM2M_RES_DATA_FLAG_RO); + /* add power source resource instances */ + lwm2m_create_res_inst(&LWM2M_OBJ(3, 0, 6, 0)); + lwm2m_set_res_buf(&LWM2M_OBJ(3, 0, 6, 0), &bat_idx, sizeof(bat_idx), sizeof(bat_idx), 0); + lwm2m_create_res_inst(&LWM2M_OBJ(3, 0, 7, 0)); + lwm2m_set_res_buf(&LWM2M_OBJ(3, 0, 7, 0), &bat_mv, sizeof(bat_mv), sizeof(bat_mv), 0); + lwm2m_create_res_inst(&LWM2M_OBJ(3, 0, 8, 0)); + lwm2m_set_res_buf(&LWM2M_OBJ(3, 0, 8, 0), &bat_ma, sizeof(bat_ma), sizeof(bat_ma), 0); + lwm2m_create_res_inst(&LWM2M_OBJ(3, 0, 6, 1)); + lwm2m_set_res_buf(&LWM2M_OBJ(3, 0, 6, 1), &usb_idx, sizeof(usb_idx), sizeof(usb_idx), 0); + lwm2m_create_res_inst(&LWM2M_OBJ(3, 0, 7, 1)); + lwm2m_set_res_buf(&LWM2M_OBJ(3, 0, 7, 1), &usb_mv, sizeof(usb_mv), sizeof(usb_mv), 0); + lwm2m_create_res_inst(&LWM2M_OBJ(3, 0, 8, 1)); + lwm2m_set_res_buf(&LWM2M_OBJ(3, 0, 8, 1), &usb_ma, sizeof(usb_ma), sizeof(usb_ma), 0); + return 0; } diff --git a/tests/net/lib/lwm2m/interop/testcase.yaml b/tests/net/lib/lwm2m/interop/testcase.yaml index aeba64748df8c3..93f3fc13b1cf32 100644 --- a/tests/net/lib/lwm2m/interop/testcase.yaml +++ b/tests/net/lib/lwm2m/interop/testcase.yaml @@ -8,6 +8,7 @@ tests: platform_allow: - native_posix - qemu_cortex_m3 + - qemu_x86 tags: - testing - pytest From acd20293c2ae463b818e2908f5449f3bc9c1ce33 Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Wed, 1 Nov 2023 10:20:06 +0200 Subject: [PATCH 2/6] tests: lwm2m: Enable DTLS CID for interoperability tests Leshan Demo server seem to support it so it makes sense to use it. Signed-off-by: Seppo Takalo --- tests/net/lib/lwm2m/interop/prj.conf | 2 ++ .../net/lib/lwm2m/interop/src/lwm2m-client.c | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/net/lib/lwm2m/interop/prj.conf b/tests/net/lib/lwm2m/interop/prj.conf index b393621b58147b..07bd9a0535d144 100644 --- a/tests/net/lib/lwm2m/interop/prj.conf +++ b/tests/net/lib/lwm2m/interop/prj.conf @@ -48,6 +48,7 @@ CONFIG_COAP_EXTENDED_OPTIONS_LEN_VALUE=40 CONFIG_LWM2M_QUEUE_MODE_ENABLED=y CONFIG_LWM2M_QUEUE_MODE_UPTIME=20 CONFIG_LWM2M_UPDATE_PERIOD=30 +CONFIG_LWM2M_RD_CLIENT_STOP_POLLING_AT_IDLE=y # LwM2M configuration as OMA-ETS-LightweightM2M_INT-V1_1-20190912-D Configuration 3 CONFIG_LWM2M_ENGINE_DEFAULT_LIFETIME=30 @@ -75,6 +76,7 @@ CONFIG_NET_SOCKETS_SOCKOPT_TLS=y # LwM2M engine should not use more than one on any given time. CONFIG_NET_SOCKETS_TLS_MAX_CONTEXTS=1 CONFIG_NET_SOCKETS_ENABLE_DTLS=y +CONFIG_MBEDTLS_SSL_DTLS_CONNECTION_ID=y # Assume that IPv6 minimum MTU is accepted # MTU - IPv6 header - UDP header - DTLS header - CoAP header room diff --git a/tests/net/lib/lwm2m/interop/src/lwm2m-client.c b/tests/net/lib/lwm2m/interop/src/lwm2m-client.c index 9fe1f1dce7bb94..706091659a592a 100644 --- a/tests/net/lib/lwm2m/interop/src/lwm2m-client.c +++ b/tests/net/lib/lwm2m/interop/src/lwm2m-client.c @@ -15,6 +15,7 @@ LOG_MODULE_REGISTER(LOG_MODULE_NAME); #include #include #include +#include #define APP_BANNER "Run LWM2M client" @@ -58,6 +59,24 @@ static int device_reboot_cb(uint16_t obj_inst_id, return 0; } +int set_socketoptions(struct lwm2m_ctx *ctx) +{ + if (IS_ENABLED(CONFIG_MBEDTLS_SSL_DTLS_CONNECTION_ID) && ctx->use_dtls) { + int ret; + + /* Enable CID */ + int cid = TLS_DTLS_CID_ENABLED; + + ret = zsock_setsockopt(ctx->sock_fd, SOL_TLS, TLS_DTLS_CID, &cid, + sizeof(cid)); + if (ret) { + ret = -errno; + LOG_ERR("Failed to enable TLS_DTLS_CID: %d", ret); + } + } + return lwm2m_set_default_sockopt(ctx); +} + static int lwm2m_setup(void) { /* setup DEVICE object */ @@ -201,6 +220,7 @@ int main(void) } client.tls_tag = 1; + client.set_socketoptions = set_socketoptions; lwm2m_rd_client_start(&client, CONFIG_BOARD, 0, rd_client_event, observe_cb); lwm2m_rd_client_stop(&client, rd_client_event, false); From 4a6e3a644120d346943a778f5c5fa575eaeb270e Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Fri, 13 Oct 2023 16:34:49 +0300 Subject: [PATCH 3/6] tests: lwm2m: Add interoperability tests 222 - 281 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add remaining test cases from Device management & Service Enablement Interface * LightweightM2M-1.1-int-222 – Read on Object * LightweightM2M-1.1-int-223 – Read on Object Instance * LightweightM2M-1.1-int-224 – Read on Resource * LightweightM2M-1.1-int-225 – Read on Resource Instance * LightweightM2M-1.1-int-226 – Write (Partial Update) on Object Instance * LightweightM2M-1.1-int-227 - Write (replace) on Resource * LightweightM2M-1.1-int-228 - Write on Resource Instance * LightweightM2M-1.1-int-229 - Read-Composite Operation * LightweightM2M-1.1-int-230 - Write-Composite Operation * LightweightM2M-1.1-int-231 - Querying basic information in SenML JSON format * LightweightM2M-1.1-int-232 - Querying basic information in SenML CBOR format * LightweightM2M-1.1-int-233 - Setting basic information in SenML CBOR format * LightweightM2M-1.1-int-234 - Setting basic information in SenML JSON format * LightweightM2M-1.1-int-235 - Read-Composite Operation on root path * LightweightM2M-1.1-int-236 - Read-Composite - Partial Presence * LightweightM2M-1.1-int-237 - Read on Object without specifying Content-Type * LightweightM2M-1.1-int-241 - Executable Resource: Rebooting the device * LightweightM2M-1.1-int-256 - Write Operation Failure * LightweightM2M-1.1-int-257 - Write-Composite Operation * LightweightM2M-1.1-int-260 - Discover Command * LightweightM2M-1.1-int-261 - Write-Attribute Operation on a multiple resource * LightweightM2M-1.1-int-280 - Successful Read-Composite Operation * LightweightM2M-1.1-int-281 - Partially Successful Read-Composite Operation Modify tests binary * Allow CoAP payload of 1211 bytes. * Allow outgoing messages to use block-wise if they are bigger. * Use 4kB encode buffer for outgoing messages. * Check configured heap and stack sizes. * Limit number of interface addresses, sockets and contexts Qemu-cortex-m3 platform ran out of RAM, so reconfigure it to use * packet size of 256 bytes. * encode buffer of 2kB This exercises the block-transferring on some of the test cases. Signed-off-by: Seppo Takalo --- tests/net/lib/lwm2m/interop/README.md | 110 +++- tests/net/lib/lwm2m/interop/pytest/leshan.py | 276 +++++++++- .../lib/lwm2m/interop/pytest/test_lwm2m.py | 511 +++++++++++++----- 3 files changed, 711 insertions(+), 186 deletions(-) diff --git a/tests/net/lib/lwm2m/interop/README.md b/tests/net/lib/lwm2m/interop/README.md index 19883ac1821d40..72eb430f8e4d6d 100644 --- a/tests/net/lib/lwm2m/interop/README.md +++ b/tests/net/lib/lwm2m/interop/README.md @@ -15,7 +15,39 @@ running Zephyr is using address `192.0.2.1`. Follow [Networking with the host system](https://docs.zephyrproject.org/latest/connectivity/networking/networking_with_host.html#networking-with-the-host-system) from Zephyr's documentation how to set it up, or follow [Create NAT and routing for Zephyr native network on Linux](https://github.com/zephyrproject-rtos/net-tools/blob/master/README%20NAT.md). -### Leshan server setup + +### Run Lehan server from net-tools Docker container + +Zephyr's net-tools Docker container already contains Leshan, so if you don't want to set the environment up manually, +use the pre-made docker. + +First, build the docker container. You only need to do this one, or when you update the container. +``` +cd tools/net-tools/docker +docker build -t net-tools . +``` + +Start the docker networking +``` +cd tools/net-tools/ +sudo ./net-setup.sh --config docker.conf start +``` + +Start the docker container and run leshan +``` +docker run --hostname=net-tools --name=net-tools --ip=192.0.2.2 --ip6=2001:db8::2 -p 8080:8080 -p 8081:8081 -p 5683:5683/udp --rm -dit --network=net-tools0 net-tools +docker container exec net-tools /net-tools/start-leshan.sh +``` + +### Stop Leshan, docker and networking + +``` +cd tools/net-tools/ +docker kill net-tools +sudo ./net-setup.sh --config docker.conf stop +docker network rm net-tools0 +``` +### Leshan server setup (manual) * Leshan server must be reachable from the device using IP address `192.0.2.2`. Configure the port forwarding, if you use Docker to run Leshan. @@ -87,23 +119,59 @@ Or use the Docker based testing Tests are written from test spec; [OMA Enabler Test Specification (Interoperability) for Lightweight M2M](https://www.openmobilealliance.org/release/LightweightM2M/ETS/OMA-ETS-LightweightM2M-V1_1-20190912-D.pdf) -Following tests are implemented: -* LightweightM2M-1.1-int-0 – Client Initiated Bootstrap -* LightweightM2M-1.1-int-1 – Client Initiated Bootstrap Full (PSK) -* LightweightM2M-1.1-int-101 – Initial Registration -* LightweightM2M-1.1-int-102 – Registration Update -* LightweightM2M-1.1-int-104 – Registration Update Trigge -* LightweightM2M-1.1-int-105 - Discarded Register Update -* LightweightM2M-1.1-int-107 – Extending the lifetime of a registration -* LightweightM2M-1.1-int-108 – Turn on Queue Mode -* LightweightM2M-1.1-int-109 – Behavior in Queue Mode -* LightweightM2M-1.1-int-201 – Querying basic information in Plain Text -* LightweightM2M-1.1-int-203 – Querying basic information in TLV format -* LightweightM2M-1.1-int-204 – Querying basic information in JSON format -* LightweightM2M-1.1-int-205 – Setting basic information in Plain Text -* LightweightM2M-1.1-int-211 – Querying basic information in CBOR format -* LightweightM2M-1.1-int-212 – Setting basic information in CBOR format -* LightweightM2M-1.1-int-215 – Setting basic information in TLV format -* LightweightM2M-1.1-int-220 – Setting basic information in JSON format -* LightweightM2M-1.1-int-221 – Attempt to perform operations on Security -* LightweightM2M-1.1-int-401 – UDP Channel Security – PSK Mode +## Current status + +|Test case|Status|Notes| +|---------|------|-----| +|LightweightM2M-1.1-int-0 - Client Initiated Bootstrap |:white_check_mark:| | +|LightweightM2M-1.1-int-1 - Client Initiated Bootstrap Full (PSK) |:white_check_mark:| | +|LightweightM2M-1.1-int-101 - Initial Registration |:white_check_mark:| | +|LightweightM2M-1.1-int-102 - Registration Update |:white_check_mark:| | +|LightweightM2M-1.1-int-103 - Deregistration |:large_orange_diamond:|We don't have "disabled" functionality in server object| +|LightweightM2M-1.1-int-104 - Registration Update Trigge |:white_check_mark:| | +|LightweightM2M-1.1-int-105 - Discarded Register Update |:white_check_mark:| | +|LightweightM2M-1.1-int-107 - Extending the lifetime of a registration |:white_check_mark:| | +|LightweightM2M-1.1-int-108 - Turn on Queue Mode |:white_check_mark:| | +|LightweightM2M-1.1-int-109 - Behavior in Queue Mode |:white_check_mark:| | +|LightweightM2M-1.1-int-201 - Querying basic information in Plain Text |:white_check_mark:| | +|LightweightM2M-1.1-int-203 - Querying basic information in TLV format |:white_check_mark:| | +|LightweightM2M-1.1-int-204 - Querying basic information in JSON format |:white_check_mark:| | +|LightweightM2M-1.1-int-205 - Setting basic information in Plain Text |:white_check_mark:| | +|LightweightM2M-1.1-int-211 - Querying basic information in CBOR format |:white_check_mark:| | +|LightweightM2M-1.1-int-212 - Setting basic information in CBOR format |:white_check_mark:| | +|LightweightM2M-1.1-int-215 - Setting basic information in TLV format |:white_check_mark:| | +|LightweightM2M-1.1-int-220 - Setting basic information in JSON format |:white_check_mark:| | +|LightweightM2M-1.1-int-221 - Attempt to perform operations on Security |:white_check_mark:| | +|LightweightM2M-1.1-int-222 - Read on Object |:white_check_mark:| | +|LightweightM2M-1.1-int-223 - Read on Object Instance |:white_check_mark:| | +|LightweightM2M-1.1-int-224 - Read on Resource |:white_check_mark:| | +|LightweightM2M-1.1-int-225 - Read on Resource Instance |:white_check_mark:| | +|LightweightM2M-1.1-int-226 - Write (Partial Update) on Object Instance |:white_check_mark:| | +|LightweightM2M-1.1-int-222 - Read on Object |:white_check_mark:| | +|LightweightM2M-1.1-int-223 - Read on Object Instance |:white_check_mark:| | +|LightweightM2M-1.1-int-224 - Read on Resource |:white_check_mark:| | +|LightweightM2M-1.1-int-225 - Read on Resource Instance |:white_check_mark:| | +|LightweightM2M-1.1-int-226 - Write (Partial Update) on Object Instance |:white_check_mark:| | +|LightweightM2M-1.1-int-227 - Write (replace) on Resource |:white_check_mark:| | +|LightweightM2M-1.1-int-228 - Write on Resource Instance |:white_check_mark:|[~~#64011~~](https://github.com/zephyrproject-rtos/zephyr/issues/64011) | +|LightweightM2M-1.1-int-229 - Read-Composite Operation|:white_check_mark:|[~~#64012~~](https://github.com/zephyrproject-rtos/zephyr/issues/64012) [~~#64189~~](https://github.com/zephyrproject-rtos/zephyr/issues/64189) | +|LightweightM2M-1.1-int-230 - Write-Composite Operation|:white_check_mark:| | +|LightweightM2M-1.1-int-231 - Querying basic information in SenML JSON format|:white_check_mark:| | +|LightweightM2M-1.1-int-232 - Querying basic information in SenML CBOR format|:white_check_mark:| | +|LightweightM2M-1.1-int-233 - Setting basic information in SenML CBOR format|:white_check_mark:| | +|LightweightM2M-1.1-int-234 - Setting basic information in SenML JSON format|:white_check_mark:| | +|LightweightM2M-1.1-int-235 - Read-Composite Operation on root path|:large_orange_diamond:|Root Path is not yet supported by Leshan.| +|LightweightM2M-1.1-int-236 - Read-Composite - Partial Presence|:white_check_mark:| | +|LightweightM2M-1.1-int-237 - Read on Object without specifying Content-Type|:white_check_mark:| | +|LightweightM2M-1.1-int-241 - Executable Resource: Rebooting the device|:white_check_mark:| | +|LightweightM2M-1.1-int-256 - Write Operation Failure|:white_check_mark:| | +|LightweightM2M-1.1-int-257 - Write-Composite Operation|:white_check_mark:| | +|LightweightM2M-1.1-int-260 - Discover Command|:white_check_mark:| | +|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-401 - UDP Channel Security - PSK Mode |:white_check_mark:| | + +* :white_check_mark: Working OK. +* :large_orange_diamond: Feature or operation not implemented. +* :red_circle: Broken diff --git a/tests/net/lib/lwm2m/interop/pytest/leshan.py b/tests/net/lib/lwm2m/interop/pytest/leshan.py index 4d69a3a977f7fa..ecd14b2481c013 100644 --- a/tests/net/lib/lwm2m/interop/pytest/leshan.py +++ b/tests/net/lib/lwm2m/interop/pytest/leshan.py @@ -5,15 +5,16 @@ from __future__ import annotations import json -import requests import binascii +import requests +from datetime import datetime class Leshan: def __init__(self, url: str): self.api_url = url self.timeout = 10 - self.format = 'TLV' - # self.format = "SENML_CBOR" + #self.format = 'TLV' + self.format = "SENML_CBOR" try: resp = self.get('/security/clients') if not isinstance(resp, list): @@ -23,7 +24,18 @@ def __init__(self, url: str): @staticmethod def handle_response(resp: requests.models.Response): - """Generic response handler for all queries""" + """ + Handle the response received from the server. + + Parameters: + - response: The response object received from the server. + + Returns: + - dict: The parsed JSON response as a dictionary. + + Raises: + - Exception: If the response indicates an error condition. + """ if resp.status_code >= 300 or resp.status_code < 200: raise RuntimeError(f'Error {resp.status_code}: {resp.text}') if len(resp.text): @@ -34,11 +46,14 @@ def handle_response(resp: requests.models.Response): def get(self, path: str): """Send HTTP GET query""" - resp = requests.get(f'{self.api_url}{path}?timeout={self.timeout}&format={self.format}') + params = {'timeout': self.timeout} + if self.format is not None: + params['format'] = self.format + resp = requests.get(f'{self.api_url}{path}', params=params, timeout=self.timeout) return Leshan.handle_response(resp) def put_raw(self, path: str, data: str | dict | None = None, headers: dict | None = None): - resp = requests.put(f'{self.api_url}{path}', data=data, headers=headers) + resp = requests.put(f'{self.api_url}{path}', data=data, headers=headers, timeout=self.timeout) return Leshan.handle_response(resp) def put(self, path: str, data: str | dict, uri_options: str = ''): @@ -47,42 +62,249 @@ def put(self, path: str, data: str | dict, uri_options: str = ''): return self.put_raw(f'{path}?timeout={self.timeout}&format={self.format}' + uri_options, data=data, headers={'content-type': 'application/json'}) def post(self, path: str, data: str | dict | None = None): - resp = requests.post(f'{self.api_url}{path}', data=data, headers={'content-type': 'application/json'}) + if isinstance(data, dict): + data = json.dumps(data) + if data is not None: + headers={'content-type': 'application/json'} + uri_options = f'?timeout={self.timeout}&format={self.format}' + else: + headers=None + uri_options = '' + resp = requests.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): - resp = requests.delete(f'{self.api_url}{path}') + resp = requests.delete(f'{self.api_url}{path}', timeout=self.timeout) return Leshan.handle_response(resp) def execute(self, endpoint: str, path: str): return self.post(f'/clients/{endpoint}/{path}') def write(self, endpoint: str, path: str, value: bool | int | str): + if len(path.split('/')) == 3: + kind = 'singleResource' + else: + kind = 'resourceInstance' + rid = path.split('/')[-1] + return self.put(f'/clients/{endpoint}/{path}', self._define_resource(rid, value, kind)) + + def update_obj_instance(self, endpoint: str, path: str, resources: dict): + data = self._define_obj_inst(path, resources) + return self.put(f'/clients/{endpoint}/{path}', data, uri_options='&replace=false') + + def replace_obj_instance(self, endpoint: str, path: str, resources: dict): + data = self._define_obj_inst(path, resources) + return self.put(f'/clients/{endpoint}/{path}', data, uri_options='&replace=true') + + def create_obj_instance(self, endpoint: str, path: str, resources: dict): + data = self._define_obj_inst(path, resources) + path = '/'.join(path.split('/')[:-1]) # Create call should not have instance ID in path + return self.post(f'/clients/{endpoint}/{path}', data) + + @classmethod + def _type_to_string(cls, value): + """ + Convert a Python value to its corresponding Leshan representation. + + Parameters: + - value: The value to be converted. + + Returns: + - str: The string representation of the value. + """ if isinstance(value, bool): - type = 'boolean' - value = "true" if value else "false" - elif isinstance(value, int): - type = 'integer' - value = str(value) - elif isinstance(value, str): - type = 'string' - value = '"' + value + '"' - id = path.split('/')[2] - return self.put(f'/clients/{endpoint}/{path}', f'{{"id":{id},"kind":"singleResource","value":{value},"type":"{type}"}}') + return 'boolean' + if isinstance(value, int): + return 'integer' + if isinstance(value, datetime): + return 'time' + return 'string' + + @classmethod + def _convert_type(cls, value): + if isinstance(value, datetime): + return int(value.timestamp()) + else: + return value + + def _define_obj_inst(self, path: str, resources: dict): + data = { + "kind": "instance", + "id": int(path.split('/')[-1]), # ID is last element of path + "resources": [] + } + for key, value in resources.items(): + if isinstance(value, dict): + kind = 'multiResource' + else: + kind = 'singleResource' + data['resources'].append(self._define_resource(key, value, kind)) + return data + + def _define_resource(self, rid, value, kind='singleResource'): + if kind in ('singleResource', 'resourceInstance'): + return { + "id": rid, + "kind": kind, + "value": self._convert_type(value), + "type": self._type_to_string(value) + } + if kind == 'multiResource': + return { + "id": rid, + "kind": kind, + "values": value, + "type": self._type_to_string(list(value.values())[0]) + } + raise RuntimeError(f'Unhandled type {kind}') + + def _decode_value(self, type, value): + """ + Decode the Leshan representation of a value back to a Python value. + """ + if type == 'BOOLEAN': + return bool(value) + if type == 'INTEGER': + return int(value) + return value + + def _decode_resource(self, content): + """ + Decode the Leshan representation of a resource back to a Python dictionary. + """ + if content['kind'] == 'singleResource' or content['kind'] == 'resourceInstance': + return {content['id']: self._decode_value(content['type'], content['value'])} + elif content['kind'] == 'multiResource': + values = {} + for riid, value in content['values'].items(): + values.update({int(riid): self._decode_value(content['type'], value)}) + return {content['id']: values} + raise RuntimeError(f'Unhandled type {content["kind"]}') + + def _decode_obj_inst(self, content): + """ + Decode the Leshan representation of an object instance back to a Python dictionary. + """ + resources = {} + for resource in content['resources']: + resources.update(self._decode_resource(resource)) + return {content['id']: resources} + + def _decode_obj(self, content): + """ + Decode the Leshan representation of an object back to a Python dictionary. + """ + instances = {} + for instance in content['instances']: + instances.update(self._decode_obj_inst(instance)) + return {content['id']: instances} def read(self, endpoint: str, path: str): resp = self.get(f'/clients/{endpoint}/{path}') if not resp['success']: return resp content = resp['content'] - if content['kind'] == 'instance': - return content['resources'] - elif content['kind'] == 'singleResource': - return content['value'] + if content['kind'] == 'obj': + return self._decode_obj(content) + elif content['kind'] == 'instance': + return self._decode_obj_inst(content) + elif content['kind'] == 'singleResource' or content['kind'] == 'resourceInstance': + return self._decode_value(content['type'], content['value']) elif content['kind'] == 'multiResource': - return content['values'] + return self._decode_resource(content) raise RuntimeError(f'Unhandled type {content["kind"]}') + def composite_read(self, endpoint: str, paths: list[str]): + paths = [path if path.startswith('/') else '/' + path for path in paths] + parameters = { + 'pathformat': self.format, + 'nodeformat': self.format, + 'timeout': self.timeout, + 'paths': ','.join(paths) + } + resp = requests.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') + data = {} + for path, content in payload['content'].items(): + keys = [int(key) for key in path.lstrip("/").split('/')] + if len(keys) == 1: + data.update(self._decode_obj(content)) + elif len(keys) == 2: + if keys[0] not in data: + data[keys[0]] = {} + data[keys[0]].update(self._decode_obj_inst(content)) + elif len(keys) == 3: + if keys[0] not in data: + data[keys[0]] = {} + if keys[1] not in data[keys[0]]: + data[keys[0]][keys[1]] = {} + data[keys[0]][keys[1]].update(self._decode_resource(content)) + elif len(keys) == 4: + if keys[0] not in data: + data[keys[0]] = {} + if keys[1] not in data[keys[0]]: + data[keys[0]][keys[1]] = {} + if keys[2] not in data[keys[0]][keys[1]]: + data[keys[0]][keys[1]][keys[2]] = {} + data[keys[0]][keys[1]][keys[2]].update(self._decode_resource(content)) + else: + raise RuntimeError(f'Unhandled path {path}') + print(f'Requested paths: {paths}') + print(data) + return data + + + def composite_write(self, endpoint: str, resources: dict): + """ + Do LwM2m composite write operation. + + Targeted resources are defined as a dictionary with the following structure: + { + "/1/0/1": 60, + "/1/0/6": True, + "/16/0/0": { + "0": "aa", + "1": "bb", + "2": "cc", + "3": "dd" + } + } + + Objects or object instances cannot be targeted. + """ + data = { } + parameters = { + 'pathformat': self.format, + 'nodeformat': self.format, + 'timeout': self.timeout + } + for path, value in resources.items(): + path = path if path.startswith('/') else '/' + path + level = len(path.split('/')) - 1 + rid = int(path.split('/')[-1]) + if level == 3: + if isinstance(value, dict): + value = self._define_resource(rid, value, kind='multiResource') + else: + value = self._define_resource(rid, value) + elif level == 4: + value = self._define_resource(rid, value, kind='resourceInstance') + else: + raise RuntimeError(f'Unhandled path {path}') + data[path] = value + + resp = requests.put(f'{self.api_url}/clients/{endpoint}/composite', params=parameters, json=data, timeout=self.timeout) + return Leshan.handle_response(resp) + + def discover(self, endpoint: str, path: str): + resp = self.handle_response(requests.get(f'{self.api_url}/clients/{endpoint}/{path}/discover', timeout=self.timeout)) + data = {} + for obj in resp['objectLinks']: + data[obj['url']] = obj['attributes'] + return data + def create_psk_device(self, endpoint: str, passwd: str): psk = binascii.b2a_hex(passwd.encode()).decode() self.put('/security/clients/', f'{{"endpoint":"{endpoint}","tls":{{"mode":"psk","details":{{"identity":"{endpoint}","key":"{psk}"}} }} }}') @@ -90,13 +312,13 @@ def create_psk_device(self, endpoint: str, passwd: str): def delete_device(self, endpoint: str): self.delete(f'/security/clients/{endpoint}') - def create_bs_device(self, endpoint: str, server_uri: str, passwd: str): - psk = binascii.b2a_hex(passwd.encode()).decode() + def create_bs_device(self, endpoint: str, server_uri: str, bs_passwd: str, passwd: str): + psk = binascii.b2a_hex(bs_passwd.encode()).decode() data = f'{{"tls":{{"mode":"psk","details":{{"identity":"{endpoint}","key":"{psk}"}}}},"endpoint":"{endpoint}"}}' self.put('/security/clients/', data) - id = str([ord(n) for n in endpoint]) + ep = str([ord(n) for n in endpoint]) key = str([ord(n) for n in passwd]) - content = '{"servers":{"0":{"binding":"U","defaultMinPeriod":1,"lifetime":86400,"notifIfDisabled":false,"shortId":1}},"security":{"1":{"bootstrapServer":false,"clientOldOffTime":1,"publicKeyOrId":' + id + ',"secretKey":' + key + ',"securityMode":"PSK","serverId":1,"serverSmsNumber":"","smsBindingKeyParam":[],"smsBindingKeySecret":[],"smsSecurityMode":"NO_SEC","uri":"'+server_uri+'"}},"oscore":{},"toDelete":["/0","/1"]}' + content = '{"servers":{"0":{"binding":"U","defaultMinPeriod":1,"lifetime":86400,"notifIfDisabled":false,"shortId":1}},"security":{"1":{"bootstrapServer":false,"clientOldOffTime":1,"publicKeyOrId":' + ep + ',"secretKey":' + key + ',"securityMode":"PSK","serverId":1,"serverSmsNumber":"","smsBindingKeyParam":[],"smsBindingKeySecret":[],"smsSecurityMode":"NO_SEC","uri":"'+server_uri+'"}},"oscore":{},"toDelete":["/0","/1"]}' self.post(f'/bootstrap/{endpoint}', content) def delete_bs_device(self, endpoint: str): diff --git a/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py b/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py index 53a205fadb9177..384e3cadda5c71 100644 --- a/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py +++ b/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py @@ -12,6 +12,7 @@ import string from twister_harness import Shell +from datetime import datetime LESHAN_IP: str = '192.0.2.2' COAP_PORT: int = 5683 @@ -67,8 +68,8 @@ def verify_LightweightM2M_1_1_int_101(shell: Shell, leshan: Leshan, endpoint: st def verify_LightweightM2M_1_1_int_102(shell: Shell, leshan: Leshan, endpoint: str): logger.info("LightweightM2M-1.1-int-102 - Registration Update") lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/1 -u32')) - litetime = int(lines[0]) - lifetime = litetime + 10 + lifetime = int(lines[0]) + lifetime = lifetime + 10 start_time = time.time() * 1000 leshan.write(endpoint, '1/0/1', lifetime) shell._device.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) @@ -95,10 +96,10 @@ def verify_LightweightM2M_1_1_int_105(shell: Shell, leshan: Leshan, endpoint: st if status["secure"]: logger.debug("Skip, requires non-secure connection") return - id = status["registrationId"] - assert id + regid = status["registrationId"] + assert regid # Fake unregister message - helperclient.delete(f'rd/{id}', timeout=0.1) + helperclient.delete(f'rd/{regid}', timeout=0.1) helperclient.stop() time.sleep(1) shell.exec_command('lwm2m update') @@ -122,7 +123,6 @@ def verify_LightweightM2M_1_1_int_108(leshan, endpoint): def verify_LightweightM2M_1_1_int_109(shell: Shell, leshan: Leshan, endpoint: str): logger.info("LightweightM2M-1.1-int-109 - Behavior in Queue Mode") - verify_LightweightM2M_1_1_int_107(shell, leshan, endpoint) logger.debug('Wait for Queue RX OFF') shell._device.readlines_until(regex='.*Queue mode RX window closed', timeout=120) # Restore previous value @@ -130,74 +130,39 @@ def verify_LightweightM2M_1_1_int_109(shell: Shell, leshan: Leshan, endpoint: st shell._device.readlines_until(regex='.*Registration update complete', timeout=10) def verify_LightweightM2M_1_1_int_201(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-201 - Querying basic information in Plain Text format") fmt = leshan.format leshan.format = 'TEXT' - assert leshan.get(f'/clients/{endpoint}/3/0/0')['content']['value'] == 'Zephyr' - assert leshan.get(f'/clients/{endpoint}/3/0/1')['content']['value'] == 'client-1' - assert leshan.get(f'/clients/{endpoint}/3/0/2')['content']['value'] == 'serial-1' + assert leshan.read(endpoint, '3/0/0') == 'Zephyr' + assert leshan.read(endpoint, '3/0/1') == 'client-1' + assert leshan.read(endpoint, '3/0/2') == 'serial-1' leshan.format = fmt def verify_device_object(resp): ''' Verify that Device object match Configuration 3 ''' - assert resp['valid'] is True - found = 0 - for res in resp['content']['resources']: - if res['id'] == 0: - assert res['value'] == 'Zephyr' - found += 1 - elif res['id'] == 1: - assert res['value'] == 'client-1' - found += 1 - elif res['id'] == 2: - assert res['value'] == 'serial-1' - found += 1 - elif res['id'] == 3: - assert res['value'] == '1.2.3' - found += 1 - elif res['id'] == 11: - assert res['kind'] == 'multiResource' - assert res['values']['0'] == '0' - found += 1 - elif res['id'] == 16: - assert res['value'] == 'U' - found += 1 - assert found == 6 + assert resp[0][0] == 'Zephyr' + assert resp[0][1] == 'client-1' + assert resp[0][2] == 'serial-1' + assert resp[0][3] == '1.2.3' + assert resp[0][11][0] == 0 + assert resp[0][16] == 'U' def verify_server_object(obj): ''' Verify that server object match Configuration 3 ''' - found = 0 - for res in obj['resources']: - if res['id'] == 0: - assert res['value'] == '1' - found += 1 - elif res['id'] == 1: - assert res['value'] == '86400' - found += 1 - elif res['id'] == 2: - assert res['value'] == '1' - found += 1 - elif res['id'] == 3: - assert res['value'] == '10' - found += 1 - elif res['id'] == 5: - assert res['value'] == '86400' - found += 1 - elif res['id'] == 6: - assert res['value'] is False - found += 1 - elif res['id'] == 7: - assert res['value'] == 'U' - found += 1 - assert found == 7 + assert obj[0][0] == 1 + assert obj[0][1] == 86400 + assert obj[0][2] == 1 + assert obj[0][3] == 10 + assert obj[0][5] == 86400 + assert obj[0][6] is False + assert obj[0][7] == 'U' def verify_LightweightM2M_1_1_int_203(shell: Shell, leshan: Leshan, endpoint: str): shell.exec_command('lwm2m update') logger.info('LightweightM2M-1.1-int-203 - Querying basic information in TLV format') fmt = leshan.format leshan.format = 'TLV' - resp = leshan.get(f'/clients/{endpoint}/3/0') + resp = leshan.read(endpoint,'3/0') verify_device_object(resp) leshan.format = fmt @@ -206,7 +171,7 @@ def verify_LightweightM2M_1_1_int_204(shell: Shell, leshan: Leshan, endpoint: st logger.info('LightweightM2M-1.1-int-204 - Querying basic information in JSON format') fmt = leshan.format leshan.format = 'JSON' - resp = leshan.get(f'/clients/{endpoint}/3/0') + resp = leshan.read(endpoint, '3/0') verify_device_object(resp) leshan.format = fmt @@ -217,15 +182,15 @@ def verify_LightweightM2M_1_1_int_205(shell: Shell, leshan: Leshan, endpoint: st leshan.write(endpoint, '1/0/2', 101) leshan.write(endpoint, '1/0/3', 1010) leshan.write(endpoint, '1/0/5', 2000) - assert leshan.read(endpoint, '1/0/2') == '101' - assert leshan.read(endpoint, '1/0/3') == '1010' - assert leshan.read(endpoint, '1/0/5') == '2000' + assert leshan.read(endpoint, '1/0/2') == 101 + assert leshan.read(endpoint, '1/0/3') == 1010 + assert leshan.read(endpoint, '1/0/5') == 2000 leshan.write(endpoint, '1/0/2', 1) leshan.write(endpoint, '1/0/3', 10) leshan.write(endpoint, '1/0/5', 86400) - assert leshan.read(endpoint, '1/0/2') == '1' - assert leshan.read(endpoint, '1/0/3') == '10' - assert leshan.read(endpoint, '1/0/5') == '86400' + assert leshan.read(endpoint, '1/0/2') == 1 + assert leshan.read(endpoint, '1/0/3') == 10 + assert leshan.read(endpoint, '1/0/5') == 86400 leshan.format = fmt def verify_LightweightM2M_1_1_int_211(shell: Shell, leshan: Leshan, endpoint: str): @@ -233,8 +198,8 @@ def verify_LightweightM2M_1_1_int_211(shell: Shell, leshan: Leshan, endpoint: st fmt = leshan.format leshan.format = 'CBOR' lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/0 -u16')) - id = lines[0] - assert leshan.read(endpoint, '1/0/0') == id + short_id = int(lines[0]) + assert leshan.read(endpoint, '1/0/0') == short_id assert leshan.read(endpoint, '1/0/6') is False assert leshan.read(endpoint, '1/0/7') == 'U' leshan.format = fmt @@ -246,8 +211,8 @@ def verify_LightweightM2M_1_1_int_212(shell: Shell, leshan: Leshan, endpoint: st leshan.write(endpoint, '1/0/2', 101) leshan.write(endpoint, '1/0/3', 1010) leshan.write(endpoint, '1/0/6', True) - assert leshan.read(endpoint, '1/0/2') == '101' - assert leshan.read(endpoint, '1/0/3') == '1010' + assert leshan.read(endpoint, '1/0/2') == 101 + assert leshan.read(endpoint, '1/0/3') == 1010 assert leshan.read(endpoint, '1/0/6') is True leshan.write(endpoint, '1/0/2', 1) leshan.write(endpoint, '1/0/3', 10) @@ -257,71 +222,26 @@ def verify_LightweightM2M_1_1_int_212(shell: Shell, leshan: Leshan, endpoint: st def verify_setting_basic_in_format(shell, leshan, endpoint, format): fmt = leshan.format leshan.format = format - server_obj = leshan.get(f'/clients/{endpoint}/1/0')['content'] + server_obj = leshan.read(endpoint, '1/0') verify_server_object(server_obj) # Remove Read-Only resources, so we don't end up writing those - for res in server_obj['resources']: - if res['id'] in (0, 11, 12): - server_obj['resources'].remove(res) - data = '''{ - "kind": "instance", - "id": 0, - "resources": [ - { - "id": 2, - "kind": "singleResource", - "value": "101", - "type": "integer" - }, - { - "id": 3, - "kind": "singleResource", - "value": "1010", - "type": "integer" - }, - { - "id": 5, - "kind": "singleResource", - "value": "2000", - "type": "integer" - }, - { - "id": 6, - "kind": "singleResource", - "value": true, - "type": "boolean" - }, - { - "id": 7, - "kind": "singleResource", - "value": "U", - "type": "string" - } - ] - }''' - assert leshan.put(f'/clients/{endpoint}/1/0', data, uri_options = '&replace=false')['status'] == 'CHANGED(204)' - resp = leshan.get(f'/clients/{endpoint}/1/0') - assert resp['valid'] is True - found = 0 - for res in resp['content']['resources']: - if res['id'] == 2: - assert res['value'] == '101' - found += 1 - elif res['id'] == 3: - assert res['value'] == '1010' - found += 1 - elif res['id'] == 5: - assert res['value'] == '2000' - found += 1 - elif res['id'] == 6: - assert res['value'] is True - found += 1 - elif res['id'] == 7: - assert res['value'] == 'U' - found += 1 - assert found == 5 - assert leshan.put(f'/clients/{endpoint}/1/0', data = server_obj, uri_options = '&replace=true')['status'] == 'CHANGED(204)' - server_obj = leshan.get(f'/clients/{endpoint}/1/0')['content'] + del server_obj[0][0] + data = { + 2: 101, + 3: 1010, + 5: 2000, + 6: True, + 7: 'U' + } + assert leshan.update_obj_instance(endpoint, '1/0', data)['status'] == 'CHANGED(204)' + resp = leshan.read(endpoint, '1/0') + assert resp[0][2] == 101 + assert resp[0][3] == 1010 + assert resp[0][5] == 2000 + assert resp[0][6] is True + assert resp[0][7] == 'U' + assert leshan.replace_obj_instance(endpoint, '1/0', server_obj[0])['status'] == 'CHANGED(204)' + server_obj = leshan.read(endpoint, '1/0') verify_server_object(server_obj) leshan.format = fmt @@ -339,6 +259,297 @@ def verify_LightweightM2M_1_1_int_221(shell: Shell, leshan: Leshan, endpoint: st assert leshan.write(endpoint, '0/0/0', 'coap://localhost')['status'] == 'UNAUTHORIZED(401)' assert leshan.put_raw(f'/clients/{endpoint}/0/attributes?pmin=10')['status'] == 'UNAUTHORIZED(401)' +def verify_LightweightM2M_1_1_int_222(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-222 - Read on Object") + resp = leshan.read(endpoint, '1') + assert len(resp) == 1 + assert len(resp[1][0]) == 9 + resp = leshan.read(endpoint, '3') + assert len(resp) == 1 + assert len(resp[3]) == 1 + assert len(resp[3][0]) == 15 + assert resp[3][0][0] == 'Zephyr' + +def verify_LightweightM2M_1_1_int_223(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-223 - Read on Object Instance") + resp = leshan.read(endpoint, '1/0') + assert len(resp[0]) == 9 + resp = leshan.read(endpoint, '3/0') + assert len(resp[0]) == 15 + assert resp[0][0] == 'Zephyr' + +def verify_LightweightM2M_1_1_int_224(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-224 - Read on Resource") + assert leshan.read(endpoint, '1/0/0') == 1 + assert leshan.read(endpoint, '1/0/1') == 86400 + assert leshan.read(endpoint, '1/0/6') is False + assert leshan.read(endpoint, '1/0/7') == 'U' + +def verify_LightweightM2M_1_1_int_225(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-225 - Read on Resource Instance") + assert leshan.read(endpoint, '3/0/11/0') == 0 + +def verify_LightweightM2M_1_1_int_226(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-226 - Write (Partial Update) on Object Instance") + lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/1 -u32')) + lifetime = int(lines[0]) + resources = { + 1: 60, + 6: True + } + assert leshan.update_obj_instance(endpoint, '1/0', resources)['status'] == 'CHANGED(204)' + assert leshan.read(endpoint, '1/0/1') == 60 + assert leshan.read(endpoint, '1/0/6') is True + resources = { + 1: lifetime, + 6: False + } + assert leshan.update_obj_instance(endpoint, '1/0', resources)['status'] == 'CHANGED(204)' + +def verify_LightweightM2M_1_1_int_227(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-227 - Write (replace) on Resource") + lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/1 -u32')) + lifetime = int(lines[0]) + assert leshan.write(endpoint, '1/0/1', int(63))['status'] == 'CHANGED(204)' + shell._device.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) + latest = leshan.get(f'/clients/{endpoint}') + assert latest["lifetime"] == 63 + assert leshan.read(endpoint, '1/0/1') == 63 + assert leshan.write(endpoint, '1/0/1', lifetime)['status'] == 'CHANGED(204)' + +def verify_LightweightM2M_1_1_int_228(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-228 - Write on Resource Instance") + resources = { + 0: {0: 'a', 1: 'b'} + } + assert leshan.create_obj_instance(endpoint, '16/0', resources)['status'] == 'CREATED(201)' + shell._device.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) + assert leshan.write(endpoint, '16/0/0/0', 'test')['status'] == 'CHANGED(204)' + assert leshan.read(endpoint, '16/0/0/0') == 'test' + +def verify_LightweightM2M_1_1_int_229(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-229 - Read-Composite Operation") + old_fmt = leshan.format + for fmt in ['SENML_JSON', 'SENML_CBOR']: + leshan.format = fmt + resp = leshan.composite_read(endpoint, ['/3', '1/0']) + assert len(resp.keys()) == 2 + assert resp[3] is not None + assert resp[1][0] is not None + assert len(resp[3][0]) == 15 + assert len(resp[1][0]) == 9 + + resp = leshan.composite_read(endpoint, ['1/0/1', '/3/0/11/0']) + logger.debug(resp) + assert len(resp.keys()) == 2 + assert resp[1][0][1] is not None + assert resp[3][0][11][0] is not None + leshan.format = old_fmt + +def verify_LightweightM2M_1_1_int_230(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-230 - Write-Composite Operation") + resources = { + "/1/0/1": 60, + "/1/0/6": True, + "/16/0/0": { + "0": "aa", + "1": "bb", + "2": "cc", + "3": "dd" + } + } + old_fmt = leshan.format + for fmt in ['SENML_JSON', 'SENML_CBOR']: + leshan.format = fmt + assert leshan.composite_write(endpoint, resources)['status'] == 'CHANGED(204)' + resp = leshan.read(endpoint, '1/0') + assert resp[0][1] == 60 + assert resp[0][6] is True + resp = leshan.read(endpoint, '16/0/0') + assert resp[0][0] == "aa" + assert resp[0][1] == "bb" + assert resp[0][2] == "cc" + assert resp[0][3] == "dd" + # Return to default + shell.exec_command('lwm2m write /1/0/1 -u32 86400') + shell.exec_command('lwm2m write /1/0/6 -u8 0') + leshan.format = old_fmt + +def query_basic_in_senml(leshan: Leshan, endpoint: str, fmt: str): + old_fmt = leshan.format + leshan.format = fmt + verify_server_object(leshan.read(endpoint, '1')[1]) + verify_device_object(leshan.read(endpoint, '3/0')) + assert leshan.read(endpoint, '3/0/16') == 'U' + assert leshan.read(endpoint, '3/0/11/0') == 0 + leshan.format = old_fmt + +def verify_LightweightM2M_1_1_int_231(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-231 - Querying basic information in SenML JSON format") + query_basic_in_senml(leshan, endpoint, 'SENML_JSON') + +def verify_LightweightM2M_1_1_int_232(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-232 - Querying basic information in SenML CBOR format") + query_basic_in_senml(leshan, endpoint, 'SENML_CBOR') + +def setting_basic_senml(shell: Shell, leshan: Leshan, endpoint: str, fmt: str): + old_fmt = leshan.format + leshan.format = fmt + resources = { + 1: 61, + 6: True, + } + assert leshan.update_obj_instance(endpoint, '1/0', resources)['status'] == 'CHANGED(204)' + srv_obj = leshan.read(endpoint, '1/0') + assert srv_obj[0][1] == 61 + assert srv_obj[0][6] is True + assert leshan.write(endpoint, '16/0/0/0', 'test_value')['status'] == 'CHANGED(204)' + portfolio = leshan.read(endpoint, '16') + assert portfolio[16][0][0][0] == 'test_value' + assert leshan.write(endpoint, '1/0/1', 63)['status'] == 'CHANGED(204)' + assert leshan.read(endpoint, '1/0/1') == 63 + shell.exec_command('lwm2m write /1/0/1 -u32 86400') + shell.exec_command('lwm2m write /1/0/6 -u8 0') + leshan.format = old_fmt + +def verify_LightweightM2M_1_1_int_233(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-233 - Setting basic information in SenML CBOR format") + setting_basic_senml(shell, leshan, endpoint, 'SENML_CBOR') + +def verify_LightweightM2M_1_1_int_234(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-234 - Setting basic information in SenML JSON format") + setting_basic_senml(shell, leshan, endpoint, 'SENML_JSON') + +def verify_LightweightM2M_1_1_int_235(): + """LightweightM2M-1.1-int-235 - Read-Composite Operation on root path""" + # Unsupported. Leshan does not allow this. + +def verify_LightweightM2M_1_1_int_236(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-236 - Read-Composite - Partial Presence") + resp = leshan.composite_read(endpoint, ['1/0', '/3/0/11/0', '/3339/0/5522', '/3353/0/6030']) + assert resp[1][0][1] is not None + assert resp[3][0][11][0] is not None + assert len(resp) == 2 + +def verify_LightweightM2M_1_1_int_237(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-237 - Read on Object without specifying Content-Type") + old_fmt = leshan.format + leshan.format = None + assert leshan.read(endpoint, '1')[1][0][1] is not None + assert leshan.read(endpoint, '3')[3][0][0] == 'Zephyr' + leshan.format = old_fmt + +def verify_LightweightM2M_1_1_int_241(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-241 - Executable Resource: Rebooting the device") + leshan.execute(endpoint, '3/0/4') + shell._device.readlines_until(regex='.*DEVICE: REBOOT', timeout=5.0) + shell._device.readlines_until(regex='.*rd_client_event: Disconnected', timeout=5.0) + shell.exec_command(f'lwm2m start {endpoint} -b 0') + shell._device.readlines_until(regex='.*Registration Done', timeout=5.0) + assert leshan.get(f'/clients/{endpoint}') + +def verify_LightweightM2M_1_1_int_256(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-256 - Write Operation Failure") + lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/0 -u16')) + short_id = int(lines[0]) + assert leshan.write(endpoint, '1/0/0', 123)['status'] == 'METHOD_NOT_ALLOWED(405)' + assert leshan.read(endpoint, '1/0/0') == short_id + +def verify_LightweightM2M_1_1_int_257(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-257 - Write-Composite Operation") + resources = { + "/1/0/2": 102, + "/1/0/6": True, + "/3/0/13": datetime.fromtimestamp(0) + } + old_fmt = leshan.format + for fmt in ['SENML_JSON', 'SENML_CBOR']: + leshan.format = fmt + assert leshan.composite_write(endpoint, resources)['status'] == 'CHANGED(204)' + assert leshan.read(endpoint, '1/0/2') == 102 + assert leshan.read(endpoint, '1/0/6') is True + # Cannot verify the /3/0/13, it is a timestamp that moves forward. + + # Return to default + shell.exec_command(f'lwm2m write /3/0/13 -u32 {int(datetime.now().timestamp())}') + shell.exec_command('lwm2m write /1/0/6 -u8 0') + shell.exec_command('lwm2m write /1/0/2 -u32 1') + leshan.format = old_fmt + +def verify_LightweightM2M_1_1_int_260(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-260 - Discover Command") + resp = leshan.discover(endpoint, '3') + expected_keys = ['/3', '/3/0', '/3/0/1', '/3/0/2', '/3/0/3', '/3/0/4', '/3/0/6', '/3/0/7', '/3/0/8', '/3/0/9', '/3/0/11', '/3/0/16'] + missing_keys = [key for key in expected_keys if key not in resp.keys()] + assert len(missing_keys) == 0 + assert leshan.put_raw(f'/clients/{endpoint}/3/attributes?pmin=10')['status'] == 'CHANGED(204)' + assert leshan.put_raw(f'/clients/{endpoint}/3/attributes?pmax=200')['status'] == 'CHANGED(204)' + resp = leshan.discover(endpoint, '3/0') + assert int(resp['/3/0/6']['dim']) == 2 + assert int(resp['/3/0/7']['dim']) == 2 + assert int(resp['/3/0/8']['dim']) == 2 + assert leshan.put_raw(f'/clients/{endpoint}/3/0/7/attributes?lt=1')['status'] == 'CHANGED(204)' + assert leshan.put_raw(f'/clients/{endpoint}/3/0/7/attributes?gt=6')['status'] == 'CHANGED(204)' + assert leshan.put_raw(f'/clients/{endpoint}/3/0/7/attributes?st=1')['status'] == 'CHANGED(204)' + resp = leshan.discover(endpoint, '3/0') + expected_keys = ['/3/0', '/3/0/1', '/3/0/2', '/3/0/3', '/3/0/4', '/3/0/6', '/3/0/7', '/3/0/8', '/3/0/9', '/3/0/11', '/3/0/16'] + missing_keys = [key for key in expected_keys if key not in resp.keys()] + assert len(missing_keys) == 0 + assert int(resp['/3/0/7']['dim']) == 2 + assert float(resp['/3/0/7']['lt']) == 1.0 + assert float(resp['/3/0/7']['gt']) == 6.0 + assert float(resp['/3/0/7']['st']) == 1.0 + resp = leshan.discover(endpoint, '3/0/7') + expected_keys = ['/3/0/7', '/3/0/7/0', '/3/0/7/1'] + missing_keys = [key for key in expected_keys if key not in resp.keys()] + assert len(missing_keys) == 0 + assert len(resp) == len(expected_keys) + +def verify_LightweightM2M_1_1_int_261(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-261 - Write-Attribute Operation on a multiple resource") + resp = leshan.discover(endpoint, '3/0/11') + logger.debug(resp) + expected_keys = ['/3/0/11', '/3/0/11/0'] + missing_keys = [key for key in expected_keys if key not in resp.keys()] + assert len(missing_keys) == 0 + assert len(resp) == len(expected_keys) + assert int(resp['/3/0/11']['dim']) == 1 + assert leshan.put_raw(f'/clients/{endpoint}/3/attributes?pmin=10')['status'] == 'CHANGED(204)' + assert leshan.put_raw(f'/clients/{endpoint}/3/attributes?pmax=200')['status'] == 'CHANGED(204)' + assert leshan.put_raw(f'/clients/{endpoint}/3/0/attributes?pmax=320')['status'] == 'CHANGED(204)' + assert leshan.put_raw(f'/clients/{endpoint}/3/0/11/0/attributes?pmax=100')['status'] == 'CHANGED(204)' + assert leshan.put_raw(f'/clients/{endpoint}/3/0/11/0/attributes?epmin=1')['status'] == 'CHANGED(204)' + assert leshan.put_raw(f'/clients/{endpoint}/3/0/11/0/attributes?epmax=20')['status'] == 'CHANGED(204)' + resp = leshan.discover(endpoint, '3/0/11') + logger.debug(resp) + assert int(resp['/3/0/11']['pmin']) == 10 + assert int(resp['/3/0/11']['pmax']) == 320 + assert int(resp['/3/0/11/0']['pmax']) == 100 + assert int(resp['/3/0/11/0']['epmin']) == 1 + assert int(resp['/3/0/11/0']['epmax']) == 20 + +def verify_LightweightM2M_1_1_int_280(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-280 - Successful Read-Composite Operation") + resp = leshan.composite_read(endpoint, ['/3/0/16', '/3/0/11/0', '/1/0']) + logger.debug(resp) + assert len(resp) == 2 + assert len(resp[3]) == 1 + assert len(resp[3][0]) == 2 # No extra resources + assert resp[3][0][11][0] == 0 + assert resp[3][0][16] == 'U' + assert resp[1][0][0] == 1 + assert resp[1][0][1] == 86400 + assert resp[1][0][6] is False + assert resp[1][0][7] == 'U' + +def verify_LightweightM2M_1_1_int_281(shell: Shell, leshan: Leshan, endpoint: str): + logger.info("LightweightM2M-1.1-int-281 - Partially Successful Read-Composite Operation") + resp = leshan.composite_read(endpoint, ['/1/0/1', '/1/0/7', '/1/0/8']) + assert len(resp) == 1 + 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' + def verify_LightweightM2M_1_1_int_401(shell: Shell, leshan: Leshan, endpoint: str): logger.info("LightweightM2M-1.1-int-401 - UDP Channel Security - Pre-shared Key Mode") lines = shell.get_filtered_output(shell.exec_command('lwm2m read 0/0/0 -s')) @@ -354,11 +565,15 @@ def test_lwm2m_bootstrap_psk(shell: Shell, leshan, leshan_bootstrap): try: # Generate randon device id and password (PSK key) endpoint = 'client_' + binascii.b2a_hex(os.urandom(1)).decode() + bs_passwd = ''.join(random.choice(string.ascii_lowercase) for i in range(16)) passwd = ''.join(random.choice(string.ascii_lowercase) for i in range(16)) + logger.debug('Endpoint: %s', endpoint) + logger.debug('Boostrap PSK: %s', binascii.b2a_hex(bs_passwd.encode()).decode()) + logger.debug('PSK: %s', binascii.b2a_hex(passwd.encode()).decode()) # Create device entries in Leshan and Bootstrap server - leshan_bootstrap.create_bs_device(endpoint, f'coaps://{LESHAN_IP}:{COAPS_PORT}', passwd) + leshan_bootstrap.create_bs_device(endpoint, f'coaps://{LESHAN_IP}:{COAPS_PORT}', bs_passwd, passwd) leshan.create_psk_device(endpoint, passwd) # Allow engine to start & stop once. @@ -373,7 +588,7 @@ def test_lwm2m_bootstrap_psk(shell: Shell, leshan, leshan_bootstrap): shell.exec_command('lwm2m write 0/0/1 -b 1') shell.exec_command('lwm2m write 0/0/2 -u8 0') shell.exec_command(f'lwm2m write 0/0/3 -s {endpoint}') - shell.exec_command(f'lwm2m write 0/0/5 -s {passwd}') + shell.exec_command(f'lwm2m write 0/0/5 -s {bs_passwd}') shell.exec_command(f'lwm2m start {endpoint} -b 1') @@ -403,6 +618,30 @@ def test_lwm2m_bootstrap_psk(shell: Shell, leshan, leshan_bootstrap): verify_LightweightM2M_1_1_int_211(shell, leshan, endpoint) verify_LightweightM2M_1_1_int_212(shell, leshan, endpoint) verify_LightweightM2M_1_1_int_215(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_220(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_221(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_222(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_223(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_224(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_225(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_226(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_227(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_228(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_229(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_230(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_231(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_232(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_233(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_234(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_236(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_237(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_241(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_256(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_257(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_260(shell, leshan, endpoint) + # skip, not supported in Leshan, verify_LightweightM2M_1_1_int_261(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_280(shell, leshan, endpoint) + verify_LightweightM2M_1_1_int_281(shell, leshan, endpoint) shell.exec_command('lwm2m stop') shell._device.readlines_until(regex=r'.*Deregistration success', timeout=10.0) @@ -413,7 +652,6 @@ def test_lwm2m_bootstrap_psk(shell: Shell, leshan, leshan_bootstrap): leshan.delete_device(endpoint) leshan_bootstrap.delete_bs_device(endpoint) - def test_lwm2m_nosecure(shell: Shell, leshan, helperclient): # Allow engine to start & stop once. @@ -438,9 +676,6 @@ def test_lwm2m_nosecure(shell: Shell, leshan, helperclient): verify_LightweightM2M_1_1_int_101(shell, leshan, endpoint) verify_LightweightM2M_1_1_int_105(shell, leshan, endpoint, helperclient) # needs no-security - verify_LightweightM2M_1_1_int_215(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_220(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_221(shell, leshan, endpoint) # All done shell.exec_command('lwm2m stop') From 0bedcf5c05042214778fca248c0b43e5de19c163 Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Thu, 26 Oct 2023 13:13:42 +0300 Subject: [PATCH 4/6] tests: lwm2m: Implement event stream support for Leshan Implement support for reading stream of events from Leshan. This allows testing the LwM2M SEND/Notify/Update operations and reading content of those. Also convert the whole Leshan class to use requests.session() so it pools up connections and uses keep-alive. Signed-off-by: Seppo Takalo --- tests/net/lib/lwm2m/interop/pytest/leshan.py | 138 +++++++++++++------ 1 file changed, 95 insertions(+), 43 deletions(-) diff --git a/tests/net/lib/lwm2m/interop/pytest/leshan.py b/tests/net/lib/lwm2m/interop/pytest/leshan.py index ecd14b2481c013..307a390bf9b639 100644 --- a/tests/net/lib/lwm2m/interop/pytest/leshan.py +++ b/tests/net/lib/lwm2m/interop/pytest/leshan.py @@ -8,6 +8,8 @@ import binascii import requests from datetime import datetime +import time +from contextlib import contextmanager class Leshan: def __init__(self, url: str): @@ -15,6 +17,7 @@ def __init__(self, url: str): self.timeout = 10 #self.format = 'TLV' self.format = "SENML_CBOR" + self._s = requests.Session() try: resp = self.get('/security/clients') if not isinstance(resp, list): @@ -41,19 +44,18 @@ def handle_response(resp: requests.models.Response): if len(resp.text): obj = json.loads(resp.text) return obj - else: - return None + return None def get(self, path: str): """Send HTTP GET query""" params = {'timeout': self.timeout} if self.format is not None: params['format'] = self.format - resp = requests.get(f'{self.api_url}{path}', params=params, timeout=self.timeout) + resp = self._s.get(f'{self.api_url}{path}', params=params, timeout=self.timeout) return Leshan.handle_response(resp) def put_raw(self, path: str, data: str | dict | None = None, headers: dict | None = None): - resp = requests.put(f'{self.api_url}{path}', data=data, headers=headers, timeout=self.timeout) + resp = self._s.put(f'{self.api_url}{path}', data=data, headers=headers, timeout=self.timeout) return Leshan.handle_response(resp) def put(self, path: str, data: str | dict, uri_options: str = ''): @@ -70,11 +72,11 @@ def post(self, path: str, data: str | dict | None = None): else: headers=None uri_options = '' - resp = requests.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) def delete(self, path: str): - resp = requests.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) def execute(self, endpoint: str, path: str): @@ -127,7 +129,8 @@ def _convert_type(cls, value): else: return value - def _define_obj_inst(self, path: str, resources: dict): + @classmethod + def _define_obj_inst(cls, path: str, resources: dict): data = { "kind": "instance", "id": int(path.split('/')[-1]), # ID is last element of path @@ -138,65 +141,70 @@ def _define_obj_inst(self, path: str, resources: dict): kind = 'multiResource' else: kind = 'singleResource' - data['resources'].append(self._define_resource(key, value, kind)) + data['resources'].append(cls._define_resource(key, value, kind)) return data - def _define_resource(self, rid, value, kind='singleResource'): + @classmethod + def _define_resource(cls, rid, value, kind='singleResource'): if kind in ('singleResource', 'resourceInstance'): return { "id": rid, "kind": kind, - "value": self._convert_type(value), - "type": self._type_to_string(value) + "value": cls._convert_type(value), + "type": cls._type_to_string(value) } if kind == 'multiResource': return { "id": rid, "kind": kind, "values": value, - "type": self._type_to_string(list(value.values())[0]) + "type": cls._type_to_string(list(value.values())[0]) } raise RuntimeError(f'Unhandled type {kind}') - def _decode_value(self, type, value): + @classmethod + def _decode_value(cls, val_type: str, value: str): """ Decode the Leshan representation of a value back to a Python value. """ - if type == 'BOOLEAN': + if val_type == 'BOOLEAN': return bool(value) - if type == 'INTEGER': + if val_type == 'INTEGER': return int(value) return value - def _decode_resource(self, content): + @classmethod + def _decode_resource(cls, content: dict): """ Decode the Leshan representation of a resource back to a Python dictionary. """ if content['kind'] == 'singleResource' or content['kind'] == 'resourceInstance': - return {content['id']: self._decode_value(content['type'], content['value'])} + return {content['id']: cls._decode_value(content['type'], content['value'])} elif content['kind'] == 'multiResource': values = {} for riid, value in content['values'].items(): - values.update({int(riid): self._decode_value(content['type'], value)}) + values.update({int(riid): cls._decode_value(content['type'], value)}) return {content['id']: values} raise RuntimeError(f'Unhandled type {content["kind"]}') - def _decode_obj_inst(self, content): + @classmethod + def _decode_obj_inst(cls, content): """ Decode the Leshan representation of an object instance back to a Python dictionary. """ resources = {} for resource in content['resources']: - resources.update(self._decode_resource(resource)) + resources.update(cls._decode_resource(resource)) return {content['id']: resources} - def _decode_obj(self, content): + @classmethod + def _decode_obj(cls, content): """ Decode the Leshan representation of an object back to a Python dictionary. """ instances = {} for instance in content['instances']: - instances.update(self._decode_obj_inst(instance)) + instances.update(cls._decode_obj_inst(instance)) return {content['id']: instances} def read(self, endpoint: str, path: str): @@ -214,33 +222,23 @@ def read(self, endpoint: str, path: str): return self._decode_resource(content) raise RuntimeError(f'Unhandled type {content["kind"]}') - def composite_read(self, endpoint: str, paths: list[str]): - paths = [path if path.startswith('/') else '/' + path for path in paths] - parameters = { - 'pathformat': self.format, - 'nodeformat': self.format, - 'timeout': self.timeout, - 'paths': ','.join(paths) - } - resp = requests.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') + @classmethod + def parse_composite(cls, content: dict): data = {} - for path, content in payload['content'].items(): + for path, content in content.items(): keys = [int(key) for key in path.lstrip("/").split('/')] if len(keys) == 1: - data.update(self._decode_obj(content)) + data.update(cls._decode_obj(content)) elif len(keys) == 2: if keys[0] not in data: data[keys[0]] = {} - data[keys[0]].update(self._decode_obj_inst(content)) + data[keys[0]].update(cls._decode_obj_inst(content)) elif len(keys) == 3: if keys[0] not in data: data[keys[0]] = {} if keys[1] not in data[keys[0]]: data[keys[0]][keys[1]] = {} - data[keys[0]][keys[1]].update(self._decode_resource(content)) + data[keys[0]][keys[1]].update(cls._decode_resource(content)) elif len(keys) == 4: if keys[0] not in data: data[keys[0]] = {} @@ -248,13 +246,24 @@ def composite_read(self, endpoint: str, paths: list[str]): data[keys[0]][keys[1]] = {} if keys[2] not in data[keys[0]][keys[1]]: data[keys[0]][keys[1]][keys[2]] = {} - data[keys[0]][keys[1]][keys[2]].update(self._decode_resource(content)) + data[keys[0]][keys[1]][keys[2]].update(cls._decode_resource(content)) else: raise RuntimeError(f'Unhandled path {path}') - print(f'Requested paths: {paths}') - print(data) return data + def composite_read(self, endpoint: str, paths: list[str]): + paths = [path if path.startswith('/') else '/' + path for path in paths] + parameters = { + 'pathformat': self.format, + 'nodeformat': self.format, + 'timeout': self.timeout, + 'paths': ','.join(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']) def composite_write(self, endpoint: str, resources: dict): """ @@ -295,11 +304,11 @@ def composite_write(self, endpoint: str, resources: dict): raise RuntimeError(f'Unhandled path {path}') data[path] = value - resp = requests.put(f'{self.api_url}/clients/{endpoint}/composite', params=parameters, json=data, timeout=self.timeout) + resp = self._s.put(f'{self.api_url}/clients/{endpoint}/composite', params=parameters, json=data, timeout=self.timeout) return Leshan.handle_response(resp) def discover(self, endpoint: str, path: str): - resp = self.handle_response(requests.get(f'{self.api_url}/clients/{endpoint}/{path}/discover', timeout=self.timeout)) + resp = self.handle_response(self._s.get(f'{self.api_url}/clients/{endpoint}/{path}/discover', timeout=self.timeout)) data = {} for obj in resp['objectLinks']: data[obj['url']] = obj['attributes'] @@ -324,3 +333,46 @@ def create_bs_device(self, endpoint: str, server_uri: str, bs_passwd: str, passw def delete_bs_device(self, endpoint: str): self.delete(f'/security/clients/{endpoint}') self.delete(f'/bootstrap/{endpoint}') + + @contextmanager + def get_event_stream(self, endpoint: str): + """ + Get stream of events regarding the given endpoint. + + Events are notifications, updates and sends. + + The event stream must be closed after the use, so this must be used in 'with' statement like this: + with leshan.get_event_stream('native_posix') as events: + data = events.next_event('SEND') + + 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 r.encoding is None: + r.encoding = 'utf-8' + try: + yield LeshanEventsIterator(r, self.timeout) + finally: + r.close() + +class LeshanEventsIterator: + def __init__(self, req: requests.Response, timeout: int): + self._it = req.iter_lines(chunk_size=1, decode_unicode=True) + self._timeout = timeout + + def next_event(self, event: str): + timeout = time.time() + self._timeout + try: + for line in self._it: + if line == f'event: {event}': + for line in self._it: + if not line.startswith('data: '): + continue + data = json.loads(line.lstrip('data: ')) + if event == 'SEND': + return Leshan.parse_composite(data['val']) + return data + if time.time() > timeout: + return None + except requests.exceptions.Timeout: + return None From 3f4d38d9c9aa75e5f95798a894cc720b20b2ad4b Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Mon, 30 Oct 2023 15:25:33 +0200 Subject: [PATCH 5/6] tests: lwm2m: Refactor to use module scoped DUT When testcases share one DUT per module, we save the time from running bootstrap on each testcase. Each testcase start with DUT that is registered. Signed-off-by: Seppo Takalo --- .../net/lib/lwm2m/interop/pytest/conftest.py | 148 +++++++ tests/net/lib/lwm2m/interop/pytest/leshan.py | 63 ++- .../lwm2m/interop/pytest/test_bootstrap.py | 58 +++ .../lib/lwm2m/interop/pytest/test_lwm2m.py | 405 +++++------------- .../lib/lwm2m/interop/pytest/test_nosec.py | 53 +++ tests/net/lib/lwm2m/interop/testcase.yaml | 2 + 6 files changed, 421 insertions(+), 308 deletions(-) create mode 100644 tests/net/lib/lwm2m/interop/pytest/conftest.py create mode 100644 tests/net/lib/lwm2m/interop/pytest/test_bootstrap.py create mode 100644 tests/net/lib/lwm2m/interop/pytest/test_nosec.py diff --git a/tests/net/lib/lwm2m/interop/pytest/conftest.py b/tests/net/lib/lwm2m/interop/pytest/conftest.py new file mode 100644 index 00000000000000..9ca971b85c7c8d --- /dev/null +++ b/tests/net/lib/lwm2m/interop/pytest/conftest.py @@ -0,0 +1,148 @@ +""" +Common test fixtures +#################### + +Copyright (c) 2023 Nordic Semiconductor ASA + +SPDX-License-Identifier: Apache-2.0 + +""" + +import time +import logging +import os +import binascii +import random +import string +import pytest +from leshan import Leshan + +from twister_harness import Shell +from twister_harness import DeviceAdapter + +LESHAN_IP: str = '192.0.2.2' +COAP_PORT: int = 5683 +COAPS_PORT: int = 5684 +BOOTSTRAP_COAPS_PORT: int = 5784 + +logger = logging.getLogger(__name__) + +@pytest.fixture(scope='session') +def leshan() -> Leshan: + """ + Fixture that returns a Leshan object for interacting with the Leshan server. + + :return: The Leshan object. + :rtype: Leshan + """ + try: + return Leshan("http://localhost:8080/api") + except RuntimeError: + pytest.skip('Leshan server not available') + +@pytest.fixture(scope='session') +def leshan_bootstrap() -> Leshan: + """ + Fixture that returns a Leshan object for interacting with the Bootstrap Leshan server. + + :return: The Leshan object. + :rtype: Leshan + """ + try: + return Leshan("http://localhost:8081/api") + except RuntimeError: + pytest.skip('Leshan Bootstrap server not available') + +@pytest.fixture(scope='module') +def helperclient() -> object: + """ + Fixture that returns a helper client object for testing. + + :return: The helper client object. + :rtype: object + """ + try: + from coapthon.client.helperclient import HelperClient + except ModuleNotFoundError: + pytest.skip('CoAPthon3 package not installed') + return HelperClient(server=('127.0.0.1', COAP_PORT)) + + +@pytest.fixture(scope='module') +def endpoint_nosec(shell: Shell, dut: DeviceAdapter, leshan: Leshan) -> str: + """Fixture that returns an endpoint that starts on no-secure mode""" + # Allow engine to start & stop once. + time.sleep(2) + + # Generate randon device id and password (PSK key) + ep = 'client_' + binascii.b2a_hex(os.urandom(1)).decode() + + # + # Registration Interface test cases (using Non-secure mode) + # + shell.exec_command(f'lwm2m write 0/0/0 -s coap://{LESHAN_IP}:{COAP_PORT}') + shell.exec_command('lwm2m write 0/0/1 -b 0') + shell.exec_command('lwm2m write 0/0/2 -u8 3') + shell.exec_command(f'lwm2m write 0/0/3 -s {ep}') + shell.exec_command('lwm2m create 1/0') + shell.exec_command('lwm2m write 0/0/10 -u16 1') + shell.exec_command('lwm2m write 1/0/0 -u16 1') + shell.exec_command('lwm2m write 1/0/1 -u32 86400') + shell.exec_command(f'lwm2m start {ep} -b 0') + dut.readlines_until(regex=f"RD Client started with endpoint '{ep}'", timeout=10.0) + + yield ep + + # All done + shell.exec_command('lwm2m stop') + dut.readlines_until(regex=r'.*Deregistration success', timeout=10.0) + +@pytest.fixture(scope='module') +def endpoint_bootstrap(shell: Shell, dut: DeviceAdapter, leshan: Leshan, leshan_bootstrap: Leshan) -> str: + """Fixture that returns an endpoint that starts the bootstrap.""" + try: + # Generate randon device id and password (PSK key) + ep = 'client_' + binascii.b2a_hex(os.urandom(1)).decode() + bs_passwd = ''.join(random.choice(string.ascii_lowercase) for i in range(16)) + passwd = ''.join(random.choice(string.ascii_lowercase) for i in range(16)) + + logger.debug('Endpoint: %s', ep) + logger.debug('Boostrap PSK: %s', binascii.b2a_hex(bs_passwd.encode()).decode()) + logger.debug('PSK: %s', binascii.b2a_hex(passwd.encode()).decode()) + + # Create device entries in Leshan and Bootstrap server + leshan_bootstrap.create_bs_device(ep, f'coaps://{LESHAN_IP}:{COAPS_PORT}', bs_passwd, passwd) + leshan.create_psk_device(ep, passwd) + + # Allow engine to start & stop once. + time.sleep(2) + + # Write bootsrap server information and PSK keys + shell.exec_command(f'lwm2m write 0/0/0 -s coaps://{LESHAN_IP}:{BOOTSTRAP_COAPS_PORT}') + shell.exec_command('lwm2m write 0/0/1 -b 1') + shell.exec_command('lwm2m write 0/0/2 -u8 0') + shell.exec_command(f'lwm2m write 0/0/3 -s {ep}') + shell.exec_command(f'lwm2m write 0/0/5 -s {bs_passwd}') + shell.exec_command(f'lwm2m start {ep} -b 1') + + yield ep + + shell.exec_command('lwm2m stop') + dut.readlines_until(regex=r'.*Deregistration success', timeout=10.0) + + finally: + # Remove device and bootstrap information + # Leshan does not accept non-secure connection if device information is provided with PSK + leshan.delete_device(ep) + leshan_bootstrap.delete_bs_device(ep) + +@pytest.fixture(scope='module') +def endpoint_registered(endpoint_bootstrap, shell: Shell, dut: DeviceAdapter) -> str: + """Fixture that returns an endpoint that is registered.""" + dut.readlines_until(regex='.*Registration Done', timeout=5.0) + return endpoint_bootstrap + +@pytest.fixture(scope='module') +def endpoint(endpoint_registered) -> str: + """Fixture that returns an endpoint that is registered.""" + return endpoint_registered diff --git a/tests/net/lib/lwm2m/interop/pytest/leshan.py b/tests/net/lib/lwm2m/interop/pytest/leshan.py index 307a390bf9b639..88030139d97564 100644 --- a/tests/net/lib/lwm2m/interop/pytest/leshan.py +++ b/tests/net/lib/lwm2m/interop/pytest/leshan.py @@ -1,6 +1,12 @@ -# Copyright (c) 2023 Nordic Semiconductor ASA -# -# SPDX-License-Identifier: Apache-2.0 +""" +REST client for Leshan demo server +################################## + +Copyright (c) 2023 Nordic Semiconductor ASA + +SPDX-License-Identifier: Apache-2.0 + +""" from __future__ import annotations @@ -12,7 +18,9 @@ from contextlib import contextmanager class Leshan: + """This class represents a Leshan client that interacts with demo server's REAT API""" def __init__(self, url: str): + """Initialize Leshan client and check if server is available""" self.api_url = url self.timeout = 10 #self.format = 'TLV' @@ -22,8 +30,8 @@ def __init__(self, url: str): resp = self.get('/security/clients') if not isinstance(resp, list): raise RuntimeError('Did not receive list of endpoints') - except requests.exceptions.ConnectionError: - raise RuntimeError('Leshan not responding') + except requests.exceptions.ConnectionError as exc: + raise RuntimeError('Leshan not responding') from exc @staticmethod def handle_response(resp: requests.models.Response): @@ -47,7 +55,7 @@ def handle_response(resp: requests.models.Response): return None def get(self, path: str): - """Send HTTP GET query""" + """Send HTTP GET query with typical parameters""" params = {'timeout': self.timeout} if self.format is not None: params['format'] = self.format @@ -55,15 +63,18 @@ def get(self, path: str): return Leshan.handle_response(resp) def put_raw(self, path: str, data: str | dict | None = None, headers: dict | None = None): + """Send HTTP PUT query without any default parameters""" resp = self._s.put(f'{self.api_url}{path}', data=data, headers=headers, timeout=self.timeout) return Leshan.handle_response(resp) def put(self, path: str, data: str | dict, uri_options: str = ''): + """Send HTTP PUT query with typical parameters""" if isinstance(data, dict): data = json.dumps(data) return self.put_raw(f'{path}?timeout={self.timeout}&format={self.format}' + uri_options, data=data, headers={'content-type': 'application/json'}) def post(self, path: str, data: str | dict | None = None): + """Send HTTP POST query""" if isinstance(data, dict): data = json.dumps(data) if data is not None: @@ -76,13 +87,16 @@ def post(self, path: str, data: str | dict | None = None): return Leshan.handle_response(resp) def delete(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 execute(self, endpoint: str, path: str): + """Send LwM2M EXECUTE command""" return self.post(f'/clients/{endpoint}/{path}') def write(self, endpoint: str, path: str, value: bool | int | str): + """Send LwM2M WRITE command to a single resource or resource instance""" if len(path.split('/')) == 3: kind = 'singleResource' else: @@ -91,14 +105,17 @@ def write(self, endpoint: str, path: str, value: bool | int | str): return self.put(f'/clients/{endpoint}/{path}', self._define_resource(rid, value, kind)) def update_obj_instance(self, endpoint: str, path: str, resources: dict): + """Update object instance""" data = self._define_obj_inst(path, resources) return self.put(f'/clients/{endpoint}/{path}', data, uri_options='&replace=false') def replace_obj_instance(self, endpoint: str, path: str, resources: dict): + """Replace object instance""" data = self._define_obj_inst(path, resources) return self.put(f'/clients/{endpoint}/{path}', data, uri_options='&replace=true') def create_obj_instance(self, endpoint: str, path: str, resources: dict): + """Send LwM2M CREATE command""" data = self._define_obj_inst(path, resources) path = '/'.join(path.split('/')[:-1]) # Create call should not have instance ID in path return self.post(f'/clients/{endpoint}/{path}', data) @@ -124,6 +141,7 @@ def _type_to_string(cls, value): @classmethod def _convert_type(cls, value): + """Wrapper for special types that are not understood by Json""" if isinstance(value, datetime): return int(value.timestamp()) else: @@ -131,6 +149,7 @@ def _convert_type(cls, value): @classmethod def _define_obj_inst(cls, path: str, resources: dict): + """Define an object instance for Leshan""" data = { "kind": "instance", "id": int(path.split('/')[-1]), # ID is last element of path @@ -146,6 +165,7 @@ def _define_obj_inst(cls, path: str, resources: dict): @classmethod def _define_resource(cls, rid, value, kind='singleResource'): + """Define a resource for Leshan""" if kind in ('singleResource', 'resourceInstance'): return { "id": rid, @@ -208,6 +228,7 @@ def _decode_obj(cls, content): return {content['id']: instances} def read(self, endpoint: str, path: str): + """Send LwM2M READ command and decode the response to a Python dictionary""" resp = self.get(f'/clients/{endpoint}/{path}') if not resp['success']: return resp @@ -223,9 +244,10 @@ def read(self, endpoint: str, path: str): raise RuntimeError(f'Unhandled type {content["kind"]}') @classmethod - def parse_composite(cls, content: dict): + def parse_composite(cls, payload: dict): + """Decode the Leshan's response to composite query back to a Python dictionary""" data = {} - for path, content in content.items(): + for path, content in payload.items(): keys = [int(key) for key in path.lstrip("/").split('/')] if len(keys) == 1: data.update(cls._decode_obj(content)) @@ -251,14 +273,22 @@ def parse_composite(cls, content: dict): raise RuntimeError(f'Unhandled path {path}') return data - def composite_read(self, endpoint: str, paths: list[str]): - paths = [path if path.startswith('/') else '/' + path for path in paths] + def _composite_params(self, paths: list[str] | None = None): + """Common URI parameters for composite query""" parameters = { 'pathformat': self.format, 'nodeformat': self.format, - 'timeout': self.timeout, - 'paths': ','.join(paths) + 'timeout': self.timeout } + if paths is not None: + paths = [path if path.startswith('/') else '/' + path for path in paths] + parameters['paths'] = ','.join(paths) + + return parameters + + def composite_read(self, endpoint: str, paths: list[str]): + """Send LwM2M Composite-Read command and decode the response to a Python dictionary""" + 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)': @@ -267,7 +297,7 @@ def composite_read(self, endpoint: str, paths: list[str]): def composite_write(self, endpoint: str, resources: dict): """ - Do LwM2m composite write operation. + Send LwM2m Composite-Write operation. Targeted resources are defined as a dictionary with the following structure: { @@ -356,11 +386,18 @@ def get_event_stream(self, endpoint: str): r.close() class LeshanEventsIterator: + """Iterator for Leshan event stream""" def __init__(self, req: requests.Response, timeout: int): + """Initialize the iterator in line mode""" self._it = req.iter_lines(chunk_size=1, decode_unicode=True) self._timeout = timeout def next_event(self, event: str): + """ + Finds the next occurrence of a specific event in the stream. + + If timeout happens, the returns None. + """ timeout = time.time() + self._timeout try: for line in self._it: diff --git a/tests/net/lib/lwm2m/interop/pytest/test_bootstrap.py b/tests/net/lib/lwm2m/interop/pytest/test_bootstrap.py new file mode 100644 index 00000000000000..65561db067670a --- /dev/null +++ b/tests/net/lib/lwm2m/interop/pytest/test_bootstrap.py @@ -0,0 +1,58 @@ +""" +LwM2M Bootstrap interface tests +############################### + +Copyright (c) 2023 Nordic Semiconductor ASA + +SPDX-License-Identifier: Apache-2.0 + +Test specification: +=================== +https://www.openmobilealliance.org/release/LightweightM2M/ETS/OMA-ETS-LightweightM2M-V1_1-20190912-D.pdf + + +This module contains only testcases that verify the bootstrap. +""" + +import logging +from leshan import Leshan +from twister_harness import Shell +from twister_harness import DeviceAdapter + +logger = logging.getLogger(__name__) + + +# +# Test specification: +# https://www.openmobilealliance.org/release/LightweightM2M/ETS/OMA-ETS-LightweightM2M-V1_1-20190912-D.pdf +# +# Bootstrap Interface: [0-99] +# + +def verify_LightweightM2M_1_1_int_0(shell: Shell, dut: DeviceAdapter): + """LightweightM2M-1.1-int-0 - Client Initiated Bootstrap""" + dut.readlines_until(regex='.*Bootstrap started with endpoint', timeout=5.0) + dut.readlines_until(regex='.*Bootstrap registration done', timeout=5.0) + dut.readlines_until(regex='.*Bootstrap data transfer done', timeout=5.0) + +def test_LightweightM2M_1_1_int_1(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint_bootstrap: str): + """LightweightM2M-1.1-int-1 - Client Initiated Bootstrap Full (PSK)""" + verify_LightweightM2M_1_1_int_0(shell, dut) + verify_LightweightM2M_1_1_int_101(shell, dut, leshan, endpoint_bootstrap) + verify_LightweightM2M_1_1_int_401(shell, leshan, endpoint_bootstrap) + +def verify_LightweightM2M_1_1_int_101(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-101 - Initial Registration""" + dut.readlines_until(regex='.*Registration Done', timeout=5.0) + assert leshan.get(f'/clients/{endpoint}') + +def verify_LightweightM2M_1_1_int_401(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-401 - UDP Channel Security - Pre-shared Key Mode""" + lines = shell.get_filtered_output(shell.exec_command('lwm2m read 0/0/0 -s')) + host = lines[0] + assert 'coaps://' in host + lines = shell.get_filtered_output(shell.exec_command('lwm2m read 0/0/2 -u8')) + mode = int(lines[0]) + assert mode == 0 + resp = leshan.get(f'/clients/{endpoint}') + assert resp["secure"] diff --git a/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py b/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py index 384e3cadda5c71..bdeb87838474d9 100644 --- a/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py +++ b/tests/net/lib/lwm2m/interop/pytest/test_lwm2m.py @@ -1,136 +1,81 @@ -# Copyright (c) 2023 Nordic Semiconductor ASA -# -# SPDX-License-Identifier: Apache-2.0 +""" +Various LwM2M interoperability tests +#################################### + +Copyright (c) 2023 Nordic Semiconductor ASA + +SPDX-License-Identifier: Apache-2.0 + +Test specification: +=================== +https://www.openmobilealliance.org/release/LightweightM2M/ETS/OMA-ETS-LightweightM2M-V1_1-20190912-D.pdf + + +This module contains testcases for + * Registration Interface [100-199] + * Device management & Service Enablement Interface [200-299] + * Information Reporting Interface [300-399] + +""" import time import logging +from datetime import datetime import pytest from leshan import Leshan -import os -import binascii -import random -import string from twister_harness import Shell -from datetime import datetime - -LESHAN_IP: str = '192.0.2.2' -COAP_PORT: int = 5683 -COAPS_PORT: int = 5684 -BOOTSTRAP_COAPS_PORT: int = 5784 +from twister_harness import DeviceAdapter logger = logging.getLogger(__name__) -@pytest.fixture(scope='module') -def helperclient() -> object: - try: - from coapthon.client.helperclient import HelperClient - except ModuleNotFoundError: - pytest.skip('CoAPthon3 package not installed') - return HelperClient(server=('127.0.0.1', COAP_PORT)) - -@pytest.fixture(scope='session') -def leshan() -> Leshan: - try: - return Leshan("http://localhost:8080/api") - except RuntimeError: - pytest.skip('Leshan server not available') - -@pytest.fixture(scope='session') -def leshan_bootstrap() -> Leshan: - try: - return Leshan("http://localhost:8081/api") - except RuntimeError: - pytest.skip('Leshan Bootstrap server not available') - -# -# Test specification: -# https://www.openmobilealliance.org/release/LightweightM2M/ETS/OMA-ETS-LightweightM2M-V1_1-20190912-D.pdf -# - -def verify_LightweightM2M_1_1_int_0(shell: Shell): - logger.info("LightweightM2M-1.1-int-0 - Client Initiated Bootstrap") - shell._device.readlines_until(regex='.*Bootstrap started with endpoint', timeout=5.0) - shell._device.readlines_until(regex='.*Bootstrap registration done', timeout=5.0) - shell._device.readlines_until(regex='.*Bootstrap data transfer done', timeout=5.0) - -def verify_LightweightM2M_1_1_int_1(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-1 - Client Initiated Bootstrap Full (PSK)") - verify_LightweightM2M_1_1_int_0(shell) - verify_LightweightM2M_1_1_int_101(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_401(shell, leshan, endpoint) - -def verify_LightweightM2M_1_1_int_101(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-101 - Initial Registration") - shell._device.readlines_until(regex='.*Registration Done', timeout=5.0) - assert leshan.get(f'/clients/{endpoint}') -def verify_LightweightM2M_1_1_int_102(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-102 - Registration Update") +def test_LightweightM2M_1_1_int_102(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-102 - Registration Update""" lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/1 -u32')) lifetime = int(lines[0]) lifetime = lifetime + 10 start_time = time.time() * 1000 leshan.write(endpoint, '1/0/1', lifetime) - shell._device.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) + dut.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) latest = leshan.get(f'/clients/{endpoint}') assert latest["lastUpdate"] > start_time assert latest["lastUpdate"] <= time.time()*1000 assert latest["lifetime"] == lifetime shell.exec_command('lwm2m write 1/0/1 -u32 86400') -def verify_LightweightM2M_1_1_int_103(): - """LightweightM2M-1.1-int-103 - Deregistration""" - # Unsupported. We don't have "disabled" functionality in server object - -def verify_LightweightM2M_1_1_int_104(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-104 - Registration Update Trigger") +def test_LightweightM2M_1_1_int_104(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-104 - Registration Update Trigger""" shell.exec_command('lwm2m update') - shell._device.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) + dut.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) leshan.execute(endpoint, '1/0/8') - shell._device.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) - -def verify_LightweightM2M_1_1_int_105(shell: Shell, leshan: Leshan, endpoint: str, helperclient: object): - logger.info("LightweightM2M-1.1-int-105 - Discarded Register Update") - status = leshan.get(f'/clients/{endpoint}') - if status["secure"]: - logger.debug("Skip, requires non-secure connection") - return - regid = status["registrationId"] - assert regid - # Fake unregister message - helperclient.delete(f'rd/{regid}', timeout=0.1) - helperclient.stop() - time.sleep(1) - shell.exec_command('lwm2m update') - shell._device.readlines_until(regex=r'.*Failed with code 4\.4', timeout=5.0) - shell._device.readlines_until(regex='.*Registration Done', timeout=10.0) + dut.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) -def verify_LightweightM2M_1_1_int_107(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-107 - Extending the lifetime of a registration") +def test_LightweightM2M_1_1_int_107(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-107 - Extending the lifetime of a registration""" leshan.write(endpoint, '1/0/1', 120) - shell._device.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) + dut.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/1 -u32')) lifetime = int(lines[0]) assert lifetime == 120 logger.debug(f'Wait for update, max {lifetime} s') - shell._device.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=lifetime) + dut.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=lifetime) assert leshan.get(f'/clients/{endpoint}') -def verify_LightweightM2M_1_1_int_108(leshan, endpoint): - logger.info("LightweightM2M-1.1-int-108 - Turn on Queue Mode") +def test_LightweightM2M_1_1_int_108(leshan, endpoint): + """LightweightM2M-1.1-int-108 - Turn on Queue Mode""" assert leshan.get(f'/clients/{endpoint}')["queuemode"] -def verify_LightweightM2M_1_1_int_109(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-109 - Behavior in Queue Mode") +def test_LightweightM2M_1_1_int_109(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-109 - Behavior in Queue Mode""" logger.debug('Wait for Queue RX OFF') - shell._device.readlines_until(regex='.*Queue mode RX window closed', timeout=120) + dut.readlines_until(regex='.*Queue mode RX window closed', timeout=120) # Restore previous value shell.exec_command('lwm2m write 1/0/1 -u32 86400') - shell._device.readlines_until(regex='.*Registration update complete', timeout=10) + dut.readlines_until(regex='.*Registration update complete', timeout=10) -def verify_LightweightM2M_1_1_int_201(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-201 - Querying basic information in Plain Text format") +def test_LightweightM2M_1_1_int_201(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-201 - Querying basic information in Plain Text format""" fmt = leshan.format leshan.format = 'TEXT' assert leshan.read(endpoint, '3/0/0') == 'Zephyr' @@ -157,26 +102,24 @@ def verify_server_object(obj): assert obj[0][6] is False assert obj[0][7] == 'U' -def verify_LightweightM2M_1_1_int_203(shell: Shell, leshan: Leshan, endpoint: str): - shell.exec_command('lwm2m update') - logger.info('LightweightM2M-1.1-int-203 - Querying basic information in TLV format') +def test_LightweightM2M_1_1_int_203(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-203 - Querying basic information in TLV format""" fmt = leshan.format leshan.format = 'TLV' resp = leshan.read(endpoint,'3/0') verify_device_object(resp) leshan.format = fmt -def verify_LightweightM2M_1_1_int_204(shell: Shell, leshan: Leshan, endpoint: str): - shell.exec_command('lwm2m update') - logger.info('LightweightM2M-1.1-int-204 - Querying basic information in JSON format') +def test_LightweightM2M_1_1_int_204(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-204 - Querying basic information in JSON format""" fmt = leshan.format leshan.format = 'JSON' resp = leshan.read(endpoint, '3/0') verify_device_object(resp) leshan.format = fmt -def verify_LightweightM2M_1_1_int_205(shell: Shell, leshan: Leshan, endpoint: str): - logger.info('LightweightM2M-1.1-int-205 - Setting basic information in Plain Text format') +def test_LightweightM2M_1_1_int_205(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-205 - Setting basic information in Plain Text format""" fmt = leshan.format leshan.format = 'TEXT' leshan.write(endpoint, '1/0/2', 101) @@ -193,8 +136,8 @@ def verify_LightweightM2M_1_1_int_205(shell: Shell, leshan: Leshan, endpoint: st assert leshan.read(endpoint, '1/0/5') == 86400 leshan.format = fmt -def verify_LightweightM2M_1_1_int_211(shell: Shell, leshan: Leshan, endpoint: str): - logger.info('LightweightM2M-1.1-int-211 - Querying basic information in CBOR format') +def test_LightweightM2M_1_1_int_211(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-211 - Querying basic information in CBOR format""" fmt = leshan.format leshan.format = 'CBOR' lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/0 -u16')) @@ -204,8 +147,8 @@ def verify_LightweightM2M_1_1_int_211(shell: Shell, leshan: Leshan, endpoint: st assert leshan.read(endpoint, '1/0/7') == 'U' leshan.format = fmt -def verify_LightweightM2M_1_1_int_212(shell: Shell, leshan: Leshan, endpoint: str): - logger.info('LightweightM2M-1.1-int-212 - Setting basic information in CBOR format') +def test_LightweightM2M_1_1_int_212(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-212 - Setting basic information in CBOR format""" fmt = leshan.format leshan.format = 'CBOR' leshan.write(endpoint, '1/0/2', 101) @@ -245,22 +188,22 @@ def verify_setting_basic_in_format(shell, leshan, endpoint, format): verify_server_object(server_obj) leshan.format = fmt -def verify_LightweightM2M_1_1_int_215(shell: Shell, leshan: Leshan, endpoint: str): - logger.info('LightweightM2M-1.1-int-215 - Setting basic information in TLV format') +def test_LightweightM2M_1_1_int_215(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-215 - Setting basic information in TLV format""" verify_setting_basic_in_format(shell, leshan, endpoint, 'TLV') -def verify_LightweightM2M_1_1_int_220(shell: Shell, leshan: Leshan, endpoint: str): - logger.info('LightweightM2M-1.1-int-220 - Setting basic information in JSON format') +def test_LightweightM2M_1_1_int_220(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-220 - Setting basic information in JSON format""" verify_setting_basic_in_format(shell, leshan, endpoint, 'JSON') -def verify_LightweightM2M_1_1_int_221(shell: Shell, leshan: Leshan, endpoint: str): - logger.info('LightweightM2M-1.1-int-221 - Attempt to perform operations on Security') +def test_LightweightM2M_1_1_int_221(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-221 - Attempt to perform operations on Security""" assert leshan.read(endpoint, '0/0')['status'] == 'UNAUTHORIZED(401)' assert leshan.write(endpoint, '0/0/0', 'coap://localhost')['status'] == 'UNAUTHORIZED(401)' assert leshan.put_raw(f'/clients/{endpoint}/0/attributes?pmin=10')['status'] == 'UNAUTHORIZED(401)' -def verify_LightweightM2M_1_1_int_222(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-222 - Read on Object") +def test_LightweightM2M_1_1_int_222(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-222 - Read on Object""" resp = leshan.read(endpoint, '1') assert len(resp) == 1 assert len(resp[1][0]) == 9 @@ -270,27 +213,27 @@ def verify_LightweightM2M_1_1_int_222(shell: Shell, leshan: Leshan, endpoint: st assert len(resp[3][0]) == 15 assert resp[3][0][0] == 'Zephyr' -def verify_LightweightM2M_1_1_int_223(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-223 - Read on Object Instance") +def test_LightweightM2M_1_1_int_223(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-223 - Read on Object Instance""" resp = leshan.read(endpoint, '1/0') assert len(resp[0]) == 9 resp = leshan.read(endpoint, '3/0') assert len(resp[0]) == 15 assert resp[0][0] == 'Zephyr' -def verify_LightweightM2M_1_1_int_224(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-224 - Read on Resource") +def test_LightweightM2M_1_1_int_224(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-224 - Read on Resource""" assert leshan.read(endpoint, '1/0/0') == 1 assert leshan.read(endpoint, '1/0/1') == 86400 assert leshan.read(endpoint, '1/0/6') is False assert leshan.read(endpoint, '1/0/7') == 'U' -def verify_LightweightM2M_1_1_int_225(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-225 - Read on Resource Instance") +def test_LightweightM2M_1_1_int_225(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-225 - Read on Resource Instance""" assert leshan.read(endpoint, '3/0/11/0') == 0 -def verify_LightweightM2M_1_1_int_226(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-226 - Write (Partial Update) on Object Instance") +def test_LightweightM2M_1_1_int_226(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-226 - Write (Partial Update) on Object Instance""" lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/1 -u32')) lifetime = int(lines[0]) resources = { @@ -306,29 +249,29 @@ def verify_LightweightM2M_1_1_int_226(shell: Shell, leshan: Leshan, endpoint: st } assert leshan.update_obj_instance(endpoint, '1/0', resources)['status'] == 'CHANGED(204)' -def verify_LightweightM2M_1_1_int_227(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-227 - Write (replace) on Resource") +def test_LightweightM2M_1_1_int_227(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-227 - Write (replace) on Resource""" lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/1 -u32')) lifetime = int(lines[0]) assert leshan.write(endpoint, '1/0/1', int(63))['status'] == 'CHANGED(204)' - shell._device.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) + dut.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) latest = leshan.get(f'/clients/{endpoint}') assert latest["lifetime"] == 63 assert leshan.read(endpoint, '1/0/1') == 63 assert leshan.write(endpoint, '1/0/1', lifetime)['status'] == 'CHANGED(204)' -def verify_LightweightM2M_1_1_int_228(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-228 - Write on Resource Instance") +def test_LightweightM2M_1_1_int_228(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-228 - Write on Resource Instance""" resources = { 0: {0: 'a', 1: 'b'} } assert leshan.create_obj_instance(endpoint, '16/0', resources)['status'] == 'CREATED(201)' - shell._device.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) + dut.readlines_until(regex='.*net_lwm2m_rd_client: Update Done', timeout=5.0) assert leshan.write(endpoint, '16/0/0/0', 'test')['status'] == 'CHANGED(204)' assert leshan.read(endpoint, '16/0/0/0') == 'test' -def verify_LightweightM2M_1_1_int_229(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-229 - Read-Composite Operation") +def test_LightweightM2M_1_1_int_229(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-229 - Read-Composite Operation""" old_fmt = leshan.format for fmt in ['SENML_JSON', 'SENML_CBOR']: leshan.format = fmt @@ -346,8 +289,8 @@ def verify_LightweightM2M_1_1_int_229(shell: Shell, leshan: Leshan, endpoint: st assert resp[3][0][11][0] is not None leshan.format = old_fmt -def verify_LightweightM2M_1_1_int_230(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-230 - Write-Composite Operation") +def test_LightweightM2M_1_1_int_230(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-230 - Write-Composite Operation""" resources = { "/1/0/1": 60, "/1/0/6": True, @@ -376,6 +319,7 @@ def verify_LightweightM2M_1_1_int_230(shell: Shell, leshan: Leshan, endpoint: st leshan.format = old_fmt def query_basic_in_senml(leshan: Leshan, endpoint: str, fmt: str): + """Querying basic information in one of the SenML formats""" old_fmt = leshan.format leshan.format = fmt verify_server_object(leshan.read(endpoint, '1')[1]) @@ -384,15 +328,16 @@ def query_basic_in_senml(leshan: Leshan, endpoint: str, fmt: str): assert leshan.read(endpoint, '3/0/11/0') == 0 leshan.format = old_fmt -def verify_LightweightM2M_1_1_int_231(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-231 - Querying basic information in SenML JSON format") +def test_LightweightM2M_1_1_int_231(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-231 - Querying basic information in SenML JSON format""" query_basic_in_senml(leshan, endpoint, 'SENML_JSON') -def verify_LightweightM2M_1_1_int_232(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-232 - Querying basic information in SenML CBOR format") +def test_LightweightM2M_1_1_int_232(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-232 - Querying basic information in SenML CBOR format""" query_basic_in_senml(leshan, endpoint, 'SENML_CBOR') def setting_basic_senml(shell: Shell, leshan: Leshan, endpoint: str, fmt: str): + """Setting basic information in one of the SenML formats""" old_fmt = leshan.format leshan.format = fmt resources = { @@ -412,51 +357,51 @@ def setting_basic_senml(shell: Shell, leshan: Leshan, endpoint: str, fmt: str): shell.exec_command('lwm2m write /1/0/6 -u8 0') leshan.format = old_fmt -def verify_LightweightM2M_1_1_int_233(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-233 - Setting basic information in SenML CBOR format") +def test_LightweightM2M_1_1_int_233(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-233 - Setting basic information in SenML CBOR format""" setting_basic_senml(shell, leshan, endpoint, 'SENML_CBOR') -def verify_LightweightM2M_1_1_int_234(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-234 - Setting basic information in SenML JSON format") +def test_LightweightM2M_1_1_int_234(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-234 - Setting basic information in SenML JSON format""" setting_basic_senml(shell, leshan, endpoint, 'SENML_JSON') -def verify_LightweightM2M_1_1_int_235(): +@pytest.mark.skip("Leshan does not allow reading root path") +def test_LightweightM2M_1_1_int_235(): """LightweightM2M-1.1-int-235 - Read-Composite Operation on root path""" - # Unsupported. Leshan does not allow this. -def verify_LightweightM2M_1_1_int_236(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-236 - Read-Composite - Partial Presence") +def test_LightweightM2M_1_1_int_236(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-236 - Read-Composite - Partial Presence""" resp = leshan.composite_read(endpoint, ['1/0', '/3/0/11/0', '/3339/0/5522', '/3353/0/6030']) assert resp[1][0][1] is not None assert resp[3][0][11][0] is not None assert len(resp) == 2 -def verify_LightweightM2M_1_1_int_237(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-237 - Read on Object without specifying Content-Type") +def test_LightweightM2M_1_1_int_237(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-237 - Read on Object without specifying Content-Type""" old_fmt = leshan.format leshan.format = None assert leshan.read(endpoint, '1')[1][0][1] is not None assert leshan.read(endpoint, '3')[3][0][0] == 'Zephyr' leshan.format = old_fmt -def verify_LightweightM2M_1_1_int_241(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-241 - Executable Resource: Rebooting the device") +def test_LightweightM2M_1_1_int_241(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-241 - Executable Resource: Rebooting the device""" leshan.execute(endpoint, '3/0/4') - shell._device.readlines_until(regex='.*DEVICE: REBOOT', timeout=5.0) - shell._device.readlines_until(regex='.*rd_client_event: Disconnected', timeout=5.0) + dut.readlines_until(regex='.*DEVICE: REBOOT', timeout=5.0) + dut.readlines_until(regex='.*rd_client_event: Disconnected', timeout=5.0) shell.exec_command(f'lwm2m start {endpoint} -b 0') - shell._device.readlines_until(regex='.*Registration Done', timeout=5.0) + dut.readlines_until(regex='.*Registration Done', timeout=5.0) assert leshan.get(f'/clients/{endpoint}') -def verify_LightweightM2M_1_1_int_256(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-256 - Write Operation Failure") +def test_LightweightM2M_1_1_int_256(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-256 - Write Operation Failure""" lines = shell.get_filtered_output(shell.exec_command('lwm2m read 1/0/0 -u16')) short_id = int(lines[0]) assert leshan.write(endpoint, '1/0/0', 123)['status'] == 'METHOD_NOT_ALLOWED(405)' assert leshan.read(endpoint, '1/0/0') == short_id -def verify_LightweightM2M_1_1_int_257(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-257 - Write-Composite Operation") +def test_LightweightM2M_1_1_int_257(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-257 - Write-Composite Operation""" resources = { "/1/0/2": 102, "/1/0/6": True, @@ -476,8 +421,8 @@ def verify_LightweightM2M_1_1_int_257(shell: Shell, leshan: Leshan, endpoint: st shell.exec_command('lwm2m write /1/0/2 -u32 1') leshan.format = old_fmt -def verify_LightweightM2M_1_1_int_260(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-260 - Discover Command") +def test_LightweightM2M_1_1_int_260(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-260 - Discover Command""" resp = leshan.discover(endpoint, '3') expected_keys = ['/3', '/3/0', '/3/0/1', '/3/0/2', '/3/0/3', '/3/0/4', '/3/0/6', '/3/0/7', '/3/0/8', '/3/0/9', '/3/0/11', '/3/0/16'] missing_keys = [key for key in expected_keys if key not in resp.keys()] @@ -505,8 +450,9 @@ def verify_LightweightM2M_1_1_int_260(shell: Shell, leshan: Leshan, endpoint: st assert len(missing_keys) == 0 assert len(resp) == len(expected_keys) -def verify_LightweightM2M_1_1_int_261(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-261 - Write-Attribute Operation on a multiple resource") +@pytest.mark.skip(reason="Leshan don't allow writing attributes to resource instance") +def test_LightweightM2M_1_1_int_261(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-261 - Write-Attribute Operation on a multiple resource""" resp = leshan.discover(endpoint, '3/0/11') logger.debug(resp) expected_keys = ['/3/0/11', '/3/0/11/0'] @@ -528,8 +474,8 @@ def verify_LightweightM2M_1_1_int_261(shell: Shell, leshan: Leshan, endpoint: st assert int(resp['/3/0/11/0']['epmin']) == 1 assert int(resp['/3/0/11/0']['epmax']) == 20 -def verify_LightweightM2M_1_1_int_280(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-280 - Successful Read-Composite Operation") +def test_LightweightM2M_1_1_int_280(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-280 - Successful Read-Composite Operation""" resp = leshan.composite_read(endpoint, ['/3/0/16', '/3/0/11/0', '/1/0']) logger.debug(resp) assert len(resp) == 2 @@ -542,141 +488,10 @@ def verify_LightweightM2M_1_1_int_280(shell: Shell, leshan: Leshan, endpoint: st assert resp[1][0][6] is False assert resp[1][0][7] == 'U' -def verify_LightweightM2M_1_1_int_281(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-281 - Partially Successful Read-Composite Operation") +def test_LightweightM2M_1_1_int_281(shell: Shell, leshan: Leshan, endpoint: str): + """LightweightM2M-1.1-int-281 - Partially Successful Read-Composite Operation""" resp = leshan.composite_read(endpoint, ['/1/0/1', '/1/0/7', '/1/0/8']) assert len(resp) == 1 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' - -def verify_LightweightM2M_1_1_int_401(shell: Shell, leshan: Leshan, endpoint: str): - logger.info("LightweightM2M-1.1-int-401 - UDP Channel Security - Pre-shared Key Mode") - lines = shell.get_filtered_output(shell.exec_command('lwm2m read 0/0/0 -s')) - host = lines[0] - assert 'coaps://' in host - lines = shell.get_filtered_output(shell.exec_command('lwm2m read 0/0/2 -u8')) - mode = int(lines[0]) - assert mode == 0 - resp = leshan.get(f'/clients/{endpoint}') - assert resp["secure"] - -def test_lwm2m_bootstrap_psk(shell: Shell, leshan, leshan_bootstrap): - try: - # Generate randon device id and password (PSK key) - endpoint = 'client_' + binascii.b2a_hex(os.urandom(1)).decode() - bs_passwd = ''.join(random.choice(string.ascii_lowercase) for i in range(16)) - passwd = ''.join(random.choice(string.ascii_lowercase) for i in range(16)) - - logger.debug('Endpoint: %s', endpoint) - logger.debug('Boostrap PSK: %s', binascii.b2a_hex(bs_passwd.encode()).decode()) - logger.debug('PSK: %s', binascii.b2a_hex(passwd.encode()).decode()) - - # Create device entries in Leshan and Bootstrap server - leshan_bootstrap.create_bs_device(endpoint, f'coaps://{LESHAN_IP}:{COAPS_PORT}', bs_passwd, passwd) - leshan.create_psk_device(endpoint, passwd) - - # Allow engine to start & stop once. - time.sleep(2) - - # - # Verify PSK security using Bootstrap - # - - # Write bootsrap server information and PSK keys - shell.exec_command(f'lwm2m write 0/0/0 -s coaps://{LESHAN_IP}:{BOOTSTRAP_COAPS_PORT}') - shell.exec_command('lwm2m write 0/0/1 -b 1') - shell.exec_command('lwm2m write 0/0/2 -u8 0') - shell.exec_command(f'lwm2m write 0/0/3 -s {endpoint}') - shell.exec_command(f'lwm2m write 0/0/5 -s {bs_passwd}') - shell.exec_command(f'lwm2m start {endpoint} -b 1') - - - # - # Bootstrap Interface test cases - # LightweightM2M-1.1-int-0 (included) - # LightweightM2M-1.1-int-401 (included) - verify_LightweightM2M_1_1_int_1(shell, leshan, endpoint) - - # - # Registration Interface test cases (using PSK security) - # - verify_LightweightM2M_1_1_int_102(shell, leshan, endpoint) - # skip, not implemented verify_LightweightM2M_1_1_int_103() - verify_LightweightM2M_1_1_int_104(shell, leshan, endpoint) - # skip, included in 109: verify_LightweightM2M_1_1_int_107(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_108(leshan, endpoint) - verify_LightweightM2M_1_1_int_109(shell, leshan, endpoint) - - # - # Device management & Service Enablement Interface test cases - # - verify_LightweightM2M_1_1_int_201(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_203(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_204(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_205(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_211(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_212(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_215(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_220(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_221(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_222(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_223(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_224(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_225(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_226(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_227(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_228(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_229(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_230(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_231(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_232(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_233(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_234(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_236(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_237(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_241(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_256(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_257(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_260(shell, leshan, endpoint) - # skip, not supported in Leshan, verify_LightweightM2M_1_1_int_261(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_280(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_281(shell, leshan, endpoint) - - shell.exec_command('lwm2m stop') - shell._device.readlines_until(regex=r'.*Deregistration success', timeout=10.0) - - finally: - # Remove device and bootstrap information - # Leshan does not accept non-secure connection if device information is provided with PSK - leshan.delete_device(endpoint) - leshan_bootstrap.delete_bs_device(endpoint) - -def test_lwm2m_nosecure(shell: Shell, leshan, helperclient): - - # Allow engine to start & stop once. - time.sleep(2) - - # Generate randon device id and password (PSK key) - endpoint = 'client_' + binascii.b2a_hex(os.urandom(1)).decode() - - # - # Registration Interface test cases (using Non-secure mode) - # - shell.exec_command(f'lwm2m write 0/0/0 -s coap://{LESHAN_IP}:{COAP_PORT}') - shell.exec_command('lwm2m write 0/0/1 -b 0') - shell.exec_command('lwm2m write 0/0/2 -u8 3') - shell.exec_command(f'lwm2m write 0/0/3 -s {endpoint}') - shell.exec_command('lwm2m create 1/0') - shell.exec_command('lwm2m write 0/0/10 -u16 1') - shell.exec_command('lwm2m write 1/0/0 -u16 1') - shell.exec_command('lwm2m write 1/0/1 -u32 86400') - shell.exec_command(f'lwm2m start {endpoint} -b 0') - shell._device.readlines_until(regex=f"RD Client started with endpoint '{endpoint}'", timeout=10.0) - - verify_LightweightM2M_1_1_int_101(shell, leshan, endpoint) - verify_LightweightM2M_1_1_int_105(shell, leshan, endpoint, helperclient) # needs no-security - - # All done - shell.exec_command('lwm2m stop') - shell._device.readlines_until(regex=r'.*Deregistration success', timeout=10.0) diff --git a/tests/net/lib/lwm2m/interop/pytest/test_nosec.py b/tests/net/lib/lwm2m/interop/pytest/test_nosec.py new file mode 100644 index 00000000000000..15ac98f2d1a7d6 --- /dev/null +++ b/tests/net/lib/lwm2m/interop/pytest/test_nosec.py @@ -0,0 +1,53 @@ +""" +Tests for No-security mode +########################## + +Copyright (c) 2023 Nordic Semiconductor ASA + +SPDX-License-Identifier: Apache-2.0 + +Test specification: +=================== +https://www.openmobilealliance.org/release/LightweightM2M/ETS/OMA-ETS-LightweightM2M-V1_1-20190912-D.pdf + + +This module contains only testcases that are able to run on non-secure mode. +""" + +import time +import logging +from leshan import Leshan + +from twister_harness import Shell +from twister_harness import DeviceAdapter + +logger = logging.getLogger(__name__) + +def test_LightweightM2M_1_1_int_101(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint_nosec: str): + """ + Verify that the client is registered. + Note that this MUST be the first testcase executed, otherwise it will fail to get the + correct log output. + """ + logger.info("LightweightM2M-1.1-int-101 - Initial Registration") + dut.readlines_until(regex='.*Registration Done', timeout=5.0) + assert leshan.get(f'/clients/{endpoint_nosec}') + +def test_LightweightM2M_1_1_int_105(shell: Shell, dut: DeviceAdapter, leshan: Leshan, endpoint_nosec: str, helperclient: object): + """ + Run testcase LightweightM2M-1.1-int-105 - Discarded Register Update + """ + logger.info("LightweightM2M-1.1-int-105 - Discarded Register Update") + status = leshan.get(f'/clients/{endpoint_nosec}') + if status["secure"]: + logger.debug("Skip, requires non-secure connection") + return + regid = status["registrationId"] + assert regid + # Fake unregister message + helperclient.delete(f'rd/{regid}', timeout=0.1) + helperclient.stop() + time.sleep(1) + shell.exec_command('lwm2m update') + dut.readlines_until(regex=r'.*Failed with code 4\.4', timeout=5.0) + dut.readlines_until(regex='.*Registration Done', timeout=10.0) diff --git a/tests/net/lib/lwm2m/interop/testcase.yaml b/tests/net/lib/lwm2m/interop/testcase.yaml index 93f3fc13b1cf32..1b9818e777e4b6 100644 --- a/tests/net/lib/lwm2m/interop/testcase.yaml +++ b/tests/net/lib/lwm2m/interop/testcase.yaml @@ -3,6 +3,8 @@ tests: harness: pytest timeout: 300 slow: true + harness_config: + pytest_dut_scope: module integration_platforms: - native_posix platform_allow: From cf6cd38a0c1d0f30d6edfb9e102205d71a376a1c Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Tue, 24 Oct 2023 17:01:55 +0300 Subject: [PATCH 6/6] 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: