#include "../../unity/unity.h" #include #include #include #include #include #include #include #include #include /* usage(int) is defined in the including translation unit above. set_program_name is declared via system.h already included above. */ static int str_contains(const char *haystack, const char *needle) { if (!haystack || !needle) return 0; return strstr(haystack, needle) != NULL; } static int make_pipe(int p[2]) { #if defined(O_CLOEXEC) if (pipe(p) == 0) { /* Set close-on-exec just in case (not strictly needed as we don't exec) */ fcntl(p[0], F_SETFD, FD_CLOEXEC); fcntl(p[1], F_SETFD, FD_CLOEXEC); return 0; } return -1; #else return pipe(p); #endif } static char *read_all_fd(int fd, size_t *out_len) { size_t cap = 1024; size_t len = 0; char *buf = (char *)malloc(cap); if (!buf) return NULL; for (;;) { if (len == cap) { size_t ncap = cap * 2; char *nbuf = (char *)realloc(buf, ncap); if (!nbuf) { free(buf); return NULL; } buf = nbuf; cap = ncap; } ssize_t r = read(fd, buf + len, cap - len); if (r > 0) { len += (size_t)r; continue; } else if (r == 0) { break; } else { if (errno == EINTR) continue; free(buf); return NULL; } } /* Ensure null-termination for string ops */ if (len == cap) { char *nbuf = (char *)realloc(buf, cap + 1); if (!nbuf) { free(buf); return NULL; } buf = nbuf; } buf[len] = '\0'; if (out_len) *out_len = len; return buf; } typedef struct { char *out_buf; size_t out_len; char *err_buf; size_t err_len; int exited; int exit_status; } usage_capture_t; static int run_usage_and_capture(int status, usage_capture_t *cap) { int outp[2], errp[2]; if (make_pipe(outp) < 0) return -1; if (make_pipe(errp) < 0) { close(outp[0]); close(outp[1]); return -1; } fflush(stdout); fflush(stderr); pid_t pid = fork(); if (pid < 0) { close(outp[0]); close(outp[1]); close(errp[0]); close(errp[1]); return -1; } if (pid == 0) { /* Child: redirect stdout and stderr to pipes and call usage */ /* Ensure new locale and program name for predictable messages */ setenv("LC_ALL", "C", 1); setlocale(LC_ALL, "C"); /* Close read ends in child */ close(outp[0]); close(errp[0]); /* Redirect */ if (dup2(outp[1], STDOUT_FILENO) < 0) _exit(127); if (dup2(errp[1], STDERR_FILENO) < 0) _exit(127); /* Close original write ends after dup */ close(outp[1]); close(errp[1]); /* Ensure program_name is set for messages */ set_program_name("logname"); /* Call the function under test: this will exit(status) */ usage(status); /* Should not reach */ _exit(127); } /* Parent: close write ends and read from read ends */ close(outp[1]); close(errp[1]); usage_capture_t tmp = {0}; tmp.out_buf = read_all_fd(outp[0], &tmp.out_len); tmp.err_buf = read_all_fd(errp[0], &tmp.err_len); close(outp[0]); close(errp[0]); int wstatus = 0; pid_t w = waitpid(pid, &wstatus, 0); if (w < 0) { if (tmp.out_buf) free(tmp.out_buf); if (tmp.err_buf) free(tmp.err_buf); return -1; } tmp.exited = WIFEXITED(wstatus); tmp.exit_status = tmp.exited ? WEXITSTATUS(wstatus) : -1; if (cap) *cap = tmp; else { if (tmp.out_buf) free(tmp.out_buf); if (tmp.err_buf) free(tmp.err_buf); } return 0; } void setUp(void) { /* Ensure predictable locale in parent too for any indirect operations */ setenv("LC_ALL", "C", 1); setlocale(LC_ALL, "C"); } void tearDown(void) { /* nothing */ } static void free_capture(usage_capture_t *cap) { if (!cap) return; free(cap->out_buf); free(cap->err_buf); memset(cap, 0, sizeof(*cap)); } void test_usage_failure_emits_try_help_to_stderr_only(void) { usage_capture_t cap = {0}; int rc = run_usage_and_capture(EXIT_FAILURE, &cap); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "run_usage_and_capture failed"); TEST_ASSERT_TRUE_MESSAGE(cap.exited, "Child did not exit normally"); TEST_ASSERT_EQUAL_INT(EXIT_FAILURE, cap.exit_status); /* On failure, expect no stdout and a 'Try ... --help' message on stderr. */ TEST_ASSERT_NOT_NULL(cap.err_buf); TEST_ASSERT_TRUE(cap.err_len > 0); TEST_ASSERT_TRUE_MESSAGE(str_contains(cap.err_buf, "Try '"), "stderr lacks 'Try '"); TEST_ASSERT_TRUE_MESSAGE(str_contains(cap.err_buf, "logname"), "stderr lacks program name"); TEST_ASSERT_TRUE_MESSAGE(str_contains(cap.err_buf, "--help"), "stderr lacks --help hint"); /* stdout should be empty */ if (cap.out_buf) { TEST_ASSERT_EQUAL_UINT32_MESSAGE(0u, (unsigned)cap.out_len, "stdout should be empty on failure"); TEST_ASSERT_EQUAL_STRING_MESSAGE("", cap.out_buf, "stdout should be empty on failure"); } else { /* NULL treated as empty */ TEST_ASSERT_NULL(cap.out_buf); } free_capture(&cap); } void test_usage_success_prints_full_help_to_stdout_and_exits_success(void) { usage_capture_t cap = {0}; int rc = run_usage_and_capture(EXIT_SUCCESS, &cap); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "run_usage_and_capture failed"); TEST_ASSERT_TRUE_MESSAGE(cap.exited, "Child did not exit normally"); TEST_ASSERT_EQUAL_INT(EXIT_SUCCESS, cap.exit_status); /* On success, expect usage banner and description on stdout, and empty stderr */ TEST_ASSERT_NOT_NULL_MESSAGE(cap.out_buf, "stdout buffer is NULL"); TEST_ASSERT_TRUE_MESSAGE(cap.out_len > 0, "stdout should have content"); TEST_ASSERT_TRUE_MESSAGE(str_contains(cap.out_buf, "Usage: logname [OPTION]"), "stdout lacks Usage line"); TEST_ASSERT_TRUE_MESSAGE(str_contains(cap.out_buf, "Print the user's login name."), "stdout lacks primary description"); /* Also expect help/version option hints to be present somewhere in help text */ TEST_ASSERT_TRUE_MESSAGE(str_contains(cap.out_buf, "--help"), "stdout lacks --help option description"); TEST_ASSERT_TRUE_MESSAGE(str_contains(cap.out_buf, "--version"), "stdout lacks --version option description"); /* stderr should be empty on success */ if (cap.err_buf) { TEST_ASSERT_EQUAL_UINT32_MESSAGE(0u, (unsigned)cap.err_len, "stderr should be empty on success"); TEST_ASSERT_EQUAL_STRING_MESSAGE("", cap.err_buf, "stderr should be empty on success"); } else { TEST_ASSERT_NULL(cap.err_buf); } free_capture(&cap); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_usage_failure_emits_try_help_to_stderr_only); RUN_TEST(test_usage_success_prints_full_help_to_stdout_and_exits_success); return UNITY_END(); }