File size: 6,919 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
#include "../../unity/unity.h"

#include <stdio.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 <stdbool.h>

/* Helper structure to hold captured results from a child process run of usage(). */
struct CaptureResult {
    int exited;          /* boolean: 1 if child exited normally */
    int exit_status;     /* exit code if exited == 1 */
    char *out;           /* captured stdout */
    size_t out_len;
    char *err;           /* captured stderr */
    size_t err_len;
};

/* Read all data from fd into a malloc'd buffer, return pointer and set *len. */
static char *read_all_from_fd(int fd, size_t *len_out) {
    const size_t chunk = 4096;
    size_t cap = 0;
    size_t len = 0;
    char *buf = NULL;

    for (;;) {
        if (len + chunk + 1 > cap) {
            size_t new_cap = cap ? cap * 2 : 8192;
            if (new_cap < len + chunk + 1) new_cap = len + chunk + 1;
            char *nb = (char *)realloc(buf, new_cap);
            if (!nb) {
                free(buf);
                *len_out = 0;
                return NULL;
            }
            buf = nb;
            cap = new_cap;
        }
        ssize_t r = read(fd, buf + len, chunk);
        if (r < 0) {
            if (errno == EINTR) continue;
            /* On read error, stop and return what we have. */
            break;
        }
        if (r == 0) break;
        len += (size_t)r;
    }
    if (!buf) {
        buf = (char *)malloc(1);
        if (!buf) {
            *len_out = 0;
            return NULL;
        }
    }
    buf[len] = '\0';
    *len_out = len;
    return buf;
}

/* Run usage(status_arg) in a child process, capturing stdout/stderr and exit status. */
static struct CaptureResult run_usage_and_capture(int status_arg) {
    int out_pipe[2];
    int err_pipe[2];
    struct CaptureResult res;
    memset(&res, 0, sizeof(res));

    if (pipe(out_pipe) != 0) {
        /* Fatal setup error; emulate failed child with errno-coded status. */
        res.exited = 1;
        res.exit_status = 127;
        return res;
    }
    if (pipe(err_pipe) != 0) {
        close(out_pipe[0]); close(out_pipe[1]);
        res.exited = 1;
        res.exit_status = 127;
        return res;
    }

    fflush(stdout);
    fflush(stderr);
    pid_t pid = fork();
    if (pid < 0) {
        /* Fork failed. */
        close(out_pipe[0]); close(out_pipe[1]);
        close(err_pipe[0]); close(err_pipe[1]);
        res.exited = 1;
        res.exit_status = 127;
        return res;
    }

    if (pid == 0) {
        /* Child: redirect stdout/stderr and call usage(). */
        /* Close read ends in child. */
        close(out_pipe[0]);
        close(err_pipe[0]);

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

        /* Close original write fds after dup. */
        close(out_pipe[1]);
        close(err_pipe[1]);

        /* Call the target function; it should exit. */
        usage(status_arg);

        /* If it returns unexpectedly, ensure child terminates. */
        _exit(255);
    }

    /* Parent: close write ends, read from read ends. */
    close(out_pipe[1]);
    close(err_pipe[1]);

    res.out = read_all_from_fd(out_pipe[0], &res.out_len);
    res.err = read_all_from_fd(err_pipe[0], &res.err_len);
    close(out_pipe[0]);
    close(err_pipe[0]);

    int status = 0;
    if (waitpid(pid, &status, 0) < 0) {
        res.exited = 0;
        res.exit_status = -1;
        return res;
    }

    if (WIFEXITED(status)) {
        res.exited = 1;
        res.exit_status = WEXITSTATUS(status);
    } else {
        res.exited = 0;
        res.exit_status = -1;
    }

    return res;
}

void setUp(void) {
    /* No global setup needed. */
}

void tearDown(void) {
    /* No global teardown needed. */
}

/* Test that usage(EXIT_SUCCESS) writes help to stdout only, not stderr,
   and exits with status 0. */
void test_usage_success_outputs_to_stdout_only_and_mentions_program_name(void) {
    struct CaptureResult r = run_usage_and_capture(EXIT_SUCCESS);

    TEST_ASSERT_TRUE_MESSAGE(r.exited, "Child did not exit normally");
    TEST_ASSERT_EQUAL_INT_MESSAGE(0, r.exit_status, "Exit status should be 0 for success");

    /* Ensure stdout has content and stderr is empty. */
    TEST_ASSERT_NOT_NULL(r.out);
    TEST_ASSERT_TRUE_MESSAGE(r.out_len > 0, "Expected non-empty stdout for success");
    TEST_ASSERT_TRUE_MESSAGE(r.err_len == 0, "Expected empty stderr for success");

    /* The usage text should mention the program name 'du'. */
    bool mentions_du = (r.out && strstr(r.out, "du") != NULL);
    TEST_ASSERT_TRUE_MESSAGE(mentions_du, "Expected usage output to mention 'du'");

    free(r.out);
    free(r.err);
}

/* Test that usage(nonzero) writes brief message to stderr only, not stdout,
   and exits with the provided non-zero status. */
void test_usage_nonzero_status_outputs_to_stderr_only_and_mentions_help(void) {
    int code = 2;
    struct CaptureResult r = run_usage_and_capture(code);

    TEST_ASSERT_TRUE_MESSAGE(r.exited, "Child did not exit normally");
    TEST_ASSERT_EQUAL_INT_MESSAGE(code, r.exit_status, "Exit status should match provided code");

    /* Ensure stderr has content and stdout is empty. */
    TEST_ASSERT_TRUE_MESSAGE(r.out_len == 0, "Expected empty stdout for error status");
    TEST_ASSERT_TRUE_MESSAGE(r.err_len > 0, "Expected non-empty stderr for error status");

    /* The try-help message should include 'du' and '--help' tokens. */
    bool mentions_du = (r.err && strstr(r.err, "du") != NULL);
    bool mentions_help = (r.err && strstr(r.err, "--help") != NULL);
    TEST_ASSERT_TRUE_MESSAGE(mentions_du, "Expected error hint to mention 'du'");
    TEST_ASSERT_TRUE_MESSAGE(mentions_help, "Expected error hint to mention '--help'");

    free(r.out);
    free(r.err);
}

/* Test that arbitrary non-zero statuses are propagated. */
void test_usage_propagates_exact_exit_status(void) {
    int code = 123;
    struct CaptureResult r = run_usage_and_capture(code);

    TEST_ASSERT_TRUE_MESSAGE(r.exited, "Child did not exit normally");
    TEST_ASSERT_EQUAL_INT_MESSAGE(code, r.exit_status, "Exit status should be exactly the provided code");

    /* For non-zero, expect stderr output and empty stdout. */
    TEST_ASSERT_TRUE_MESSAGE(r.out_len == 0, "Expected empty stdout for non-zero status");
    TEST_ASSERT_TRUE_MESSAGE(r.err_len > 0, "Expected non-empty stderr for non-zero status");

    free(r.out);
    free(r.err);
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_usage_success_outputs_to_stdout_only_and_mentions_program_name);
    RUN_TEST(test_usage_nonzero_status_outputs_to_stderr_only_and_mentions_help);
    RUN_TEST(test_usage_propagates_exact_exit_status);
    return UNITY_END();
}