| | #include "../../unity/unity.h" |
| | #include <stdlib.h> |
| | #include <string.h> |
| | #include <unistd.h> |
| | #include <sys/wait.h> |
| | #include <fcntl.h> |
| | #include <errno.h> |
| | #include <stdio.h> |
| | #include <stdbool.h> |
| |
|
| | |
| | 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; |
| | } usage_result; |
| |
|
| | |
| | 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) |
| | { |
| | |
| | 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]); |
| |
|
| | |
| | usage(status); |
| |
|
| | |
| | _exit(255); |
| | } |
| | else |
| | { |
| | |
| | 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; |
| | } |
| | } |
| |
|
| | |
| | static bool contains_substr(const char *hay, const char *needle) |
| | { |
| | if (!hay || !needle) return false; |
| | return strstr(hay, needle) != NULL; |
| | } |
| |
|
| | |
| | extern const char *program_name; |
| |
|
| | void setUp(void) { |
| | |
| | setenv("LC_ALL", "C", 1); |
| | |
| | program_name = "pathchk"; |
| | } |
| |
|
| | void tearDown(void) { |
| | |
| | } |
| |
|
| | |
| | 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"); |
| |
|
| | |
| | TEST_ASSERT_EQUAL_INT_MESSAGE(0, r.exit_code, "usage(EXIT_SUCCESS) did not exit(0)"); |
| |
|
| | |
| | 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"); |
| |
|
| | |
| | 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); |
| | } |
| |
|
| | |
| | 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"); |
| |
|
| | |
| | TEST_ASSERT_EQUAL_INT_MESSAGE(requested_status, r.exit_code, |
| | "usage(nonzero) did not propagate exit status"); |
| |
|
| | |
| | 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"); |
| |
|
| | |
| | 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(); |
| | } |