#include "../../unity/unity.h" #include #include #include #include #include #include #include #include /* Helper to read all data from an fd into a NUL-terminated buffer. */ static int read_all(int fd, char **out_buf, size_t *out_len) { char tmp[4096]; size_t cap = 0; size_t len = 0; char *buf = NULL; for (;;) { ssize_t n = read(fd, tmp, sizeof tmp); if (n < 0) { if (errno == EINTR) continue; free(buf); return -1; } if (n == 0) break; if (len + (size_t)n + 1 > cap) { size_t newcap = cap ? cap * 2 : 8192; while (newcap < len + (size_t)n + 1) newcap *= 2; char *nb = (char *)realloc(buf, newcap); if (!nb) { free(buf); errno = ENOMEM; return -1; } buf = nb; cap = newcap; } memcpy(buf + len, tmp, (size_t)n); len += (size_t)n; } if (!buf) { buf = (char *)malloc(1); if (!buf) return -1; buf[0] = '\0'; len = 0; } else { buf[len] = '\0'; } *out_buf = buf; *out_len = len; return 0; } typedef struct { char *out_buf; size_t out_len; char *err_buf; size_t err_len; int exit_code; /* -1 on abnormal termination */ } usage_result; /* Run usage(status) in a child process capturing stdout/stderr and exit status. */ static int run_usage_and_capture(int status, usage_result *res) { int out_pipe[2]; int err_pipe[2]; 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; } else if (pid == 0) { /* Child: redirect stdout/stderr to pipes and invoke usage. */ if (dup2(out_pipe[1], STDOUT_FILENO) < 0) _exit(127); if (dup2(err_pipe[1], STDERR_FILENO) < 0) _exit(127); close(out_pipe[0]); close(out_pipe[1]); close(err_pipe[0]); close(err_pipe[1]); /* Call the target function; this should not return. */ usage(status); /* Safety fallback if usage returns unexpectedly. */ _exit(255); } else { /* Parent: close write ends, read from read ends, wait for child. */ close(out_pipe[1]); close(err_pipe[1]); usage_result tmp = {0}; if (read_all(out_pipe[0], &tmp.out_buf, &tmp.out_len) < 0) { close(out_pipe[0]); close(err_pipe[0]); return -1; } if (read_all(err_pipe[0], &tmp.err_buf, &tmp.err_len) < 0) { close(out_pipe[0]); close(err_pipe[0]); free(tmp.out_buf); return -1; } close(out_pipe[0]); close(err_pipe[0]); int wstatus = 0; if (waitpid(pid, &wstatus, 0) < 0) { free(tmp.out_buf); free(tmp.err_buf); return -1; } if (WIFEXITED(wstatus)) tmp.exit_code = WEXITSTATUS(wstatus); else tmp.exit_code = -1; *res = tmp; return 0; } } /* Simple substring checker. */ static bool contains_substr(const char *hay, const char *needle) { if (!hay || !needle) return false; return strstr(hay, needle) != NULL; } /* program_name is provided by the program environment via system.h/progname.h. */ extern const char *program_name; void setUp(void) { /* Ensure deterministic English output. */ setenv("LC_ALL", "C", 1); /* Ensure program_name is suitable for messages. */ program_name = "pathchk"; } void tearDown(void) { /* No-op */ } /* Test: usage(EXIT_SUCCESS) prints full help to stdout and exits with 0. */ void test_usage_success_outputs_help_and_exits_zero(void) { usage_result r = {0}; int rc = run_usage_and_capture(EXIT_SUCCESS, &r); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to run usage in child process"); /* Check exit status. */ TEST_ASSERT_EQUAL_INT_MESSAGE(0, r.exit_code, "usage(EXIT_SUCCESS) did not exit(0)"); /* Check stdout contains key help elements. */ TEST_ASSERT_TRUE_MESSAGE(contains_substr(r.out_buf, "Usage:"), "Help text missing 'Usage:'"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(r.out_buf, "pathchk"), "Help text missing program name"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(r.out_buf, "-p"), "Help text missing '-p' option"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(r.out_buf, "-P"), "Help text missing '-P' option"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(r.out_buf, "--portability"), "Help text missing '--portability' option"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(r.out_buf, "--help"), "Help text missing '--help' description"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(r.out_buf, "--version"), "Help text missing '--version' description"); /* On success, stderr should be empty. */ TEST_ASSERT_TRUE_MESSAGE(r.err_len == 0 || (r.err_buf && r.err_buf[0] == '\0'), "Expected no stderr output for usage(EXIT_SUCCESS)"); free(r.out_buf); free(r.err_buf); } /* Test: usage(nonzero) emits a 'Try ... --help ...' hint to stderr and exits with the given status. */ void test_usage_failure_emits_try_help_on_stderr_and_propagates_status(void) { int requested_status = 2; usage_result r = {0}; int rc = run_usage_and_capture(requested_status, &r); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to run usage in child process"); /* Check exit status propagates. */ TEST_ASSERT_EQUAL_INT_MESSAGE(requested_status, r.exit_code, "usage(nonzero) did not propagate exit status"); /* Check stderr contains a 'Try' hint mentioning --help and program name. */ TEST_ASSERT_TRUE_MESSAGE(contains_substr(r.err_buf, "Try"), "Expected stderr to contain a 'Try' hint"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(r.err_buf, "--help"), "Expected stderr hint to mention '--help'"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(r.err_buf, "pathchk"), "Expected stderr hint to mention the program name"); /* stdout should not contain the full usage header in this path. */ TEST_ASSERT_FALSE_MESSAGE(contains_substr(r.out_buf, "Usage:"), "Did not expect full help on stdout for nonzero status"); free(r.out_buf); free(r.err_buf); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_usage_success_outputs_help_and_exits_zero); RUN_TEST(test_usage_failure_emits_try_help_on_stderr_and_propagates_status); return UNITY_END(); }