|
|
#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(); |
|
|
} |