#define _GNU_SOURCE #include #include #include #include #include #include #include #include "flagspec.h" /* ---- fixture: a made-up two-flag test command ------------------------- * "testcmd -h [-c ]" -- one required flag with a short * and long form, one optional flag with a short and long form. Used by * every test below so the same descriptor table exercises both * flagspec_build_opts() and flagspec_reject(), and so a rejection's usage * summary is provably the SAME string no matter which of the two flags is * the malformed one (the task's stated observable-completion criterion). * ------------------------------------------------------------------- */ static const flagspec_t g_testcmd_spec[2] = { { 'h', "host", "host", required_argument, 1, NULL }, { 'c', "command", "command-line", no_argument, 0, "off" }, }; static const char g_testcmd_usage[] = "testcmd -h [-c]"; /* ---- stream-capture helpers -------------------------------------------- * Mirrors test_rcfile.c's dup/dup2/freopen technique for proving what a * function wrote to stderr, extended to capture stdout AND stderr at the * same time so a test can prove Requirement 3.1: a rejection message * lands on stderr only, never stdout. Uses a fresh temp dir per test * (mkdtemp, like test_rcfile.c/test_configcompare.c's fresh_dir()) so * capture files never collide with anything else in this shared /tmp. */ static char g_dir[256]; static void fresh_dir(void) { strcpy(g_dir, "/tmp/xce_flagspec_test_XXXXXX"); TEST_ASSERT_NOT_NULL(mkdtemp(g_dir)); } static void cleanup_dir(void) { char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", g_dir); system(cmd); } typedef struct { int saved_stdout_fd; int saved_stderr_fd; char stdout_path[300]; char stderr_path[300]; } capture_t; static void capture_begin(capture_t* cap) { fflush(stdout); fflush(stderr); cap->saved_stdout_fd = dup(fileno(stdout)); cap->saved_stderr_fd = dup(fileno(stderr)); TEST_ASSERT_TRUE(cap->saved_stdout_fd >= 0); TEST_ASSERT_TRUE(cap->saved_stderr_fd >= 0); snprintf(cap->stdout_path, sizeof(cap->stdout_path), "%s/out", g_dir); snprintf(cap->stderr_path, sizeof(cap->stderr_path), "%s/err", g_dir); TEST_ASSERT_NOT_NULL(freopen(cap->stdout_path, "w", stdout)); TEST_ASSERT_NOT_NULL(freopen(cap->stderr_path, "w", stderr)); } /* Restores the real stdout/stderr fds and reads both captured files back * into caller-provided, NUL-terminated buffers. */ static void capture_end(capture_t* cap, char* out_stdout, size_t out_stdout_cap, char* out_stderr, size_t out_stderr_cap) { fflush(stdout); fflush(stderr); TEST_ASSERT_TRUE(dup2(cap->saved_stdout_fd, fileno(stdout)) >= 0); TEST_ASSERT_TRUE(dup2(cap->saved_stderr_fd, fileno(stderr)) >= 0); close(cap->saved_stdout_fd); close(cap->saved_stderr_fd); clearerr(stdout); clearerr(stderr); FILE* f; size_t n; f = fopen(cap->stdout_path, "r"); TEST_ASSERT_NOT_NULL(f); n = fread(out_stdout, 1, out_stdout_cap - 1, f); out_stdout[n] = '\0'; fclose(f); f = fopen(cap->stderr_path, "r"); TEST_ASSERT_NOT_NULL(f); n = fread(out_stderr, 1, out_stderr_cap - 1, f); out_stderr[n] = '\0'; fclose(f); } /* ---- flagspec_build_opts() tests --------------------------------------- */ static void test_build_opts_produces_correct_table_for_two_flag_command(void) { struct option opts[3]; char optstring[8]; int rc = flagspec_build_opts(g_testcmd_spec, 2, opts, optstring, sizeof(optstring)); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_STRING("host", opts[0].name); TEST_ASSERT_EQUAL_INT(required_argument, opts[0].has_arg); TEST_ASSERT_NULL(opts[0].flag); TEST_ASSERT_EQUAL_INT('h', opts[0].val); TEST_ASSERT_EQUAL_STRING("command", opts[1].name); TEST_ASSERT_EQUAL_INT(no_argument, opts[1].has_arg); TEST_ASSERT_NULL(opts[1].flag); TEST_ASSERT_EQUAL_INT('c', opts[1].val); /* trailing all-zero sentinel getopt_long() requires */ TEST_ASSERT_NULL(opts[2].name); TEST_ASSERT_EQUAL_INT(0, opts[2].has_arg); TEST_ASSERT_NULL(opts[2].flag); TEST_ASSERT_EQUAL_INT(0, opts[2].val); TEST_ASSERT_EQUAL_STRING("h:c", optstring); } static void test_build_opts_assigns_synthetic_val_to_long_only_flag(void) { flagspec_t spec[3]; memcpy(spec, g_testcmd_spec, sizeof(g_testcmd_spec)); spec[2] = (flagspec_t){ 0, "json", "output-format", no_argument, 0, "table" }; struct option opts[4]; char optstring[8]; int rc = flagspec_build_opts(spec, 3, opts, optstring, sizeof(optstring)); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_STRING("json", opts[2].name); TEST_ASSERT_EQUAL_INT(no_argument, opts[2].has_arg); TEST_ASSERT_EQUAL_INT(FLAGSPEC_LONG_ONLY_BASE + 2, opts[2].val); /* a long-only flag contributes nothing to the short optstring */ TEST_ASSERT_EQUAL_STRING("h:c", optstring); /* still terminated correctly at the new, later index */ TEST_ASSERT_NULL(opts[3].name); TEST_ASSERT_EQUAL_INT(0, opts[3].val); } static void test_build_opts_rejects_null_spec(void) { struct option opts[3]; char optstring[8]; TEST_ASSERT_EQUAL_INT(-EINVAL, flagspec_build_opts(NULL, 2, opts, optstring, sizeof(optstring))); } static void test_build_opts_rejects_zero_count(void) { struct option opts[3]; char optstring[8]; TEST_ASSERT_EQUAL_INT( -EINVAL, flagspec_build_opts(g_testcmd_spec, 0, opts, optstring, sizeof(optstring))); } static void test_build_opts_rejects_entry_with_null_long_opt(void) { flagspec_t bad_spec[1] = { { 'x', NULL, "x", no_argument, 0, NULL }, }; struct option opts[2]; char optstring[8]; TEST_ASSERT_EQUAL_INT( -EINVAL, flagspec_build_opts(bad_spec, 1, opts, optstring, sizeof(optstring))); } static void test_build_opts_rejects_too_small_optstring_buffer(void) { struct option opts[3]; /* g_testcmd_spec builds "h:c" (h requires a value, c does not) -- * exactly 4 bytes including the terminating NUL. A 3-byte buffer is * one short. */ char optstring[3]; TEST_ASSERT_EQUAL_INT(-ENOSPC, flagspec_build_opts(g_testcmd_spec, 2, opts, optstring, sizeof(optstring))); } /* ---- flagspec_reject() tests: real getopt_long() pipeline ------------- * Each case builds the real option table via flagspec_build_opts(), then * drives an actual getopt_long() call over a fabricated argv so the * bad_flag handed to flagspec_reject() is exactly what a real cmd_* * caller would see from its own `case '?':` arm -- not a hand-picked * string. */ static int run_getopt_long_expect_error(int argc, char* argv[], const struct option* opts, const char* optstring) { optind = 1; opterr = 0; int opt = getopt_long(argc, argv, optstring, opts, NULL); TEST_ASSERT_EQUAL_INT('?', opt); return optind; } static void test_reject_rejects_unrecognized_long_flag(void) { struct option opts[3]; char optstring[8]; TEST_ASSERT_EQUAL_INT(0, flagspec_build_opts(g_testcmd_spec, 2, opts, optstring, sizeof(optstring))); char* argv[] = { "testcmd", "--bogus", NULL }; int after = run_getopt_long_expect_error(2, argv, opts, optstring); const char* bad_flag = argv[after - 1]; TEST_ASSERT_EQUAL_STRING("--bogus", bad_flag); fresh_dir(); capture_t cap; capture_begin(&cap); int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, bad_flag, g_testcmd_usage); char out_buf[512], err_buf[512]; capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf)); TEST_ASSERT_TRUE(rc < 0); TEST_ASSERT_EQUAL_STRING("", out_buf); TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--bogus'")); TEST_ASSERT_NOT_NULL(strstr(err_buf, "usage: testcmd -h [-c]")); cleanup_dir(); } static void test_reject_rejects_unrecognized_short_flag(void) { struct option opts[3]; char optstring[8]; TEST_ASSERT_EQUAL_INT(0, flagspec_build_opts(g_testcmd_spec, 2, opts, optstring, sizeof(optstring))); char* argv[] = { "testcmd", "-z", NULL }; int after = run_getopt_long_expect_error(2, argv, opts, optstring); const char* bad_flag = argv[after - 1]; TEST_ASSERT_EQUAL_STRING("-z", bad_flag); fresh_dir(); capture_t cap; capture_begin(&cap); int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, bad_flag, g_testcmd_usage); char out_buf[512], err_buf[512]; capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf)); TEST_ASSERT_TRUE(rc < 0); TEST_ASSERT_EQUAL_STRING("", out_buf); TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '-z'")); TEST_ASSERT_NOT_NULL(strstr(err_buf, "usage: testcmd -h [-c]")); cleanup_dir(); } static void test_reject_rejects_missing_value_long_flag(void) { struct option opts[3]; char optstring[8]; TEST_ASSERT_EQUAL_INT(0, flagspec_build_opts(g_testcmd_spec, 2, opts, optstring, sizeof(optstring))); /* "--host" is the last argv element -- getopt_long() has nothing to * consume as its required value. */ char* argv[] = { "testcmd", "--host", NULL }; int after = run_getopt_long_expect_error(2, argv, opts, optstring); const char* bad_flag = argv[after - 1]; TEST_ASSERT_EQUAL_STRING("--host", bad_flag); fresh_dir(); capture_t cap; capture_begin(&cap); int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, bad_flag, g_testcmd_usage); char out_buf[512], err_buf[512]; capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf)); TEST_ASSERT_TRUE(rc < 0); TEST_ASSERT_EQUAL_STRING("", out_buf); TEST_ASSERT_NOT_NULL(strstr(err_buf, "flag '--host' requires a value")); TEST_ASSERT_NOT_NULL(strstr(err_buf, "usage: testcmd -h [-c]")); cleanup_dir(); } static void test_reject_rejects_missing_value_short_flag(void) { struct option opts[3]; char optstring[8]; TEST_ASSERT_EQUAL_INT(0, flagspec_build_opts(g_testcmd_spec, 2, opts, optstring, sizeof(optstring))); char* argv[] = { "testcmd", "-h", NULL }; int after = run_getopt_long_expect_error(2, argv, opts, optstring); const char* bad_flag = argv[after - 1]; TEST_ASSERT_EQUAL_STRING("-h", bad_flag); fresh_dir(); capture_t cap; capture_begin(&cap); int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, bad_flag, g_testcmd_usage); char out_buf[512], err_buf[512]; capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf)); TEST_ASSERT_TRUE(rc < 0); TEST_ASSERT_EQUAL_STRING("", out_buf); TEST_ASSERT_NOT_NULL(strstr(err_buf, "flag '-h' requires a value")); TEST_ASSERT_NOT_NULL(strstr(err_buf, "usage: testcmd -h [-c]")); cleanup_dir(); } /* Direct-call (non-getopt_long) tests: exercises flagspec_reject()'s own * argument handling in isolation, including NULL inputs it must survive * without crashing. */ static void test_reject_handles_null_bad_flag_and_null_usage_gracefully(void) { fresh_dir(); capture_t cap; capture_begin(&cap); int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, NULL, NULL); char out_buf[512], err_buf[512]; capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf)); TEST_ASSERT_TRUE(rc < 0); TEST_ASSERT_EQUAL_STRING("", out_buf); TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag")); cleanup_dir(); } static void test_reject_no_argument_flag_given_a_value_reports_distinctly(void) { /* "-c" never requires a value; a caller that somehow reaches * flagspec_reject() with the bare "-c" token (matches an entry, but * that entry takes no_argument) must be told a different thing than * either the unrecognized-flag or missing-value message. */ fresh_dir(); capture_t cap; capture_begin(&cap); int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, "-c", g_testcmd_usage); char out_buf[512], err_buf[512]; capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf)); TEST_ASSERT_TRUE(rc < 0); TEST_ASSERT_EQUAL_STRING("", out_buf); TEST_ASSERT_NOT_NULL(strstr(err_buf, "flag '-c' does not take a value")); TEST_ASSERT_NOT_NULL(strstr(err_buf, "usage: testcmd -h [-c]")); cleanup_dir(); } /* ---- flagspec_reject() typo-suggestion tests (Requirement 10) --------- * A second fixture, g_ambiguous_spec, is purpose-built for the * ambiguous-match case below so its edit distances are deliberate rather * than incidental to g_testcmd_spec's two very dissimilar flag names. */ static const flagspec_t g_ambiguous_spec[2] = { { 0, "targeta", "target-a", no_argument, 0, NULL }, { 0, "targetb", "target-b", no_argument, 0, NULL }, }; static void test_reject_suggests_single_close_match_flag_typo(void) { /* "--hst" is "--host" with the 'o' deleted -- one edit, well inside * the closeness threshold for a 6-character long-form spelling -- and * is nowhere near "--command". Requirement 10.1: name the * likely-intended flag in the rejection message. */ fresh_dir(); capture_t cap; capture_begin(&cap); int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, "--hst", g_testcmd_usage); char out_buf[512], err_buf[512]; capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf)); TEST_ASSERT_TRUE(rc < 0); TEST_ASSERT_EQUAL_STRING("", out_buf); TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--hst'")); TEST_ASSERT_NOT_NULL(strstr(err_buf, "did you mean")); TEST_ASSERT_NOT_NULL(strstr(err_buf, "--host")); cleanup_dir(); } static void test_reject_gives_no_suggestion_for_a_distant_typo(void) { /* "--wombat" does not closely resemble "--host" or "--command" by any * reasonable edit-distance threshold. Requirement 10.2: report * unrecognized without guessing. */ fresh_dir(); capture_t cap; capture_begin(&cap); int rc = flagspec_reject("testcmd", g_testcmd_spec, 2, "--wombat", g_testcmd_usage); char out_buf[512], err_buf[512]; capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf)); TEST_ASSERT_TRUE(rc < 0); TEST_ASSERT_EQUAL_STRING("", out_buf); TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--wombat'")); TEST_ASSERT_NULL(strstr(err_buf, "did you mean")); cleanup_dir(); } static void test_reject_gives_no_suggestion_when_typo_is_ambiguous(void) { /* "--targetc" is exactly one substitution away from BOTH "--targeta" * and "--targetb". Requirement 10.1 only promises a guess when * exactly one flag qualifies -- two equally-plausible candidates must * produce no suggestion at all, the same as a typo that matches * nothing. */ fresh_dir(); capture_t cap; capture_begin(&cap); int rc = flagspec_reject("testcmd", g_ambiguous_spec, 2, "--targetc", "testcmd --targeta|--targetb"); char out_buf[512], err_buf[512]; capture_end(&cap, out_buf, sizeof(out_buf), err_buf, sizeof(err_buf)); TEST_ASSERT_TRUE(rc < 0); TEST_ASSERT_EQUAL_STRING("", out_buf); TEST_ASSERT_NOT_NULL(strstr(err_buf, "unrecognized flag '--targetc'")); TEST_ASSERT_NULL(strstr(err_buf, "did you mean")); cleanup_dir(); } /* ---- flagspec_render_help() tests (Requirement 8.2) -------------------- */ static void test_render_help_formats_multi_flag_table(void) { flagspec_t spec[3]; memcpy(spec, g_testcmd_spec, sizeof(g_testcmd_spec)); spec[2] = (flagspec_t){ 0, "json", "output-format", no_argument, 0, "table" }; char* help = flagspec_render_help(spec, 3); TEST_ASSERT_NOT_NULL(help); /* required flag: short + long form, marked required */ TEST_ASSERT_NOT_NULL(strstr(help, "-h")); TEST_ASSERT_NOT_NULL(strstr(help, "--host")); TEST_ASSERT_NOT_NULL(strstr(help, "required")); /* optional flag: short + long form, marked optional, default shown */ TEST_ASSERT_NOT_NULL(strstr(help, "-c")); TEST_ASSERT_NOT_NULL(strstr(help, "--command")); TEST_ASSERT_NOT_NULL(strstr(help, "optional")); TEST_ASSERT_NOT_NULL(strstr(help, "off")); /* long-only flag: no short form, still shows long form + default */ TEST_ASSERT_NOT_NULL(strstr(help, "--json")); TEST_ASSERT_NOT_NULL(strstr(help, "table")); free(help); } static void test_render_help_rejects_null_and_empty_inputs(void) { TEST_ASSERT_NULL(flagspec_render_help(NULL, 2)); TEST_ASSERT_NULL(flagspec_render_help(g_testcmd_spec, 0)); } void test_flagspec(void) { UnitySetTestFile(__FILE__); RUN_TEST(test_build_opts_produces_correct_table_for_two_flag_command); RUN_TEST(test_build_opts_assigns_synthetic_val_to_long_only_flag); RUN_TEST(test_build_opts_rejects_null_spec); RUN_TEST(test_build_opts_rejects_zero_count); RUN_TEST(test_build_opts_rejects_entry_with_null_long_opt); RUN_TEST(test_build_opts_rejects_too_small_optstring_buffer); RUN_TEST(test_reject_rejects_unrecognized_long_flag); RUN_TEST(test_reject_rejects_unrecognized_short_flag); RUN_TEST(test_reject_rejects_missing_value_long_flag); RUN_TEST(test_reject_rejects_missing_value_short_flag); RUN_TEST(test_reject_handles_null_bad_flag_and_null_usage_gracefully); RUN_TEST(test_reject_no_argument_flag_given_a_value_reports_distinctly); RUN_TEST(test_reject_suggests_single_close_match_flag_typo); RUN_TEST(test_reject_gives_no_suggestion_for_a_distant_typo); RUN_TEST(test_reject_gives_no_suggestion_when_typo_is_ambiguous); RUN_TEST(test_render_help_formats_multi_flag_table); RUN_TEST(test_render_help_rejects_null_and_empty_inputs); }