diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efee6ca..2bcb659 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,6 +168,32 @@ jobs: files=$(git ls-files 'source/*.c' ':!source/zephyr') clang-tidy -p build $files + zephyr: + name: zephyr native_sim (examples + tests) + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Cache the Zephyr build image + id: image-cache + uses: actions/cache@v4 + with: + path: /tmp/dmosal-zephyr.tar + key: zephyr-image-${{ hashFiles('zephyr/Dockerfile') }} + - name: Load cached image + if: steps.image-cache.outputs.cache-hit == 'true' + run: docker load -i /tmp/dmosal-zephyr.tar + - name: Build the Zephyr image + if: steps.image-cache.outputs.cache-hit != 'true' + run: | + docker build -t dmosal-zephyr:local -f zephyr/Dockerfile zephyr + docker save -o /tmp/dmosal-zephyr.tar dmosal-zephyr:local + - name: Build examples + run: zephyr/docker.sh build + - name: Run examples + run: zephyr/docker.sh run + - name: Build and run cmocka tests + run: zephyr/docker.sh test + docs: name: docs (build) runs-on: ubuntu-22.04 diff --git a/.gitignore b/.gitignore index cc6cc41..61d235b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build GPATH GRTAGS GTAGS +build-*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 65e4880..542db1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,12 +78,12 @@ set(osal_SRC source/osal_log.c source/osal_rm.c source/osal_tmcheck.c - source/posix/osal_mutex.c - source/posix/osal_queue.c - source/posix/osal_sem.c - source/posix/osal_task.c - source/posix/osal_time.c - source/posix/osal_timer.c + source/linux/osal_mutex.c + source/linux/osal_queue.c + source/linux/osal_sem.c + source/linux/osal_task.c + source/linux/osal_time.c + source/linux/osal_timer.c ) # Generated headers live under the build tree — never in the source tree. diff --git a/README.md b/README.md index c63bfcd..d03ad04 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ semantics across POSIX and RTOS platforms. Everything (tasks, mutexes, semaphores, queues, timers, ...) is allocated statically up front through a central resource manager, so there's no malloc on the hot path and the worst-case memory usage is known ahead of time. A -POSIX/Linux backend and a Zephyr backend are included; other targets can be +Linux backend and a Zephyr backend are included; other targets can be added behind the same API without breaking existing callers. ``` @@ -87,7 +87,7 @@ backend maps to Zephyr primitives. ## Examples Sample programs live in [examples/](examples/). Build them with `just build` -(CMake target `examples`) for POSIX, or with `zephyr/docker.sh build` for +(CMake target `examples`) for Linux, or with `zephyr/docker.sh build` for Zephyr. ## Documentation diff --git a/docs/README.md b/docs/README.md index 0293d82..2479012 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ Doxygen-generated API reference (built via `make doc`). | [architecture.md](architecture.md) | Layered design, subsystem overview, resource-manager pattern, callback threading model, backend model. | | [api-spec.md](api-spec.md) | Behavioral specification per subsystem — functions, states, error codes, invariants. | | [lifetime.md](lifetime.md) | Init/deinit ordering, resource ownership, when it is safe to use / release a resource. | -| [porting.md](porting.md) | How to add a new backend (e.g. an RTOS or bare-metal target). Reference: the POSIX backend. | +| [porting.md](porting.md) | How to add a new backend (e.g. an RTOS or bare-metal target). Reference: the Linux backend. | | [thread-safety.md](thread-safety.md) | Per-API table classifying every public call as thread-safe, single-owner, or init-thread-only. | If you're just getting started, read them in this order: diff --git a/docs/architecture.md b/docs/architecture.md index dbc2852..0edd6cf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,7 +18,7 @@ behaviour matters more than dynamic flexibility. | dmosal (public API) | +---------------+---------------+ | portable | backend | - | layer | (POSIX...) | + | layer | (Linux...) | +---------------+---------------+ | OS user-space API | +-------------------------------+ @@ -34,7 +34,7 @@ behaviour matters more than dynamic flexibility. across every backend (resource manager, LIFO free-list, error strings, logging, time-checkpoint utility, top-level init). - **Backend** — `source//*.c`. Only these files touch OS-specific - APIs. The POSIX backend under `source/posix/` is the reference + APIs. The Linux backend under `source/linux/` is the reference implementation. ## Subsystems @@ -93,7 +93,7 @@ Two subsystems invoke user code from a thread other than the caller's: - **Task** — `osal_task_create(cfg)` spawns a new thread that runs `cfg->task_handler`. The handler runs to completion on that thread. - **Timer** — the timer callback fires on a backend-managed context, not - the caller's thread. On the POSIX backend that means a fresh helper + the caller's thread. On the Linux backend that means a fresh helper thread per expiration (via `SIGEV_THREAD`). User code must not assume any particular thread identity inside the callback. diff --git a/docs/lifetime.md b/docs/lifetime.md index 0b5937f..18d87bf 100644 --- a/docs/lifetime.md +++ b/docs/lifetime.md @@ -133,7 +133,7 @@ create -> [started/stopped, callback fires 0+ times] -> stop -> delete create --opens or attaches--> [send/recv N times] --unlinks if creator--> delete ``` -- On the POSIX backend, `osal_queue_create` may either create the +- On the Linux backend, `osal_queue_create` may either create the underlying kernel queue or attach to an existing one with the same name. `osal_queue_delete` unlinks the kernel queue only when this handle originally created it. diff --git a/docs/porting.md b/docs/porting.md index e3608a4..5d3c1e3 100644 --- a/docs/porting.md +++ b/docs/porting.md @@ -5,7 +5,7 @@ under `source/`. The rest of the library — the resource manager, LIFO, logging, error strings, tmcheck, and top-level init — is portable C and unchanged across backends. -The reference implementation lives at `source/posix/` and targets Linux / +The reference implementation lives at `source/linux/` and targets Linux / glibc. This document walks through what a new backend must provide. ## Files a backend must implement @@ -31,7 +31,7 @@ For each function, honour the behavior documented in [lifetime.md](lifetime.md). Concretely: - **Pool ops must be thread-safe** — `_create` and `_delete` need to - serialize their pool touches. On POSIX we do this by threading the + serialize their pool touches. On Linux we do this by threading the shared `osal_mutex_t *mutex` through each `_man_t`'s `osal_rm_t`. Reuse that pattern via `OSAL_RM_USEROBJMAN_DECLARE` / `_INIT` if the target OS has a mutex primitive. @@ -43,7 +43,7 @@ For each function, honour the behavior documented in without a helper-thread mechanism, provide one (e.g. a dedicated timer dispatch task). - **`osal_timer_delete` must reject in-callback invocation.** Use the - `in_callback` bool pattern from the POSIX backend: set true around the + `in_callback` bool pattern from the Linux backend: set true around the callback invocation, `OSAL_RUNTIME_ASSERT` in delete if it's true. ## Steps @@ -61,13 +61,13 @@ For each function, honour the behavior documented in 2. **Add sources to `CMakeLists.txt`.** Extend the `osal_SRC` list to include `source/mybackend/*.c` when the target backend is selected. - The simplest scheme: add an option `DMOSAL_BACKEND=posix|mybackend` + The simplest scheme: add an option `DMOSAL_BACKEND=linux|mybackend` and switch the source glob accordingly. Only one backend is compiled in at a time. 3. **Implement `osal_mutex.c` first.** Everything else depends on it — the shared mutex from this pool is passed to every other subsystem's - init. See `source/posix/osal_mutex.c` for the reference: + init. See `source/linux/osal_mutex.c` for the reference: - `s_mutex_man` static holding the pool. - A bare backend-level mutex (`s_mutex_man.resrc_mutex`) is used inside `osal_mutex_create` because it can't recursively use its own @@ -77,11 +77,11 @@ For each function, honour the behavior documented in 4. **Implement `osal_sem.c`, `osal_task.c`, `osal_timer.c`, `osal_queue.c`, `osal_time.c`.** Each mirrors the same `_man_t` + `osal_rm_t` structure. - Copy the POSIX file and swap the OS calls. + Copy the Linux file and swap the OS calls. 5. **Wire in the build.** `CMakeLists.txt` is the only place where the backend directory is referenced. Neither the public headers nor the - portable `source/*.c` files hard-code POSIX. + portable `source/*.c` files hard-code Linux. 6. **Run the existing test suite.** All six cmocka tests exercise only the public API and should pass unchanged. If they fail, the failure @@ -96,8 +96,8 @@ Some behaviors are inherently backend-specific and may need adjustment: the microsecond field will just always show 0 — no code change needed. - **`osal_task_cfg_t` hints** — `stack_addr`, `stack_size`, `priority`, `name` are best-effort. Honour them if your target OS supports them. - The POSIX reference implementation ignores them. -- **Timer callback thread** — the POSIX backend spawns one helper thread + The Linux reference implementation ignores them. +- **Timer callback thread** — the Linux backend spawns one helper thread per expiration (`SIGEV_THREAD`). On memory-constrained targets, use a single dedicated timer-dispatch task that walks a pending list. diff --git a/include/dmosal/osal_tmcheck.h b/include/dmosal/osal_tmcheck.h index 4578b98..9489fe4 100644 --- a/include/dmosal/osal_tmcheck.h +++ b/include/dmosal/osal_tmcheck.h @@ -66,6 +66,14 @@ extern "C" { * * A static checkpoint is created (if not already allocated) based on the function * name and line number, and the current timestamp is captured for it. + * + * @warning The checkpoint is a per-call-site static, so the same code in the same + * function must not be run from multiple threads through this macro: all threads would + * share one checkpoint (capture is one-shot, so only the first timestamp is kept), and + * the lazy creation is not thread-safe. In that case, allocate a unique id per thread, + * create one checkpoint per id with osal_tmcheck_create() (for example named + * "func:line:id"), and call osal_tmcheck_capture_ts() with that index instead of + * using this macro. */ #define OSAL_TMCHECK_CAPTURE() \ do { \ diff --git a/justfile b/justfile index 2381ea5..4ccef6a 100644 --- a/justfile +++ b/justfile @@ -18,6 +18,7 @@ # just tidy # run clang-tidy # just zephyr-build # build the examples for Zephyr native_sim (Docker) # just zephyr-run # run the Zephyr examples +# just zephyr-test # run the cmocka tests on Zephyr native_sim (TEST= for one) # just zephyr-shell # shell inside the Zephyr container # just clean # remove build directories @@ -53,7 +54,8 @@ check-asan: # Build and run tests with ThreadSanitizer check-tsan: just build_dir=build-tsan configure -DDMOSAL_ENABLE_TSAN=ON - just build_dir=build-tsan check + # TSan aborts with "unexpected memory mapping" when ASLR is on; disable it for the run. + setarch "$(uname -m)" -R just build_dir=build-tsan check # Build and run tests with gcov coverage, generate HTML report check-coverage: @@ -76,7 +78,7 @@ doc: configure format: pre-commit run clang-format --all-files -# Run clang-tidy over the portable + POSIX sources via pre-commit +# Run clang-tidy over the portable + Linux sources via pre-commit tidy: cmake -S . -B {{build_dir}} -DCMAKE_BUILD_TYPE={{build_type}} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON cmake --build {{build_dir}} --target dmosal -j{{jobs}} @@ -90,6 +92,10 @@ zephyr-build: zephyr-run: zephyr/docker.sh run +# Build and run the cmocka tests (test/osal) on Zephyr native_sim +zephyr-test: + zephyr/docker.sh test + # Open a shell in the Zephyr container zephyr-shell: zephyr/docker.sh shell @@ -100,4 +106,4 @@ clean: # Remove all build directories (default + sanitizer variants) distclean: - rm -rf {{build_dir}} build-asan build-tsan build-zephyr + rm -rf {{build_dir}} build-asan build-tsan build-zephyr build-zephyr-test diff --git a/source/posix/osal_mutex.c b/source/linux/osal_mutex.c similarity index 100% rename from source/posix/osal_mutex.c rename to source/linux/osal_mutex.c diff --git a/source/posix/osal_queue.c b/source/linux/osal_queue.c similarity index 100% rename from source/posix/osal_queue.c rename to source/linux/osal_queue.c diff --git a/source/posix/osal_sem.c b/source/linux/osal_sem.c similarity index 100% rename from source/posix/osal_sem.c rename to source/linux/osal_sem.c diff --git a/source/posix/osal_task.c b/source/linux/osal_task.c similarity index 100% rename from source/posix/osal_task.c rename to source/linux/osal_task.c diff --git a/source/posix/osal_time.c b/source/linux/osal_time.c similarity index 100% rename from source/posix/osal_time.c rename to source/linux/osal_time.c diff --git a/source/posix/osal_timer.c b/source/linux/osal_timer.c similarity index 100% rename from source/posix/osal_timer.c rename to source/linux/osal_timer.c diff --git a/source/zephyr/osal_queue.c b/source/zephyr/osal_queue.c index ed73ddb..e4e4a4d 100644 --- a/source/zephyr/osal_queue.c +++ b/source/zephyr/osal_queue.c @@ -36,7 +36,7 @@ #include "osal_assert.h" #include "osal_config.h" -/* Shared by every handle created with the same name (mirrors POSIX mq_open attach). */ +/* Shared by every handle created with the same name (mirrors Linux mq_open attach). */ typedef struct { struct k_msgq msgq; char buf[CONFIG_DMOSAL_QUEUE_BUF_SIZE] __aligned(sizeof(void *)); diff --git a/source/zephyr/osal_time.c b/source/zephyr/osal_time.c index 9a6dc59..d66c04a 100644 --- a/source/zephyr/osal_time.c +++ b/source/zephyr/osal_time.c @@ -48,6 +48,8 @@ osal_error_t osal_clock_time(uint64_t *nsec) return OSAL_E_PARAM; } - *nsec = k_ticks_to_ns_floor64(k_uptime_ticks()); + /* Uptime is 0 right after boot (always so on native_sim before time advances); callers + * such as tmcheck use 0 as "not set", so never return it. */ + *nsec = k_ticks_to_ns_floor64(k_uptime_ticks()) + 1U; return OSAL_E_OK; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a56c1d0..6afca12 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -79,3 +79,10 @@ add_executable(${OSAL_TEST} osal/osal_test.c) target_link_libraries(${OSAL_TEST} dmosal ${CMOCKA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) add_dependencies(check ${OSAL_TEST}) add_test(${OSAL_TEST} ${OSAL_TEST}) + +# Linux-backend-only tests (not portable to other backends). +set(QUEUE_UNLINK_TEST queue_unlink_test) +add_executable(${QUEUE_UNLINK_TEST} linux/queue_unlink_test.c) +target_link_libraries(${QUEUE_UNLINK_TEST} dmosal ${CMOCKA_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) +add_dependencies(check ${QUEUE_UNLINK_TEST}) +add_test(${QUEUE_UNLINK_TEST} ${QUEUE_UNLINK_TEST}) diff --git a/test/linux/queue_unlink_test.c b/test/linux/queue_unlink_test.c new file mode 100644 index 0000000..a2268e9 --- /dev/null +++ b/test/linux/queue_unlink_test.c @@ -0,0 +1,82 @@ +/* BSD 2-Clause License +* +* Copyright (c) 2023, nguyenvannam142@gmail.com +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, this +* list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* Linux-only: the Linux queue backend is backed by a POSIX message queue, visible in + * /dev/mqueue. Other backends have no such kernel object. */ +#include +#include "cmocka_include.h" +#include "osal.h" + +static void test_queue_delete_unlinks(void **state) +{ + (void)state; + osal_queue_cfg_t cfg; + osal_queue_t *queue; + char path[256]; + + memset(&cfg, 0, sizeof(cfg)); + snprintf((char *)cfg.name, sizeof(cfg.name), "dmosal_qu_%d", (int)getpid()); + cfg.msglen = 8; + cfg.qsize = 2; + + queue = osal_queue_create(&cfg); + assert_non_null(queue); + + snprintf(path, sizeof(path), "/dev/mqueue/%s", (char *)cfg.name); + assert_int_equal(access(path, F_OK), 0); + + osal_queue_delete(queue); + + assert_int_not_equal(access(path, F_OK), 0); + assert_int_equal(errno, ENOENT); +} + +static int setup(void **state) +{ + osal_config_t cfg; + + (void)state; + memset(&cfg, 0, sizeof(cfg)); + osal_init(&cfg); + return 0; +} + +static int teardown(void **state) +{ + (void)state; + osal_deinit(); + return 0; +} + +int main(void) +{ + setenv("CMOCKA_TEST_ABORT", "1", 1); + + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_queue_delete_unlinks, setup, teardown), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/osal/queue_test.c b/test/osal/queue_test.c index 640e5be..bf0420c 100644 --- a/test/osal/queue_test.c +++ b/test/osal/queue_test.c @@ -24,7 +24,6 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include #include "cmocka_include.h" #include "osal.h" @@ -224,30 +223,6 @@ static void test_queue_reopen_mismatch(void **state) osal_queue_delete(creator); } -static void test_queue_delete_unlinks(void **state) -{ - (void)state; - osal_queue_cfg_t cfg; - osal_queue_t *queue; - char path[256]; - - memset(&cfg, 0, sizeof(cfg)); - unique_name((char *)cfg.name, sizeof(cfg.name), "unlink", 0); - cfg.msglen = 8; - cfg.qsize = 2; - - queue = osal_queue_create(&cfg); - assert_non_null(queue); - - snprintf(path, sizeof(path), "/dev/mqueue/%s", (char *)cfg.name); - assert_int_equal(access(path, F_OK), 0); - - osal_queue_delete(queue); - - assert_int_not_equal(access(path, F_OK), 0); - assert_int_equal(errno, ENOENT); -} - static void test_queue_pool_exhaustion(void **state) { (void)state; @@ -361,7 +336,6 @@ int main(void) cmocka_unit_test_setup_teardown(test_queue_recv_timeout, setup, teardown), cmocka_unit_test_setup_teardown(test_queue_reopen_match, setup, teardown), cmocka_unit_test_setup_teardown(test_queue_reopen_mismatch, setup, teardown), - cmocka_unit_test_setup_teardown(test_queue_delete_unlinks, setup, teardown), cmocka_unit_test_setup_teardown(test_queue_pool_exhaustion, setup, teardown), cmocka_unit_test_setup_teardown(test_queue_send_recv_invalid_params, setup, teardown), cmocka_unit_test_setup_teardown(test_queue_delete_null, setup, teardown), diff --git a/test/osal/task_test.c b/test/osal/task_test.c index 684810f..59932d2 100644 --- a/test/osal/task_test.c +++ b/test/osal/task_test.c @@ -93,7 +93,7 @@ static void test_task_create(void **state) tasks[i] = osal_task_create(&cfg); assert_non_null(tasks[i]); - usleep(1000); + osal_usleep(1000); } task = osal_task_create(&cfg); assert_null(task); @@ -127,7 +127,7 @@ static void test_task_delete(void **state) task = osal_task_create(&cfg); assert_non_null(task); - usleep(1000); + osal_usleep(1000); osal_task_delete(task); } task = osal_task_create(&cfg); diff --git a/test/osal/timer_test.c b/test/osal/timer_test.c index 4b4e0a0..8d8db14 100644 --- a/test/osal/timer_test.c +++ b/test/osal/timer_test.c @@ -112,7 +112,7 @@ static void test_timer_oneshot(void **state) assert_int_equal(get_timer_count(), 1); /* One-shot: confirm it really doesn't repeat. */ - usleep(50000); + osal_usleep(50000); assert_int_equal(get_timer_count(), 1); osal_timer_delete(timer); @@ -141,9 +141,9 @@ static void test_timer_periodic(void **state) osal_timer_stop(timer); /* Let any expiration already queued right before stop drain out. */ - usleep(50000); + osal_usleep(50000); stopped_count = get_timer_count(); - usleep(50000); + osal_usleep(50000); assert_int_equal(get_timer_count(), stopped_count); osal_timer_delete(timer); @@ -164,7 +164,7 @@ static void test_timer_stop_before_fire(void **state) assert_int_equal(res, OSAL_E_OK); osal_timer_stop(timer); - usleep(50000); + osal_usleep(50000); assert_int_equal(get_timer_count(), 0); osal_timer_delete(timer); diff --git a/test/osal/tmcheck_test.c b/test/osal/tmcheck_test.c index 6c13959..c59f2b5 100644 --- a/test/osal/tmcheck_test.c +++ b/test/osal/tmcheck_test.c @@ -139,7 +139,7 @@ static void test_tmcheck_capture_first_hit_only(void **state) ts1 = osal_tmcheck_get_captured_ts(idx); assert_true(ts1 > 0); - usleep(20000); + osal_usleep(20000); /* Second capture on the same checkpoint is a no-op until reset. */ osal_tmcheck_capture_ts(idx); ts2 = osal_tmcheck_get_captured_ts(idx); @@ -220,7 +220,7 @@ static void test_tmcheck_name_apis(void **state) assert_true(idx2 >= 0); osal_tmcheck_capture_ts(idx1); - usleep(20000); + osal_usleep(20000); osal_tmcheck_capture_ts(idx2); res = osal_tmcheck_name_get_diff(name1, name2, &out_ns); @@ -280,6 +280,7 @@ static void test_tmcheck_diff_ns_large(void **state) static int g_mt_idx[TMCHECK_MT_TASKS]; static bool g_mt_ok[TMCHECK_MT_TASKS]; +static osal_sem_t *g_mt_done; static void tmcheck_mt_task(void *arg) { @@ -299,6 +300,7 @@ static void tmcheck_mt_task(void *arg) osal_tmcheck_capture_ts(idx); ts = osal_tmcheck_get_captured_ts(idx); g_mt_ok[i] = (ts > 0); + osal_sem_post(g_mt_done); } static void test_tmcheck_multithread_capture(void **state) @@ -310,6 +312,8 @@ static void test_tmcheck_multithread_capture(void **state) memset(g_mt_idx, 0xff, sizeof(g_mt_idx)); memset(g_mt_ok, 0, sizeof(g_mt_ok)); + g_mt_done = osal_sem_create(); + assert_non_null(g_mt_done); for (i = 0; i < TMCHECK_MT_TASKS; i++) { memset(&cfg, 0, sizeof(cfg)); @@ -318,8 +322,12 @@ static void test_tmcheck_multithread_capture(void **state) tasks[i] = osal_task_create(&cfg); assert_non_null(tasks[i]); } + /* Backends differ on whether delete waits for the task, so wait for each to finish. */ for (i = 0; i < TMCHECK_MT_TASKS; i++) { - osal_task_delete(tasks[i]); /* joins the thread */ + assert_int_equal(osal_sem_waittime(g_mt_done, 5000000), OSAL_E_OK); + } + for (i = 0; i < TMCHECK_MT_TASKS; i++) { + osal_task_delete(tasks[i]); } for (i = 0; i < TMCHECK_MT_TASKS; i++) { @@ -327,6 +335,7 @@ static void test_tmcheck_multithread_capture(void **state) assert_true(g_mt_idx[i] >= 0); osal_tmcheck_delete(g_mt_idx[i]); } + osal_sem_delete(g_mt_done); } static void test_tmcheck_print_diff(void **state) @@ -345,7 +354,7 @@ static void test_tmcheck_print_diff(void **state) assert_true(idx2 >= 0); osal_tmcheck_capture_ts(idx1); - usleep(20000); + osal_usleep(20000); osal_tmcheck_capture_ts(idx2); reset_capture(); @@ -435,9 +444,9 @@ static void test_tmcheck_print_diff_all(void **state) assert_true(idx3 >= 0); osal_tmcheck_capture_ts(idx1); - usleep(10000); + osal_usleep(10000); osal_tmcheck_capture_ts(idx2); - usleep(10000); + osal_usleep(10000); osal_tmcheck_capture_ts(idx3); /* With 3 captured checkpoints, print_diff_all prints consecutive @@ -498,7 +507,7 @@ static void test_tmcheck_name_print_diff(void **state) assert_true(idx2 >= 0); osal_tmcheck_capture_ts(idx1); - usleep(20000); + osal_usleep(20000); osal_tmcheck_capture_ts(idx2); reset_capture(); diff --git a/zephyr/Dockerfile b/zephyr/Dockerfile index e66bc42..2d9a50f 100644 --- a/zephyr/Dockerfile +++ b/zephyr/Dockerfile @@ -13,7 +13,7 @@ ENV DEBIAN_FRONTEND=noninteractive \ RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential ca-certificates cmake device-tree-compiler git gperf \ - ninja-build python3 python3-venv \ + ninja-build python3 python3-venv curl xz-utils \ && rm -rf /var/lib/apt/lists/* \ && python3 -m venv /opt/venv \ && pip install --no-cache-dir west @@ -27,6 +27,12 @@ RUN west init --mr ${ZEPHYR_VERSION} /opt/zephyrws \ && pip install --no-cache-dir -r scripts/requirements-base.txt \ && chmod -R a+rX /opt/zephyrws +# cmocka (Zephyr 4.0 ships no cmocka module): the tests in test/osal are built against its +# sources by zephyr/app. +ARG CMOCKA_VERSION=1.1.7 +RUN mkdir -p /opt/cmocka \ + && curl -fsSL https://cmocka.org/files/1.1/cmocka-${CMOCKA_VERSION}.tar.xz \ + | tar -xJ -C /opt/cmocka --strip-components=1 # west and cmake write caches inside the Zephyr tree, and the container runs as the host user. RUN mkdir -p /opt/zephyrws/zephyr/.cache && chmod -R a+rwX /opt/zephyrws/.west /opt/zephyrws/zephyr/.cache diff --git a/zephyr/README.md b/zephyr/README.md index 099ebe3..daadeee 100644 --- a/zephyr/README.md +++ b/zephyr/README.md @@ -12,8 +12,8 @@ normal Linux process, no real hardware). Only Docker is needed on the host. | `zephyr/app/` | Zephyr app (CMake + `prj.conf` only). Builds a file from `examples/`, shared with all platforms. | | `zephyr/docker.sh`, `zephyr/Dockerfile` | Slim build container + helper. | -The portable code (`source/*.c`) is shared with the POSIX build; do not edit it -for Zephyr. Use `source/posix/*.c` as the reference for behaviour. +The portable code (`source/*.c`) is shared with the Linux build; do not edit it +for Zephyr. Use `source/linux/*.c` as the reference for behaviour. ## Quick start @@ -22,6 +22,8 @@ zephyr/docker.sh build # first run builds the image (~1.8 GB, Zephyr v4.0.0 in zephyr/docker.sh run # run every example zephyr/docker.sh shell # interactive shell (west, cmake, gcc available) EXAMPLE=task_sample zephyr/docker.sh build # just one example; same for run +zephyr/docker.sh test # build + run every test/osal/*_test.c on native_sim +TEST=mutex_test zephyr/docker.sh test # just one test ``` `run` passes `-stop_at=3` (3 s of simulated time) because `zephyr.exe` never exits on its own. @@ -52,5 +54,10 @@ Add new examples under `examples/` (portable code only) and select them with `EX - The app finds the module via `ZEPHYR_EXTRA_MODULES` in `zephyr/app/CMakeLists.txt`. Your own app does the same and sets `CONFIG_DMOSAL=y`. - Zephyr source: inside the image at `/opt/zephyrws/zephyr` (browse with `zephyr/docker.sh shell`). +- Tests: `test/osal/*_test.c` are portable cmocka tests shared with the Linux build; they use + only the OSAL API. Backend-specific tests live in `test//` (for example + `test/linux/`) and are not built for Zephyr. `zephyr/app` builds one test with + `-DTEST=`, compiling cmocka from the image (`/opt/cmocka`) against the host libc + (`zephyr/app/test.conf`); `test_main.c` runs the test's `main` and exits with its result. - Only one `main()` per Zephyr image; run extra "apps" as OSAL tasks. - Log level, `osal_log_*` etc. come from the portable code and print via stdout. diff --git a/zephyr/app/CMakeLists.txt b/zephyr/app/CMakeLists.txt index da5ffe8..17d85e1 100644 --- a/zephyr/app/CMakeLists.txt +++ b/zephyr/app/CMakeLists.txt @@ -3,10 +3,35 @@ cmake_minimum_required(VERSION 3.20.0) # Pick up the dmosal Zephyr module from this repository without a west manifest. list(APPEND ZEPHYR_EXTRA_MODULES "${CMAKE_CURRENT_LIST_DIR}/../..") -# Which file in examples/ to build: west build ... -- -DEXAMPLE=task_sample +# Build one file from examples/ ... west build ... -- -DEXAMPLE=task_sample +# ... or one cmocka test from test/osal/: west build ... -- -DTEST=mutex_test set(EXAMPLE queue_sample CACHE STRING "Example from examples/ to build") +set(TEST "" CACHE STRING "Test from test/osal/ to build instead of an example") +set(CMOCKA_DIR "$ENV{CMOCKA_DIR}" CACHE PATH "cmocka source tree (tests only)") + +if(TEST) + # cmocka needs signal.h, setenv() etc., which the minimal libc lacks: use the host libc. + set(EXTRA_CONF_FILE ${CMAKE_CURRENT_LIST_DIR}/test.conf) +endif() find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(dmosal_zephyr_app) -target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../examples/${EXAMPLE}.c) +set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..) + +if(TEST) + target_sources(app PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/test_main.c + ${REPO_ROOT}/test/osal/${TEST}.c + ${CMOCKA_DIR}/src/cmocka.c) + # The test keeps its own main(); test_main.c calls it and reports the result. + set_source_files_properties(${REPO_ROOT}/test/osal/${TEST}.c + PROPERTIES COMPILE_DEFINITIONS main=dmosal_test_main) + # -std=c11 hides POSIX declarations (signal, setenv, strdup) that cmocka and the tests use. + # HAVE_CONFIG_H makes cmocka.c read cmocka/config.h. + target_compile_definitions(app PRIVATE _GNU_SOURCE HAVE_CONFIG_H) + target_include_directories(app PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/cmocka ${CMOCKA_DIR}/include ${REPO_ROOT}/test) +else() + target_sources(app PRIVATE ${REPO_ROOT}/examples/${EXAMPLE}.c) +endif() diff --git a/zephyr/app/cmocka/config.h b/zephyr/app/cmocka/config.h new file mode 100644 index 0000000..88b868a --- /dev/null +++ b/zephyr/app/cmocka/config.h @@ -0,0 +1,39 @@ +/* Minimal cmocka build configuration for Zephyr native_sim (host libc). */ +#ifndef DMOSAL_CMOCKA_CONFIG_H +#define DMOSAL_CMOCKA_CONFIG_H + +#define HAVE_ASSERT_H 1 +#define HAVE_INTTYPES_H 1 +#define HAVE_MALLOC_H 1 +#define HAVE_SETJMP_H 1 +#define HAVE_SIGNAL_H 1 +#define HAVE_STDARG_H 1 +#define HAVE_STDDEF_H 1 +#define HAVE_STDINT_H 1 +#define HAVE_STDIO_H 1 +#define HAVE_STDLIB_H 1 +#define HAVE_STRING_H 1 +#define HAVE_TIME_H 1 +#define HAVE_STRUCT_TIMESPEC 1 +#define HAVE_UINTPTR_T 1 +#define HAVE_CALLOC 1 +#define HAVE_EXIT 1 +#define HAVE_FPRINTF 1 +#define HAVE_SNPRINTF 1 +#define HAVE_VSNPRINTF 1 +#define HAVE_FREE 1 +#define HAVE_LONGJMP 1 +#define HAVE_MALLOC 1 +#define HAVE_MEMCPY 1 +#define HAVE_MEMSET 1 +#define HAVE_PRINTF 1 +#define HAVE_SETJMP 1 +#define HAVE_SIGNAL 1 +#define HAVE_STRCMP 1 +#define HAVE_STRCPY 1 +#define HAVE_GCC_THREAD_LOCAL_STORAGE 1 +#define HAVE_CLOCK_GETTIME 1 +#define HAVE_CLOCK_REALTIME 1 +#define WORDS_SIZEOF_VOID_P 8 + +#endif diff --git a/zephyr/app/test.conf b/zephyr/app/test.conf new file mode 100644 index 0000000..d1bcafa --- /dev/null +++ b/zephyr/app/test.conf @@ -0,0 +1,2 @@ +CONFIG_EXTERNAL_LIBC=y +CONFIG_MAIN_STACK_SIZE=16384 diff --git a/zephyr/app/test_main.c b/zephyr/app/test_main.c new file mode 100644 index 0000000..97e6384 --- /dev/null +++ b/zephyr/app/test_main.c @@ -0,0 +1,10 @@ +/* Runs the cmocka test whose main() was renamed to dmosal_test_main, then ends the + * native_sim process with the test result so CI can use the exit code. */ +#include + +extern int dmosal_test_main(void); + +int main(void) +{ + exit(dmosal_test_main() == 0 ? 0 : 1); +} diff --git a/zephyr/docker.sh b/zephyr/docker.sh index c25a1ca..c689e9c 100755 --- a/zephyr/docker.sh +++ b/zephyr/docker.sh @@ -5,9 +5,11 @@ # # zephyr/docker.sh build # build every examples/*.c into build-zephyr/ # zephyr/docker.sh run # run them all +# zephyr/docker.sh test # build + run every test/osal/*_test.c (cmocka) on native_sim # zephyr/docker.sh shell # interactive shell in the container # # EXAMPLE= limits build/run to one file from examples/. +# TEST= limits test to one file from test/osal/ (for example TEST=mutex_test). set -euo pipefail IMAGE=dmosal-zephyr:local @@ -21,7 +23,14 @@ else for f in "$REPO"/examples/*.c; do EXAMPLES+=("$(basename "$f" .c)"); done fi -DOCKER_RUN=(docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$REPO":/dmosal) +if [ -n "${TEST:-}" ]; then + TESTS=("$TEST") +else + TESTS=() + for f in "$REPO"/test/osal/*_test.c; do TESTS+=("$(basename "$f" .c)"); done +fi + +DOCKER_RUN=(docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -e CMOCKA_DIR=/opt/cmocka -v "$REPO":/dmosal) if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then docker build -t "$IMAGE" -f "$REPO/zephyr/Dockerfile" "$REPO/zephyr" @@ -40,6 +49,19 @@ run) "${DOCKER_RUN[@]}" "$IMAGE" "build-zephyr/$ex/zephyr/zephyr.exe" -stop_at=3 done ;; +test) + failed=() + for t in "${TESTS[@]}"; do + "${DOCKER_RUN[@]}" "$IMAGE" west build -b "$BOARD" -d "build-zephyr-test/$t" zephyr/app -p auto \ + -- -DTEST="$t" >/dev/null + echo "=== $t ===" + "${DOCKER_RUN[@]}" "$IMAGE" "build-zephyr-test/$t/zephyr/zephyr.exe" -stop_at=120 || failed+=("$t") + done + if [ ${#failed[@]} -ne 0 ]; then + echo "FAILED: ${failed[*]}" + exit 1 + fi + ;; shell) "${DOCKER_RUN[@]}" -it "$IMAGE" bash ;;