]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
test-options: add tests for option macros and flags
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Thu, 19 Mar 2026 16:45:17 +0000 (17:45 +0100)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Sun, 22 Mar 2026 15:52:22 +0000 (16:52 +0100)
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 <noreply@anthropic.com>
src/test/test-options.c

index 56e8dd1d61390d40f81415d454ae81e30796fb3e..6ca29fb849ab907e4d1ecde5f9fad557acfcc136 100644 (file)
@@ -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);