#include "../../unity/unity.h" #include #include #include #include #include #include #include #include /* Helpers to capture output from a forked child running usage(). */ static char* read_all_fd(int fd, size_t* out_len) { size_t cap = 0; size_t len = 0; char* buf = NULL; char tmp[4096]; for (;;) { ssize_t n = read(fd, tmp, sizeof tmp); if (n > 0) { if (len + (size_t)n + 1 > cap) { size_t new_cap = (cap ? cap * 2 : 8192); while (len + (size_t)n + 1 > new_cap) new_cap *= 2; char* new_buf = (char*)realloc(buf, new_cap); if (!new_buf) { free(buf); *out_len = 0; return NULL; } buf = new_buf; cap = new_cap; } memcpy(buf + len, tmp, (size_t)n); len += (size_t)n; } else if (n == 0) { break; /* EOF */ } else { if (errno == EINTR) continue; break; /* error, but continue with what we have */ } } if (!buf) { buf = (char*)malloc(1); if (!buf) { *out_len = 0; return NULL; } } buf[len] = '\0'; *out_len = len; return buf; } /* Run usage(status) in a child with given program name, capturing stdout/stderr and exit status. */ static void run_usage_capture_named(int status, const char* progname, char** out_str, size_t* out_len, char** err_str, size_t* err_len, int* exit_status) { int out_pipe[2] = {-1, -1}; int err_pipe[2] = {-1, -1}; *out_str = NULL; *out_len = 0; *err_str = NULL; *err_len = 0; *exit_status = -1; if (pipe(out_pipe) < 0) return; if (pipe(err_pipe) < 0) { close(out_pipe[0]); close(out_pipe[1]); return; } fflush(stdout); fflush(stderr); pid_t pid = fork(); if (pid == 0) { /* Child: redirect stdout/stderr to pipes, set program name, and call usage. */ (void)setlocale(LC_ALL, "C"); /* Best effort to ensure C locale if environment changes between processes. */ dup2(out_pipe[1], STDOUT_FILENO); dup2(err_pipe[1], STDERR_FILENO); /* Close pipe fds no longer needed in child */ close(out_pipe[0]); close(out_pipe[1]); close(err_pipe[0]); close(err_pipe[1]); /* Provided by gnulib via system.h/progname.h included in the program source. */ set_program_name(progname ? progname : "groups"); usage(status); _exit(127); /* Should not reach here. */ } else if (pid > 0) { /* Parent */ close(out_pipe[1]); close(err_pipe[1]); char* out_buf = read_all_fd(out_pipe[0], out_len); char* err_buf = read_all_fd(err_pipe[0], err_len); close(out_pipe[0]); close(err_pipe[0]); int wstatus = 0; if (waitpid(pid, &wstatus, 0) >= 0 && WIFEXITED(wstatus)) *exit_status = WEXITSTATUS(wstatus); else *exit_status = -1; if (!out_buf) { out_buf = (char*)malloc(1); if (out_buf) { out_buf[0] = '\0'; *out_len = 0; } } if (!err_buf) { err_buf = (char*)malloc(1); if (err_buf) { err_buf[0] = '\0'; *err_len = 0; } } *out_str = out_buf; *err_str = err_buf; } else { /* fork failed */ close(out_pipe[0]); close(out_pipe[1]); close(err_pipe[0]); close(err_pipe[1]); } } void setUp(void) { /* Ensure deterministic English messages. */ setenv("LC_ALL", "C", 1); setlocale(LC_ALL, "C"); } void tearDown(void) { /* Nothing to clean up */ } /* Tests */ static void assert_contains(const char* haystack, const char* needle) { TEST_ASSERT_NOT_NULL(haystack); TEST_ASSERT_NOT_NULL(needle); TEST_ASSERT_TRUE_MESSAGE(strstr(haystack, needle) != NULL, "Expected substring not found"); } void test_usage_exit_success_outputs_help_to_stdout_and_exits_zero(void) { char* out = NULL; size_t out_len = 0; char* err = NULL; size_t err_len = 0; int status = -1; run_usage_capture_named(EXIT_SUCCESS, "groups", &out, &out_len, &err, &err_len, &status); TEST_ASSERT_EQUAL_INT(0, status); TEST_ASSERT_TRUE(out_len > 0); TEST_ASSERT_EQUAL_INT(0, (int)err_len); assert_contains(out, "Usage: "); assert_contains(out, "Usage: groups"); assert_contains(out, "--help"); assert_contains(out, "--version"); assert_contains(out, "USERNAME"); free(out); free(err); } void test_usage_nonzero_status_prints_try_help_to_stderr_and_exits_status(void) { char* out = NULL; size_t out_len = 0; char* err = NULL; size_t err_len = 0; int status = -1; run_usage_capture_named(2, "groups", &out, &out_len, &err, &err_len, &status); TEST_ASSERT_EQUAL_INT(2, status); TEST_ASSERT_EQUAL_INT(0, (int)out_len); TEST_ASSERT_TRUE(err_len > 0); assert_contains(err, "--help"); assert_contains(err, "groups"); /* should mention the program name */ free(out); free(err); } void test_usage_uses_program_name_in_usage_line(void) { char* out = NULL; size_t out_len = 0; char* err = NULL; size_t err_len = 0; int status = -1; const char* custom_name = "gnugroups"; run_usage_capture_named(EXIT_SUCCESS, custom_name, &out, &out_len, &err, &err_len, &status); TEST_ASSERT_EQUAL_INT(0, status); TEST_ASSERT_TRUE(out_len > 0); TEST_ASSERT_EQUAL_INT(0, (int)err_len); /* Ensure the program name used matches what we set. */ char expect[128]; snprintf(expect, sizeof(expect), "Usage: %s", custom_name); assert_contains(out, expect); free(out); free(err); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_usage_exit_success_outputs_help_to_stdout_and_exits_zero); RUN_TEST(test_usage_nonzero_status_prints_try_help_to_stderr_and_exits_status); RUN_TEST(test_usage_uses_program_name_in_usage_line); return UNITY_END(); }