mirror of
https://github.com/apache/nuttx-apps.git
synced 2026-09-09 02:06:33 +00:00
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>
494 lines
14 KiB
C
494 lines
14 KiB
C
/****************************************************************************
|
|
* 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);
|
|
}
|