From: Zbigniew Jędrzejewski-Szmek Date: Thu, 19 Mar 2026 16:45:17 +0000 (+0100) Subject: test-options: add tests for option macros and flags X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=4bf73e6980fb8f440fbf38253041ca08ff2e0e1d;p=thirdparty%2Fsystemd.git test-options: add tests for option macros and flags Add tests for OPTION_STOPS_PARSING, OPTION_GROUP_MARKER, and OPTION_OPTIONAL_ARG flags with manual Option arrays, and a separate test exercising the OPTION, OPTION_LONG, OPTION_SHORT, OPTION_FULL, and OPTION_GROUP macros via FOREACH_OPTION_FULL in a switch statement, as they would be used in real code. Co-developed-by: Claude Opus 4.6 --- diff --git a/src/test/test-options.c b/src/test/test-options.c index 56e8dd1d613..6ca29fb849a 100644 --- a/src/test/test-options.c +++ b/src/test/test-options.c @@ -7,6 +7,7 @@ typedef struct Entry { const char *long_code; const char *argument; + char short_code; } Entry; static void test_option_parse_one( @@ -27,7 +28,7 @@ static void test_option_parse_one( for (const Option *o = options; o->short_code != 0 || o->long_code; o++) n_options++; - for (const Entry *e = entries; e && e->long_code; e++) + for (const Entry *e = entries; e && (e->long_code || e->short_code != 0); e++) n_entries++; OptionParser state = {}; @@ -43,7 +44,10 @@ static void test_option_parse_one( strnull(opt->metavar), strnull(arg)); ASSERT_LT(i, n_entries); - ASSERT_TRUE(streq_ptr(opt->long_code, entries[i].long_code)); + if (entries[i].long_code) + ASSERT_TRUE(streq_ptr(opt->long_code, entries[i].long_code)); + if (entries[i].short_code != 0) + ASSERT_EQ(opt->short_code, entries[i].short_code); ASSERT_TRUE(streq_ptr(arg, entries[i].argument)); i++; } @@ -55,6 +59,30 @@ static void test_option_parse_one( ASSERT_STREQ(argv[0], saved_argv0); } +static void test_option_invalid_one( + char **argv, + const Option options[static 1]) { + + _cleanup_free_ char *joined = strv_join(argv, ", "); + log_debug("/* %s(%s) */", __func__, joined); + + _cleanup_free_ char *saved_argv0 = NULL; + ASSERT_NOT_NULL(saved_argv0 = strdup(argv[0])); + + int argc = strv_length(argv); + + size_t n_options = 0; + for (const Option *o = options; o->short_code != 0 || o->long_code; o++) + n_options++; + + OptionParser state = {}; + const Option *opt; + const char *arg; + + int c = option_parse(options, options + n_options, &state, argc, argv, &opt, &arg); + ASSERT_ERROR(c, EINVAL); +} + TEST(option_parse) { static const Option options[] = { { 1, .short_code = 'h', .long_code = "help" }, @@ -372,4 +400,596 @@ TEST(option_parse) { "--optional1")); } +TEST(option_stops_parsing) { + static const Option options[] = { + { 1, .short_code = 'h', .long_code = "help" }, + { 2, .long_code = "version" }, + { 3, .short_code = 'r', .long_code = "required", .metavar = "ARG" }, + { 4, .long_code = "exec", .flags = OPTION_STOPS_PARSING }, + {} + }; + + /* --exec stops parsing, subsequent --help is positional */ + test_option_parse_one(STRV_MAKE("arg0", + "--exec", + "--help", + "foo"), + options, + (Entry[]) { + { "exec" }, + {} + }, + STRV_MAKE("--help", + "foo")); + + /* Options before --exec are still parsed */ + test_option_parse_one(STRV_MAKE("arg0", + "--help", + "--exec", + "--version", + "bar"), + options, + (Entry[]) { + { "help" }, + { "exec" }, + {} + }, + STRV_MAKE("--version", + "bar")); + + /* --exec with no trailing args */ + test_option_parse_one(STRV_MAKE("arg0", + "--exec"), + options, + (Entry[]) { + { "exec" }, + {} + }, + NULL); + + /* --exec after positional args */ + test_option_parse_one(STRV_MAKE("arg0", + "pos1", + "--exec", + "--help", + "--required", "val"), + options, + (Entry[]) { + { "exec" }, + {} + }, + STRV_MAKE("pos1", + "--help", + "--required", + "val")); + + /* "--" after --exec: "--" is still consumed as end-of-options marker. This is needed for + * backwards compatibility, systemd-dissect implemented this behaviour. But also, it makes + * sense: we're unlikely to ever want to specify "--" as the first argument of whatever + * sequence, but the user may want to specify it for clarity. */ + test_option_parse_one(STRV_MAKE("arg0", + "--exec", + "--", + "--help"), + options, + (Entry[]) { + { "exec" }, + {} + }, + STRV_MAKE("--help")); + + /* "--" before --exec: "--" terminates first, --exec is positional */ + test_option_parse_one(STRV_MAKE("arg0", + "--", + "--exec", + "--help"), + options, + NULL, + STRV_MAKE("--exec", + "--help")); + + /* Multiple options then --exec then more option-like args */ + test_option_parse_one(STRV_MAKE("arg0", + "--help", + "-r", "val1", + "--exec", + "-h", + "--required", "val2"), + options, + (Entry[]) { + { "help" }, + { "required", "val1" }, + { "exec" }, + {} + }, + STRV_MAKE("-h", + "--required", + "val2")); +} + +TEST(option_group_marker) { + static const Option options[] = { + { 1, .short_code = 'h', .long_code = "help" }, + { 2, .long_code = "version" }, + { 0, .long_code = "AdvancedGroup", .flags = OPTION_GROUP_MARKER }, + { 3, .long_code = "debug" }, + { 4, .long_code = "Advance" }, /* prefix match with the group */ + { 5, .long_code = "defilbrilate" }, + {} + }; + + /* Group markers are skipped by the parser — only real options are returned */ + test_option_parse_one(STRV_MAKE("arg0", + "--help", + "--debug"), + options, + (Entry[]) { + { "help" }, + { "debug" }, + {} + }, + NULL); + + /* Check that group marker name is ignored */ + test_option_parse_one(STRV_MAKE("arg0", + "--debug", + "--version"), + options, + (Entry[]) { + { "debug" }, + { "version" }, + {} + }, + NULL); + + /* Verify that the group marker is not mistaken for an option */ + test_option_invalid_one(STRV_MAKE("arg0", + "--AdvancedGroup"), + options); + + /* Verify that the group marker is not mistaken for an option */ + test_option_invalid_one(STRV_MAKE("arg0", + "--AdvancedGroup=2"), + options); + + /* Verify that the group marker is not mistaken for an option, prefix match */ + test_option_invalid_one(STRV_MAKE("arg0", + "--Advanced"), + options); + + /* Check that group marker name is ignored */ + test_option_parse_one(STRV_MAKE("arg0", + "--Advance", + "--Advan"), /* prefix match with unique prefix */ + options, + (Entry[]) { + { "Advance" }, + { "Advance" }, + {} + }, + NULL); + + /* Partial match with multiple candidates */ + test_option_invalid_one(STRV_MAKE("arg0", + "--de"), + options); +} + +TEST(option_optional_arg) { + static const Option options[] = { + { 1, .short_code = 'o', .long_code = "output", .metavar = "FILE", .flags = OPTION_OPTIONAL_ARG }, + { 2, .short_code = 'h', .long_code = "help" }, + {} + }; + + /* Long option with = gets the argument */ + test_option_parse_one(STRV_MAKE("arg0", + "--output=foo.txt"), + options, + (Entry[]) { + { "output", "foo.txt" }, + {} + }, + NULL); + + /* Long option without = does NOT consume the next arg */ + test_option_parse_one(STRV_MAKE("arg0", + "--output", "foo.txt"), + options, + (Entry[]) { + { "output", NULL }, + {} + }, + STRV_MAKE("foo.txt")); + + /* Short option with inline arg */ + test_option_parse_one(STRV_MAKE("arg0", + "-ofoo.txt"), + options, + (Entry[]) { + { "output", "foo.txt" }, + {} + }, + NULL); + + /* Short option without inline arg does NOT consume the next arg */ + test_option_parse_one(STRV_MAKE("arg0", + "-o", "foo.txt"), + options, + (Entry[]) { + { "output", NULL }, + {} + }, + STRV_MAKE("foo.txt")); + + /* Optional arg option at end of argv */ + test_option_parse_one(STRV_MAKE("arg0", + "--output"), + options, + (Entry[]) { + { "output", NULL }, + {} + }, + NULL); + + /* Mixed: optional arg with other options */ + test_option_parse_one(STRV_MAKE("arg0", + "--help", + "--output=bar", + "--help"), + options, + (Entry[]) { + { "help" }, + { "output", "bar" }, + { "help" }, + {} + }, + NULL); + + /* Short combo: -ho (h then o with no arg) */ + test_option_parse_one(STRV_MAKE("arg0", + "-ho", "pos1"), + options, + (Entry[]) { + { "help" }, + { "output", NULL }, + {} + }, + STRV_MAKE("pos1")); + + /* Short combo: -hobar (h then o with inline arg "bar") */ + test_option_parse_one(STRV_MAKE("arg0", + "-hobar"), + options, + (Entry[]) { + { "help" }, + { "output", "bar" }, + {} + }, + NULL); +} + +/* Test the OPTION, OPTION_LONG, OPTION_SHORT, OPTION_FULL, OPTION_GROUP macros + * by using them in a FOREACH_OPTION_FULL switch, as they would be used in real code. */ + +static void test_macros_parse_one( + char **argv, + const Entry *entries, + char **remaining) { + + _cleanup_free_ char *joined = strv_join(argv, ", "); + log_debug("/* %s(%s) */", __func__, joined); + + _cleanup_free_ char *saved_argv0 = NULL; + ASSERT_NOT_NULL(saved_argv0 = strdup(argv[0])); + + int argc = strv_length(argv); + size_t i = 0, n_entries = 0; + + for (const Entry *e = entries; e && (e->long_code || e->short_code != 0); e++) + n_entries++; + + OptionParser state = {}; + const Option *opt; + const char *arg; + + FOREACH_OPTION_FULL(&state, c, argc, argv, &opt, &arg, ASSERT_TRUE(false)) { + log_debug("%c %s: %s=%s", + opt->short_code != 0 ? opt->short_code : ' ', + opt->long_code ?: "", + strnull(opt->metavar), strnull(arg)); + + ASSERT_LT(i, n_entries); + if (entries[i].long_code) + ASSERT_TRUE(streq_ptr(opt->long_code, entries[i].long_code)); + if (entries[i].short_code != 0) + ASSERT_EQ(opt->short_code, entries[i].short_code); + ASSERT_TRUE(streq_ptr(arg, entries[i].argument)); + i++; + + switch (c) { + + /* OPTION: short + long, no arg */ + OPTION('h', "help", NULL, "Show this help"): + break; + + /* OPTION_LONG: long only, no arg */ + OPTION_LONG("version", NULL, "Show package version"): + break; + + /* OPTION_SHORT: short only, no arg */ + OPTION_SHORT('v', NULL, "Enable verbose mode"): + break; + + /* OPTION: short + long, required arg */ + OPTION('r', "required", "ARG", "Required arg option"): + break; + + /* OPTION_FULL: optional arg */ + OPTION_FULL(OPTION_OPTIONAL_ARG, 'o', "optional", "ARG", "Optional arg option"): + break; + + /* OPTION_FULL: stops parsing */ + OPTION_FULL(OPTION_STOPS_PARSING, 0, "exec", NULL, "Stop parsing after this"): + break; + + /* OPTION_GROUP: group marker (never returned by parser) */ + OPTION_GROUP("Advanced"): + break; + + /* OPTION_LONG: long only, in the "Advanced" group */ + OPTION_LONG("debug", NULL, "Enable debug mode"): + break; + + default: + log_error("Unexpected option id: %d", c); + ASSERT_TRUE(false); + } + } + + ASSERT_EQ(i, n_entries); + + char **args = option_parser_get_args(&state, argc, argv); + ASSERT_TRUE(strv_equal(args, remaining)); + ASSERT_STREQ(argv[0], saved_argv0); +} + +TEST(option_macros) { + /* OPTION: long form */ + test_macros_parse_one(STRV_MAKE("arg0", + "--help"), + (Entry[]) { + { "help" }, + {} + }, + NULL); + + /* OPTION: short form */ + test_macros_parse_one(STRV_MAKE("arg0", + "-h"), + (Entry[]) { + { "help" }, + {} + }, + NULL); + + /* OPTION_LONG: only accessible via long form */ + test_macros_parse_one(STRV_MAKE("arg0", + "--version"), + (Entry[]) { + { "version" }, + {} + }, + NULL); + + /* OPTION_SHORT: only accessible via short form */ + test_macros_parse_one(STRV_MAKE("arg0", + "-v"), + (Entry[]) { + { .short_code = 'v' }, + {} + }, + NULL); + + /* OPTION with required arg: long --required=ARG */ + test_macros_parse_one(STRV_MAKE("arg0", + "--required=val1"), + (Entry[]) { + { "required", "val1" }, + {} + }, + NULL); + + /* OPTION with required arg: long --required ARG */ + test_macros_parse_one(STRV_MAKE("arg0", + "--required", "val1"), + (Entry[]) { + { "required", "val1" }, + {} + }, + NULL); + + /* OPTION with required arg: short -r ARG */ + test_macros_parse_one(STRV_MAKE("arg0", + "-r", "val1"), + (Entry[]) { + { "required", "val1" }, + {} + }, + NULL); + + /* OPTION with required arg: short -rARG */ + test_macros_parse_one(STRV_MAKE("arg0", + "-rval1"), + (Entry[]) { + { "required", "val1" }, + {} + }, + NULL); + + /* OPTION_FULL with OPTION_OPTIONAL_ARG: long with = */ + test_macros_parse_one(STRV_MAKE("arg0", + "--optional=val1"), + (Entry[]) { + { "optional", "val1" }, + {} + }, + NULL); + + /* OPTION_FULL with OPTION_OPTIONAL_ARG: long without = doesn't consume next */ + test_macros_parse_one(STRV_MAKE("arg0", + "--optional", "pos1"), + (Entry[]) { + { "optional", NULL }, + {} + }, + STRV_MAKE("pos1")); + + /* OPTION_FULL with OPTION_OPTIONAL_ARG: short inline */ + test_macros_parse_one(STRV_MAKE("arg0", + "-oval1"), + (Entry[]) { + { "optional", "val1" }, + {} + }, + NULL); + + /* OPTION_FULL with OPTION_OPTIONAL_ARG: short without inline */ + test_macros_parse_one(STRV_MAKE("arg0", + "-o", "pos1"), + (Entry[]) { + { "optional", NULL }, + {} + }, + STRV_MAKE("pos1")); + + /* OPTION_FULL with OPTION_STOPS_PARSING: stops further option parsing */ + test_macros_parse_one(STRV_MAKE("arg0", + "--exec", + "--help", + "--version"), + (Entry[]) { + { "exec" }, + {} + }, + STRV_MAKE("--help", + "--version")); + + /* OPTION_STOPS_PARSING: options before are still parsed */ + test_macros_parse_one(STRV_MAKE("arg0", + "--help", + "--exec", + "-h", + "--debug"), + (Entry[]) { + { "help" }, + { "exec" }, + {} + }, + STRV_MAKE("-h", + "--debug")); + + /* OPTION_STOPS_PARSING with "--": "--" after exec is still consumed */ + test_macros_parse_one(STRV_MAKE("arg0", + "--exec", + "--", + "--help"), + (Entry[]) { + { "exec" }, + {} + }, + STRV_MAKE("--help")); + + /* OPTION_STOPS_PARSING with "--": "--" before exec takes precedence */ + test_macros_parse_one(STRV_MAKE("arg0", + "--", + "--exec", + "--help"), + (Entry[]) { + {} + }, + STRV_MAKE("--exec", + "--help")); + + /* OPTION_GROUP: group marker is transparent to parsing, --debug in Advanced group works */ + test_macros_parse_one(STRV_MAKE("arg0", + "--debug"), + (Entry[]) { + { "debug" }, + {} + }, + NULL); + + /* Mixed: all macro types together */ + test_macros_parse_one(STRV_MAKE("arg0", + "pos1", + "-h", + "--version", + "-v", + "--required=rval", + "--optional=oval", + "--debug", + "pos2", + "-o", + "--help"), + (Entry[]) { + { "help" }, + { "version" }, + { .short_code = 'v' }, + { "required", "rval" }, + { "optional", "oval" }, + { "debug" }, + { "optional", NULL }, + { "help" }, + {} + }, + STRV_MAKE("pos1", + "pos2")); + + /* Short option combos with macros: -hv (help + verbose) */ + test_macros_parse_one(STRV_MAKE("arg0", + "-hv"), + (Entry[]) { + { "help" }, + { .short_code = 'v' }, + {} + }, + NULL); + + /* Short option combo with required arg: -hrval (help + required with arg "val") */ + test_macros_parse_one(STRV_MAKE("arg0", + "-hrval"), + (Entry[]) { + { "help" }, + { "required", "val" }, + {} + }, + NULL); + + /* Short option combo with optional arg: -hoval (help + optional with arg "val") */ + test_macros_parse_one(STRV_MAKE("arg0", + "-hoval"), + (Entry[]) { + { "help" }, + { "optional", "val" }, + {} + }, + NULL); + + /* OPTION_STOPS_PARSING then "--": "--" is still consumed after exec */ + test_macros_parse_one(STRV_MAKE("arg0", + "--help", + "--exec", + "--version", + "--", + "-h"), + (Entry[]) { + { "help" }, + { "exec" }, + {} + }, + STRV_MAKE("--version", + "-h")); +} + DEFINE_TEST_MAIN(LOG_DEBUG);