Skip to content

system/nxinit: add cmocka unit tests for parser/action/service - #3755

Open
JianyuWang0623 wants to merge 4 commits into
apache:masterfrom
JianyuWang0623:nxinit-unit-tests-upstream
Open

system/nxinit: add cmocka unit tests for parser/action/service#3755
JianyuWang0623 wants to merge 4 commits into
apache:masterfrom
JianyuWang0623:nxinit-unit-tests-upstream

Conversation

@JianyuWang0623

@JianyuWang0623 JianyuWang0623 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix a real bug in init_parse_config_lines(): a dead early continue for a truly-empty line (buf == "\0") skipped the memmove() bookkeeping its sibling whitespace-only-line branch performs, corrupting the remaining-length tracking and silently dropping every subsequent line in that refill chunk (not just one bookkeeping entry). Since "blank line between sections" is one of the most common patterns in an init.rc file, this is a real, reachable bug, not just dead-code cleanup. The whitespace-skip loop right below already handles the empty-string case correctly, so the buggy early exit is simply redundant and removed.

  • Fix a second real bug found during review, in init_parse_config_buffer() (the buffer-based counterpart of init_parse_config_file(), used to parse the builtin "preset" rc content): it computed the per-refill copy length as MIN(len - off, sizeof(tmp)) without subtracting the n leftover bytes already held at the front of tmp from a previous refill, so memcpy(&tmp[n], ..., r) could write past tmp[]. Reproduced locally with an ASan host harness at RC_LINE_MAX=32/48; fixed to MIN(len - off, sizeof(tmp) - n) and reverified clean at 32/48/64/128 (the current default). SYSTEM_NXINIT_RC_LINE_MAX also had no lower bound, so it now has range 64 4096 to stop this from being reachable by lowering that Kconfig value.

  • Add a test/ subdirectory (mirroring apps/system/uorb/test/) with cmocka-based unit tests covering the logic most prone to regression in NxInit:

    • init_parse_arguments(): plain/quoted arguments, -- separator vs. --option long options (regression coverage for a previously fixed bug), argv-capacity truncation (now also asserting the exact folded contents of the last slot, not just that it exists).
    • init_parse_config_file()/init_parse_config_lines()/init_parse_config_buffer(): section routing, blank/whitespace-only line skipping, unknown-section rejection, over-length line rejection, and a line straddling two read-buffer refills — exercised through both the file-based and buffer-based entry points (the latter added specifically to catch the buffer-overflow bug above; init_parse_config_buffer() is exposed in parser.h behind CONFIG_SYSTEM_NXINIT_TEST for this purpose only).
    • Action event matching (init_action_parse()/init_action_trigger_event()): exact match, invert (!=), fnmatch wildcards, AND semantics across multiple events per action. CONFIG_SYSTEM_NXINIT_ACTION_EVENTS_MAX now defaults to 2 under CONFIG_SYSTEM_NXINIT_TEST specifically so this case actually runs instead of being permanently skipped (production default is unchanged at 1). All action test cases now also release the events/commands they allocate.
    • Service conflict detection (init_service_parse()/init_service_check()): duplicate service name rejection, override replacing an earlier duplicate, SERVICE_ARGS_MAX boundary (built dynamically from the configured CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX value rather than hardcoding the default of 8, so the test still exercises the real boundary under a different config).

    Test sources compile action.c/parser.c/service.c a second time into a separate nxinit_unit_test program, gated behind new CONFIG_SYSTEM_NXINIT_TEST (depends on TESTING_CMOCKA); the default init program is unaffected. CONFIG_SYSTEM_NXINIT_TEST_STACKSIZE now defaults to 8192 (was DEFAULT_TASK_STACKSIZE = 2048): several test cases build multi-hundred-byte stack buffers on top of cmocka's own overhead, and on real hardware the smaller default caused the test task to silently overflow its stack and stop producing output (no crash dump, no watchdog reset) partway through the suite.

    The two build systems handle the "compile a second time" differently: CMake actually rebuilds action.c/parser.c/service.c into a second, separate nxinit_unit_test target (verified below); Make instead appends the test sources into the same shared CSRCS list used by init, so parser/action/service are compiled once and linked into both init and nxinit_unit_test from the same objects. Both are correct, just not equivalent in what gets compiled where.

Opened as draft for early review/CI feedback.

Impact

  • Users: NO (test-only addition; the two parser fixes only remove dead/buggy code paths not reachable at default configuration, no behavior change for well-formed configs at default Kconfig values).
  • Build: adds one new optional Kconfig-gated test target; default build unaffected.
  • Hardware: NO.
  • Documentation: NO.
  • Security: Fixes a stack-buffer-overflow in init_parse_config_buffer(), but it is unreachable at the default SYSTEM_NXINIT_RC_LINE_MAX=128 (the builtin preset content is 95 bytes); only reachable if that Kconfig value is lowered below ~90, which is now bounded to a 64 minimum by this PR.
  • Compatibility: NO.

Testing

Build Host: Ubuntu 22.04 LTS x86_64, gcc 13.4.0 (Ubuntu 13.4.0-6ubuntu122ppa2)
Target: sim:citest / sim:nsh (nuttx sim config, no cross toolchain needed; NxInit is pure apps-layer logic) plus a real esp32p4-pico-wifi-wareshare hardware target for the latest commit (see below).

nxstyle/checkpatch.sh pass on all changed files.

Note on CI coverage: no upstream defconfig currently enables CONFIG_SYSTEM_NXINIT_TEST (checked via a full search of boards/), so this test target is not yet exercised by the existing CI test matrix. It has been verified locally (sim Make/CMake builds below) and on real esp32p4 hardware. Happy to wire it into an existing cmocka-enabled defconfig (e.g. sim:citest) if that's preferred — it would need CONFIG_EXPERIMENTAL, CONFIG_LIBC_EXECFUNCS, CONFIG_SCHED_CHILD_STATUS, CONFIG_SYSTEM_NXINIT, and CONFIG_SYSTEM_NXINIT_TEST added, none of which sim:citest currently has.

Make build (sim:citest, default Kconfig, ACTION_EVENTS_MAX=1)

nsh> nxinit_unit_test
[==========] nxinit_tests: Running 17 test(s).
[ RUN      ] test_nxinit_parser_arguments_spaces
[       OK ] test_nxinit_parser_arguments_spaces
[ RUN      ] test_nxinit_parser_arguments_quoted
[       OK ] test_nxinit_parser_arguments_quoted
[ RUN      ] test_nxinit_parser_arguments_dashdash_separator
[       OK ] test_nxinit_parser_arguments_dashdash_separator
[ RUN      ] test_nxinit_parser_arguments_long_option
[       OK ] test_nxinit_parser_arguments_long_option
[ RUN      ] test_nxinit_parser_arguments_truncate
[       OK ] test_nxinit_parser_arguments_truncate
[ RUN      ] test_nxinit_parser_config_sections
[       OK ] test_nxinit_parser_config_sections
[ RUN      ] test_nxinit_parser_config_skip_blank_lines
[       OK ] test_nxinit_parser_config_skip_blank_lines
[ RUN      ] test_nxinit_parser_config_unknown_section
[       OK ] test_nxinit_parser_config_unknown_section
[ RUN      ] test_nxinit_parser_config_line_too_long
[       OK ] test_nxinit_parser_config_line_too_long
[ RUN      ] test_nxinit_parser_config_line_crosses_boundary
[       OK ] test_nxinit_parser_config_line_crosses_boundary
[ RUN      ] test_nxinit_action_event_match_exact
[       OK ] test_nxinit_action_event_match_exact
[ RUN      ] test_nxinit_action_event_match_invert
[       OK ] test_nxinit_action_event_match_invert
[ RUN      ] test_nxinit_action_event_match_fnmatch
[       OK ] test_nxinit_action_event_match_fnmatch
[ RUN      ] test_nxinit_action_event_and_semantics
[  SKIPPED ] test_nxinit_action_event_and_semantics
[ RUN      ] test_nxinit_service_duplicate_conflict
[       OK ] test_nxinit_service_duplicate_conflict
[ RUN      ] test_nxinit_service_override_replaces_duplicate
[       OK ] test_nxinit_service_override_replaces_duplicate
[ RUN      ] test_nxinit_service_args_max_boundary
[       OK ] test_nxinit_service_args_max_boundary
[==========] nxinit_tests: 17 test(s) run.
[  PASSED  ] 16 test(s).
[  SKIPPED ] nxinit_tests: 1 test(s), listed below:
[  SKIPPED ] test_nxinit_action_event_and_semantics

With CONFIG_SYSTEM_NXINIT_ACTION_EVENTS_MAX=4, test_nxinit_action_event_and_semantics runs and passes (17/17 PASSED), confirming the AND-semantics test is not dead code.

With CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX=10 (instead of the default 8), test_nxinit_service_args_max_boundary still passes (16/17, same as default), confirming the args-max test now tracks the configured value instead of a hardcoded 8.

CMake build (sim:nsh, default Kconfig)

$ cmake -B build -DBOARD_CONFIG=sim:nsh -GNinja
...
-- Build files have been written to: .../build
$ kconfig-tweak --file build/.config --enable CONFIG_EXPERIMENTAL \
    --enable CONFIG_SCHED_CHILD_STATUS --enable CONFIG_SYSTEM_NXINIT \
    --enable CONFIG_SYSTEM_NXINIT_TEST --enable CONFIG_TESTING_CMOCKA
$ cmake --build build -j$(nproc)
...
[1246/1248] Linking C executable nuttx
[1247/1248] Generating System.map
[1248/1248] Pac SIM with dynamic libs in nuttx.tgz
$ echo -e "nxinit_unit_test\nexit\n" | ./build/nuttx
NuttShell (NSH)
nsh> nxinit_unit_test
[==========] nxinit_tests: Running 17 test(s).
...
[  PASSED  ] 16 test(s).
[  SKIPPED ] nxinit_tests: 1 test(s), listed below:
[  SKIPPED ] test_nxinit_action_event_and_semantics

This CMake build previously failed to link (undefined reference to 'nxinit_unit_test_main') because nuttx_add_application() only renames the first SRCS entry's main(), and test/test_nxinit.c was not first in TEST_SRCS; fixed by reordering TEST_SRCS in CMakeLists.txt.

Real hardware build (esp32p4-pico-wifi-wareshare:nsh, latest commit)

Built via make CROSSDEV=riscv-none-elf- with CONFIG_SYSTEM_NXINIT_TEST=y (which now defaults ACTION_EVENTS_MAX=2 and TEST_STACKSIZE=8192), flashed and ran nxinit_unit_test over UART on real esp32p4-pico-wifi-wareshare hardware:

nsh> nxinit_unit_test
[==========] nxinit_tests: Running 18 test(s).
[ RUN      ] test_nxinit_parser_arguments_spaces
[       OK ] test_nxinit_parser_arguments_spaces
[ RUN      ] test_nxinit_parser_arguments_quoted
[       OK ] test_nxinit_parser_arguments_quoted
[ RUN      ] test_nxinit_parser_arguments_dashdash_separator
[       OK ] test_nxinit_parser_arguments_dashdash_separator
[ RUN      ] test_nxinit_parser_arguments_long_option
[       OK ] test_nxinit_parser_arguments_long_option
[ RUN      ] test_nxinit_parser_arguments_truncate
[       OK ] test_nxinit_parser_arguments_truncate
[ RUN      ] test_nxinit_parser_config_sections
[       OK ] test_nxinit_parser_config_sections
[ RUN      ] test_nxinit_parser_config_skip_blank_lines
[       OK ] test_nxinit_parser_config_skip_blank_lines
[ RUN      ] test_nxinit_parser_config_unknown_section
[       OK ] test_nxinit_parser_config_unknown_section
[ RUN      ] test_nxinit_parser_config_line_too_long
[       OK ] test_nxinit_parser_config_line_too_long
[ RUN      ] test_nxinit_parser_config_line_crosses_boundary
[       OK ] test_nxinit_parser_config_line_crosses_boundary
[ RUN      ] test_nxinit_parser_config_buffer_crosses_boundary
[       OK ] test_nxinit_parser_config_buffer_crosses_boundary
[ RUN      ] test_nxinit_action_event_match_exact
[       OK ] test_nxinit_action_event_match_exact
[ RUN      ] test_nxinit_action_event_match_invert
[       OK ] test_nxinit_action_event_match_invert
[ RUN      ] test_nxinit_action_event_match_fnmatch
[       OK ] test_nxinit_action_event_match_fnmatch
[ RUN      ] test_nxinit_action_event_and_semantics
[       OK ] test_nxinit_action_event_and_semantics
[ RUN      ] test_nxinit_service_duplicate_conflict
[       OK ] test_nxinit_service_duplicate_conflict
[ RUN      ] test_nxinit_service_override_replaces_duplicate
[       OK ] test_nxinit_service_override_replaces_duplicate
[ RUN      ] test_nxinit_service_args_max_boundary
[       OK ] test_nxinit_service_args_max_boundary
[==========] nxinit_tests: 18 test(s) run.
[  PASSED  ] 18 test(s).

All commits carry Assisted-by: trailers indicating AI usage, and the branch has been rebased onto current apache/master (no conflicts).

@JianyuWang0623
JianyuWang0623 force-pushed the nxinit-unit-tests-upstream branch 2 times, most recently from 3352764 to 1aacc30 Compare August 25, 2026 14:40
@cederom
cederom requested a review from linguini1 August 25, 2026 18:17

@linguini1 linguini1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the Assisted-by field to your commits to indicate AI usage.

@JianyuWang0623
JianyuWang0623 force-pushed the nxinit-unit-tests-upstream branch 2 times, most recently from 4845600 to 89958f9 Compare August 27, 2026 02:53
@JianyuWang0623

Copy link
Copy Markdown
Contributor Author

The sim/lua build failure on Linux (sim-02) in CI is unrelated to this PR. Root cause: the kernel renamed the Kconfig option PSEUDOFS_SOFTLINKSFS_LINKS, but interpreters/luamodules/luv/Kconfig in apps still uses the old name (depends on PSEUDOFS_SOFTLINKS). Since that symbol no longer exists, CONFIG_LUA_LUV_MODULE becomes unselectable and gets silently dropped by savedefconfig, leaving the sim/lua defconfig dirty and failing the build.

The fix is already up as #3753 (fs: rename PSEUDOFS_SOFTLINKS to FS_LINKS), which renames the dependency in luv/Kconfig (and other apps files). Once that merges, re-running CI here should be green. This PR itself only touches system/nxinit/.

init_parse_config_lines() had a dead early "continue" for a truly
empty line (buf == "\0") that skipped the memmove() bookkeeping its
sibling whitespace-only-line branch performs. When a real empty line
appeared mid-buffer, subsequent bytes were never shifted to the front
of the working buffer, corrupting the remaining-length tracking and
silently dropping every line after it for that refill chunk.

The whitespace-skip loop right below already handles the empty-string
case correctly (the loop body never executes, so it falls straight
into the "only whitespace" -> memmove -> continue path), so the buggy
early exit is simply redundant and removed.

Assisted-by: GitHubCopilot:claude-sonnet-5
Signed-off-by: wangjianyu3 <wangjianyu3@xiaomi.com>
init_parse_config_file() declares 'r' inside the read loop with no
blank line before the following 'if (r < 0)' statement, violating the
NuttX coding standard (nxstyle: "Missing blank line after
declarations"). checkpatch.sh runs a whole-file nxstyle check on any
file a commit touches, not diff-only, so this pre-existing issue
surfaced on this PR's CI once parser.c was touched again.

Assisted-by: GitHubCopilot:claude-sonnet-5
Signed-off-by: wangjianyu3 <wangjianyu3@xiaomi.com>
@JianyuWang0623
JianyuWang0623 force-pushed the nxinit-unit-tests-upstream branch from 89958f9 to 2d45ed3 Compare August 27, 2026 18:25
Comment thread system/nxinit/parser.h Outdated
Comment thread system/nxinit/Kconfig Outdated
init_parse_config_buffer() computed the per-refill copy length as
MIN(len - off, sizeof(tmp)) without subtracting the 'n' leftover bytes
already held at the front of 'tmp' from a previous refill, so
memcpy(&tmp[n], ..., r) could write past the end of tmp[]. The
file-based twin, init_parse_config_file(), already gets this right
(read(fd, &buf[n], sizeof(buf) - n)).

Reproduced locally with an AddressSanitizer host harness feeding the
real 95-byte builtin "preset" rc content through
init_parse_config_buffer() at CONFIG_SYSTEM_NXINIT_RC_LINE_MAX=32/48:
ASan reports a stack-buffer-overflow on the 'tmp' array. Fixed to
MIN(len - off, sizeof(tmp) - n) and reverified clean at
RC_LINE_MAX=32/48/64/128.

The default config never hits this (the builtin preset is 95 bytes and
the default RC_LINE_MAX is 128), but SYSTEM_NXINIT_RC_LINE_MAX had no
lower bound, so lowering it towards 32/48 for a smaller build would
silently corrupt the stack while parsing the preset during boot. Add a
"range 64 4096" bound so the value can no longer be set below the
builtin preset's needs.

Assisted-by: opencode:mimo-v2.5-pro
Signed-off-by: wangjianyu3 <wangjianyu3@xiaomi.com>
@JianyuWang0623
JianyuWang0623 force-pushed the nxinit-unit-tests-upstream branch from 2d45ed3 to dd7113f Compare August 28, 2026 03:36
Add a test/ subdirectory (mirroring apps/system/uorb/test/) with
cmocka-based unit tests covering the NxInit logic most prone to
regression:

- init_parse_arguments(): plain/quoted arguments, "--" separator vs.
  "--option" long options (regression coverage for a previously fixed
  bug), argv-capacity truncation (asserting the exact folded contents
  of the last slot, not just its presence).
- init_parse_config_file()/init_parse_config_lines()/
  init_parse_config_buffer(): section routing, blank/whitespace-only
  line skipping, unknown-section rejection, over-length line rejection,
  and a line straddling two read-buffer refills, exercised through both
  the file-based and buffer-based entry points.
- Action event matching: exact match, invert (!=), fnmatch wildcards,
  and AND semantics across multiple events per action.
- Service conflict detection: duplicate service name rejection,
  override replacing an earlier duplicate, and the SERVICE_ARGS_MAX
  boundary built dynamically from CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX
  rather than a hardcoded value.

Test sources compile action.c/parser.c/service.c a second time into a
separate nxinit_unit_test program, gated behind new
CONFIG_SYSTEM_NXINIT_TEST (depends on TESTING_CMOCKA); the default init
program is unaffected. The CMake path builds a dedicated
nxinit_unit_test target (with test/test_nxinit.c placed first in SRCS
so nuttx_add_application() renames its main() correctly); the Make path
appends the test sources into the shared CSRCS list.

Supporting bits required to make the suite exercise the real code:

- init_parse_config_buffer() is declared in parser.h and made
  non-static so the buffer-based boundary test can call it directly,
  alongside the existing init_parse_config_file() entry point.
- CONFIG_SYSTEM_NXINIT_ACTION_EVENTS_MAX default is raised from 1 to 2
  so an action can carry more than one event ("on evA && evB"), which
  the multi-event AND-semantics test exercises; a single event slot
  made that test dead code.
- CONFIG_SYSTEM_NXINIT_TEST_STACKSIZE defaults to 8192: several parser
  test cases build multi-hundred-byte stack buffers on top of cmocka's
  own overhead, and the previous DEFAULT_TASK_STACKSIZE (2048)
  overflowed the test task's stack silently on real hardware (no crash
  dump, no watchdog reset, output just stopped) partway through the
  suite.

Testing:
Built via `make CROSSDEV=riscv-none-elf-` for
esp32p4-pico-wifi-wareshare:nsh (CONFIG_SYSTEM_NXINIT_TEST=y) and ran
nxinit_unit_test on real esp32p4-pico-wifi-wareshare hardware over
UART:

  nsh> nxinit_unit_test
  [==========] nxinit_tests: Running 18 test(s).
  ...
  [==========] nxinit_tests: 18 test(s) run.
  [  PASSED  ] 18 test(s).

nxstyle clean on all touched files.

Assisted-by: GitHubCopilot:claude-sonnet-5
Signed-off-by: wangjianyu3 <wangjianyu3@xiaomi.com>
@JianyuWang0623
JianyuWang0623 force-pushed the nxinit-unit-tests-upstream branch from dd7113f to 7707278 Compare August 28, 2026 03:46
@JianyuWang0623
JianyuWang0623 marked this pull request as ready for review August 28, 2026 03:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants