feat(xce-test-cli-ux): build flag-descriptor parsing/rejection core

Adds flagspec_t descriptor table, getopt_long option-table builder, and
the shared stderr-routed rejection path that every flag-accepting
command will migrate onto in later tasks. TDD'd via test_flagspec.c
(1273/1273 controller suite passing).

Task 1.1 of .kiro/specs/xce-test-cli-ux/tasks.md.
Requirements: 2.1, 2.2, 2.3, 2.4, 3.1

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jaMf9QEwpugH2W9KAuc7f
This commit is contained in:
Rongsong Shen 2026-09-09 07:16:36 +00:00
parent 7bcafa68e7
commit 0748f10cc6
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
6 changed files with 827 additions and 1 deletions

View file

@ -55,7 +55,7 @@ controller = executable('xce_controller',
link_args: wrap_link_args,
install: true)
xce_test_src = [ 'testsuite/test.c', 'testsuite/waldump.c', 'testsuite/walcompare.c', 'testsuite/snapverify.c', 'testsuite/snapdiff.c', 'testsuite/configcompare.c', 'testsuite/resval_typeresolve.c', 'testsuite/rcfile.c']
xce_test_src = [ 'testsuite/test.c', 'testsuite/waldump.c', 'testsuite/walcompare.c', 'testsuite/snapverify.c', 'testsuite/snapdiff.c', 'testsuite/configcompare.c', 'testsuite/resval_typeresolve.c', 'testsuite/rcfile.c', 'testsuite/flagspec.c']
# BSD-licensed readline replacement (scheduler-core spec, task 12.1 review
# finding): GNU readline is GPL; xce_test's REPL only needs readline()/
@ -168,6 +168,8 @@ controller_unit_test_src = [
'testsuite/configcompare.c',
'testsuite/test_rcfile.c',
'testsuite/rcfile.c',
'testsuite/test_flagspec.c',
'testsuite/flagspec.c',
'testsuite/test_persist_snapshot_entities.c',
'testsuite/test_apply_seam.c',
'testsuite/test_apply_committed_entry.c',

View file

@ -71,6 +71,7 @@ extern void test_snapverify(void);
extern void test_snapdiff(void);
extern void test_configcompare(void);
extern void test_rcfile(void);
extern void test_flagspec(void);
extern void test_drive_async_config(void);
extern void test_slowlane(void);
extern void test_commit_admission_pool(void);
@ -251,6 +252,7 @@ int main(int argc, char* argv[])
TEST(test_snapdiff);
TEST(test_configcompare);
TEST(test_rcfile);
TEST(test_flagspec);
TEST(test_drive_async_config);
TEST(test_slowlane);
TEST(test_commit_admission_pool);

View file

@ -0,0 +1,119 @@
#define _GNU_SOURCE
#include "flagspec.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>
/* True iff `token` (a raw command-line argument, e.g. "-h", "--host", or
* -- defensively -- a dash-free bare name) identifies the flag described
* by `fs`. A "--name" token is matched against fs->long_opt; a "-X" token
* is matched against fs->short_opt (single character only, so "-xy" never
* falsely matches a single-letter short_opt). */
static int flag_token_matches(const char* token, const flagspec_t* fs)
{
if (!token || !fs)
return 0;
if (token[0] == '-' && token[1] == '-')
return fs->long_opt != NULL && strcmp(token + 2, fs->long_opt) == 0;
if (token[0] == '-' && token[1] != '\0')
return fs->short_opt != 0 && token[1] == fs->short_opt &&
token[2] == '\0';
/* No leading dash at all -- not how a real getopt_long() failure
* token looks, but handled so this helper never mis-signals on
* unexpected input from a caller that skips the usual
* argv[optind - 1] convention. */
if (fs->long_opt != NULL && strcmp(token, fs->long_opt) == 0)
return 1;
return fs->short_opt != 0 && token[0] == fs->short_opt &&
token[1] == '\0';
}
int flagspec_build_opts(const flagspec_t* spec, size_t n,
struct option* out_opts, char* out_optstring,
size_t out_optstring_cap)
{
if (!spec || !out_opts || !out_optstring || n == 0 ||
out_optstring_cap == 0)
return -EINVAL;
size_t str_idx = 0;
for (size_t i = 0; i < n; i++) {
const flagspec_t* fs = &spec[i];
if (!fs->long_opt || !*fs->long_opt)
return -EINVAL;
if (fs->has_arg != no_argument && fs->has_arg != required_argument)
return -EINVAL;
out_opts[i].name = fs->long_opt;
out_opts[i].has_arg = fs->has_arg;
out_opts[i].flag = NULL;
out_opts[i].val = fs->short_opt
? (int)fs->short_opt
: (int)(FLAGSPEC_LONG_ONLY_BASE + i);
if (fs->short_opt != 0) {
size_t need = (fs->has_arg == required_argument) ? 2 : 1;
/* Leave room for the final NUL written after the loop. */
if (str_idx + need >= out_optstring_cap)
return -ENOSPC;
out_optstring[str_idx++] = fs->short_opt;
if (fs->has_arg == required_argument)
out_optstring[str_idx++] = ':';
}
}
out_optstring[str_idx] = '\0';
/* Trailing all-zero sentinel getopt_long() requires to terminate the
* table; out_opts must have room for n+1 entries (precondition). */
out_opts[n].name = NULL;
out_opts[n].has_arg = 0;
out_opts[n].flag = NULL;
out_opts[n].val = 0;
return 0;
}
int flagspec_reject(const char* cmd_name, const flagspec_t* spec, size_t n,
const char* bad_flag, const char* usage_text)
{
const char* display = bad_flag ? bad_flag : "?";
const char* who = cmd_name ? cmd_name : "?";
const flagspec_t* match = NULL;
if (spec) {
for (size_t i = 0; i < n; i++) {
if (flag_token_matches(bad_flag, &spec[i])) {
match = &spec[i];
break;
}
}
}
if (match && match->has_arg == required_argument) {
fprintf(stderr, "xce_test: %s: flag '%s' requires a value\n", who,
display);
} else if (match) {
fprintf(stderr, "xce_test: %s: flag '%s' does not take a value\n",
who, display);
} else {
fprintf(stderr, "xce_test: %s: unrecognized flag '%s'\n", who,
display);
}
if (usage_text && *usage_text) {
size_t len = strlen(usage_text);
if (len > 0 && usage_text[len - 1] == '\n')
fprintf(stderr, "usage: %s", usage_text);
else
fprintf(stderr, "usage: %s\n", usage_text);
}
return -EINVAL;
}

View file

@ -0,0 +1,130 @@
#pragma once
/*
* flagspec.h -- flag-descriptor table format and getopt_long-based parsing
* core for xce_test's command-line surface (xce-test-cli-ux spec, task 1.1;
* Requirements 2.1-2.4, 3.1; the flagspec_t shape is a stable contract also
* relied on by task 1.2 (typo suggestion, flagspec_render_help) and task
* 3.1 (migrating every existing cmd_* command onto this table)).
*
* bug-1711's root cause was that xce_test's flag parsing had no shared
* helper at all: a plain getopt() loop per command, each with its own
* hand-written switch/default, drifted independently from its
* hand-authored USAGE_* string. This module gives every flag-accepting
* command ONE descriptor table to build its getopt_long() option table
* from and ONE shared rejection path to call on any parse failure, so the
* table is the single source of truth for both.
*
* THIS FILE/TASK (1.1) declares and implements only flagspec_build_opts()
* and flagspec_reject(). flagspec_render_help() (the help-text flag-block
* renderer) and flagspec_reject()'s typo-suggestion behavior are task
* 1.2 and are intentionally NOT declared here yet -- task 1.2 extends
* this header when it adds them.
*/
#include <getopt.h>
#include <stddef.h>
/* One flag's full description: its short/long spellings, the real-world
* concept it represents (for cross-command naming consistency, see
* design.md Requirement 6.2), whether getopt_long() expects a value, and
* documentation state consumed by later tasks (help rendering).
*
* Any two flagspec_t[] tables (i.e. any two commands) that describe the
* "same concept" (e.g. a target host, a job id) MUST use the identical
* long_opt string for it -- enforced by table review, not at runtime. */
typedef struct {
char short_opt; /* 0 if long-only */
const char* long_opt; /* stable, cross-command-consistent name;
* spelled WITHOUT leading dashes, e.g.
* "host", not "--host" */
const char* concept; /* e.g. "host", "jobid", "command-line";
* consumed by help rendering (task 1.2),
* not by flagspec_build_opts()/
* flagspec_reject() */
int has_arg; /* no_argument | required_argument, per
* <getopt.h>; this design uses no
* optional_argument flags anywhere */
int required; /* 1 if the command fails without it;
* enforcement is each command's own
* post-parse-loop check (see design.md's
* bug-1704/cmd_qctrl note), not this
* module's job */
const char* default_desc; /* human text describing the default when
* the flag is omitted, NULL if none;
* consumed by help rendering (task 1.2) */
} flagspec_t;
/* Synthetic getopt_long() `val` assigned to a long-only flag (short_opt
* == 0) at spec index i: FLAGSPEC_LONG_ONLY_BASE + i. Chosen safely above
* the full unsigned-char range (0-255) so it can never collide with a
* real short_opt value, with getopt_long()'s own -1 end-of-options
* sentinel, or with '?' (parse error). Every flagspec_t entry -- long-only
* or not -- therefore returns a value from getopt_long() that a caller's
* switch() can distinguish. */
#define FLAGSPEC_LONG_ONLY_BASE 0x100
/* Builds a getopt_long()-compatible option table and optstring from a
* flagspec_t descriptor table.
*
* `out_opts` must have room for at least n+1 entries -- this function
* appends the trailing all-zero sentinel entry getopt_long() requires to
* terminate the table at index n. `out_optstring` is filled with the
* short-option spec getopt_long()'s `optstring` argument expects: each
* flag with a nonzero short_opt contributes that character, followed by
* ':' when has_arg == required_argument; a long-only flag (short_opt ==
* 0) contributes nothing. The result is NUL-terminated.
*
* Invariant: the built optstring never begins with ':' -- this module
* deliberately does not use getopt_long()'s own leading-':' convention
* for distinguishing "missing argument" from "unrecognized option";
* flagspec_reject() makes that distinction itself by looking the
* offending token up in `spec`. The caller must set `opterr = 0` before
* its getopt_long() loop so libc's own diagnostic never doubles up with
* flagspec_reject()'s.
*
* Returns 0 on success. Returns a negative errno on failure:
* -EINVAL spec/out_opts/out_optstring is NULL, n == 0,
* out_optstring_cap == 0, or a spec entry is malformed
* (long_opt is NULL/empty, or has_arg is neither no_argument
* nor required_argument)
* -ENOSPC out_optstring_cap is too small to hold the built optstring
* (including its terminating NUL)
*
* On any failure, the contents of out_opts/out_optstring are unspecified
* -- the caller must not use them. */
int flagspec_build_opts(const flagspec_t* spec, size_t n,
struct option* out_opts, char* out_optstring,
size_t out_optstring_cap);
/* Shared rejection path for an unrecognized or malformed flag, called
* from a command's getopt_long() loop's `case '?':` arm (or right after
* the loop, for a bad_flag captured some other way).
*
* `bad_flag` is the literal command-line token that failed -- e.g. "-z"
* or "--bogus" -- ordinarily read as `argv[optind - 1]` right after
* getopt_long() returns '?'. flagspec_reject() looks `bad_flag` up
* against `spec` (matching a "-X" token by short_opt, a "--name" token by
* long_opt) to decide which case applies:
* - `bad_flag` matches no entry in `spec` ->
* "unrecognized flag" (Requirement 2.1)
* - `bad_flag` matches an entry with has_arg == required_argument ->
* "flag ... requires a value" (Requirement 2.2)
* - `bad_flag` matches an entry that takes no argument (getopt_long()
* still returned '?', e.g. "--flag=value" given for a no_argument
* long flag) -> "flag ... does not take a value"
* Either way, it prints the offending flag and `usage_text` (Requirement
* 2.4) to stderr only (Requirement 3.1 -- never stdout) and returns a
* negative value; it never partially executes the command.
*
* Typo suggestion ("did you mean --host?", Requirement 10) is task 1.2,
* not implemented by this function yet.
*
* `usage_text` is expected to be the command's one-line usage summary
* (e.g. "jobkill -j <jobid> [-s <signo>]"), not its full help block; it
* may be NULL to omit the usage line entirely (defensive only -- every
* real call site always has one).
*
* Returns -EINVAL (a fixed, nonzero, negative-errno result) in every
* case. */
int flagspec_reject(const char* cmd_name, const flagspec_t* spec, size_t n,
const char* bad_flag, const char* usage_text);

View file

@ -0,0 +1,393 @@
#define _GNU_SOURCE
#include <errno.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unity/unity.h>
#include "flagspec.h"
/* ---- fixture: a made-up two-flag test command -------------------------
* "testcmd -h <host> [-c <cmdline>]" -- 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 <host> [-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 <host> [-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 <host> [-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 <host> [-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 <host> [-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 <host> [-c]"));
cleanup_dir();
}
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);
}