#include "../../unity/unity.h" #include #include #include #include #include #include #include #include #include /* Access the target function and globals from the program */ extern void usage (int status); extern char const *program_name; /* Helper: read all data from a file descriptor into a NUL-terminated buffer. Returns malloc'd buffer (caller must free). Sets *out_len to length (not including NUL). On error, returns NULL and *out_len = 0. */ static char *read_all_from_fd (int fd, size_t *out_len) { size_t cap = 4096; size_t len = 0; char *buf = (char *)malloc(cap + 1); if (!buf) { if (out_len) *out_len = 0; return NULL; } for (;;) { if (len == cap) { size_t ncap = cap * 2; char *nbuf = (char *)realloc(buf, ncap + 1); if (!nbuf) { free(buf); if (out_len) *out_len = 0; return NULL; } buf = nbuf; cap = ncap; } ssize_t n = read(fd, buf + len, cap - len); if (n > 0) { len += (size_t)n; continue; } else if (n == 0) { break; /* EOF */ } else { if (errno == EINTR) continue; free(buf); if (out_len) *out_len = 0; return NULL; } } buf[len] = '\0'; if (out_len) *out_len = len; return buf; } /* Helper: run usage(status) in a child, with program_name overridden, capture stdout/stderr and exit code. - progname_override: if non-NULL, set program_name = progname_override in the child before calling usage. - status: the status to pass to usage(). Returns 0 on success (captures filled), -1 on failure to set up pipes/fork. */ static int spawn_usage_and_capture (const char *progname_override, int status, char **out_stdout, size_t *out_stdout_len, char **out_stderr, size_t *out_stderr_len, int *out_exit_code) { int out_pipe[2] = {-1,-1}; int err_pipe[2] = {-1,-1}; if (pipe(out_pipe) < 0) return -1; if (pipe(err_pipe) < 0) { close(out_pipe[0]); close(out_pipe[1]); return -1; } fflush(stdout); fflush(stderr); pid_t pid = fork(); if (pid < 0) { close(out_pipe[0]); close(out_pipe[1]); close(err_pipe[0]); close(err_pipe[1]); return -1; } if (pid == 0) { /* Child: redirect stdout/stderr and call usage(). No Unity assertions here. */ /* Ensure predictable messages. */ setenv("LC_ALL", "C", 1); setlocale(LC_ALL, "C"); if (progname_override && *progname_override) { /* Assign the global program_name (pointer to const char). */ program_name = progname_override; } else { /* Ensure default in case not already set. */ program_name = "link"; } /* Redirect stdout and stderr */ close(out_pipe[0]); close(err_pipe[0]); if (dup2(out_pipe[1], STDOUT_FILENO) < 0) _exit(127); if (dup2(err_pipe[1], STDERR_FILENO) < 0) _exit(127); /* Close the extra fds after dup */ close(out_pipe[1]); close(err_pipe[1]); /* Call the function (will exit). */ usage(status); /* Should not reach here, but just in case, exit with sentinel. */ _exit(255); } /* Parent: close write ends and read captured outputs. */ close(out_pipe[1]); close(err_pipe[1]); char *cap_out = read_all_from_fd(out_pipe[0], out_stdout_len); char *cap_err = read_all_from_fd(err_pipe[0], out_stderr_len); close(out_pipe[0]); close(err_pipe[0]); int wstatus = 0; int rc; do { rc = waitpid(pid, &wstatus, 0); } while (rc < 0 && errno == EINTR); int exit_code = -1; if (rc >= 0) { if (WIFEXITED(wstatus)) { exit_code = WEXITSTATUS(wstatus); } else if (WIFSIGNALED(wstatus)) { exit_code = 128 + WTERMSIG(wstatus); } else { exit_code = -1; } } if (!cap_out) { /* Allocation/read failure: still provide empty buffers to avoid NULL deref in tests. */ cap_out = (char *)malloc(1); if (cap_out) cap_out[0] = '\0'; if (out_stdout_len) *out_stdout_len = 0; } if (!cap_err) { cap_err = (char *)malloc(1); if (cap_err) cap_err[0] = '\0'; if (out_stderr_len) *out_stderr_len = 0; } if (out_stdout) *out_stdout = cap_out; else free(cap_out); if (out_stderr) *out_stderr = cap_err; else free(cap_err); if (out_exit_code) *out_exit_code = exit_code; return 0; } void setUp(void) { /* No global setup needed for these tests */ } void tearDown(void) { /* No global teardown needed */ } /* Test: Nonzero status EXIT_FAILURE path prints try-help to stderr, no stdout, exit code == EXIT_FAILURE */ void test_usage_failure_prints_try_help_and_exits_failure(void) { char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int code = -1; int rc = spawn_usage_and_capture("link", EXIT_FAILURE, &out, &out_len, &err, &err_len, &code); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to spawn child for usage(EXIT_FAILURE)"); TEST_ASSERT_EQUAL_INT(EXIT_FAILURE, code); TEST_ASSERT_NOT_NULL(out); TEST_ASSERT_NOT_NULL(err); TEST_ASSERT_EQUAL_UINT64_MESSAGE(0, (uint64_t)out_len, "Expected no stdout on failure"); /* Expect: Try 'link --help' for more information. */ TEST_ASSERT_TRUE_MESSAGE(strstr(err, "Try 'link --help' for more information.") != NULL, "stderr should contain the try-help hint for link"); free(out); free(err); } /* Test: Nonzero arbitrary status is propagated to exit code and prints try-help */ void test_usage_failure_with_arbitrary_status_propagated(void) { const int arbitrary = 42; char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int code = -1; int rc = spawn_usage_and_capture("link", arbitrary, &out, &out_len, &err, &err_len, &code); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to spawn child for usage(42)"); TEST_ASSERT_EQUAL_INT(arbitrary, code); TEST_ASSERT_EQUAL_UINT64_MESSAGE(0, (uint64_t)out_len, "Expected no stdout on failure"); TEST_ASSERT_TRUE_MESSAGE(strstr(err, "Try 'link --help' for more information.") != NULL, "stderr should contain the try-help hint for link"); free(out); free(err); } /* Test: Success path prints usage/help to stdout, nothing to stderr, and exits with 0 */ void test_usage_success_prints_usage_and_exits_zero(void) { char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int code = -1; int rc = spawn_usage_and_capture("link", EXIT_SUCCESS, &out, &out_len, &err, &err_len, &code); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to spawn child for usage(EXIT_SUCCESS)"); TEST_ASSERT_EQUAL_INT(EXIT_SUCCESS, code); TEST_ASSERT_NOT_NULL(out); TEST_ASSERT_NOT_NULL(err); TEST_ASSERT_EQUAL_UINT64_MESSAGE(0, (uint64_t)err_len, "Expected no stderr on success"); /* Check key substrings */ TEST_ASSERT_TRUE_MESSAGE(strstr(out, "Usage: link FILE1 FILE2") != NULL, "stdout should contain the main usage line with program name"); TEST_ASSERT_TRUE_MESSAGE(strstr(out, "or: link OPTION") != NULL, "stdout should contain the alternative usage line"); TEST_ASSERT_TRUE_MESSAGE(strstr(out, "Call the link function to create a link named FILE2") != NULL, "stdout should contain the description line"); /* Ensure help/version options are advertised (exact wording can vary by gettext/lib). */ TEST_ASSERT_TRUE_MESSAGE(strstr(out, "--help") != NULL, "stdout should mention the --help option"); TEST_ASSERT_TRUE_MESSAGE(strstr(out, "--version") != NULL, "stdout should mention the --version option"); free(out); free(err); } /* Test: program_name variable is respected in the header lines */ void test_usage_respects_program_name_variable(void) { const char *custom = "linky"; char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int code = -1; int rc = spawn_usage_and_capture(custom, EXIT_SUCCESS, &out, &out_len, &err, &err_len, &code); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to spawn child for usage(EXIT_SUCCESS) with custom program_name"); TEST_ASSERT_EQUAL_INT(EXIT_SUCCESS, code); TEST_ASSERT_EQUAL_UINT64_MESSAGE(0, (uint64_t)err_len, "Expected no stderr on success with custom name"); TEST_ASSERT_TRUE_MESSAGE(strstr(out, "Usage: linky FILE1 FILE2") != NULL, "stdout should use custom program_name in main usage line"); TEST_ASSERT_TRUE_MESSAGE(strstr(out, "or: linky OPTION") != NULL, "stdout should use custom program_name in alternative usage line"); free(out); free(err); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_usage_failure_prints_try_help_and_exits_failure); RUN_TEST(test_usage_failure_with_arbitrary_status_propagated); RUN_TEST(test_usage_success_prints_usage_and_exits_zero); RUN_TEST(test_usage_respects_program_name_variable); return UNITY_END(); }