#include "../../unity/unity.h" #include #include #include #include #include #include #include #include #include /* set_program_name is declared via system.h in the parent translation unit, since this test file is included after the program includes. */ extern void set_program_name (const char *); /* Helper to append data to a dynamic buffer. Ensures NUL-termination. */ static void append_buf(char **buf, size_t *len, const char *data, size_t n) { char *newp = realloc(*buf, *len + n + 1); if (!newp) return; memcpy(newp + *len, data, n); *len += n; newp[*len] = '\0'; *buf = newp; } /* Run usage(status) in a child process, capturing stdout and stderr. Returns 0 on success, and populates out_stdout/out_stderr and exit_status. Caller must free *out_stdout and *out_stderr. */ static int run_usage_and_capture(int status, char **out_stdout, size_t *out_stdout_len, char **out_stderr, size_t *out_stderr_len, int *exit_status) { int out_pipe[2] = {-1, -1}; int err_pipe[2] = {-1, -1}; 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: set locale and program name, redirect fds, call usage. */ /* Ensure C locale to avoid translation variability. */ setenv("LC_ALL", "C", 1); setenv("LANG", "C", 1); /* Ensure program_name is initialized for usage() output. */ set_program_name("chmod"); /* Redirect stdout and stderr. */ close(out_pipe[0]); close(err_pipe[0]); if (dup2(out_pipe[1], STDOUT_FILENO) < 0) _exit(127); if (dup2(err_pipe[1], STDERR_FILENO) < 0) _exit(127); close(out_pipe[1]); close(err_pipe[1]); extern void usage (int status); usage(status); /* Should not reach here. */ _exit(126); } /* Parent: close write ends, read from read ends, then wait for child. */ close(out_pipe[1]); close(err_pipe[1]); char *stdout_buf = NULL; size_t stdout_len = 0; char *stderr_buf = NULL; size_t stderr_len = 0; char tmp[4096]; ssize_t n; /* Read stdout */ while ((n = read(out_pipe[0], tmp, sizeof tmp)) > 0) { append_buf(&stdout_buf, &stdout_len, tmp, (size_t)n); } close(out_pipe[0]); /* Read stderr */ while ((n = read(err_pipe[0], tmp, sizeof tmp)) > 0) { append_buf(&stderr_buf, &stderr_len, tmp, (size_t)n); } close(err_pipe[0]); int wstatus = 0; if (waitpid(pid, &wstatus, 0) < 0) { free(stdout_buf); free(stderr_buf); return -1; } int es = -1; if (WIFEXITED(wstatus)) { es = WEXITSTATUS(wstatus); } else if (WIFSIGNALED(wstatus)) { es = 128 + WTERMSIG(wstatus); } *out_stdout = stdout_buf ? stdout_buf : strdup(""); *out_stdout_len = stdout_len; *out_stderr = stderr_buf ? stderr_buf : strdup(""); *out_stderr_len = stderr_len; *exit_status = es; return 0; } void setUp(void) { /* Setup code here, or leave empty */ } void tearDown(void) { /* Cleanup code here, or leave empty */ } static bool contains_substr(const char *haystack, const char *needle) { return haystack && needle && strstr(haystack, needle) != NULL; } /* Test that usage(EXIT_SUCCESS) prints the full usage to stdout, nothing to stderr, and exits with code 0. */ void test_usage_success_outputs_to_stdout_and_exit_zero(void) { char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int es = -1; int rc = run_usage_and_capture(EXIT_SUCCESS, &out, &out_len, &err, &err_len, &es); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to run usage in child"); TEST_ASSERT_EQUAL_INT_MESSAGE(0, es, "usage(EXIT_SUCCESS) did not exit with 0"); TEST_ASSERT_TRUE_MESSAGE(out_len > 0, "Expected non-empty stdout for success usage"); TEST_ASSERT_EQUAL_size_t_MESSAGE(0, err_len, "Expected empty stderr for success usage"); /* Check for key strings that should be in usage output. Use option flags/syntax that are not localized. */ TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "Usage:"), "Missing 'Usage:' prefix"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "chmod"), "Missing program name 'chmod'"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "-R, --recursive"), "Missing recursive option flag"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "--reference"), "Missing --reference option flag"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "--no-dereference"), "Missing --no-dereference option flag"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "--dereference"), "Missing --dereference option flag"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "--preserve-root"), "Missing --preserve-root option flag"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "--help"), "Missing --help in usage text"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "--version"), "Missing --version in usage text"); free(out); free(err); } /* Test that usage(nonzero) prints a 'try --help' message to stderr, nothing to stdout, and exits with the provided status. */ void test_usage_failure_outputs_try_help_to_stderr_and_exit_status(void) { char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int es = -1; int desired_status = 2; int rc = run_usage_and_capture(desired_status, &out, &out_len, &err, &err_len, &es); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to run usage in child"); TEST_ASSERT_EQUAL_INT_MESSAGE(desired_status, es, "usage(nonzero) did not exit with provided status"); TEST_ASSERT_EQUAL_size_t_MESSAGE(0, out_len, "Expected empty stdout for error usage"); TEST_ASSERT_TRUE_MESSAGE(err_len > 0, "Expected non-empty stderr for error usage"); /* Check for generic 'try --help' hint and program name; avoid localization issues. */ TEST_ASSERT_TRUE_MESSAGE(contains_substr(err, "--help"), "Missing '--help' hint in stderr"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(err, "chmod"), "Missing program name 'chmod' in stderr"); free(out); free(err); } /* Test that usage text includes key mode syntax and option groups. */ void test_usage_includes_mode_syntax_and_option_groups(void) { char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int es = -1; int rc = run_usage_and_capture(EXIT_SUCCESS, &out, &out_len, &err, &err_len, &es); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "Failed to run usage in child"); TEST_ASSERT_EQUAL_INT_MESSAGE(0, es, "usage(EXIT_SUCCESS) did not exit with 0"); TEST_ASSERT_TRUE_MESSAGE(out_len > 0, "Expected non-empty stdout for success usage"); /* Look for some characteristic substrings that should be present. */ TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "MODE[,MODE]"), "Missing 'MODE[,MODE]' synopsis"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "OCTAL-MODE"), "Missing 'OCTAL-MODE' synopsis"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "--reference=RFILE"), "Missing '--reference=RFILE' synopsis"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "-c, --changes"), "Missing '-c, --changes' option"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "-v, --verbose"), "Missing '-v, --verbose' option"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "-f, --silent"), "Missing '-f, --silent' option"); /* The exact phrasing of the "Each MODE is of the form" line may be localized, but the bracket characters and pattern are likely stable. We'll check for key tokens. */ TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "[ugoa]"), "Missing token '[ugoa]' in mode description"); TEST_ASSERT_TRUE_MESSAGE(contains_substr(out, "[-+=]"), "Missing token '[-+=]' in mode description"); free(out); free(err); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_usage_success_outputs_to_stdout_and_exit_zero); RUN_TEST(test_usage_failure_outputs_try_help_to_stderr_and_exit_status); RUN_TEST(test_usage_includes_mode_syntax_and_option_groups); return UNITY_END(); }