#include "../../unity/unity.h" #include #include #include #include /* Helpers to build and free minimal struct line objects for prjoin. */ static struct line *T_make_line(const char **fields, size_t n) { struct line *l = (struct line *)calloc(1, sizeof(*l)); TEST_ASSERT_NOT_NULL_MESSAGE(l, "alloc line"); l->nfields = (idx_t)n; l->nfields_allocated = (idx_t)n; if (n) { l->fields = (struct field *)calloc(n, sizeof(struct field)); TEST_ASSERT_NOT_NULL_MESSAGE(l->fields, "alloc fields"); for (size_t i = 0; i < n; ++i) { size_t len = strlen(fields[i]); char *s = (char *)malloc(len + 1); TEST_ASSERT_NOT_NULL_MESSAGE(s, "alloc field string"); memcpy(s, fields[i], len + 1); l->fields[i].beg = s; l->fields[i].len = (idx_t)len; } } return l; } static void T_free_line(struct line *l) { if (!l) return; for (size_t i = 0; i < (size_t)l->nfields; ++i) { free(l->fields[i].beg); } free(l->fields); free(l); } /* Reset and free the global outlist. */ static void T_reset_outlist(void) { struct outlist *o = outlist_head.next; while (o) { struct outlist *next = o->next; free(o); o = next; } outlist_head.next = NULL; outlist_end = &outlist_head; } /* Capture stdout while invoking prjoin. Do not use Unity asserts while stdout is redirected. */ typedef struct { char *data; size_t len; } TCap; static TCap T_capture_prjoin(const struct line *l1, const struct line *l2) { TCap r = {0, 0}; fflush(stdout); int saved_fd = dup(fileno(stdout)); FILE *tmp = tmpfile(); if (!tmp || saved_fd < 0) { if (tmp) fclose(tmp); return r; } int tmp_fd = fileno(tmp); fflush(stdout); dup2(tmp_fd, fileno(stdout)); /* Call the function under test. */ prjoin(l1, l2); fflush(stdout); long pos = ftell(tmp); if (pos < 0) pos = 0; r.len = (size_t)pos; r.data = (char *)malloc(r.len + 1); if (r.data) { memset(r.data, 0, r.len + 1); rewind(tmp); fread(r.data, 1, r.len, tmp); } fflush(stdout); dup2(saved_fd, fileno(stdout)); close(saved_fd); fclose(tmp); return r; } static void T_free_cap(TCap *c) { if (!c) return; free(c->data); c->data = NULL; c->len = 0; } /* Save/restore selected globals around tests. */ static const char *saved_output_separator; static idx_t saved_output_seplen; static char saved_eolchar; static const char *saved_empty_filler; static ptrdiff_t saved_join_field_1; static ptrdiff_t saved_join_field_2; static bool saved_autoformat; static idx_t saved_autocount_1; static idx_t saved_autocount_2; void setUp(void) { /* Save state */ saved_output_separator = output_separator; saved_output_seplen = output_seplen; saved_eolchar = eolchar; saved_empty_filler = empty_filler; saved_join_field_1 = join_field_1; saved_join_field_2 = join_field_2; saved_autoformat = autoformat; saved_autocount_1 = autocount_1; saved_autocount_2 = autocount_2; /* Start with a clean outlist for each test. */ T_reset_outlist(); /* Default common settings */ output_separator = " "; output_seplen = 1; eolchar = '\n'; empty_filler = NULL; join_field_1 = 0; join_field_2 = 0; autoformat = false; autocount_1 = 0; autocount_2 = 0; } void tearDown(void) { /* Clean any remaining outlist nodes and restore saved state. */ T_reset_outlist(); output_separator = saved_output_separator; output_seplen = saved_output_seplen; eolchar = saved_eolchar; empty_filler = saved_empty_filler; join_field_1 = saved_join_field_1; join_field_2 = saved_join_field_2; autoformat = saved_autoformat; autocount_1 = saved_autocount_1; autocount_2 = saved_autocount_2; } /* Test 1: Default format, both lines present. */ static void test_prjoin_default_basic(void) { const char *f1[] = {"join", "a", "b"}; const char *f2[] = {"join", "c"}; struct line *l1 = T_make_line(f1, 3); struct line *l2 = T_make_line(f2, 2); /* Defaults already set in setUp: space separator, '\n', join field 0 */ TCap cap = T_capture_prjoin(l1, l2); TEST_ASSERT_NOT_NULL(cap.data); TEST_ASSERT_EQUAL_STRING("join a b c\n", cap.data); T_free_cap(&cap); T_free_line(l1); T_free_line(l2); } /* Test 2: Default format, first line is uni_blank so join field is taken from second line. */ static void test_prjoin_default_blank_first(void) { const char *f2[] = {"K", "x"}; struct line *l2 = T_make_line(f2, 2); /* join_field_2 = 0 already; expect: K x\n */ TCap cap = T_capture_prjoin(&uni_blank, l2); TEST_ASSERT_NOT_NULL(cap.data); TEST_ASSERT_EQUAL_STRING("K x\n", cap.data); T_free_cap(&cap); T_free_line(l2); } /* Test 3: Custom outlist ordering, including '0' (join field), with custom separator. */ static void test_prjoin_outlist_custom_order_and_separator(void) { const char *f1[] = {"J", "A", "B"}; const char *f2[] = {"X", "C"}; struct line *l1 = T_make_line(f1, 3); struct line *l2 = T_make_line(f2, 2); output_separator = ","; output_seplen = 1; join_field_1 = 0; /* '0' should print J from l1 */ char spec[] = "1.3,0,2.2,1.2"; /* B,J,C,A */ add_field_list(spec); TCap cap = T_capture_prjoin(l1, l2); TEST_ASSERT_NOT_NULL(cap.data); TEST_ASSERT_EQUAL_STRING("B,J,C,A\n", cap.data); T_free_cap(&cap); T_free_line(l1); T_free_line(l2); } /* Test 4: Custom outlist with missing and empty fields, using empty_filler and custom separator. */ static void test_prjoin_outlist_missing_and_empty_filler(void) { const char *f1[] = {"J"}; const char *f2[] = {"J", ""}; struct line *l1 = T_make_line(f1, 1); struct line *l2 = T_make_line(f2, 2); empty_filler = ""; output_separator = "|"; output_seplen = 1; char spec[] = "1.2,2.2,2.3"; /* missing in l1, empty in l2, missing in l2 */ add_field_list(spec); TCap cap = T_capture_prjoin(l1, l2); TEST_ASSERT_NOT_NULL(cap.data); TEST_ASSERT_EQUAL_STRING("||\n", cap.data); T_free_cap(&cap); T_free_line(l1); T_free_line(l2); } /* Test 5: Default format with autoformat limiting the number of fields printed from each line. */ static void test_prjoin_autoformat_limits(void) { const char *f1[] = {"J", "A", "B"}; const char *f2[] = {"J", "C", "D", "E"}; struct line *l1 = T_make_line(f1, 3); struct line *l2 = T_make_line(f2, 4); autoformat = true; autocount_1 = 1; /* Only 1 field (index 0) available from line1 */ autocount_2 = 2; /* Two fields total in line2 => only field 1 gets printed */ join_field_1 = 0; join_field_2 = 0; TCap cap = T_capture_prjoin(l1, l2); TEST_ASSERT_NOT_NULL(cap.data); TEST_ASSERT_EQUAL_STRING("J C\n", cap.data); T_free_cap(&cap); T_free_line(l1); T_free_line(l2); } /* Test 6: Zero-terminated records and non-space separator; verify bytes including NUL. */ static void test_prjoin_zero_terminated_eol(void) { const char *f1[] = {"K"}; const char *f2[] = {"K", "X"}; struct line *l1 = T_make_line(f1, 1); struct line *l2 = T_make_line(f2, 2); eolchar = '\0'; output_separator = "="; output_seplen = 1; join_field_1 = 0; join_field_2 = 0; TCap cap = T_capture_prjoin(l1, l2); TEST_ASSERT_NOT_NULL(cap.data); TEST_ASSERT_EQUAL_UINT32(4, (uint32_t)cap.len); const char expected[4] = {'K', '=', 'X', '\0'}; TEST_ASSERT_EQUAL_INT(0, memcmp(expected, cap.data, 4)); T_free_cap(&cap); T_free_line(l1); T_free_line(l2); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_prjoin_default_basic); RUN_TEST(test_prjoin_default_blank_first); RUN_TEST(test_prjoin_outlist_custom_order_and_separator); RUN_TEST(test_prjoin_outlist_missing_and_empty_filler); RUN_TEST(test_prjoin_autoformat_limits); RUN_TEST(test_prjoin_zero_terminated_eol); return UNITY_END(); }