#include "../../unity/unity.h" #include #include #include #include #include /* Helper to free any existing outlist entries and reset to defaults. */ static void clear_outlist(void) { /* outlist_head and outlist_end are defined in the program under test. */ while (outlist_head.next) { struct outlist *tmp = outlist_head.next; outlist_head.next = tmp->next; free(tmp); } outlist_end = &outlist_head; } /* Reset relevant globals to safe defaults before each test. */ static void reset_join_state(void) { /* Output formatting */ output_separator = " "; output_seplen = 1; empty_filler = NULL; /* Field separators: default to blanks (tab.len == 0) */ tab.ch = 0; tab.len = 0; /* Behavior flags */ ignore_case = false; hard_LC_COLLATE = false; autoformat = false; join_header_lines = false; print_pairables = true; print_unpairables_1 = false; print_unpairables_2 = false; seen_unpairable = false; issued_disorder_warning[0] = false; issued_disorder_warning[1] = false; /* Default input-order checking behavior */ check_input_order = CHECK_ORDER_DEFAULT; /* Join fields: use the first field (index 0) in both files */ join_field_1 = 0; join_field_2 = 0; /* Line tracking and buffers */ prevline[0] = NULL; prevline[1] = NULL; line_no[0] = 0; line_no[1] = 0; spareline[0] = NULL; spareline[1] = NULL; /* EOL */ eolchar = '\n'; /* Clear any outlist configuration */ clear_outlist(); /* Optional: set input names (only used in diagnostics) */ g_names[0] = (char *)"f1"; g_names[1] = (char *)"f2"; } /* Create a temporary stream loaded with 'data'. */ static FILE* make_stream_from_str(const char* data) { FILE* f = tmpfile(); if (!f) return NULL; size_t len = strlen(data); if (len > 0) { if (fwrite(data, 1, len, f) != len) { fclose(f); return NULL; } } rewind(f); return f; } /* Capture stdout while calling join(fp1, fp2). Returns malloc'd buffer with output or NULL on error. */ static char* capture_join_output(FILE* fp1, FILE* fp2) { fflush(stdout); int saved_stdout = dup(STDOUT_FILENO); if (saved_stdout < 0) { return NULL; } FILE* cap = tmpfile(); if (!cap) { close(saved_stdout); return NULL; } int cap_fd = fileno(cap); if (cap_fd < 0) { fclose(cap); close(saved_stdout); return NULL; } if (dup2(cap_fd, STDOUT_FILENO) < 0) { fclose(cap); close(saved_stdout); return NULL; } /* Call the function under test. Do not use Unity asserts while stdout is redirected. */ join(fp1, fp2); /* Flush and collect the output. */ fflush(stdout); fflush(cap); if (fseek(cap, 0, SEEK_END) != 0) { /* Restore stdout before returning */ dup2(saved_stdout, STDOUT_FILENO); close(saved_stdout); fclose(cap); return NULL; } long sz = ftell(cap); if (sz < 0) { dup2(saved_stdout, STDOUT_FILENO); close(saved_stdout); fclose(cap); return NULL; } rewind(cap); char* buf = (char*)malloc((size_t)sz + 1); if (!buf) { dup2(saved_stdout, STDOUT_FILENO); close(saved_stdout); fclose(cap); return NULL; } size_t rd = fread(buf, 1, (size_t)sz, cap); buf[rd] = '\0'; /* Restore stdout. */ dup2(saved_stdout, STDOUT_FILENO); close(saved_stdout); fclose(cap); return buf; } void setUp(void) { reset_join_state(); } void tearDown(void) { /* Nothing special; outlist cleared in setUp */ } /* Test 1: Basic pairable join, default whitespace-separated fields. */ void test_join_basic_pairables(void) { const char* s1 = "a 1\nb 2\n"; const char* s2 = "a x\nb y\n"; FILE* f1 = make_stream_from_str(s1); FILE* f2 = make_stream_from_str(s2); TEST_ASSERT_NOT_NULL(f1); TEST_ASSERT_NOT_NULL(f2); print_pairables = true; join_field_1 = 0; join_field_2 = 0; char* out = capture_join_output(f1, f2); fclose(f1); fclose(f2); TEST_ASSERT_NOT_NULL(out); TEST_ASSERT_EQUAL_STRING("a 1 x\nb 2 y\n", out); free(out); } /* Test 2: Print unpairable lines from file1. */ void test_join_unpairables_file1(void) { const char* s1 = "a 1\nb 2\nc 3\n"; const char* s2 = "a x\nb y\n"; FILE* f1 = make_stream_from_str(s1); FILE* f2 = make_stream_from_str(s2); TEST_ASSERT_NOT_NULL(f1); TEST_ASSERT_NOT_NULL(f2); print_pairables = true; print_unpairables_1 = true; join_field_1 = 0; join_field_2 = 0; char* out = capture_join_output(f1, f2); fclose(f1); fclose(f2); TEST_ASSERT_NOT_NULL(out); TEST_ASSERT_EQUAL_STRING("a 1 x\nb 2 y\nc 3\n", out); free(out); } /* Test 3: Multiple matches cross-product */ void test_join_multiple_matches_cross_product(void) { const char* s1 = "a 1\na 2\n"; const char* s2 = "a x\na y\n"; FILE* f1 = make_stream_from_str(s1); FILE* f2 = make_stream_from_str(s2); TEST_ASSERT_NOT_NULL(f1); TEST_ASSERT_NOT_NULL(f2); print_pairables = true; char* out = capture_join_output(f1, f2); fclose(f1); fclose(f2); TEST_ASSERT_NOT_NULL(out); /* Expected order: (1,x), (1,y), (2,x), (2,y) */ TEST_ASSERT_EQUAL_STRING("a 1 x\na 1 y\na 2 x\na 2 y\n", out); free(out); } /* Test 4: Header-line handling with autoformat */ void test_join_header_and_autoformat(void) { const char* s1 = "ID A\n1 AA\n2 BB\n"; const char* s2 = "ID B\n1 XX\n3 YY\n"; FILE* f1 = make_stream_from_str(s1); FILE* f2 = make_stream_from_str(s2); TEST_ASSERT_NOT_NULL(f1); TEST_ASSERT_NOT_NULL(f2); join_header_lines = true; autoformat = true; print_pairables = true; char* out = capture_join_output(f1, f2); fclose(f1); fclose(f2); TEST_ASSERT_NOT_NULL(out); /* Header line, then only the pairable 1 */ TEST_ASSERT_EQUAL_STRING("ID A B\n1 AA XX\n", out); free(out); } /* Test 5: Case-insensitive join */ void test_join_ignore_case(void) { const char* s1 = "A 1\n"; const char* s2 = "a x\n"; FILE* f1 = make_stream_from_str(s1); FILE* f2 = make_stream_from_str(s2); TEST_ASSERT_NOT_NULL(f1); TEST_ASSERT_NOT_NULL(f2); ignore_case = true; print_pairables = true; char* out = capture_join_output(f1, f2); fclose(f1); fclose(f2); TEST_ASSERT_NOT_NULL(out); TEST_ASSERT_EQUAL_STRING("A 1 x\n", out); free(out); } /* Test 6: Empty field with filler using tab as the field separator */ void test_join_empty_field_with_filler_and_tab(void) { const char* s1 = "a\t\t1\n"; /* fields: "a", "", "1" */ const char* s2 = "a\tx\n"; /* fields: "a", "x" */ FILE* f1 = make_stream_from_str(s1); FILE* f2 = make_stream_from_str(s2); TEST_ASSERT_NOT_NULL(f1); TEST_ASSERT_NOT_NULL(f2); /* Configure tab as the input separator, ensure output sep is space. */ tab.ch = '\t'; tab.len = 1; empty_filler = "<>"; print_pairables = true; char* out = capture_join_output(f1, f2); fclose(f1); fclose(f2); TEST_ASSERT_NOT_NULL(out); /* Expect filler for the empty field */ TEST_ASSERT_EQUAL_STRING("a <> 1 x\n", out); free(out); } /* Test 7: Custom outlist: select specific fields */ void test_join_custom_outlist_selection(void) { const char* s1 = "a 1 2\n"; const char* s2 = "a 3 4\n"; FILE* f1 = make_stream_from_str(s1); FILE* f2 = make_stream_from_str(s2); TEST_ASSERT_NOT_NULL(f1); TEST_ASSERT_NOT_NULL(f2); print_pairables = true; /* Configure outlist: file2 field index 2 ("4"), then join field (0), then file1 field index 1 ("2") */ add_field(2, 2); add_field(0, 0); add_field(1, 1); char* out = capture_join_output(f1, f2); fclose(f1); fclose(f2); TEST_ASSERT_NOT_NULL(out); TEST_ASSERT_EQUAL_STRING("4 a 2\n", out); free(out); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_join_basic_pairables); RUN_TEST(test_join_unpairables_file1); RUN_TEST(test_join_multiple_matches_cross_product); RUN_TEST(test_join_header_and_autoformat); RUN_TEST(test_join_ignore_case); RUN_TEST(test_join_empty_field_with_filler_and_tab); RUN_TEST(test_join_custom_outlist_selection); return UNITY_END(); }