#include "../../unity/unity.h" #include #include #include #include #include #include #include #include #include #include /* program_name is defined in gnulib; referenced by usage(). */ extern char const *program_name; /* Unity fixtures */ void setUp(void) { /* Setup code here, or leave empty */ } void tearDown(void) { /* Cleanup code here, or leave empty */ } /* Helper: read all data from fd into a malloc'd buffer. Returns 0 on success. On success, *out points to a NUL-terminated buffer and *out_len is the number of bytes read (not including the NUL). */ static int read_all_fd(int fd, char **out, size_t *out_len) { size_t cap = 4096; size_t len = 0; char *buf = (char *)malloc(cap); if (!buf) return -1; for (;;) { if (len == cap) { size_t ncap = cap * 2; char *nbuf = (char *)realloc(buf, ncap); if (!nbuf) { free(buf); return -1; } buf = nbuf; cap = ncap; } ssize_t n = read(fd, buf + len, cap - len); if (n < 0) { if (errno == EINTR) continue; free(buf); return -1; } if (n == 0) break; len += (size_t)n; } /* ensure NUL terminator */ if (len == cap) { char *nbuf = (char *)realloc(buf, cap + 1); if (!nbuf) { free(buf); return -1; } buf = nbuf; } buf[len] = '\0'; *out = buf; *out_len = len; return 0; } /* Helper: run usage(status) in a child process with program_name set to pname. Captures stdout/stderr into malloc'd buffers. Returns 0 on success, -1 on failure to set up/execute child. exit_code is set to the child's exit status if it exited normally, or -1 otherwise. */ static int run_usage_and_capture(const char *pname, int call_status, char **out_buf, size_t *out_len, char **err_buf, size_t *err_len, int *exit_code) { int pout[2] = {-1, -1}; int perr[2] = {-1, -1}; if (pipe(pout) < 0) return -1; if (pipe(perr) < 0) { close(pout[0]); close(pout[1]); return -1; } fflush(stdout); fflush(stderr); pid_t pid = fork(); if (pid < 0) { close(pout[0]); close(pout[1]); close(perr[0]); close(perr[1]); return -1; } if (pid == 0) { /* Child: redirect stdout/stderr, set program_name, then call usage(). */ /* Ensure C locale for predictable output where applicable. */ setenv("LC_ALL", "C", 1); /* Redirect stdout/stderr to pipes. */ if (dup2(pout[1], STDOUT_FILENO) < 0) _exit(127); if (dup2(perr[1], STDERR_FILENO) < 0) _exit(127); close(pout[0]); close(pout[1]); close(perr[0]); close(perr[1]); /* Set program_name used by usage(). */ program_name = pname; /* Call the target function; it will exit() the process. */ usage(call_status); /* Should never reach here. */ _exit(127); } /* Parent: close write ends, read from read ends. */ close(pout[1]); close(perr[1]); int status; char *out_local = NULL, *err_local = NULL; size_t out_local_len = 0, err_local_len = 0; /* Read outputs before waiting to avoid potential deadlocks on full pipes. */ int r1 = read_all_fd(pout[0], &out_local, &out_local_len); int r2 = read_all_fd(perr[0], &err_local, &err_local_len); close(pout[0]); close(perr[0]); if (r1 != 0 || r2 != 0) { if (out_local) free(out_local); if (err_local) free(err_local); /* Reap child to avoid zombies. */ waitpid(pid, &status, 0); return -1; } if (waitpid(pid, &status, 0) < 0) { free(out_local); free(err_local); return -1; } if (WIFEXITED(status)) { *exit_code = WEXITSTATUS(status); } else { *exit_code = -1; } *out_buf = out_local; *out_len = out_local_len; *err_buf = err_local; *err_len = err_local_len; return 0; } /* Test: usage(EXIT_SUCCESS) prints help to stdout, stderr empty, exits 0. */ static void test_usage_success_outputs_help_to_stdout_and_exits_zero(void) { char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = -2; int rc = run_usage_and_capture("cat", EXIT_SUCCESS, &out, &out_len, &err, &err_len, &ec); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to run usage() in child"); TEST_ASSERT_EQUAL_INT_MESSAGE(EXIT_SUCCESS, ec, "usage(EXIT_SUCCESS) did not exit with 0"); TEST_ASSERT_NOT_NULL_MESSAGE(out, "stdout buffer is NULL"); TEST_ASSERT_TRUE_MESSAGE(out_len > 0, "stdout is empty for usage(EXIT_SUCCESS)"); TEST_ASSERT_NOT_NULL_MESSAGE(err, "stderr buffer is NULL"); TEST_ASSERT_EQUAL_UINT_MESSAGE(0, err_len, "stderr not empty for usage(EXIT_SUCCESS)"); /* Check for stable option flag substrings in the help text. */ TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "-A, --show-all"), "Help missing '-A, --show-all'"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "-n, --number"), "Help missing '-n, --number'"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "-T, --show-tabs"), "Help missing '-T, --show-tabs'"); free(out); free(err); } /* Test: usage(nonzero) prints 'try --help' to stderr, stdout empty, exits with status. */ static void test_usage_nonzero_status_outputs_try_help_to_stderr_and_exits_code(void) { char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = -2; int rc = run_usage_and_capture("cat", 3, &out, &out_len, &err, &err_len, &ec); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to run usage() in child"); TEST_ASSERT_EQUAL_INT_MESSAGE(3, ec, "usage(nonzero) did not exit with given status"); TEST_ASSERT_NOT_NULL_MESSAGE(out, "stdout buffer is NULL"); TEST_ASSERT_EQUAL_UINT_MESSAGE(0, out_len, "stdout should be empty for usage(nonzero)"); TEST_ASSERT_NOT_NULL_MESSAGE(err, "stderr buffer is NULL"); TEST_ASSERT_TRUE_MESSAGE(err_len > 0, "stderr is empty for usage(nonzero)"); /* Look for stable markers: program name and '--help'. */ TEST_ASSERT_NOT_NULL_MESSAGE(strstr(err, "cat"), "stderr missing program name"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(err, "--help"), "stderr missing '--help' hint"); free(out); free(err); } /* Test: program_name reflected in outputs for both success and failure cases. */ static void test_usage_reflects_program_name_in_outputs(void) { /* Success case: program_name should appear in full help (e.g., Usage line or Examples). */ { char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = -2; int rc = run_usage_and_capture("mycat", EXIT_SUCCESS, &out, &out_len, &err, &err_len, &ec); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(EXIT_SUCCESS, ec); TEST_ASSERT_TRUE(out_len > 0); TEST_ASSERT_NOT_NULL(strstr(out, "mycat")); /* Should appear in Usage or Examples lines. */ TEST_ASSERT_EQUAL_UINT(0, err_len); free(out); free(err); } /* Non-success case: program_name should appear in the 'try --help' diagnostic. */ { char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = -2; int rc = run_usage_and_capture("mycat", 2, &out, &out_len, &err, &err_len, &ec); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(2, ec); TEST_ASSERT_EQUAL_UINT(0, out_len); TEST_ASSERT_TRUE(err_len > 0); TEST_ASSERT_NOT_NULL(strstr(err, "mycat")); TEST_ASSERT_NOT_NULL(strstr(err, "--help")); free(out); free(err); } } int main(void) { UNITY_BEGIN(); RUN_TEST(test_usage_success_outputs_help_to_stdout_and_exits_zero); RUN_TEST(test_usage_nonzero_status_outputs_try_help_to_stderr_and_exits_code); RUN_TEST(test_usage_reflects_program_name_in_outputs); return UNITY_END(); }