diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0780ba7..38922b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,10 +95,22 @@ jobs: working-directory: app run: west twister --testsuite-root src -p native_sim --inline-logs -O /tmp/twister-out + - name: Install infra-test Python deps + run: pip install aiocoap pytest-asyncio + + - name: Run infra tests on native_sim + run: | + west twister \ + --testsuite-root modules/lib/zephlet/tests \ + -p native_sim --inline-logs \ + -O /tmp/twister-out-infra + - name: Upload twister artefacts on failure if: failure() uses: actions/upload-artifact@v4 with: name: twister-out - path: /tmp/twister-out + path: | + /tmp/twister-out + /tmp/twister-out-infra if-no-files-found: ignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..39ad826 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM ghcr.io/zephyrproject-rtos/zephyr-build:main + +USER root + +# Codegen runtime deps + pytest deps for the zephlet test suites. +# Keeping these in the image avoids a pip install on every twister +# invocation. The image's venv lives at /opt/python/venv and is +# owned by root, hence the explicit USER root above. +RUN pip install --quiet \ + aiocoap pytest pytest-asyncio \ + proto-schema-parser jinja2 pkl-python copier \ + tree-sitter tree-sitter-c diff --git a/Kconfig b/Kconfig index a951bd2..aed2d01 100644 --- a/Kconfig +++ b/Kconfig @@ -21,8 +21,20 @@ config ZEPHLETS_COAP select COAP select COAP_SERVER select COAP_URI_WILDCARD + select COAP_EXTENDED_OPTIONS_LEN help - Expose opted-in zephlets over CoAP (UDP). + Expose opted-in zephlets over CoAP (UDP). Extended option length + is required so URI-Path segments up to + `CONFIG_ZEPHLETS_COAP_MAX_SEGMENT_LEN` bytes survive + `coap_find_options` without truncation. + +config COAP_EXTENDED_OPTIONS_LEN_VALUE + # Leave headroom above CONFIG_ZEPHLETS_COAP_MAX_SEGMENT_LEN so the + # CoAP parser accepts segments slightly longer than what zephlet + # handlers will copy out; the handler then surfaces 4.04 for the + # overflow case. A flat 32 here would make the parser reject the + # frame before the handler ever runs. + default 64 if ZEPHLETS_COAP config ZEPHLETS_COAP_DTLS bool "DTLS for the CoAP frontend" @@ -30,6 +42,42 @@ config ZEPHLETS_COAP_DTLS help Wrap the CoAP frontend in DTLS (PSK). +config ZEPHLETS_COAP_MAX_SEGMENT_LEN + int "Maximum length of a CoAP URI segment under /zlet/" + depends on ZEPHLETS_COAP + default 32 + help + Stack-buffer size (excluding the null terminator) for each URI + segment that a zephlet's CoAP handler copies out of the request's + `COAP_OPTION_URI_PATH` options. Inbound segments longer than this + are rejected with 4.04. The reasonable floor is the longest + `name` used in `ZEPHLET_NEW(...)`; the floor for method segments + is the longest rpc name declared in any opted-in `.proto`. + +config ZEPHLETS_COAP_MAX_URI_SEGMENTS + int "Maximum number of CoAP URI path segments per request" + depends on ZEPHLETS_COAP + default 8 + help + Stack-array size handed to `coap_find_options` when a zephlet's + CoAP handler decodes a request's URI path. RPC dispatch needs + 4 segments (`zlet///`); the + `events/stats` form needs 5. The default leaves headroom for + future path shapes; requests with more segments than this are + rejected with 4.04. + +config ZEPHLETS_COAP_CMD_TIMEOUT_MS + int "Timeout for publishing a CoAP-originated call onto the command channel (ms)" + depends on ZEPHLETS_COAP + default 100 + help + Upper bound the CoAP handler is willing to wait for + `zbus_chan_pub()` on a zephlet's `command` channel before + surfacing 5.03 to the caller. Only the channel mutex + contention is relevant — the sync listener runs in the + publisher's own thread, so this never times out a handler + that is making forward progress. + module = ZEPHLET module-str = zephlet source "subsys/logging/Kconfig.template.log_config" diff --git a/codegen/templates/zephlet_coap_interface.c.jinja b/codegen/templates/zephlet_coap_interface.c.jinja index 8d39090..08850be 100644 --- a/codegen/templates/zephlet_coap_interface.c.jinja +++ b/codegen/templates/zephlet_coap_interface.c.jinja @@ -1,29 +1,51 @@ /* GENERATED FILE — DO NOT EDIT. Source: {{ prefix }}.proto. */ {% if coap_opt_in %} +#include #include +#include +#include #include +#include +#include +#include #include +#include + +#include +#include #include "zephlet.h" +#include "zephlet_coap_consts.h" +#include "zephlet_coap_send.h" #include "zephlet_coap_types.h" +#include "zephlet_coap_uri.h" #include "{{ prefix }}_interface.h" #include "{{ type_snake }}/{{ prefix }}.pb.h" /** * @file - * @brief CoAP frontend descriptor for the `{{ type_snake }}` zephlet type. + * @brief CoAP frontend for the `{{ type_snake }}` zephlet type. + * + * Compiled only when `CONFIG_ZEPHLETS_COAP=y`. Owns: + * - the per-method descriptor table walked by discovery code, + * - the `zephlet_coap_type` section-iterable record, + * - the type's `COAP_RESOURCE_DEFINE` registered against + * `zlet_coap_service` at path `/zlet/{{ type_snake }}/#`, + * - the POST handler that proto-decodes the request, runs the + * `{{ type_snake }}` dispatch trampoline via zbus, and proto-encodes + * the response. * - * Compiled only when `CONFIG_ZEPHLETS_COAP=y` (CMake gate). Emits one - * per-method descriptor table and a single section-iterable - * `zephlet_coap_type` record that the Phase 3 catch-all dispatcher - * walks. The `_{{ type_snake }}_coap_event_cb` body is a stub here; - * Phase 5 fills in the per-observer CoAP Observe send loop. + * The handler's stack frame is sized by the compiler from the typed + * per-method locals in each `if (strcmp(...))` branch — there is no + * cross-method scratch and no opaque buffer. */ +LOG_MODULE_DECLARE(zlet_coap); + static const struct zephlet_coap_method {{ type_snake }}_coap_methods[] = { - {% for cmd in commands %} +{% for cmd in commands %} { .path_segment = "{{ cmd.name }}", .method_id = {{ cmd.method_id }}, @@ -32,7 +54,7 @@ static const struct zephlet_coap_method {{ type_snake }}_coap_methods[] = { .req_max_size = {% if cmd.req_is_empty %}0{% else %}{{ cmd.req_c_name|upper }}_SIZE{% endif %}, .resp_max_size = {% if cmd.resp_is_empty %}0{% else %}{{ cmd.resp_c_name|upper }}_SIZE{% endif %}, }, - {% endfor %} +{% endfor %} }; const STRUCT_SECTION_ITERABLE(zephlet_coap_type, {{ type_snake }}_coap_type) = { @@ -47,14 +69,117 @@ void _{{ type_snake }}_coap_event_cb(const struct zephlet *z, { ARG_UNUSED(z); ARG_UNUSED(ev); - /* Phase 5: walk Zephyr's global observer list filtered by - * `observer->user_data == z` and send one NON per match. */ } + +static int {{ type_snake }}_resolve(struct coap_packet *req, const struct zephlet **z_out, + char *method_buf, size_t method_buf_size) +{ + struct coap_option opts[CONFIG_ZEPHLETS_COAP_MAX_URI_SEGMENTS]; + size_t n; + int err = zephlet_coap_parse_uri_path(req, opts, ARRAY_SIZE(opts), &n); + if (err != 0 || n != 4) { + return -ENODEV; + } + + /* opts[0] == "zlet" and opts[1] == "{{ type_snake }}" are guaranteed + * by the resource path match. Instance and method are the variables. */ + char instance[CONFIG_ZEPHLETS_COAP_MAX_SEGMENT_LEN + 1]; + if (zephlet_coap_option_to_cstr(&opts[2], instance, sizeof(instance)) != 0) { + return -ENODEV; + } + if (zephlet_coap_option_to_cstr(&opts[3], method_buf, method_buf_size) != 0) { + return -ENODEV; + } + + const struct zephlet *z = zephlet_get_by_name(instance); + if (z == NULL || z->api != &{{ type_snake }}_api) { + return -ENODEV; + } + *z_out = z; + return 0; +} + +static int {{ type_snake }}_coap_rpc_post(struct coap_resource *res, struct coap_packet *req, + struct sockaddr *addr, socklen_t addr_len) +{ + const struct zephlet *z; + char method[CONFIG_ZEPHLETS_COAP_MAX_SEGMENT_LEN + 1]; + + int err = {{ type_snake }}_resolve(req, &z, method, sizeof(method)); + if (err != 0) { + return zephlet_coap_send_error(res, req, addr, addr_len, err); + } + +{% for cmd in commands %} + if (strcmp(method, "{{ cmd.name }}") == 0) { +{% if not cmd.req_is_empty %} + uint16_t body_len; + const uint8_t *body = coap_packet_get_payload(req, &body_len); + struct {{ cmd.req_c_name }} req_struct; + pb_istream_t in = pb_istream_from_buffer(body, body_len); + if (!pb_decode(&in, &{{ cmd.req_desc }}, &req_struct)) { + return zephlet_coap_send_error(res, req, addr, addr_len, -EINVAL); + } +{% endif %} +{% if not cmd.resp_is_empty %} + struct {{ cmd.resp_c_name }} resp = {0}; + uint8_t payload[{{ cmd.resp_c_name|upper }}_SIZE]; +{% endif %} + struct zephlet_call call = { + .method_id = {{ cmd.method_id }}, +{% if not cmd.req_is_empty %} + .req_desc = &{{ cmd.req_desc }}, + .req = &req_struct, +{% endif %} +{% if not cmd.resp_is_empty %} + .resp_desc = &{{ cmd.resp_desc }}, + .resp = &resp, +{% endif %} + }; + struct zephlet_call *cp = &call; + int pub = zbus_chan_pub(z->channel.command, &cp, + K_MSEC(CONFIG_ZEPHLETS_COAP_CMD_TIMEOUT_MS)); + if (pub != 0) { + return zephlet_coap_send_error(res, req, addr, addr_len, -EBUSY); + } + if (call.return_code != 0) { + return zephlet_coap_send_error(res, req, addr, addr_len, + call.return_code); + } +{% if cmd.resp_is_empty %} + return zephlet_coap_send_response(res, req, addr, addr_len, 0, NULL, 0); +{% else %} + pb_ostream_t out = pb_ostream_from_buffer(payload, sizeof(payload)); + if (!pb_encode(&out, &{{ cmd.resp_desc }}, &resp)) { + return zephlet_coap_send_error(res, req, addr, addr_len, -ENOMEM); + } + return zephlet_coap_send_response(res, req, addr, addr_len, 0, payload, + out.bytes_written); +{% endif %} + } +{% endfor %} + + return zephlet_coap_send_error(res, req, addr, addr_len, -ENOSYS); +} + +static int {{ type_snake }}_coap_get(struct coap_resource *res, struct coap_packet *req, + struct sockaddr *addr, socklen_t addr_len) +{ + return zephlet_coap_send_error(res, req, addr, addr_len, -ENOSYS); +} + +static const char *const {{ type_snake }}_coap_resource_path[] = { + "zlet", "{{ type_snake }}", "#", NULL, +}; + +COAP_RESOURCE_DEFINE({{ type_snake }}_coap_resource, zlet_coap_service, { + .path = {{ type_snake }}_coap_resource_path, + .post = {{ type_snake }}_coap_rpc_post, + .get = {{ type_snake }}_coap_get, +}); {% else %} /* * `{{ prefix }}.proto` does not opt in to the CoAP frontend, so this - * translation unit emits no symbols. The CMake gate compiles this file - * unconditionally under `CONFIG_ZEPHLETS_COAP=y`; an empty TU contributes - * zero to the section-hash gate. + * translation unit emits no symbols. */ {% endif %} diff --git a/codegen/templates/zephlet_coap_interface.h.jinja b/codegen/templates/zephlet_coap_interface.h.jinja index e975cb6..74111b3 100644 --- a/codegen/templates/zephlet_coap_interface.h.jinja +++ b/codegen/templates/zephlet_coap_interface.h.jinja @@ -27,8 +27,9 @@ /** * @brief Event-channel observer callback for `{{ type_snake }}` instances. * - * Defined in `{{ prefix }}_coap_interface.c`. Phase 1 emits a stub body; - * Phase 5 fills in the per-observer CoAP Observe send logic. + * Defined in `{{ prefix }}_coap_interface.c`. The body is a stub today; + * the events-bridge implementation will fill in the per-observer CoAP + * Observe send logic. */ void _{{ type_snake }}_coap_event_cb(const struct zephlet *z, const struct {{ type_snake }}_events *ev); diff --git a/codegen/zephyr_zephlet_codegen.cmake b/codegen/zephyr_zephlet_codegen.cmake index 70c9b4e..7f4b320 100644 --- a/codegen/zephyr_zephlet_codegen.cmake +++ b/codegen/zephyr_zephlet_codegen.cmake @@ -82,4 +82,18 @@ function(zephyr_zephlet_generate) zephyr_library_sources("${CMAKE_CURRENT_SOURCE_DIR}/${_src}") endforeach() add_dependencies(${ZEPHYR_CURRENT_LIBRARY} ${_zg_PREFIX}_codegen) + + # The user's `app` library also #includes the generated _interface.h + # transitively (via the user's .h). Without an explicit dependency, + # ninja may try to compile `main.c` before the codegen finishes, which + # surfaces on slower hosts as a missing-header build error. Defer the + # `add_dependencies` call so it runs after the user's CMakeLists has + # declared the `app` target. + # + # `DEFER CALL` evaluates variable references at the time the deferred call + # runs, when `_zg_PREFIX` is already out of scope. Use `EVAL CODE` to + # substitute the prefix into the deferred command at registration time. + cmake_language(EVAL CODE + "cmake_language(DEFER DIRECTORY \${CMAKE_SOURCE_DIR} \ + CALL add_dependencies app ${_zg_PREFIX}_codegen)") endfunction() diff --git a/frontends/coap/CMakeLists.txt b/frontends/coap/CMakeLists.txt index 09d3daa..08b7abd 100644 --- a/frontends/coap/CMakeLists.txt +++ b/frontends/coap/CMakeLists.txt @@ -3,6 +3,8 @@ if(CONFIG_ZEPHLETS_COAP) zephyr_library_sources( zephlet_coap_frontend.c zephlet_coap_translate.c + zephlet_coap_send.c + zephlet_coap_uri.c ) zephyr_include_directories(include) endif() diff --git a/frontends/coap/include/zephlet_coap_send.h b/frontends/coap/include/zephlet_coap_send.h new file mode 100644 index 0000000..414bcf0 --- /dev/null +++ b/frontends/coap/include/zephlet_coap_send.h @@ -0,0 +1,73 @@ +#ifndef ZEPHLET_FRONTENDS_COAP_SEND_H +#define ZEPHLET_FRONTENDS_COAP_SEND_H + +#include +#include + +#include +#include + +/** + * @file + * @brief Shared CoAP response helpers for the zephlet frontend. + * + * Type-agnostic helpers used by every codegen-emitted per-type RPC + * handler. They own the CoAP packet construction (response init, + * token/id propagation from the request, code mapping, errno option, + * Content-Format option, payload) so the per-type handler can stay + * focused on the proto decode/encode and the zbus publish. + */ + +/** + * @brief Build and send a CoAP response carrying just a result code. + * + * Maps @p rc to a CoAP response code via `zephlet_coap_map_return_code`, + * appends the verbatim-errno option (number `ZEPHLET_COAP_OPT_ERRNO`) + * carrying @p rc as `uint32_t`, and sends. No payload, no + * Content-Format option. + * + * @param res The matched `coap_resource` from the inbound + * handler signature. + * @param req The inbound request packet (token + message id are + * copied into the response). + * @param addr Inbound peer address (forwarded to + * `coap_resource_send`). + * @param addr_len Length of @p addr. + * @param rc Handler return code. `rc == 0` produces 2.04 + * Changed; negative errno is mapped per the table in + * `zephlet_coap_map_return_code`. + * + * @return 0 on success; negative errno from the underlying CoAP + * primitives on failure. + */ +int zephlet_coap_send_error(struct coap_resource *res, struct coap_packet *req, + struct sockaddr *addr, socklen_t addr_len, int rc); + +/** + * @brief Build and send a CoAP response carrying a result code plus + * payload bytes. + * + * @p rc == 0 + @p payload_len > 0 produces 2.05 Content with a + * Content-Format option set to `ZEPHLET_COAP_CT_NANOPB`. @p rc == 0 + * with @p payload_len == 0 collapses to the no-payload `send_error` + * equivalent (2.04 Changed). Negative @p rc is treated the same as + * `send_error`, i.e. the payload is dropped. + * + * The verbatim-errno option is always appended. + * + * @param res Matched `coap_resource`. + * @param req Inbound request packet. + * @param addr Inbound peer address. + * @param addr_len Length of @p addr. + * @param rc Handler return code. + * @param payload Encoded payload bytes (may be NULL when + * @p payload_len == 0). + * @param payload_len Number of payload bytes. + * + * @return 0 on success; negative errno on failure. + */ +int zephlet_coap_send_response(struct coap_resource *res, struct coap_packet *req, + struct sockaddr *addr, socklen_t addr_len, int rc, + const uint8_t *payload, size_t payload_len); + +#endif /* ZEPHLET_FRONTENDS_COAP_SEND_H */ diff --git a/frontends/coap/include/zephlet_coap_uri.h b/frontends/coap/include/zephlet_coap_uri.h new file mode 100644 index 0000000..562dce8 --- /dev/null +++ b/frontends/coap/include/zephlet_coap_uri.h @@ -0,0 +1,52 @@ +#ifndef ZEPHLET_FRONTENDS_COAP_URI_H +#define ZEPHLET_FRONTENDS_COAP_URI_H + +#include +#include + +#include + +/** + * @file + * @brief URI-path helpers for codegen-emitted zephlet CoAP handlers. + * + * Allocation-free; works against caller-provided `struct coap_option` + * storage. Segment values are NUL-terminated into caller stack buffers + * so the handler body can `strcmp` directly. + */ + +/** + * @brief Decode a request's URI-path into a caller-provided option array. + * + * Thin wrapper around `coap_find_options(COAP_OPTION_URI_PATH, ...)` + * with bounds-checking. The caller sizes @p opts at compile time; + * `CONFIG_ZEPHLETS_COAP_MAX_URI_SEGMENTS` is the standard ceiling. + * + * @param req Inbound request packet. + * @param opts Caller-provided option storage. + * @param max Capacity of @p opts. + * @param count Set to the number of segments found (only when the + * function returns 0). + * + * @return 0 on success; + * `-E2BIG` if @p req carries more URI segments than @p max; + * negative errno on transport failure. + */ +int zephlet_coap_parse_uri_path(struct coap_packet *req, struct coap_option *opts, size_t max, + size_t *count); + +/** + * @brief Copy a `coap_option` value into a NUL-terminated C-string buffer. + * + * @param opt Parsed URI-path option. + * @param buf Destination buffer. + * @param buf_size Size of @p buf in bytes (must accommodate + * `opt->len + 1`). + * + * @return 0 on success; + * `-EINVAL` on NULL inputs or zero-sized buffer; + * `-ENOBUFS` if `opt->len + 1 > buf_size` (caller surfaces 4.04). + */ +int zephlet_coap_option_to_cstr(const struct coap_option *opt, char *buf, size_t buf_size); + +#endif /* ZEPHLET_FRONTENDS_COAP_URI_H */ diff --git a/frontends/coap/zephlet_coap_frontend.c b/frontends/coap/zephlet_coap_frontend.c index f9cec91..b95c37b 100644 --- a/frontends/coap/zephlet_coap_frontend.c +++ b/frontends/coap/zephlet_coap_frontend.c @@ -1,9 +1,22 @@ +#include + +#include +#include +#include + +LOG_MODULE_REGISTER(zlet_coap, CONFIG_ZEPHLET_LOG_LEVEL); + /** * @file - * @brief CoAP frontend runtime — translation unit placeholder. + * @brief CoAP frontend runtime — service declaration. * - * Compiled only when `CONFIG_ZEPHLETS_COAP=y`. The frontend has no - * runtime symbols yet; this TU exists so the library has a source to - * link and downstream consumers can `#include ` - * from `frontends/coap/include/` without a separate library target. + * Owns the `coap_service` instance every codegen-emitted per-type + * resource registers against. Each opted-in zephlet type contributes + * a `COAP_RESOURCE_DEFINE(_coap_resource, zlet_coap_service, ...)` + * from its `_coap_interface.c`; this TU has no per-type + * knowledge. */ + +static uint16_t zlet_coap_port = 5683; + +COAP_SERVICE_DEFINE(zlet_coap_service, "0.0.0.0", &zlet_coap_port, COAP_SERVICE_AUTOSTART); diff --git a/frontends/coap/zephlet_coap_send.c b/frontends/coap/zephlet_coap_send.c new file mode 100644 index 0000000..b299b6b --- /dev/null +++ b/frontends/coap/zephlet_coap_send.c @@ -0,0 +1,116 @@ +#include "zephlet_coap_send.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include "zephlet_coap_consts.h" +#include "zephlet_coap_translate.h" + +LOG_MODULE_DECLARE(zlet_coap); + +/** + * @file + * @brief CoAP response builder shared by every per-type zephlet handler. + * + * The two entrypoints construct a response packet on the handler's + * stack, carry token + message id from the request, map the handler + * return code to a CoAP code, attach the verbatim-errno option, and + * optionally append a payload + Content-Format. They then forward the + * packet to `coap_resource_send`. + */ + +/* Response buffer size: matches Zephyr's coap_service convention so + * the dispatcher's stack frame uses the same allocator-friendly width + * as samples and other server resources. */ +#define ZEPHLET_COAP_SEND_BUF_SIZE CONFIG_COAP_SERVER_MESSAGE_SIZE + +static int build_response_header(const struct coap_packet *req, uint8_t *buf, size_t buf_size, + uint8_t code, struct coap_packet *out) +{ + uint8_t token[COAP_TOKEN_MAX_LEN]; + uint8_t tkl = coap_header_get_token(req, token); + uint16_t id = coap_header_get_id(req); + uint8_t req_type = coap_header_get_type(req); + uint8_t resp_type = (req_type == COAP_TYPE_CON) ? COAP_TYPE_ACK : COAP_TYPE_NON_CON; + + return coap_packet_init(out, buf, buf_size, COAP_VERSION_1, resp_type, tkl, token, code, + id); +} + +static int append_errno_option(struct coap_packet *cpkt, int rc) +{ + return coap_append_option_int(cpkt, ZEPHLET_COAP_OPT_ERRNO, (uint32_t)rc); +} + +int zephlet_coap_send_error(struct coap_resource *res, struct coap_packet *req, + struct sockaddr *addr, socklen_t addr_len, int rc) +{ + uint8_t buf[ZEPHLET_COAP_SEND_BUF_SIZE]; + struct coap_packet response; + uint8_t code = zephlet_coap_map_return_code(rc, false); + + int err = build_response_header(req, buf, sizeof(buf), code, &response); + if (err < 0) { + LOG_WRN("coap_packet_init failed: %d", err); + return err; + } + + err = append_errno_option(&response, rc); + if (err < 0) { + LOG_WRN("append errno option failed: %d", err); + return err; + } + + return coap_resource_send(res, &response, addr, addr_len, NULL); +} + +int zephlet_coap_send_response(struct coap_resource *res, struct coap_packet *req, + struct sockaddr *addr, socklen_t addr_len, int rc, + const uint8_t *payload, size_t payload_len) +{ + if (rc != 0 || payload_len == 0) { + return zephlet_coap_send_error(res, req, addr, addr_len, rc); + } + + uint8_t buf[ZEPHLET_COAP_SEND_BUF_SIZE]; + struct coap_packet response; + uint8_t code = zephlet_coap_map_return_code(rc, true); + + int err = build_response_header(req, buf, sizeof(buf), code, &response); + if (err < 0) { + LOG_WRN("coap_packet_init failed: %d", err); + return err; + } + + err = append_errno_option(&response, rc); + if (err < 0) { + LOG_WRN("append errno option failed: %d", err); + return err; + } + + err = coap_append_option_int(&response, COAP_OPTION_CONTENT_FORMAT, ZEPHLET_COAP_CT_NANOPB); + if (err < 0) { + LOG_WRN("append content-format option failed: %d", err); + return err; + } + + err = coap_packet_append_payload_marker(&response); + if (err < 0) { + LOG_WRN("append payload marker failed: %d", err); + return err; + } + + err = coap_packet_append_payload(&response, (uint8_t *)payload, (uint16_t)payload_len); + if (err < 0) { + LOG_WRN("append payload failed: %d", err); + return err; + } + + return coap_resource_send(res, &response, addr, addr_len, NULL); +} diff --git a/frontends/coap/zephlet_coap_uri.c b/frontends/coap/zephlet_coap_uri.c new file mode 100644 index 0000000..2834592 --- /dev/null +++ b/frontends/coap/zephlet_coap_uri.c @@ -0,0 +1,53 @@ +#include "zephlet_coap_uri.h" + +#include +#include +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(zlet_coap); + +/** + * @file + * @brief URI-path helpers for zephlet CoAP handlers. + */ + +int zephlet_coap_parse_uri_path(struct coap_packet *req, struct coap_option *opts, size_t max, + size_t *count) +{ + if (req == NULL || opts == NULL || count == NULL) { + return -EINVAL; + } + if (max == 0 || max > UINT16_MAX) { + return -EINVAL; + } + + int n = coap_find_options(req, COAP_OPTION_URI_PATH, opts, (uint16_t)max); + if (n < 0) { + return n; + } + if ((size_t)n > max) { + return -E2BIG; + } + + *count = (size_t)n; + return 0; +} + +int zephlet_coap_option_to_cstr(const struct coap_option *opt, char *buf, size_t buf_size) +{ + if (opt == NULL || buf == NULL || buf_size == 0) { + return -EINVAL; + } + if ((size_t)opt->len + 1U > buf_size) { + return -ENOBUFS; + } + + memcpy(buf, opt->value, opt->len); + buf[opt->len] = '\0'; + return 0; +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..85a7f29 --- /dev/null +++ b/justfile @@ -0,0 +1,43 @@ +# Zephlet infra — dev tasks. Run from this directory (modules/lib/zephlet/). + +docker_image := "zephlet-tester:latest" + +# Workspace root is three levels up from this justfile (the west topdir +# that contains `.west/`, `zephyr/`, `ports_adapters_zbus/`, and the +# `modules/lib/zephlet/` infra checkout). +workspace_root := justfile_directory() + "/../../.." + +default: + @just --list + +# Build the local image baked with codegen + pytest Python deps. +# Re-run whenever the upstream base image or the pinned deps change. +# Recent Docker Desktop prints a legacy-builder deprecation notice; +# install the `buildx` plugin (Docker Desktop → Settings → Builders, +# or `brew install docker-buildx`) to silence it. The legacy builder +# still works otherwise. +docker-build: + docker build -t {{ docker_image }} {{ justfile_directory() }} + +# Run the full test suite inside the local image. `platform` defaults +# to native_sim/native/64 (the variant aarch64 hosts require); pass +# `native_sim` explicitly for x86_64 hosts. +docker-test platform="native_sim/native/64": + docker run --rm -u root \ + --cap-add=NET_ADMIN --device=/dev/net/tun \ + -v {{ workspace_root }}:/workdir -w /workdir \ + {{ docker_image }} \ + west twister \ + --testsuite-root modules/lib/zephlet/tests \ + -p {{ platform }} \ + -O /tmp/twister-out \ + --inline-logs + +# Drop into an interactive shell inside the image with the workspace +# mounted — useful for debugging a single failing case. +docker-shell: + docker run --rm -ti -u root \ + --cap-add=NET_ADMIN --device=/dev/net/tun \ + -v {{ workspace_root }}:/workdir -w /workdir \ + {{ docker_image }} \ + bash diff --git a/tests/coap_functional/CMakeLists.txt b/tests/coap_functional/CMakeLists.txt index 0e965b4..63f2b4b 100644 --- a/tests/coap_functional/CMakeLists.txt +++ b/tests/coap_functional/CMakeLists.txt @@ -2,19 +2,39 @@ cmake_minimum_required(VERSION 3.20.0) get_filename_component(ZEPHLET_INFRA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) -list(APPEND EXTRA_ZEPHYR_MODULES "${ZEPHLET_INFRA_ROOT}") +set_property(GLOBAL PROPERTY PROTO_FILES_LIST) + +list(APPEND EXTRA_ZEPHYR_MODULES + "${ZEPHLET_INFRA_ROOT}" + "${CMAKE_CURRENT_SOURCE_DIR}/zephlets/tick" + "${CMAKE_CURRENT_SOURCE_DIR}/zephlets/ui" +) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(zephlet_coap_functional_test) +list(APPEND CMAKE_MODULE_PATH ${ZEPHYR_BASE}/modules/nanopb) +include(nanopb) +set(NANOPB_OPTIONS "--c-style") + +get_property(LOCAL_PROTO_FILES_LIST GLOBAL PROPERTY PROTO_FILES_LIST) + +# Anchor nanopb output under tests/coap_functional/zephlets/ so each +# proto lands at ${CMAKE_BINARY_DIR}//.pb.h, matching the +# `#include "/.pb.h"` pattern that codegen emits. +set(_saved_csd ${CMAKE_CURRENT_SOURCE_DIR}) +set(CMAKE_CURRENT_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/zephlets") +zephyr_nanopb_sources(app ${LOCAL_PROTO_FILES_LIST}) +set(CMAKE_CURRENT_SOURCE_DIR ${_saved_csd}) + target_sources(app PRIVATE src/main.c) -# Each COAP_SERVICE_DEFINE produces a custom-named iterable section -# (`coap_resource_`); the app owns its linker registration. The LD -# template covers the pre-link pass; zephyr_iterable_section covers the -# CMake linker generator path. +# `COAP_SERVICE_DEFINE(zlet_coap_service, ...)` declares an iterable +# section named `coap_resource_zlet_coap_service`. The test app owns +# the linker-side registration: sections-ram.ld covers the pre-link +# pass, zephyr_iterable_section covers the CMake linker generator path. zephyr_linker_sources(DATA_SECTIONS sections-ram.ld) zephyr_iterable_section( - NAME coap_resource_zlet_coap_test_service + NAME coap_resource_zlet_coap_service GROUP DATA_REGION ${XIP_ALIGN_WITH_INPUT}) diff --git a/tests/coap_functional/prj.conf b/tests/coap_functional/prj.conf index 7edd830..a42dc2f 100644 --- a/tests/coap_functional/prj.conf +++ b/tests/coap_functional/prj.conf @@ -3,23 +3,26 @@ CONFIG_ZEPHLETS=y CONFIG_ZEPHLETS_COAP=y CONFIG_NANOPB=y +CONFIG_ZEPHLET_TICK=y +CONFIG_ZEPHLET_UI=y + CONFIG_LOG=y CONFIG_ASSERT=y CONFIG_TEST_RANDOM_GENERATOR=y -CONFIG_NETWORKING=y -CONFIG_NET_IPV4=y -CONFIG_NET_UDP=y +# Offloaded sockets driver: the Zephyr binary forwards socket calls to +# the host BSD stack via the native simulator. The pytest fixture +# reaches the guest CoAP server at 127.0.0.1:5683 with no TAP setup, +# no root, and no host-side networking configuration. Works the same +# under bare Linux, Docker, and Colima. +CONFIG_NET_DRIVERS=y CONFIG_NET_SOCKETS=y -CONFIG_NET_L2_ETHERNET=y -CONFIG_ETH_NATIVE_TAP=y - -CONFIG_NET_CONFIG_SETTINGS=y -CONFIG_NET_CONFIG_AUTO_INIT=y -CONFIG_NET_CONFIG_NEED_IPV4=y -CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.0.2.1" -CONFIG_NET_CONFIG_PEER_IPV4_ADDR="192.0.2.2" +CONFIG_NET_SOCKETS_OFFLOAD=y +CONFIG_NET_NATIVE_OFFLOADED_SOCKETS=y +CONFIG_NET_IPV6_DAD=n CONFIG_COAP=y CONFIG_COAP_SERVER=y CONFIG_COAP_URI_WILDCARD=y + +CONFIG_MAIN_STACK_SIZE=2048 diff --git a/tests/coap_functional/pytest/conftest.py b/tests/coap_functional/pytest/conftest.py index 0a85951..c992df8 100644 --- a/tests/coap_functional/pytest/conftest.py +++ b/tests/coap_functional/pytest/conftest.py @@ -1,9 +1,12 @@ """Pytest fixtures for the zephlet CoAP functional harness. -Twister runs the Zephyr binary on `native_sim` with `eth_native_tap`; -the host-side TAP endpoint reaches the guest at `192.0.2.1:5683`. This -fixture provides an `aiocoap` client targeting that endpoint, with a -startup wait so the guest has time to bind the port. +The Zephyr binary runs on `native_sim` with the native-simulator +offloaded sockets driver (`CONFIG_NET_NATIVE_OFFLOADED_SOCKETS=y`), +so its `socket()`/`bind()` calls forward into the host BSD stack and +the CoAP server listens on a real host socket. The pytest fixture +reaches the guest at `127.0.0.1:5683` with no TAP setup, no root, +and no host-side networking config — works identically on bare +Linux, Docker, and Colima. """ from __future__ import annotations @@ -17,7 +20,7 @@ from aiocoap import GET, Context, Message from aiocoap.error import NetworkError -COAP_HOST = os.environ.get("ZEPHLET_COAP_HOST", "192.0.2.1") +COAP_HOST = os.environ.get("ZEPHLET_COAP_HOST", "127.0.0.1") COAP_PORT = int(os.environ.get("ZEPHLET_COAP_PORT", "5683")) READY_TIMEOUT_S = float(os.environ.get("ZEPHLET_COAP_READY_TIMEOUT_S", "20")) READY_POLL_INTERVAL_S = 0.5 diff --git a/tests/coap_functional/pytest/test_rpc.py b/tests/coap_functional/pytest/test_rpc.py new file mode 100644 index 0000000..cd1afc3 --- /dev/null +++ b/tests/coap_functional/pytest/test_rpc.py @@ -0,0 +1,102 @@ +"""Functional cases for the zephlet CoAP RPC dispatch path. + +The Zephyr binary boots a `tick_fast` instance of the `tick` type and a +`ui_main` instance of the `ui` type, both opted into CoAP. The +codegen-emitted per-type resources sit at `/zlet/tick/#` and `/zlet/ui/#`. + +The cases exercise: + - the happy POST path with a non-empty response envelope (`tick.start` + returns `Lifecycle.Status`), + - five unhappy paths bracketing the routing + decode error map, + - the GET stub that returns 4.05 until the events bridge lands. +""" + +from __future__ import annotations + +import pytest +from aiocoap import GET, POST, BAD_REQUEST, CHANGED, CONTENT, METHOD_NOT_ALLOWED, NOT_FOUND, Message +from twister_harness import DeviceAdapter + + +def _post(host: str, port: int, path: str, payload: bytes = b"") -> Message: + return Message(code=POST, uri=f"coap://{host}:{port}/{path}", payload=payload) + + +def _get(host: str, port: int, path: str) -> Message: + return Message(code=GET, uri=f"coap://{host}:{port}/{path}") + + +@pytest.mark.asyncio +async def test_happy_post_returns_2_05(dut: DeviceAdapter, aiocoap_client, coap_endpoint): + host, port = coap_endpoint + req = _post(host, port, "zlet/tick/tick_fast/start") + resp = await aiocoap_client.request(req).response + assert resp.code in (CHANGED, CONTENT), f"expected 2.04/2.05, got {resp.code}" + # tick.start returns a non-empty Lifecycle.Status, so the dispatcher + # always upgrades to 2.05 Content when encode produces bytes. + assert resp.code == CONTENT, f"expected 2.05 Content, got {resp.code}" + assert resp.payload, "expected a non-empty protobuf payload" + + +@pytest.mark.asyncio +async def test_unknown_type_returns_4_04(dut: DeviceAdapter, aiocoap_client, coap_endpoint): + host, port = coap_endpoint + req = _post(host, port, "zlet/zoo/tick_fast/start") + resp = await aiocoap_client.request(req).response + assert resp.code == NOT_FOUND, f"expected 4.04, got {resp.code}" + + +@pytest.mark.asyncio +async def test_unknown_instance_returns_4_04(dut: DeviceAdapter, aiocoap_client, coap_endpoint): + host, port = coap_endpoint + req = _post(host, port, "zlet/tick/no_such/start") + resp = await aiocoap_client.request(req).response + assert resp.code == NOT_FOUND, f"expected 4.04, got {resp.code}" + + +@pytest.mark.asyncio +async def test_api_mismatch_returns_4_04(dut: DeviceAdapter, aiocoap_client, coap_endpoint): + host, port = coap_endpoint + # /zlet/ui/tick_fast/start: ui resource matches, ui handler resolves + # instance "tick_fast" (a tick), api check rejects. + req = _post(host, port, "zlet/ui/tick_fast/start") + resp = await aiocoap_client.request(req).response + assert resp.code == NOT_FOUND, f"expected 4.04, got {resp.code}" + + +@pytest.mark.asyncio +async def test_unknown_method_returns_4_05(dut: DeviceAdapter, aiocoap_client, coap_endpoint): + host, port = coap_endpoint + req = _post(host, port, "zlet/tick/tick_fast/dance") + resp = await aiocoap_client.request(req).response + assert resp.code == METHOD_NOT_ALLOWED, f"expected 4.05, got {resp.code}" + + +@pytest.mark.asyncio +async def test_malformed_body_returns_4_00(dut: DeviceAdapter, aiocoap_client, coap_endpoint): + host, port = coap_endpoint + # tick.config decodes into struct tick_config (two uint32 fields). + # An all-0xFF body starts an infinite-continuation varint that nanopb + # rejects with a decode error. + req = _post(host, port, "zlet/tick/tick_fast/config", payload=b"\xff" * 10) + resp = await aiocoap_client.request(req).response + assert resp.code == BAD_REQUEST, f"expected 4.00, got {resp.code}" + + +@pytest.mark.asyncio +async def test_oversized_segment_returns_4_04(dut: DeviceAdapter, aiocoap_client, coap_endpoint): + host, port = coap_endpoint + # CONFIG_ZEPHLETS_COAP_MAX_SEGMENT_LEN default = 32. A 33-char + # instance segment overflows the handler's stack copy buffer. + long_instance = "x" * 33 + req = _post(host, port, f"zlet/tick/{long_instance}/start") + resp = await aiocoap_client.request(req).response + assert resp.code == NOT_FOUND, f"expected 4.04, got {resp.code}" + + +@pytest.mark.asyncio +async def test_get_returns_4_05(dut: DeviceAdapter, aiocoap_client, coap_endpoint): + host, port = coap_endpoint + req = _get(host, port, "zlet/tick/tick_fast/events") + resp = await aiocoap_client.request(req).response + assert resp.code == METHOD_NOT_ALLOWED, f"expected 4.05, got {resp.code}" diff --git a/tests/coap_functional/sections-ram.ld b/tests/coap_functional/sections-ram.ld index 7cb19b3..84d1e78 100644 --- a/tests/coap_functional/sections-ram.ld +++ b/tests/coap_functional/sections-ram.ld @@ -1,3 +1,3 @@ #include -ITERABLE_SECTION_RAM(coap_resource_zlet_coap_test_service, Z_LINK_ITERABLE_SUBALIGN) +ITERABLE_SECTION_RAM(coap_resource_zlet_coap_service, Z_LINK_ITERABLE_SUBALIGN) diff --git a/tests/coap_functional/src/main.c b/tests/coap_functional/src/main.c index 537d6ba..62cd322 100644 --- a/tests/coap_functional/src/main.c +++ b/tests/coap_functional/src/main.c @@ -2,48 +2,38 @@ #include #include -#include -#include + +#include "zlet_tick.h" +#include "zlet_ui.h" LOG_MODULE_REGISTER(zlet_coap_functional, LOG_LEVEL_INF); /** * @file - * @brief Minimal CoAP server host for the pytest functional harness. + * @brief Host app for the CoAP functional twister target. * - * Registers a `COAP_SERVICE_DEFINE` on UDP/5683 with a single placeholder - * resource so Zephyr's per-service iterable section is non-empty (without - * at least one resource the linker does not emit - * `_coap_resource__list_{start,end}` and the service descriptor fails - * to link). The pytest smoke test queries an unrelated path and asserts - * `4.04 Not Found`, proving the aiocoap fixture reaches the Zephyr CoAP - * stack end-to-end. + * Instantiates one `tick` and one `ui` zephlet so the codegen-emitted + * per-type CoAP resources have live targets. The CoAP service + + * resources are registered automatically via the frontend's + * `COAP_SERVICE_DEFINE` and each opted-in zephlet's + * `COAP_RESOURCE_DEFINE` — there is no inline CoAP setup here. */ -static uint16_t coap_port = 5683; - -COAP_SERVICE_DEFINE(zlet_coap_test_service, "0.0.0.0", &coap_port, COAP_SERVICE_AUTOSTART); - -static int placeholder_get(struct coap_resource *resource, struct coap_packet *request, - struct sockaddr *addr, socklen_t addr_len) -{ - ARG_UNUSED(resource); - ARG_UNUSED(request); - ARG_UNUSED(addr); - ARG_UNUSED(addr_len); - return -ENOENT; -} - -static const char *const placeholder_path[] = {"phase0_placeholder", NULL}; +static struct tick_config tick_fast_cfg = { + .duration_ms = 100, + .period_ms = 100, +}; +static struct tick_data tick_fast_data; +ZEPHLET_NEW(tick, tick_fast, &tick_fast_cfg, &tick_fast_data, tick_init_fn); -COAP_RESOURCE_DEFINE(zlet_coap_placeholder, zlet_coap_test_service, - { - .path = placeholder_path, - .get = placeholder_get, - }); +static struct ui_config ui_main_cfg = { + .blink_period_ms = 250, +}; +static struct ui_data ui_main_data; +ZEPHLET_NEW(ui, ui_main, &ui_main_cfg, &ui_main_data, ui_init_fn); int main(void) { - LOG_INF("zephlet coap functional smoke harness up on UDP/%u", coap_port); + LOG_INF("zephlet coap functional host up on UDP/5683"); return 0; } diff --git a/tests/coap_functional/testcase.yaml b/tests/coap_functional/testcase.yaml index b29d74b..7d16cfb 100644 --- a/tests/coap_functional/testcase.yaml +++ b/tests/coap_functional/testcase.yaml @@ -1,7 +1,16 @@ +common: + tags: zephlet coap pytest + harness: pytest + timeout: 60 + platform_allow: + - native_sim + - native_sim/native/64 tests: zephlet.coap_functional.smoke: - tags: zephlet coap pytest - harness: pytest - platform_allow: - - native_sim - timeout: 60 + harness_config: + pytest_root: + - "pytest/test_smoke.py" + zephlet.coap_functional.rpc: + harness_config: + pytest_root: + - "pytest/test_rpc.py" diff --git a/tests/coap_functional/zephlets/tick/CMakeLists.txt b/tests/coap_functional/zephlets/tick/CMakeLists.txt new file mode 100644 index 0000000..da36ba4 --- /dev/null +++ b/tests/coap_functional/zephlets/tick/CMakeLists.txt @@ -0,0 +1,6 @@ +if(CONFIG_ZEPHLET_TICK) + zephyr_zephlet_generate( + TYPE tick + PREFIX zlet_tick + SOURCES zlet_tick.c) +endif() diff --git a/tests/coap_functional/zephlets/tick/Kconfig b/tests/coap_functional/zephlets/tick/Kconfig new file mode 100644 index 0000000..62ee11e --- /dev/null +++ b/tests/coap_functional/zephlets/tick/Kconfig @@ -0,0 +1,17 @@ +config ZEPHLET_TICK + bool "Tick zephlet (CoAP functional test fixture)" + select NANOPB + select ZBUS + select ZBUS_ASYNC_LISTENER + help + Enable the `tick` zephlet copy that the CoAP functional test + instantiates as `tick_fast`. Mirrors the app's `tick` semantics so + the dispatch tests exercise a realistic handler. + +if ZEPHLET_TICK + +module = ZEPHLET_TICK +module-str = zlet_tick +source "subsys/logging/Kconfig.template.log_config" + +endif diff --git a/tests/coap_functional/zephlets/tick/zephyr/module.yml b/tests/coap_functional/zephlets/tick/zephyr/module.yml new file mode 100644 index 0000000..e56e64b --- /dev/null +++ b/tests/coap_functional/zephlets/tick/zephyr/module.yml @@ -0,0 +1,4 @@ +name: zlet_tick +build: + cmake: . + kconfig: Kconfig diff --git a/tests/coap_functional/zephlets/tick/zlet_tick.c b/tests/coap_functional/zephlets/tick/zlet_tick.c new file mode 100644 index 0000000..f4bb4f6 --- /dev/null +++ b/tests/coap_functional/zephlets/tick/zlet_tick.c @@ -0,0 +1,139 @@ +#include "zlet_tick.h" + +#include + +#include +#include + +LOG_MODULE_DECLARE(zlet_tick, CONFIG_ZEPHLET_TICK_LOG_LEVEL); + +static void tick_timer_handler(struct k_timer *timer_id) +{ + const struct zephlet *z = k_timer_user_data_get(timer_id); + struct tick_events ev = { + .timestamp = (int32_t)k_uptime_get(), + }; + + (void)tick_emit(z, &ev, K_NO_WAIT); +} + +static int validate_config(const struct tick_config *c) +{ + if (c->period_ms == 0 || c->duration_ms == 0) { + return -EINVAL; + } + return 0; +} + +int tick_start_impl(const struct zephlet *z, struct lifecycle_status *resp) +{ + struct tick_data *d = z->data; + struct tick_config *cfg = z->config; + + if (!d->is_ready) { + if (resp != NULL) { + resp->is_running = false; + resp->is_ready = false; + } + return -ENODEV; + } + if (d->is_running) { + if (resp != NULL) { + resp->is_running = true; + resp->is_ready = true; + } + return -EALREADY; + } + + k_timer_start(&d->timer, K_MSEC(cfg->duration_ms), K_MSEC(cfg->period_ms)); + d->is_running = true; + + if (resp != NULL) { + resp->is_running = true; + resp->is_ready = true; + } + return 0; +} + +int tick_stop_impl(const struct zephlet *z, struct lifecycle_status *resp) +{ + struct tick_data *d = z->data; + + if (!d->is_running) { + if (resp != NULL) { + resp->is_running = false; + resp->is_ready = d->is_ready; + } + return -EALREADY; + } + + k_timer_stop(&d->timer); + d->is_running = false; + + if (resp != NULL) { + resp->is_running = false; + resp->is_ready = d->is_ready; + } + return 0; +} + +int tick_get_status_impl(const struct zephlet *z, struct lifecycle_status *resp) +{ + struct tick_data *d = z->data; + + if (resp != NULL) { + resp->is_running = d->is_running; + resp->is_ready = d->is_ready; + } + return 0; +} + +int tick_config_impl(const struct zephlet *z, const struct tick_config *req, + struct tick_config *resp) +{ + struct tick_data *d = z->data; + struct tick_config *cfg = z->config; + int err = validate_config(req); + + if (err != 0) { + if (resp != NULL) { + *resp = *cfg; + } + return err; + } + + *cfg = *req; + + if (d->is_running) { + k_timer_stop(&d->timer); + k_timer_start(&d->timer, K_MSEC(cfg->duration_ms), K_MSEC(cfg->period_ms)); + } + + if (resp != NULL) { + *resp = *cfg; + } + return 0; +} + +int tick_get_config_impl(const struct zephlet *z, struct tick_config *resp) +{ + struct tick_config *cfg = z->config; + + if (resp != NULL) { + *resp = *cfg; + } + return 0; +} + +int tick_init_fn(const struct zephlet *self) +{ + struct tick_data *d = self->data; + + k_timer_init(&d->timer, tick_timer_handler, NULL); + k_timer_user_data_set(&d->timer, (void *)self); + + d->is_running = false; + d->is_ready = true; + + return 0; +} diff --git a/tests/coap_functional/zephlets/tick/zlet_tick.h b/tests/coap_functional/zephlets/tick/zlet_tick.h new file mode 100644 index 0000000..33adbd5 --- /dev/null +++ b/tests/coap_functional/zephlets/tick/zlet_tick.h @@ -0,0 +1,24 @@ +#ifndef ZEPHLET_TESTS_COAP_FUNCTIONAL_TICK_H_ +#define ZEPHLET_TESTS_COAP_FUNCTIONAL_TICK_H_ + +#include + +#include + +#include "zlet_tick_interface.h" + +/** + * @file + * @brief Test-only `tick` zephlet types — mirrors the app's tick so the + * functional CoAP test exercises a realistic dispatch path. + */ + +struct tick_data { + bool is_running; + bool is_ready; + struct k_timer timer; +}; + +int tick_init_fn(const struct zephlet *z); + +#endif /* ZEPHLET_TESTS_COAP_FUNCTIONAL_TICK_H_ */ diff --git a/tests/coap_functional/zephlets/tick/zlet_tick.proto b/tests/coap_functional/zephlets/tick/zlet_tick.proto new file mode 100644 index 0000000..b6fc591 --- /dev/null +++ b/tests/coap_functional/zephlets/tick/zlet_tick.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +import "nanopb.proto"; +import "zephlet.proto"; +import "zephlet_options.proto"; + +option (nanopb_fileopt).long_names = false; + +message Tick { + message Config { + uint32 duration_ms = 1; + uint32 period_ms = 2; + } + + message Events { + int32 timestamp = 1; + } +} + +service TickApi { + option (zephlet.coap) = true; + + rpc start(Empty) returns (Lifecycle.Status); + rpc stop(Empty) returns (Lifecycle.Status); + rpc get_status(Empty) returns (Lifecycle.Status); + rpc config(Tick.Config) returns (Tick.Config); + rpc get_config(Empty) returns (Tick.Config); +} diff --git a/tests/coap_functional/zephlets/ui/CMakeLists.txt b/tests/coap_functional/zephlets/ui/CMakeLists.txt new file mode 100644 index 0000000..3cb028c --- /dev/null +++ b/tests/coap_functional/zephlets/ui/CMakeLists.txt @@ -0,0 +1,6 @@ +if(CONFIG_ZEPHLET_UI) + zephyr_zephlet_generate( + TYPE ui + PREFIX zlet_ui + SOURCES zlet_ui.c) +endif() diff --git a/tests/coap_functional/zephlets/ui/Kconfig b/tests/coap_functional/zephlets/ui/Kconfig new file mode 100644 index 0000000..95ad917 --- /dev/null +++ b/tests/coap_functional/zephlets/ui/Kconfig @@ -0,0 +1,18 @@ +config ZEPHLET_UI + bool "UI zephlet (CoAP functional test fixture)" + select NANOPB + select ZBUS + select ZBUS_ASYNC_LISTENER + help + Enable the `ui` zephlet copy used by the CoAP functional test to + register a second opted-in type at `/zlet/ui/#`. Lets the test + exercise the api-mismatch defensive check (`/zlet/ui/tick_fast/...` + → 4.04). + +if ZEPHLET_UI + +module = ZEPHLET_UI +module-str = zlet_ui +source "subsys/logging/Kconfig.template.log_config" + +endif diff --git a/tests/coap_functional/zephlets/ui/zephyr/module.yml b/tests/coap_functional/zephlets/ui/zephyr/module.yml new file mode 100644 index 0000000..29c56ad --- /dev/null +++ b/tests/coap_functional/zephlets/ui/zephyr/module.yml @@ -0,0 +1,4 @@ +name: zlet_ui +build: + cmake: . + kconfig: Kconfig diff --git a/tests/coap_functional/zephlets/ui/zlet_ui.c b/tests/coap_functional/zephlets/ui/zlet_ui.c new file mode 100644 index 0000000..1c52bd0 --- /dev/null +++ b/tests/coap_functional/zephlets/ui/zlet_ui.c @@ -0,0 +1,46 @@ +#include "zlet_ui.h" + +#include + +#include +#include + +LOG_MODULE_DECLARE(zlet_ui, CONFIG_ZEPHLET_UI_LOG_LEVEL); + +int ui_start_impl(const struct zephlet *z, struct lifecycle_status *resp) +{ + struct ui_data *d = z->data; + + if (!d->is_ready) { + return -ENODEV; + } + if (d->is_running) { + return -EALREADY; + } + d->is_running = true; + if (resp != NULL) { + resp->is_running = true; + resp->is_ready = true; + } + return 0; +} + +int ui_get_status_impl(const struct zephlet *z, struct lifecycle_status *resp) +{ + struct ui_data *d = z->data; + + if (resp != NULL) { + resp->is_running = d->is_running; + resp->is_ready = d->is_ready; + } + return 0; +} + +int ui_init_fn(const struct zephlet *z) +{ + struct ui_data *d = z->data; + + d->is_running = false; + d->is_ready = true; + return 0; +} diff --git a/tests/coap_functional/zephlets/ui/zlet_ui.h b/tests/coap_functional/zephlets/ui/zlet_ui.h new file mode 100644 index 0000000..6b0b512 --- /dev/null +++ b/tests/coap_functional/zephlets/ui/zlet_ui.h @@ -0,0 +1,21 @@ +#ifndef ZEPHLET_TESTS_COAP_FUNCTIONAL_UI_H_ +#define ZEPHLET_TESTS_COAP_FUNCTIONAL_UI_H_ + +#include + +#include "zlet_ui_interface.h" + +/** + * @file + * @brief Test-only `ui` zephlet types — minimal placeholder used by the + * CoAP functional test to verify api-mismatch routing. + */ + +struct ui_data { + bool is_running; + bool is_ready; +}; + +int ui_init_fn(const struct zephlet *z); + +#endif /* ZEPHLET_TESTS_COAP_FUNCTIONAL_UI_H_ */ diff --git a/tests/coap_functional/zephlets/ui/zlet_ui.proto b/tests/coap_functional/zephlets/ui/zlet_ui.proto new file mode 100644 index 0000000..959e433 --- /dev/null +++ b/tests/coap_functional/zephlets/ui/zlet_ui.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +import "nanopb.proto"; +import "zephlet.proto"; +import "zephlet_options.proto"; + +option (nanopb_fileopt).long_names = false; + +message Ui { + message Config { + uint32 blink_period_ms = 1; + } + + message Events { + int32 timestamp = 1; + } +} + +service UiApi { + option (zephlet.coap) = true; + + rpc start(Empty) returns (Lifecycle.Status); + rpc get_status(Empty) returns (Lifecycle.Status); +} diff --git a/tests/coap_translate/testcase.yaml b/tests/coap_translate/testcase.yaml index 15fd21c..6053464 100644 --- a/tests/coap_translate/testcase.yaml +++ b/tests/coap_translate/testcase.yaml @@ -4,5 +4,6 @@ tests: harness: ztest platform_allow: - native_sim + - native_sim/native/64 - mps2/an385 timeout: 30 diff --git a/tests/shared/dispatch/testcase.yaml b/tests/shared/dispatch/testcase.yaml index c1a8761..8fa4f16 100644 --- a/tests/shared/dispatch/testcase.yaml +++ b/tests/shared/dispatch/testcase.yaml @@ -4,4 +4,5 @@ tests: harness: ztest platform_allow: - native_sim + - native_sim/native/64 timeout: 30