File size: 6,463 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
#include "../../unity/unity.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>

/* Helper to read all data from an fd into a NUL-terminated buffer. */
static char *read_all_fd(int fd, size_t *out_len) {
    size_t cap = 4096;
    size_t len = 0;
    char *buf = (char *)malloc(cap + 1);
    if (!buf) {
        *out_len = 0;
        return NULL;
    }
    for (;;) {
        if (len + 2048 > cap) {
            size_t ncap = cap * 2;
            char *nbuf = (char *)realloc(buf, ncap + 1);
            if (!nbuf) {
                free(buf);
                *out_len = 0;
                return NULL;
            }
            buf = nbuf;
            cap = ncap;
        }
        ssize_t r = read(fd, buf + len, (cap - len));
        if (r < 0) {
            if (errno == EINTR) continue;
            break;
        }
        if (r == 0) break;
        len += (size_t)r;
    }
    buf[len] = '\0';
    *out_len = len;
    return buf;
}

/* Spawn a child that calls usage(status) with program_name set to pname,
   capturing its stdout/stderr and exit status. Returns 0 on success. */
static int run_usage_and_capture(int status, const char *pname,
                                 char **out_buf, size_t *out_len,
                                 char **err_buf, size_t *err_len,
                                 int *exit_code) {
    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;
    }

    if (pid == 0) {
        /* Child: redirect stdout/stderr, set environment and program_name, call usage. */
        /* Ensure predictable messages. */
        setenv("LC_ALL", "C", 1);
        setenv("LANG", "C", 1);

        /* Access program_name declared earlier via included headers in this TU. */
        extern char const *program_name;
        program_name = pname ? pname : "date";

        /* Redirect stdout/stderr to pipes. */
        if (dup2(out_pipe[1], STDOUT_FILENO) == -1) _exit(127);
        if (dup2(err_pipe[1], STDERR_FILENO) == -1) _exit(127);

        close(out_pipe[0]); close(out_pipe[1]);
        close(err_pipe[0]); close(err_pipe[1]);

        /* Call the target function; it will call exit(). */
        usage(status);

        /* Should not reach here, but just in case. */
        _exit(255);
    }

    /* Parent: close write ends, read outputs, wait for child. */
    close(out_pipe[1]);
    close(err_pipe[1]);

    char *outb = read_all_fd(out_pipe[0], out_len);
    char *errb = read_all_fd(err_pipe[0], err_len);

    close(out_pipe[0]);
    close(err_pipe[0]);

    int wstatus = 0;
    pid_t w = waitpid(pid, &wstatus, 0);
    if (w < 0) {
        if (outb) free(outb);
        if (errb) free(errb);
        return -1;
    }

    int ec = -1;
    if (WIFEXITED(wstatus)) {
        ec = WEXITSTATUS(wstatus);
    } else if (WIFSIGNALED(wstatus)) {
        ec = 128 + WTERMSIG(wstatus);
    }
    *exit_code = ec;
    *out_buf = outb ? outb : strdup("");
    *err_buf = errb ? errb : strdup("");
    return 0;
}

/* Simple substring check. */
static int str_contains(const char *haystack, const char *needle) {
    if (!haystack || !needle) return 0;
    return strstr(haystack, needle) != NULL;
}

void setUp(void) {
    /* Setup code here, or leave empty */
}
void tearDown(void) {
    /* Cleanup code here, or leave empty */
}

/* Tests */

void test_usage_success_outputs_usage_and_exits_success(void) {
    char *out = NULL, *err = NULL;
    size_t out_len = 0, err_len = 0;
    int ec = -1;

    int rc = run_usage_and_capture(0, "date", &out, &out_len, &err, &err_len, &ec);
    TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to run usage() in child process");
    TEST_ASSERT_EQUAL_INT_MESSAGE(0, ec, "usage(0) should exit with status 0");

    /* stdout should contain the Usage header with program name, stderr should be empty */
    TEST_ASSERT_TRUE_MESSAGE(out_len > 0, "usage(0) should produce help on stdout");
    TEST_ASSERT_TRUE_MESSAGE(str_contains(out, "Usage: date"), "stdout should contain 'Usage: date'");
    TEST_ASSERT_EQUAL_UINT_MESSAGE(0u, err_len, "stderr should be empty for usage(0)");

    free(out);
    free(err);
}

void test_usage_nonzero_emits_try_help_and_exits_with_code(void) {
    char *out = NULL, *err = NULL;
    size_t out_len = 0, err_len = 0;
    int ec = -1;

    int rc = run_usage_and_capture(1, "date", &out, &out_len, &err, &err_len, &ec);
    TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to run usage() in child process");
    TEST_ASSERT_EQUAL_INT_MESSAGE(1, ec, "usage(1) should exit with status 1");

    /* Should not print full help to stdout; err should include --help hint. */
    TEST_ASSERT_EQUAL_UINT_MESSAGE(0u, out_len, "stdout should be empty for nonzero status");
    TEST_ASSERT_TRUE_MESSAGE(err_len > 0, "stderr should contain try-help message for nonzero status");
    TEST_ASSERT_TRUE_MESSAGE(str_contains(err, "--help"),
                             "stderr should contain '--help' in try-help message");

    free(out);
    free(err);
}

void test_usage_uses_program_name_variable(void) {
    char *out = NULL, *err = NULL;
    size_t out_len = 0, err_len = 0;
    int ec = -1;

    /* Set program name to a custom value and ensure it appears in the Usage line. */
    const char *custom = "mydate";
    int rc = run_usage_and_capture(0, custom, &out, &out_len, &err, &err_len, &ec);
    TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to run usage() in child process");
    TEST_ASSERT_EQUAL_INT_MESSAGE(0, ec, "usage(0) should exit with status 0");

    TEST_ASSERT_TRUE_MESSAGE(out_len > 0, "stdout should contain help text");
    TEST_ASSERT_TRUE_MESSAGE(str_contains(out, "Usage: mydate"),
                             "stdout should contain 'Usage: mydate'");
    TEST_ASSERT_EQUAL_UINT_MESSAGE(0u, err_len, "stderr should be empty for usage(0)");

    free(out);
    free(err);
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_usage_success_outputs_usage_and_exits_success);
    RUN_TEST(test_usage_nonzero_emits_try_help_and_exits_with_code);
    RUN_TEST(test_usage_uses_program_name_variable);
    return UNITY_END();
}