#include "../../unity/unity.h" #include #include #include #include #include #include #include #include /* Helper: create a temporary file with given content (binary-safe) and return malloc'ed path */ static char *create_tempfile_with_content(const void *data, size_t len) { char template_path[] = "/tmp/comm_test_XXXXXX"; int fd = mkstemp(template_path); if (fd < 0) return NULL; if (len > 0) { const unsigned char *p = (const unsigned char *)data; size_t written = 0; while (written < len) { ssize_t w = write(fd, p + written, len - written); if (w < 0) { close(fd); unlink(template_path); return NULL; } written += (size_t)w; } } if (close(fd) != 0) { unlink(template_path); return NULL; } /* Return a heap copy of the path */ char *ret = (char *)malloc(strlen(template_path) + 1); if (!ret) { unlink(template_path); return NULL; } strcpy(ret, template_path); return ret; } /* Helper: read all data from fd into malloc'ed buffer; returns buffer and sets *out_len. The returned buffer is NUL-terminated for convenience, but may contain embedded NULs. */ static char *read_all_from_fd(int fd, size_t *out_len) { size_t cap = 4096; size_t size = 0; char *buf = (char *)malloc(cap + 1); if (!buf) return NULL; for (;;) { if (size == cap) { cap *= 2; char *nb = (char *)realloc(buf, cap + 1); if (!nb) { free(buf); return NULL; } buf = nb; } ssize_t r = read(fd, buf + size, cap - size); if (r < 0) { if (errno == EINTR) continue; free(buf); return NULL; } if (r == 0) break; size += (size_t)r; } buf[size] = '\0'; if (out_len) *out_len = size; return buf; } /* Helper to run compare_files in a child, optionally providing stdin data. Inputs: file1, file2: paths or "-" for stdin stdin_data/stdin_len: data to feed to child's stdin if either file is "-" Outputs: *out_buf/*out_len: captured stdout *err_buf/*err_len: captured stderr returns child's exit status (>=0) or -1 on failure */ static int run_compare_files_capture(const char *file1, const char *file2, const unsigned char *stdin_data, size_t stdin_len, char **out_buf, size_t *out_len, char **err_buf, size_t *err_len) { int out_pipe[2], err_pipe[2], in_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; } int need_stdin = ((file1 && strcmp(file1, "-") == 0) || (file2 && strcmp(file2, "-") == 0)); if (need_stdin) { if (pipe(in_pipe) < 0) { close(out_pipe[0]); close(out_pipe[1]); close(err_pipe[0]); close(err_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]); if (need_stdin) { close(in_pipe[0]); close(in_pipe[1]); } return -1; } if (pid == 0) { /* Child */ /* Ensure deterministic messages */ setenv("LC_ALL", "C", 1); /* Redirect stdout/stderr */ dup2(out_pipe[1], STDOUT_FILENO); dup2(err_pipe[1], STDERR_FILENO); close(out_pipe[0]); close(out_pipe[1]); close(err_pipe[0]); close(err_pipe[1]); if (need_stdin) { dup2(in_pipe[0], STDIN_FILENO); close(in_pipe[0]); close(in_pipe[1]); } /* Call target function */ char *infiles[2]; infiles[0] = (char *)file1; infiles[1] = (char *)file2; /* compare_files never returns */ compare_files(infiles); /* Fallback if it ever returned (shouldn't) */ _exit(255); } /* Parent */ close(out_pipe[1]); close(err_pipe[1]); if (need_stdin) { close(in_pipe[0]); /* Feed stdin data if provided */ if (stdin_data && stdin_len > 0) { size_t written = 0; while (written < stdin_len) { ssize_t w = write(in_pipe[1], stdin_data + written, stdin_len - written); if (w < 0) { if (errno == EINTR) continue; break; } written += (size_t)w; } } close(in_pipe[1]); } /* Read outputs */ size_t o_len = 0, e_len = 0; char *o_buf = read_all_from_fd(out_pipe[0], &o_len); char *e_buf = read_all_from_fd(err_pipe[0], &e_len); close(out_pipe[0]); close(err_pipe[0]); int status = -1; int rc = waitpid(pid, &status, 0); int exit_code = -1; if (rc >= 0) { if (WIFEXITED(status)) exit_code = WEXITSTATUS(status); else if (WIFSIGNALED(status)) exit_code = 128 + WTERMSIG(status); else exit_code = -1; } if (out_buf) *out_buf = o_buf; else free(o_buf); if (out_len) *out_len = o_len; if (err_buf) *err_buf = e_buf; else free(e_buf); if (err_len) *err_len = e_len; return exit_code; } /* Unity setup/teardown */ void setUp(void) { /* Reset globals to defaults akin to real program behavior */ only_file_1 = true; only_file_2 = true; both = true; seen_unpairable = false; issued_disorder_warning[0] = false; issued_disorder_warning[1] = false; hard_LC_COLLATE = false; check_input_order = CHECK_ORDER_DEFAULT; col_sep = "\t"; col_sep_len = 1; delim = '\n'; total_option = false; } void tearDown(void) { /* Nothing */ } /* Test 1: Basic three-column merge with default tab separator */ void test_compare_files_basic_three_columns(void) { const char *f1c = "apple\nbanana\ncarrot\n"; const char *f2c = "banana\ncarrot\ndate\n"; char *p1 = create_tempfile_with_content(f1c, strlen(f1c)); char *p2 = create_tempfile_with_content(f2c, strlen(f2c)); TEST_ASSERT_NOT_NULL(p1); TEST_ASSERT_NOT_NULL(p2); char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = run_compare_files_capture(p1, p2, NULL, 0, &out, &out_len, &err, &err_len); const char *expected = "apple\n\t\tbanana\n\t\tcarrot\n\tdate\n"; TEST_ASSERT_EQUAL_INT(0, ec); TEST_ASSERT_EQUAL_size_t(strlen(expected), out_len); TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, out, out_len); TEST_ASSERT_EQUAL_size_t(0, err_len); free(out); free(err); unlink(p1); unlink(p2); free(p1); free(p2); } /* Test 2: Suppress columns 1 and 3, print only unique lines from file 2 without leading sep */ void test_compare_files_suppress_columns_effect(void) { const char *f1c = "a\nb\nc\n"; const char *f2c = "b\nd\n"; char *p1 = create_tempfile_with_content(f1c, strlen(f1c)); char *p2 = create_tempfile_with_content(f2c, strlen(f2c)); TEST_ASSERT_NOT_NULL(p1); TEST_ASSERT_NOT_NULL(p2); only_file_1 = false; only_file_2 = true; both = false; char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = run_compare_files_capture(p1, p2, NULL, 0, &out, &out_len, &err, &err_len); const char *expected = "d\n"; TEST_ASSERT_EQUAL_INT(0, ec); TEST_ASSERT_EQUAL_size_t(strlen(expected), out_len); TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, out, out_len); TEST_ASSERT_EQUAL_size_t(0, err_len); free(out); free(err); unlink(p1); unlink(p2); free(p1); free(p2); } /* Test 3: Custom output delimiter and total summary (col_sep_len > 1 path) */ void test_compare_files_custom_delim_and_total(void) { const char *f1c = "a\nx\n"; const char *f2c = "a\ny\n"; char *p1 = create_tempfile_with_content(f1c, strlen(f1c)); char *p2 = create_tempfile_with_content(f2c, strlen(f2c)); TEST_ASSERT_NOT_NULL(p1); TEST_ASSERT_NOT_NULL(p2); col_sep = "::"; col_sep_len = 2; total_option = true; char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = run_compare_files_capture(p1, p2, NULL, 0, &out, &out_len, &err, &err_len); const char *expected = "::::a\n" /* common "a" with two separators before */ "x\n" /* unique in file1 */ "::y\n" /* unique in file2 with one separator before */ "1::1::1::total\n"; /* summary */ TEST_ASSERT_EQUAL_INT(0, ec); TEST_ASSERT_EQUAL_size_t(strlen(expected), out_len); TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, out, out_len); TEST_ASSERT_EQUAL_size_t(0, err_len); free(out); free(err); unlink(p1); unlink(p2); free(p1); free(p2); } /* Test 4: Zero-terminated input/output; verify binary output including NUL and separators */ void test_compare_files_zero_terminated(void) { const unsigned char f1c[] = { 'a', '\0', 'c', '\0' }; const unsigned char f2c[] = { 'b', '\0', 'c', '\0' }; char *p1 = create_tempfile_with_content(f1c, sizeof(f1c)); char *p2 = create_tempfile_with_content(f2c, sizeof(f2c)); TEST_ASSERT_NOT_NULL(p1); TEST_ASSERT_NOT_NULL(p2); delim = '\0'; /* zero-terminated lines */ col_sep = "\t"; col_sep_len = 1; char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = run_compare_files_capture(p1, p2, NULL, 0, &out, &out_len, &err, &err_len); unsigned char expected[] = { 'a', 0, '\t', 'b', 0, '\t', '\t', 'c', 0 }; TEST_ASSERT_EQUAL_INT(0, ec); TEST_ASSERT_EQUAL_size_t(sizeof(expected), out_len); TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, out, out_len); TEST_ASSERT_EQUAL_size_t(0, err_len); free(out); free(err); unlink(p1); unlink(p2); free(p1); free(p2); } /* Test 5: Default check-order warning path with unsorted file, causes non-zero exit at end */ void test_compare_files_check_order_default_warns_and_exits_nonzero(void) { const char *f1c = "b\nA\n"; /* unsorted: 'b' > 'A' */ char *p1 = create_tempfile_with_content(f1c, strlen(f1c)); char *p2 = create_tempfile_with_content("", 0); /* empty file2 to guarantee unpairable lines */ TEST_ASSERT_NOT_NULL(p1); TEST_ASSERT_NOT_NULL(p2); check_input_order = CHECK_ORDER_DEFAULT; hard_LC_COLLATE = false; char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = run_compare_files_capture(p1, p2, NULL, 0, &out, &out_len, &err, &err_len); const char *expected_out = "b\nA\n"; TEST_ASSERT_TRUE(ec != 0); TEST_ASSERT_EQUAL_size_t(strlen(expected_out), out_len); TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_out, out, out_len); /* We expect some error output, but avoid locale-dependent substring checks */ TEST_ASSERT_TRUE(err_len > 0); free(out); free(err); unlink(p1); unlink(p2); free(p1); free(p2); } /* Test 6: check-order enabled (fatal): exit happens upon first disorder detection */ void test_compare_files_check_order_enabled_fatal(void) { const char *f1c = "b\nA\n"; /* unsorted */ char *p1 = create_tempfile_with_content(f1c, strlen(f1c)); char *p2 = create_tempfile_with_content("", 0); TEST_ASSERT_NOT_NULL(p1); TEST_ASSERT_NOT_NULL(p2); check_input_order = CHECK_ORDER_ENABLED; hard_LC_COLLATE = false; char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = run_compare_files_capture(p1, p2, NULL, 0, &out, &out_len, &err, &err_len); /* Only the first line should have been printed before fatal error */ const char *expected_out = "b\n"; TEST_ASSERT_TRUE(ec != 0); TEST_ASSERT_EQUAL_size_t(strlen(expected_out), out_len); TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_out, out, out_len); TEST_ASSERT_TRUE(err_len > 0); free(out); free(err); unlink(p1); unlink(p2); free(p1); free(p2); } /* Test 7: nocheck-order: unsorted input does not error */ void test_compare_files_check_order_disabled_allows_unsorted(void) { const char *f1c = "b\nA\n"; /* unsorted */ char *p1 = create_tempfile_with_content(f1c, strlen(f1c)); char *p2 = create_tempfile_with_content("", 0); TEST_ASSERT_NOT_NULL(p1); TEST_ASSERT_NOT_NULL(p2); check_input_order = CHECK_ORDER_DISABLED; hard_LC_COLLATE = false; char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = run_compare_files_capture(p1, p2, NULL, 0, &out, &out_len, &err, &err_len); const char *expected_out = "b\nA\n"; TEST_ASSERT_EQUAL_INT(0, ec); TEST_ASSERT_EQUAL_size_t(strlen(expected_out), out_len); TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_out, out, out_len); TEST_ASSERT_EQUAL_size_t(0, err_len); free(out); free(err); unlink(p1); unlink(p2); free(p1); free(p2); } /* Test 8: Using "-" to read from stdin for one file */ void test_compare_files_stdin_handling(void) { const char *f1c = "a\nb\n"; const char *stdin_c = "b\nc\n"; char *p1 = create_tempfile_with_content(f1c, strlen(f1c)); TEST_ASSERT_NOT_NULL(p1); char *out = NULL, *err = NULL; size_t out_len = 0, err_len = 0; int ec = run_compare_files_capture(p1, "-", (const unsigned char *)stdin_c, strlen(stdin_c), &out, &out_len, &err, &err_len); const char *expected = "a\n\t\tb\n\tc\n"; TEST_ASSERT_EQUAL_INT(0, ec); TEST_ASSERT_EQUAL_size_t(strlen(expected), out_len); TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, out, out_len); TEST_ASSERT_EQUAL_size_t(0, err_len); free(out); free(err); unlink(p1); free(p1); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_compare_files_basic_three_columns); RUN_TEST(test_compare_files_suppress_columns_effect); RUN_TEST(test_compare_files_custom_delim_and_total); RUN_TEST(test_compare_files_zero_terminated); RUN_TEST(test_compare_files_check_order_default_warns_and_exits_nonzero); RUN_TEST(test_compare_files_check_order_enabled_fatal); RUN_TEST(test_compare_files_check_order_disabled_allows_unsorted); RUN_TEST(test_compare_files_stdin_handling); return UNITY_END(); }