nuttx-apps/system/nxinit/parser.c
wangjianyu3 63698738a8 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 <wangjianyu3@xiaomi.com>
2026-08-28 09:33:47 -03:00

385 lines
8.1 KiB
C

/****************************************************************************
* apps/system/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 <ctype.h>
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <unistd.h>
#include "init.h"
#include "parser.h"
/****************************************************************************
* Private Functions
****************************************************************************/
static int init_parse_config_lines(FAR const struct parser_s *parser,
FAR const struct parser_s **cur,
FAR size_t *line,
FAR char *buf, FAR size_t *len)
{
bool create = false;
FAR char *nl;
int ret;
while ((nl = memchr(buf, '\n', *len)))
{
*(nl++) = '\0';
*len -= nl - buf;
init_debug("Line %-3zu '%s'", ++*line, buf);
/* Skip empty lines and lines containing only whitespace */
for (ret = 0; buf[ret] && isblank(buf[ret]); ret++);
if (buf[ret] == '\0')
{
memmove(buf, nl, *len);
continue;
}
for (ret = 0; parser[ret].key; ret++)
{
if (!strncmp(parser[ret].key, buf, strlen(parser[ret].key)))
{
create = true;
*cur = &parser[ret];
init_debug("New section (%s)", parser[ret].key);
break;
}
}
if (*cur == NULL)
{
return -EINVAL;
}
ret = (*cur)->parse(*cur, create, buf);
create = false;
if (ret < 0)
{
return ret;
}
memmove(buf, nl, *len);
}
return 0;
}
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;
size_t line = 0;
size_t off = 0;
size_t n = 0;
size_t r;
int ret;
for (; ; )
{
r = MIN(len - off, sizeof(tmp) - n);
memcpy(&tmp[n], &buf[off], r);
if (r == 0)
{
if (n == 0)
{
break;
}
tmp[n++] = '\n';
}
n += r;
off += r;
ret = init_parse_config_lines(parser, &cur, &line, tmp, &n);
if (ret < 0)
{
return ret;
}
if (n == sizeof(tmp))
{
return -E2BIG;
}
}
for (n = 0; parser[n].key; n++)
{
if (parser[n].check)
{
ret = parser[n].check(&parser[n]);
if (ret < 0)
{
return ret;
}
}
}
return 0;
}
/****************************************************************************
* Public Functions
****************************************************************************/
int init_parse_arguments(FAR char *buf, bool dup, int argc, FAR char **argv)
{
bool quote = false;
bool new = true;
int i = 0;
for (; ; )
{
while (isblank(*buf))
{
if (!quote)
{
*buf = '\0';
new = true;
}
buf++;
}
if (*buf == '-' && *(buf + 1) == '-'
&& (isblank(*(buf + 2)) || *(buf + 2) == '\0'))
{
argv[i++] = buf;
if (i >= argc || *(buf += 2) == '\0')
{
break;
}
while (isblank(*buf))
{
*buf++ = '\0';
}
argv[i++] = buf;
break;
}
if (*buf == '\"')
{
*buf = '\0';
if (quote)
{
quote = false;
buf++;
}
else
{
quote = true;
new = true;
buf++;
}
}
if (*buf == '\0')
{
break;
}
if (new)
{
argv[i++] = buf;
if (i >= argc)
{
break;
}
new = false;
}
buf++;
}
if (dup && i > 0)
{
argc = i;
for (i = 0; i < argc; i++)
{
argv[i] = strdup(argv[i]);
if (!argv[i])
{
while (i-- > 0)
{
free(argv[i]);
}
return -errno;
}
}
}
return i;
}
int init_parse_config_file(FAR const struct parser_s *parser,
FAR const char *file)
{
char buf[CONFIG_SYSTEM_NXINIT_RC_LINE_MAX];
FAR const struct parser_s *cur = NULL;
size_t line = 0;
size_t n = 0;
int ret = 0;
int fd;
init_debug("Parsing %s", file);
fd = open(file, O_RDONLY | O_CLOEXEC);
if (fd < 0)
{
init_err("Opening %s %d", file, errno);
return -errno;
}
for (; ; )
{
ssize_t r = read(fd, &buf[n], sizeof(buf) - n);
if (r < 0)
{
if (errno == EINTR)
{
continue;
}
ret = -errno;
goto out;
}
else if (r == 0)
{
if (n == 0)
{
break;
}
buf[n++] = '\n';
}
n += r;
ret = init_parse_config_lines(parser, &cur, &line, buf, &n);
if (ret < 0)
{
goto out;
}
if (n == sizeof(buf))
{
ret = -E2BIG;
goto out;
}
}
for (n = 0; parser[n].key; n++)
{
if (parser[n].check)
{
ret = parser[n].check(&parser[n]);
if (ret < 0)
{
break;
}
}
}
out:
close(fd);
if (ret < 0)
{
init_err("Parse %s %d", file, ret);
}
return ret;
}
int init_parse_configs(FAR const struct parser_s *parser)
{
static const char preset[] =
"on boot\n"
" trigger init\n"
#if defined(CONFIG_NETUTILS_NETINIT) || defined(CONFIG_SYSTEM_NXINIT_FINALINIT)
"on init\n"
#ifdef CONFIG_NETUTILS_NETINIT
" trigger netinit\n"
#endif
#ifdef CONFIG_SYSTEM_NXINIT_FINALINIT
" trigger finalinit\n"
#endif
#endif
#ifdef CONFIG_NETUTILS_NETINIT
"on netinit\n"
" netinit\n"
#endif
;
FAR const char *path = CONFIG_SYSTEM_NXINIT_RC_FILE_PATH;
FAR const char *ext;
char file[PATH_MAX];
int ret;
ret = init_parse_config_buffer(parser, preset, sizeof(preset));
if (ret < 0)
{
return ret;
}
ret = init_parse_config_file(parser, path);
if (ret < 0)
{
return ret;
}
/* Parse the optional cpu-specific config derived from the rc path, e.g.
* "/etc/init.d/init.rc" -> "/etc/init.d/init.cpu0.rc".
*/
ext = strrchr(path, '.');
if (ext == NULL)
{
return 0;
}
snprintf(file, sizeof(file), "%.*s.cpu%d%s",
(int)(ext - path), path, sched_getcpu(), ext);
if (access(file, F_OK) < 0)
{
init_debug("skipping non-exist file %s", file);
return 0;
}
return init_parse_config_file(parser, file);
}