This is an automated email from the ASF dual-hosted git repository.

acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx-apps.git

commit 63698738a889f64603f2e761831852a3a171b030
Author: wangjianyu3 <[email protected]>
AuthorDate: Thu Aug 27 10:50:26 2026 +0800

    system/nxinit: add cmocka unit tests for parser/action/service
    
    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 <[email protected]>
---
 system/nxinit/CMakeLists.txt                       |  34 ++
 system/nxinit/Kconfig                              |  41 +-
 system/nxinit/Makefile                             |  23 +-
 system/nxinit/parser.c                             |   4 +-
 system/nxinit/parser.h                             |   4 +
 system/nxinit/test/test_nxinit.c                   |  65 +++
 system/nxinit/test/test_nxinit.h                   |  81 ++++
 system/nxinit/test/test_nxinit_action.c            | 262 +++++++++++
 .../nxinit/{parser.h => test/test_nxinit_common.c} |  40 +-
 system/nxinit/test/test_nxinit_parser.c            | 494 +++++++++++++++++++++
 system/nxinit/test/test_nxinit_service.c           | 251 +++++++++++
 11 files changed, 1267 insertions(+), 32 deletions(-)

diff --git a/system/nxinit/CMakeLists.txt b/system/nxinit/CMakeLists.txt
index 92f778657..c6df81f18 100644
--- a/system/nxinit/CMakeLists.txt
+++ b/system/nxinit/CMakeLists.txt
@@ -38,4 +38,38 @@ if(CONFIG_SYSTEM_NXINIT)
     SRCS
     ${CSRCS})
 
+  if(CONFIG_SYSTEM_NXINIT_TEST)
+
+    # NxInit unit tests (cmocka). Unlike the Makefile build (which folds the
+    # test sources into the same CSRCS used by the "init" program above, so
+    # parser/action/service are compiled once and shared), CMake compiles
+    # action.c/parser.c/service.c a second time into this separate
+    # nxinit_unit_test target.
+
+    set(TEST_SRCS
+        test/test_nxinit.c
+        action.c
+        parser.c
+        service.c
+        test/test_nxinit_common.c
+        test/test_nxinit_parser.c
+        test/test_nxinit_action.c
+        test/test_nxinit_service.c)
+
+    nuttx_add_application(
+      NAME
+      nxinit_unit_test
+      PRIORITY
+      ${CONFIG_SYSTEM_NXINIT_TEST_PRIORITY}
+      STACKSIZE
+      ${CONFIG_SYSTEM_NXINIT_TEST_STACKSIZE}
+      MODULE
+      ${CONFIG_SYSTEM_NXINIT}
+      DEPENDS
+      cmocka
+      SRCS
+      ${TEST_SRCS})
+
+  endif()
+
 endif()
diff --git a/system/nxinit/Kconfig b/system/nxinit/Kconfig
index 29aa691c8..251ccae9b 100644
--- a/system/nxinit/Kconfig
+++ b/system/nxinit/Kconfig
@@ -70,7 +70,7 @@ config SYSTEM_NXINIT_ACTION_WARN_SLOW
 
 config SYSTEM_NXINIT_ACTION_EVENTS_MAX
        int "Max number of events"
-       default 1
+       default 2
        range 1 64
        ---help---
                Maximum number of event and action events.
@@ -111,6 +111,45 @@ config SYSTEM_NXINIT_SERVICE_RESTART_PERIOD
        int "Service restart period in ms"
        default 5000
 
+comment "NXInit Testing"
+
+config SYSTEM_NXINIT_TEST
+       bool "NxInit unit tests"
+       default n
+       depends on TESTING_CMOCKA
+       ---help---
+               Enable cmocka-based unit tests covering NxInit's argument/config
+               parser, action event matching, and service conflict detection.
+               Builds a separate "nxinit_unit_test" program. Under the CMake
+               build the test sources compile into a dedicated target and the
+               "init" program is unaffected; under the Make build the test
+               sources are appended to the shared CSRCS list, so they are also
+               linked into "init", enlarging it and pulling in the cmocka
+               dependency. Keep this disabled for production Make builds.
+
+if SYSTEM_NXINIT_TEST
+
+config SYSTEM_NXINIT_TEST_PRIORITY
+       int "Test task priority"
+       default 100
+
+config SYSTEM_NXINIT_TEST_STACKSIZE
+       int "Test task stack size"
+       default 8192
+       ---help---
+               Several of the parser test cases build multi-hundred-byte
+               buffers on the stack (e.g. lines several times
+               CONFIG_SYSTEM_NXINIT_RC_LINE_MAX long) on top of cmocka's own
+               framework overhead. On at least one real embedded target this
+               overflowed the default DEFAULT_TASK_STACKSIZE (2048) silently
+               (no crash dump, no watchdog reset, the test task just stopped
+               producing output), which is far harder to diagnose than an
+               outright test failure. 8192 has been verified to run all test
+               cases cleanly on that target; lower it back down only if you
+               have confirmed your target's stack usage stays within bounds.
+
+endif # SYSTEM_NXINIT_TEST
+
 comment "NXInit Log level"
 
 config SYSTEM_NXINIT_ERR
diff --git a/system/nxinit/Makefile b/system/nxinit/Makefile
index 643f4d247..095302c13 100644
--- a/system/nxinit/Makefile
+++ b/system/nxinit/Makefile
@@ -24,7 +24,7 @@ include $(APPDIR)/Make.defs
 
 # NuttX Init
 
-MAINSRC = init.c
+MAINSRC += init.c
 CSRCS += builtin.c
 CSRCS += parser.c
 CSRCS += action.c
@@ -32,9 +32,24 @@ CSRCS += service.c
 CSRCS += import.c
 CSRCS += property_simple.c
 
-PROGNAME = $(CONFIG_SYSTEM_NXINIT_PROGNAME)
-PRIORITY = $(CONFIG_SYSTEM_NXINIT_PRIORITY)
-STACKSIZE = $(CONFIG_SYSTEM_NXINIT_STACKSIZE)
+PROGNAME += $(CONFIG_SYSTEM_NXINIT_PROGNAME)
+PRIORITY += $(CONFIG_SYSTEM_NXINIT_PRIORITY)
+STACKSIZE += $(CONFIG_SYSTEM_NXINIT_STACKSIZE)
 MODULE = $(CONFIG_SYSTEM_NXINIT)
 
+# NxInit unit tests (cmocka), reusing the same parser/action/service CSRCS
+# built above.
+
+ifneq ($(CONFIG_SYSTEM_NXINIT_TEST),)
+CSRCS += test/test_nxinit_common.c
+CSRCS += test/test_nxinit_parser.c
+CSRCS += test/test_nxinit_action.c
+CSRCS += test/test_nxinit_service.c
+MAINSRC += test/test_nxinit.c
+
+PROGNAME += nxinit_unit_test
+PRIORITY += $(CONFIG_SYSTEM_NXINIT_TEST_PRIORITY)
+STACKSIZE += $(CONFIG_SYSTEM_NXINIT_TEST_STACKSIZE)
+endif
+
 include $(APPDIR)/Application.mk
diff --git a/system/nxinit/parser.c b/system/nxinit/parser.c
index c05f28917..8930dc7f5 100644
--- a/system/nxinit/parser.c
+++ b/system/nxinit/parser.c
@@ -96,8 +96,8 @@ static int init_parse_config_lines(FAR const struct parser_s 
*parser,
   return 0;
 }
 
-static int init_parse_config_buffer(FAR const struct parser_s *parser,
-                                    FAR const char *buf, size_t len)
+int init_parse_config_buffer(FAR const struct parser_s *parser,
+                             FAR const char *buf, size_t len)
 {
   char tmp[CONFIG_SYSTEM_NXINIT_RC_LINE_MAX];
   FAR const struct parser_s *cur = NULL;
diff --git a/system/nxinit/parser.h b/system/nxinit/parser.h
index 8229bbd76..8513c7534 100644
--- a/system/nxinit/parser.h
+++ b/system/nxinit/parser.h
@@ -28,6 +28,7 @@
  ****************************************************************************/
 
 #include <stdbool.h>
+#include <stddef.h>
 
 /****************************************************************************
  * Public Types
@@ -56,4 +57,7 @@ int init_parse_arguments(FAR char *buf, bool dup, int argc, 
FAR char **argv);
 int init_parse_configs(FAR const struct parser_s *parser);
 int init_parse_config_file(FAR const struct parser_s *parser,
                            FAR const char *file);
+int init_parse_config_buffer(FAR const struct parser_s *parser,
+                             FAR const char *buf, size_t len);
+
 #endif
diff --git a/system/nxinit/test/test_nxinit.c b/system/nxinit/test/test_nxinit.c
new file mode 100644
index 000000000..cfa690464
--- /dev/null
+++ b/system/nxinit/test/test_nxinit.c
@@ -0,0 +1,65 @@
+/****************************************************************************
+ * apps/system/nxinit/test/test_nxinit.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <cmocka.h>
+
+#include "test_nxinit.h"
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+int main(int argc, FAR char *argv[])
+{
+  const struct CMUnitTest nxinit_tests[] =
+    {
+      cmocka_unit_test(test_nxinit_parser_arguments_spaces),
+      cmocka_unit_test(test_nxinit_parser_arguments_quoted),
+      cmocka_unit_test(test_nxinit_parser_arguments_dashdash_separator),
+      cmocka_unit_test(test_nxinit_parser_arguments_long_option),
+      cmocka_unit_test(test_nxinit_parser_arguments_truncate),
+      cmocka_unit_test(test_nxinit_parser_config_sections),
+      cmocka_unit_test(test_nxinit_parser_config_skip_blank_lines),
+      cmocka_unit_test(test_nxinit_parser_config_unknown_section),
+      cmocka_unit_test(test_nxinit_parser_config_line_too_long),
+      cmocka_unit_test(test_nxinit_parser_config_line_crosses_boundary),
+      cmocka_unit_test(test_nxinit_parser_config_buffer_crosses_boundary),
+      cmocka_unit_test(test_nxinit_action_event_match_exact),
+      cmocka_unit_test(test_nxinit_action_event_match_invert),
+      cmocka_unit_test(test_nxinit_action_event_match_fnmatch),
+      cmocka_unit_test(test_nxinit_action_event_and_semantics),
+      cmocka_unit_test(test_nxinit_service_duplicate_conflict),
+      cmocka_unit_test(test_nxinit_service_override_replaces_duplicate),
+      cmocka_unit_test(test_nxinit_service_args_max_boundary),
+    };
+
+  return cmocka_run_group_tests(nxinit_tests, test_nxinit_group_setup,
+                                test_nxinit_group_teardown);
+}
diff --git a/system/nxinit/test/test_nxinit.h b/system/nxinit/test/test_nxinit.h
new file mode 100644
index 000000000..3921b084a
--- /dev/null
+++ b/system/nxinit/test/test_nxinit.h
@@ -0,0 +1,81 @@
+/****************************************************************************
+ * apps/system/nxinit/test/test_nxinit.h
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+#ifndef __APPS_SYSTEM_NXINIT_TEST_TEST_NXINIT_H
+#define __APPS_SYSTEM_NXINIT_TEST_TEST_NXINIT_H
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/compiler.h>
+
+/****************************************************************************
+ * Public Function Prototypes
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: test_nxinit_group_setup
+ ****************************************************************************/
+
+int test_nxinit_group_setup(FAR void **state);
+
+/****************************************************************************
+ * Name: test_nxinit_group_teardown
+ ****************************************************************************/
+
+int test_nxinit_group_teardown(FAR void **state);
+
+/****************************************************************************
+ * Name: test_nxinit_parser_*
+ ****************************************************************************/
+
+void test_nxinit_parser_arguments_spaces(FAR void **state);
+void test_nxinit_parser_arguments_quoted(FAR void **state);
+void test_nxinit_parser_arguments_dashdash_separator(FAR void **state);
+void test_nxinit_parser_arguments_long_option(FAR void **state);
+void test_nxinit_parser_arguments_truncate(FAR void **state);
+void test_nxinit_parser_config_sections(FAR void **state);
+void test_nxinit_parser_config_skip_blank_lines(FAR void **state);
+void test_nxinit_parser_config_unknown_section(FAR void **state);
+void test_nxinit_parser_config_line_too_long(FAR void **state);
+void test_nxinit_parser_config_line_crosses_boundary(FAR void **state);
+void test_nxinit_parser_config_buffer_crosses_boundary(FAR void **state);
+
+/****************************************************************************
+ * Name: test_nxinit_action_*
+ ****************************************************************************/
+
+void test_nxinit_action_event_match_exact(FAR void **state);
+void test_nxinit_action_event_match_invert(FAR void **state);
+void test_nxinit_action_event_match_fnmatch(FAR void **state);
+void test_nxinit_action_event_and_semantics(FAR void **state);
+
+/****************************************************************************
+ * Name: test_nxinit_service_*
+ ****************************************************************************/
+
+void test_nxinit_service_duplicate_conflict(FAR void **state);
+void test_nxinit_service_override_replaces_duplicate(FAR void **state);
+void test_nxinit_service_args_max_boundary(FAR void **state);
+
+#endif /* __APPS_SYSTEM_NXINIT_TEST_TEST_NXINIT_H */
diff --git a/system/nxinit/test/test_nxinit_action.c 
b/system/nxinit/test/test_nxinit_action.c
new file mode 100644
index 000000000..0bdfce10d
--- /dev/null
+++ b/system/nxinit/test/test_nxinit_action.c
@@ -0,0 +1,262 @@
+/****************************************************************************
+ * apps/system/nxinit/test/test_nxinit_action.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+#include <nuttx/list.h>
+
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/param.h>
+#include <cmocka.h>
+
+#include "../action.h"
+#include "test_nxinit.h"
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+/* Build an empty action manager, ready to accept "on <event>" sections
+ * through init_action_parse().
+ */
+
+static void action_manager_init(FAR struct action_manager_s *am)
+{
+  memset(am, 0, sizeof(*am));
+  list_initialize(&am->actions);
+  list_initialize(&am->ready_actions);
+  am->pid_running = -1;
+}
+
+/* Return true if any action is currently on the ready queue. */
+
+static bool action_manager_has_ready(FAR struct action_manager_s *am)
+{
+  return !list_is_empty(&am->ready_actions);
+}
+
+/* init_action_parse()/parse_event() strdup() each event's key/value and
+ * calloc() both the action and its commands (whose argv entries are in
+ * turn strdup'd via init_parse_arguments(..., true, ...)). None of that
+ * is released by the code under test itself (init never tears down its
+ * action manager), so release it here, mirroring
+ * service_manager_free_all() in test_nxinit_service.c.
+ */
+
+static void action_manager_free_all(FAR struct action_manager_s *am)
+{
+  FAR struct action_s *a;
+  FAR struct action_s *atmp;
+  FAR struct action_cmd_s *cmd;
+  FAR struct action_cmd_s *cmdtmp;
+  size_t i;
+  int j;
+
+  list_for_every_entry_safe(&am->actions, a, atmp, struct action_s, node)
+    {
+      for (i = 0; i < nitems(a->events); i++)
+        {
+          if (a->events[i].key)
+            {
+              free((FAR void *)a->events[i].key);
+            }
+
+          if (a->events[i].value)
+            {
+              free(a->events[i].value);
+            }
+        }
+
+      list_for_every_entry_safe(&a->cmds, cmd, cmdtmp,
+                                struct action_cmd_s, node)
+        {
+          for (j = 0; j < cmd->argc; j++)
+            {
+              free(cmd->argv[j]);
+            }
+
+          list_delete(&cmd->node);
+          free(cmd);
+        }
+
+      list_delete(&a->node);
+      free(a);
+    }
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: test_nxinit_action_event_match_exact
+ *
+ * Description:
+ *   An "on property:key=value" action becomes ready only when the exact
+ *   key/value pair is triggered.
+ ****************************************************************************/
+
+void test_nxinit_action_event_match_exact(FAR void **state)
+{
+  struct action_manager_s am;
+  struct parser_s parser =
+    {
+      "on", init_action_parse, NULL, &am
+    };
+
+  char section[] = "on property:sys.boot.reason=bootloader";
+  char cmd[] = "  trigger done";
+
+  action_manager_init(&am);
+
+  assert_int_equal(init_action_parse(&parser, true, section), 0);
+  assert_int_equal(init_action_parse(&parser, false, cmd), 0);
+
+  assert_false(action_manager_has_ready(&am));
+
+  init_action_trigger_event(&am, "sys.boot.reason", "other");
+  assert_false(action_manager_has_ready(&am));
+
+  init_action_trigger_event(&am, "sys.boot.reason", "bootloader");
+  assert_true(action_manager_has_ready(&am));
+
+  action_manager_free_all(&am);
+}
+
+/****************************************************************************
+ * Name: test_nxinit_action_event_match_invert
+ *
+ * Description:
+ *   An "on property:key!=value" action becomes ready when the value
+ *   triggered is anything other than the configured one.
+ ****************************************************************************/
+
+void test_nxinit_action_event_match_invert(FAR void **state)
+{
+  struct action_manager_s am;
+  struct parser_s parser =
+    {
+      "on", init_action_parse, NULL, &am
+    };
+
+  char section[] = "on property:sys.boot.reason!=bootloader";
+  char cmd[] = "  trigger done";
+
+  action_manager_init(&am);
+
+  assert_int_equal(init_action_parse(&parser, true, section), 0);
+  assert_int_equal(init_action_parse(&parser, false, cmd), 0);
+
+  init_action_trigger_event(&am, "sys.boot.reason", "bootloader");
+  assert_false(action_manager_has_ready(&am));
+
+  init_action_trigger_event(&am, "sys.boot.reason", "coldboot");
+  assert_true(action_manager_has_ready(&am));
+
+  action_manager_free_all(&am);
+}
+
+/****************************************************************************
+ * Name: test_nxinit_action_event_match_fnmatch
+ *
+ * Description:
+ *   Event values support fnmatch() wildcards, e.g. "bootloader*" matches
+ *   any value with that prefix.
+ ****************************************************************************/
+
+void test_nxinit_action_event_match_fnmatch(FAR void **state)
+{
+  struct action_manager_s am;
+  struct parser_s parser =
+    {
+      "on", init_action_parse, NULL, &am
+    };
+
+  char section[] = "on property:sys.boot.reason=bootloader*";
+  char cmd[] = "  trigger done";
+
+  action_manager_init(&am);
+
+  assert_int_equal(init_action_parse(&parser, true, section), 0);
+  assert_int_equal(init_action_parse(&parser, false, cmd), 0);
+
+  init_action_trigger_event(&am, "sys.boot.reason", "recovery");
+  assert_false(action_manager_has_ready(&am));
+
+  init_action_trigger_event(&am, "sys.boot.reason", "bootloader_ota");
+  assert_true(action_manager_has_ready(&am));
+
+  action_manager_free_all(&am);
+}
+
+/****************************************************************************
+ * Name: test_nxinit_action_event_and_semantics
+ *
+ * Description:
+ *   When an action has multiple events ("on evA && evB"), it only
+ *   becomes ready once every event has been satisfied, not just the
+ *   most recently triggered one.
+ ****************************************************************************/
+
+void test_nxinit_action_event_and_semantics(FAR void **state)
+{
+#if CONFIG_SYSTEM_NXINIT_ACTION_EVENTS_MAX > 1
+  struct action_manager_s am;
+  struct parser_s parser =
+    {
+      "on", init_action_parse, NULL, &am
+    };
+
+  char section[] =
+    "on property:sys.boot.reason=bootloader && property:sys.net.ready=1";
+  char cmd[] = "  trigger done";
+
+  action_manager_init(&am);
+
+  assert_int_equal(init_action_parse(&parser, true, section), 0);
+  assert_int_equal(init_action_parse(&parser, false, cmd), 0);
+
+  init_action_trigger_event(&am, "sys.boot.reason", "bootloader");
+  assert_false(action_manager_has_ready(&am));
+
+  init_action_trigger_event(&am, "sys.net.ready", "1");
+  assert_true(action_manager_has_ready(&am));
+
+  action_manager_free_all(&am);
+#else
+  /* Multi-event actions ("on evA && evB") need at least 2 event slots
+   * per action, gated the same way builtin.c gates "setprop". With the
+   * default CONFIG_SYSTEM_NXINIT_ACTION_EVENTS_MAX=1 there is nothing
+   * to exercise here.
+   */
+
+  skip();
+#endif
+}
diff --git a/system/nxinit/parser.h b/system/nxinit/test/test_nxinit_common.c
similarity index 68%
copy from system/nxinit/parser.h
copy to system/nxinit/test/test_nxinit_common.c
index 8229bbd76..afecd1d3c 100644
--- a/system/nxinit/parser.h
+++ b/system/nxinit/test/test_nxinit_common.c
@@ -1,5 +1,5 @@
 /****************************************************************************
- * apps/system/nxinit/parser.h
+ * apps/system/nxinit/test/test_nxinit_common.c
  *
  * SPDX-License-Identifier: Apache-2.0
  *
@@ -20,40 +20,30 @@
  *
  ****************************************************************************/
 
-#ifndef __APPS_SYSTEM_NXINIT_PARSER_H
-#define __APPS_SYSTEM_NXINIT_PARSER_H
-
 /****************************************************************************
  * Included Files
  ****************************************************************************/
 
-#include <stdbool.h>
+#include "test_nxinit.h"
 
 /****************************************************************************
- * Public Types
+ * Public Functions
  ****************************************************************************/
 
-struct parser_s
-{
-  FAR const char *key;
-
-  CODE int (*parse)(FAR const struct parser_s *, bool, FAR char *);
-  CODE int (*check)(FAR const struct parser_s *);
-
-  /* Type
-   *  a. struct action_manager_s
-   *  b. struct service_manager_s
-   */
+/****************************************************************************
+ * Name: test_nxinit_group_setup
+ ****************************************************************************/
 
-  FAR void *priv;
-};
+int test_nxinit_group_setup(FAR void **state)
+{
+  return 0;
+}
 
 /****************************************************************************
- * Public Function Prototypes
+ * Name: test_nxinit_group_teardown
  ****************************************************************************/
 
-int init_parse_arguments(FAR char *buf, bool dup, int argc, FAR char **argv);
-int init_parse_configs(FAR const struct parser_s *parser);
-int init_parse_config_file(FAR const struct parser_s *parser,
-                           FAR const char *file);
-#endif
+int test_nxinit_group_teardown(FAR void **state)
+{
+  return 0;
+}
diff --git a/system/nxinit/test/test_nxinit_parser.c 
b/system/nxinit/test/test_nxinit_parser.c
new file mode 100644
index 000000000..eda506c4b
--- /dev/null
+++ b/system/nxinit/test/test_nxinit_parser.c
@@ -0,0 +1,494 @@
+/****************************************************************************
+ * apps/system/nxinit/test/test_nxinit_parser.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/param.h>
+#include <unistd.h>
+#include <cmocka.h>
+
+#include "../init.h"
+#include "../parser.h"
+#include "test_nxinit.h"
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+#ifndef CONFIG_LIBC_TMPDIR
+#  define CONFIG_LIBC_TMPDIR "/tmp"
+#endif
+
+/****************************************************************************
+ * Private Types
+ ****************************************************************************/
+
+struct mock_parser_ctx_s
+{
+  int section_count;
+  int line_count;
+  int check_count;
+};
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static int mock_parse(FAR const struct parser_s *parser, bool create,
+                      FAR char *buf)
+{
+  FAR struct mock_parser_ctx_s *ctx = parser->priv;
+
+  if (create)
+    {
+      ctx->section_count++;
+    }
+  else
+    {
+      ctx->line_count++;
+    }
+
+  return 0;
+}
+
+static int mock_check(FAR const struct parser_s *parser)
+{
+  FAR struct mock_parser_ctx_s *ctx = parser->priv;
+
+  ctx->check_count++;
+  return 0;
+}
+
+/* Write 'content' to a fresh temp file and hand it to
+ * init_parse_config_file() with 'parser'. Returns the parser's return
+ * value. The temp file is removed before returning.
+ */
+
+static int parse_content(FAR const struct parser_s *parser,
+                         FAR const char *content)
+{
+  char path[] = CONFIG_LIBC_TMPDIR "/nxinit_test_XXXXXX";
+  size_t len = strlen(content);
+  int fd;
+  int ret;
+
+  fd = mkstemp(path);
+  assert_true(fd >= 0);
+
+  assert_int_equal(write(fd, content, len), (ssize_t)len);
+  close(fd);
+
+  ret = init_parse_config_file(parser, path);
+  unlink(path);
+
+  return ret;
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: test_nxinit_parser_arguments_spaces
+ *
+ * Description:
+ *   Plain space-separated arguments are split into individual tokens.
+ ****************************************************************************/
+
+void test_nxinit_parser_arguments_spaces(FAR void **state)
+{
+  char buf[] = "foo bar baz";
+  FAR char *argv[8];
+  int ret;
+
+  ret = init_parse_arguments(buf, false, nitems(argv), argv);
+  assert_int_equal(ret, 3);
+  assert_string_equal(argv[0], "foo");
+  assert_string_equal(argv[1], "bar");
+  assert_string_equal(argv[2], "baz");
+}
+
+/****************************************************************************
+ * Name: test_nxinit_parser_arguments_quoted
+ *
+ * Description:
+ *   A quoted argument containing whitespace is kept as a single token,
+ *   with the surrounding quotes stripped.
+ ****************************************************************************/
+
+void test_nxinit_parser_arguments_quoted(FAR void **state)
+{
+  char buf[] = "foo \"bar baz\"";
+  FAR char *argv[8];
+  int ret;
+
+  ret = init_parse_arguments(buf, false, nitems(argv), argv);
+  assert_int_equal(ret, 2);
+  assert_string_equal(argv[0], "foo");
+  assert_string_equal(argv[1], "bar baz");
+}
+
+/****************************************************************************
+ * Name: test_nxinit_parser_arguments_dashdash_separator
+ *
+ * Description:
+ *   A standalone "--" token stops normal splitting and folds everything
+ *   that follows into a single, final argument.
+ ****************************************************************************/
+
+void test_nxinit_parser_arguments_dashdash_separator(FAR void **state)
+{
+  char buf[] = "foo -- bar baz";
+  FAR char *argv[8];
+  int ret;
+
+  ret = init_parse_arguments(buf, false, nitems(argv), argv);
+  assert_int_equal(ret, 3);
+  assert_string_equal(argv[0], "foo");
+  assert_string_equal(argv[1], "--");
+  assert_string_equal(argv[2], "bar baz");
+}
+
+/****************************************************************************
+ * Name: test_nxinit_parser_arguments_long_option
+ *
+ * Description:
+ *   Regression test: a long option such as "--system" must not be
+ *   misinterpreted as the "--" argument separator (fixed upstream).
+ ****************************************************************************/
+
+void test_nxinit_parser_arguments_long_option(FAR void **state)
+{
+  char buf[] = "service foo /bin/foo --system --nofork";
+  FAR char *argv[8];
+  int ret;
+
+  ret = init_parse_arguments(buf, false, nitems(argv), argv);
+  assert_int_equal(ret, 5);
+  assert_string_equal(argv[0], "service");
+  assert_string_equal(argv[1], "foo");
+  assert_string_equal(argv[2], "/bin/foo");
+  assert_string_equal(argv[3], "--system");
+  assert_string_equal(argv[4], "--nofork");
+}
+
+/****************************************************************************
+ * Name: test_nxinit_parser_arguments_truncate
+ *
+ * Description:
+ *   When the number of tokens exceeds the caller-provided argv capacity,
+ *   parsing stops without overflowing argv.
+ ****************************************************************************/
+
+void test_nxinit_parser_arguments_truncate(FAR void **state)
+{
+  char buf[] = "one two three four five";
+  FAR char *argv[3];
+  int ret;
+
+  ret = init_parse_arguments(buf, false, nitems(argv), argv);
+  assert_int_equal(ret, 3);
+  assert_string_equal(argv[0], "one");
+  assert_string_equal(argv[1], "two");
+
+  /* The remaining, still-unsplit text is folded into the last argv slot
+   * rather than truncated away; pin down that exact behavior so a future
+   * change from "fold" to "drop" is caught here instead of silently
+   * changing init.rc semantics.
+   */
+
+  assert_string_equal(argv[2], "three four five");
+}
+
+/****************************************************************************
+ * Name: test_nxinit_parser_config_sections
+ *
+ * Description:
+ *   Lines are routed to the currently active section based on the
+ *   longest matching keyword, and sub-lines attach to that section
+ *   until a new section keyword is seen.
+ ****************************************************************************/
+
+void test_nxinit_parser_config_sections(FAR void **state)
+{
+  struct mock_parser_ctx_s ctx_a =
+    {
+      0
+    };
+
+  struct mock_parser_ctx_s ctx_b =
+    {
+      0
+    };
+
+  struct parser_s table[] =
+    {
+      {"secA", mock_parse, mock_check, &ctx_a},
+      {"secB", mock_parse, mock_check, &ctx_b},
+      {NULL},
+    };
+
+  int ret;
+
+  ret = parse_content(table,
+                      "secA one\n"
+                      "  line-a1\n"
+                      "  line-a2\n"
+                      "secB two\n"
+                      "  line-b1\n");
+
+  assert_int_equal(ret, 0);
+  assert_int_equal(ctx_a.section_count, 1);
+  assert_int_equal(ctx_a.line_count, 2);
+  assert_int_equal(ctx_a.check_count, 1);
+  assert_int_equal(ctx_b.section_count, 1);
+  assert_int_equal(ctx_b.line_count, 1);
+  assert_int_equal(ctx_b.check_count, 1);
+}
+
+/****************************************************************************
+ * Name: test_nxinit_parser_config_skip_blank_lines
+ *
+ * Description:
+ *   Empty lines and lines containing only whitespace are skipped and do
+ *   not reach the section's parse callback.
+ ****************************************************************************/
+
+void test_nxinit_parser_config_skip_blank_lines(FAR void **state)
+{
+  struct mock_parser_ctx_s ctx =
+    {
+      0
+    };
+
+  struct parser_s table[] =
+    {
+      {"secA", mock_parse, mock_check, &ctx},
+      {NULL},
+    };
+
+  int ret;
+
+  ret = parse_content(table,
+                      "secA one\n"
+                      "\n"
+                      "   \n"
+                      "  line-a1\n");
+
+  assert_int_equal(ret, 0);
+  assert_int_equal(ctx.section_count, 1);
+  assert_int_equal(ctx.line_count, 1);
+}
+
+/****************************************************************************
+ * Name: test_nxinit_parser_config_unknown_section
+ *
+ * Description:
+ *   A line that matches no known section keyword, while no section is
+ *   currently active, is rejected with -EINVAL.
+ ****************************************************************************/
+
+void test_nxinit_parser_config_unknown_section(FAR void **state)
+{
+  struct mock_parser_ctx_s ctx =
+    {
+      0
+    };
+
+  struct parser_s table[] =
+    {
+      {"secA", mock_parse, mock_check, &ctx},
+      {NULL},
+    };
+
+  int ret;
+
+  ret = parse_content(table, "notasection foo\n");
+
+  assert_int_equal(ret, -EINVAL);
+}
+
+/****************************************************************************
+ * Name: test_nxinit_parser_config_line_too_long
+ *
+ * Description:
+ *   A single line without a newline that exceeds
+ *   CONFIG_SYSTEM_NXINIT_RC_LINE_MAX is rejected with -E2BIG.
+ ****************************************************************************/
+
+void test_nxinit_parser_config_line_too_long(FAR void **state)
+{
+  struct mock_parser_ctx_s ctx =
+    {
+      0
+    };
+
+  struct parser_s table[] =
+    {
+      {"secA", mock_parse, mock_check, &ctx},
+      {NULL},
+    };
+
+  char content[CONFIG_SYSTEM_NXINIT_RC_LINE_MAX + 32];
+  int ret;
+  int i;
+
+  for (i = 0; i < (int)sizeof(content) - 1; i++)
+    {
+      content[i] = 'x';
+    }
+
+  content[sizeof(content) - 1] = '\0';
+
+  ret = parse_content(table, content);
+  assert_int_equal(ret, -E2BIG);
+}
+
+/****************************************************************************
+ * Name: test_nxinit_parser_config_line_crosses_boundary
+ *
+ * Description:
+ *   The config file is read in chunks no larger than
+ *   CONFIG_SYSTEM_NXINIT_RC_LINE_MAX. This test feeds a file several
+ *   times larger than that buffer to verify the leftover-bytes bookkeeping
+ *   (memmove) does not corrupt or drop lines that straddle a refill.
+ ****************************************************************************/
+
+void test_nxinit_parser_config_line_crosses_boundary(FAR void **state)
+{
+  struct mock_parser_ctx_s ctx =
+    {
+      0
+    };
+
+  struct parser_s table[] =
+    {
+      {"secA", mock_parse, mock_check, &ctx},
+      {NULL},
+    };
+
+  char content[CONFIG_SYSTEM_NXINIT_RC_LINE_MAX * 4];
+  FAR char *p = content;
+  size_t remaining = sizeof(content);
+  int blocks = 0;
+  int ret;
+  int n;
+
+  for (; ; )
+    {
+      n = snprintf(p, remaining, "secA blk%d\n  cmd%d\n", blocks, blocks);
+      if (n < 0 || (size_t)n >= remaining)
+        {
+          break;
+        }
+
+      p += n;
+      remaining -= n;
+      blocks++;
+    }
+
+  /* The final, over-budget snprintf() call still writes a truncated
+   * partial block (plus its own NUL) at 'p' before being rejected by
+   * the length check above; cut it back off so content only holds the
+   * 'blocks' complete lines that were actually accounted for.
+   */
+
+  *p = '\0';
+
+  assert_true(blocks > 4);
+
+  ret = parse_content(table, content);
+  assert_int_equal(ret, 0);
+  assert_int_equal(ctx.section_count, blocks);
+  assert_int_equal(ctx.line_count, blocks);
+  assert_int_equal(ctx.check_count, 1);
+}
+
+/****************************************************************************
+ * Name: test_nxinit_parser_config_buffer_crosses_boundary
+ *
+ * Description:
+ *   init_parse_config_buffer() is the buffer-based counterpart of
+ *   init_parse_config_file() used by init_parse_configs() to parse the
+ *   builtin "preset" rc content; it is a separate code path with its own
+ *   refill bookkeeping. Feed it the exact same multi-block, over-budget
+ *   content as test_nxinit_parser_config_line_crosses_boundary() (rather
+ *   than routing through a temp file) to make sure lines that straddle a
+ *   refill are still tracked correctly and, in particular, that copying
+ *   the next chunk on top of the 'n' leftover bytes already held in the
+ *   internal buffer does not overflow it.
+ ****************************************************************************/
+
+void test_nxinit_parser_config_buffer_crosses_boundary(FAR void **state)
+{
+  struct mock_parser_ctx_s ctx =
+    {
+      0
+    };
+
+  struct parser_s table[] =
+    {
+      {"secA", mock_parse, mock_check, &ctx},
+      {NULL},
+    };
+
+  char content[CONFIG_SYSTEM_NXINIT_RC_LINE_MAX * 4];
+  FAR char *p = content;
+  size_t remaining = sizeof(content);
+  int blocks = 0;
+  int ret;
+  int n;
+
+  for (; ; )
+    {
+      n = snprintf(p, remaining, "secA blk%d\n  cmd%d\n", blocks, blocks);
+      if (n < 0 || (size_t)n >= remaining)
+        {
+          break;
+        }
+
+      p += n;
+      remaining -= n;
+      blocks++;
+    }
+
+  assert_true(blocks > 4);
+
+  ret = init_parse_config_buffer(table, content, (size_t)(p - content));
+  assert_int_equal(ret, 0);
+  assert_int_equal(ctx.section_count, blocks);
+  assert_int_equal(ctx.line_count, blocks);
+  assert_int_equal(ctx.check_count, 1);
+}
diff --git a/system/nxinit/test/test_nxinit_service.c 
b/system/nxinit/test/test_nxinit_service.c
new file mode 100644
index 000000000..f732d037c
--- /dev/null
+++ b/system/nxinit/test/test_nxinit_service.c
@@ -0,0 +1,251 @@
+/****************************************************************************
+ * apps/system/nxinit/test/test_nxinit_service.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+#include <nuttx/list.h>
+
+#include <errno.h>
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/param.h>
+#include <cmocka.h>
+
+#include "../parser.h"
+#include "../service.h"
+#include "test_nxinit.h"
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static void service_manager_init(FAR struct service_manager_s *sm)
+{
+  list_initialize(&sm->services);
+}
+
+/* init_service_parse()/init_service_check() free failed/duplicate
+ * services internally, but successfully-parsed services in "sm" are
+ * left for the caller to release. This mirrors what init_service_refresh()
+ * would eventually do for SVC_REMOVE-flagged entries, kept minimal here
+ * since these tests never start any service (no pid, no posix_spawn).
+ */
+
+static void service_manager_free_all(FAR struct service_manager_s *sm)
+{
+  FAR struct service_s *s;
+  FAR struct service_s *tmp;
+  FAR struct service_class_s *c;
+  FAR struct service_class_s *ctmp;
+  int i;
+
+  list_for_every_entry_safe(&sm->services, s, tmp, struct service_s, node)
+    {
+      list_for_every_entry_safe(&s->classes, c, ctmp,
+                                struct service_class_s, node)
+        {
+          list_delete(&c->node);
+          free(c);
+        }
+
+      for (i = 0; i < s->argc; i++)
+        {
+          free(s->argv[i]);
+        }
+
+      list_delete(&s->node);
+      free(s);
+    }
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: test_nxinit_service_duplicate_conflict
+ *
+ * Description:
+ *   Two "service" sections sharing the same name, with no "override"
+ *   option, are rejected by init_service_check() with -EEXIST.
+ ****************************************************************************/
+
+void test_nxinit_service_duplicate_conflict(FAR void **state)
+{
+  struct service_manager_s sm;
+  struct parser_s parser =
+    {
+      "service", init_service_parse, init_service_check, &sm
+    };
+
+  char decl1[] = "service foo /bin/foo";
+  char decl2[] = "service foo /bin/bar";
+
+  service_manager_init(&sm);
+
+  assert_int_equal(init_service_parse(&parser, true, decl1), 0);
+  assert_int_equal(init_service_parse(&parser, true, decl2), 0);
+
+  assert_int_equal(init_service_check(&parser), -EEXIST);
+
+  service_manager_free_all(&sm);
+}
+
+/****************************************************************************
+ * Name: test_nxinit_service_override_replaces_duplicate
+ *
+ * Description:
+ *   When the later "service" definition carries the "override" option,
+ *   init_service_check() disables/removes the earlier one instead of
+ *   failing.
+ ****************************************************************************/
+
+void test_nxinit_service_override_replaces_duplicate(FAR void **state)
+{
+  struct service_manager_s sm;
+  struct parser_s parser =
+    {
+      "service", init_service_parse, init_service_check, &sm
+    };
+
+  char decl1[] = "service foo /bin/foo";
+  char decl2[] = "service foo /bin/bar";
+  char opt_override[] = "  override";
+  FAR struct service_s *s1;
+  FAR struct service_s *s2;
+
+  service_manager_init(&sm);
+
+  assert_int_equal(init_service_parse(&parser, true, decl1), 0);
+  assert_int_equal(init_service_parse(&parser, true, decl2), 0);
+  assert_int_equal(init_service_parse(&parser, false, opt_override), 0);
+
+  assert_int_equal(init_service_check(&parser), 0);
+
+  s1 = list_first_entry(&sm.services, struct service_s, node);
+  s2 = list_last_entry(&sm.services, struct service_s, node);
+
+  /* SVC_REMOVE sets bit 31, so the raw "int" OR of the two flags is
+   * negative; cast explicitly to uint32_t before comparing so
+   * assert_int_equal()'s intmax_t widening does not sign-extend one
+   * side and zero-extend the other.
+   */
+
+  assert_int_equal(s1->flags & (SVC_DISABLED | SVC_REMOVE),
+                   (uint32_t)(SVC_DISABLED | SVC_REMOVE));
+  assert_int_equal(s2->flags & (SVC_DISABLED | SVC_REMOVE), 0);
+
+  service_manager_free_all(&sm);
+}
+
+/****************************************************************************
+ * Name: test_nxinit_service_args_max_boundary
+ *
+ * Description:
+ *   A "service" declaration with exactly
+ *   CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX tokens is fully captured, while
+ *   one more token than the limit is folded into the last slot instead
+ *   of overflowing argv.
+ ****************************************************************************/
+
+void test_nxinit_service_args_max_boundary(FAR void **state)
+{
+  /* "service" + "<name>" + "<pathname>" already consume 3 of the
+   * CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX tokens, leaving this many
+   * "argN" tokens to reach the limit exactly. At the Kconfig lower
+   * bound of 3 this is 0, i.e. "service <name> <pathname>" already
+   * fills argv on its own with no room for even a single "argN"
+   * token, so there is nothing meaningful left to exercise here.
+   */
+
+#define NARGS_AT_LIMIT (CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX - 3)
+
+#if NARGS_AT_LIMIT < 1
+  skip();
+#else
+  struct service_manager_s sm;
+  struct parser_s parser =
+    {
+      "service", init_service_parse, init_service_check, &sm
+    };
+
+  char at_limit[32 + 8 * NARGS_AT_LIMIT] = "service foo1 /bin/foo";
+  char over_limit[32 + 8 * (NARGS_AT_LIMIT + 1)] = "service foo2 /bin/foo";
+  char last_arg[16];
+  char last_two_args[32];
+  FAR struct service_s *s;
+  int i;
+
+  for (i = 1; i <= NARGS_AT_LIMIT + 1; i++)
+    {
+      if (i <= NARGS_AT_LIMIT)
+        {
+          snprintf(at_limit + strlen(at_limit),
+                  sizeof(at_limit) - strlen(at_limit), " arg%d", i);
+        }
+
+      snprintf(over_limit + strlen(over_limit),
+              sizeof(over_limit) - strlen(over_limit), " arg%d", i);
+    }
+
+  snprintf(last_arg, sizeof(last_arg), "arg%d", NARGS_AT_LIMIT);
+  snprintf(last_two_args, sizeof(last_two_args), "arg%d arg%d",
+          NARGS_AT_LIMIT, NARGS_AT_LIMIT + 1);
+
+  service_manager_init(&sm);
+
+  /* A declaration with exactly CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX
+   * tokens is fully captured.
+   */
+
+  assert_int_equal(init_service_parse(&parser, true, at_limit), 0);
+
+  s = list_last_entry(&sm.services, struct service_s, node);
+  assert_int_equal(s->argc, CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX);
+  assert_string_equal(s->argv[CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX - 1],
+                      last_arg);
+
+  /* One token beyond the limit is folded into the last argv slot
+   * together with the remaining, still-unsplit text, instead of
+   * overflowing argv.
+   */
+
+  assert_int_equal(init_service_parse(&parser, true, over_limit), 0);
+
+  s = list_last_entry(&sm.services, struct service_s, node);
+  assert_int_equal(s->argc, CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX);
+  assert_string_equal(s->argv[CONFIG_SYSTEM_NXINIT_SERVICE_ARGS_MAX - 1],
+                      last_two_args);
+
+  service_manager_free_all(&sm);
+#endif
+
+#undef NARGS_AT_LIMIT
+}

Reply via email to