File size: 7,325 Bytes
78d2150 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | #include "../../unity/unity.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <locale.h>
#include <fcntl.h>
/* 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();
} |