#include "../../unity/unity.h" #include #include #include #include #include #include #include /* Access to program internals (these are in the same translation unit). */ /* Globals from the program that we adjust in tests. */ extern struct field_range_pair *frp; /* declared in set-fields.h, included in the program */ /* Helper: capture output while calling process_line. */ typedef struct { char *out; /* captured stdout string (malloc'd) */ int retval; /* return value from process_line */ int err; /* 0 on success, else nonzero */ } CapResult; static CapResult capture_process_line_call(const char *input, bool newline) { CapResult r = {0}; /* Create a modifiable copy of input because process_line mutates it. */ char *mutable_line = NULL; size_t inlen = strlen(input); mutable_line = (char *)malloc(inlen + 1); if (!mutable_line) { r.err = 1; return r; } memcpy(mutable_line, input, inlen + 1); /* Redirect stdout to a temporary file. */ FILE *tmp = tmpfile(); if (!tmp) { free(mutable_line); r.err = 2; return r; } fflush(stdout); int saved_fd = dup(fileno(stdout)); if (saved_fd < 0) { fclose(tmp); free(mutable_line); r.err = 3; return r; } if (dup2(fileno(tmp), fileno(stdout)) < 0) { close(saved_fd); fclose(tmp); free(mutable_line); r.err = 4; return r; } /* Call the target function while stdout is redirected. */ int rv = process_line(mutable_line, newline); /* Flush and rewind to read captured output. */ fflush(stdout); fflush(tmp); fseek(tmp, 0, SEEK_END); long sz = ftell(tmp); if (sz < 0) sz = 0; fseek(tmp, 0, SEEK_SET); char *buf = (char *)malloc((size_t)sz + 1); if (!buf) { /* Restore stdout before returning. */ dup2(saved_fd, fileno(stdout)); close(saved_fd); fclose(tmp); free(mutable_line); r.err = 5; return r; } size_t rd = fread(buf, 1, (size_t)sz, tmp); buf[rd] = '\0'; /* Restore stdout. */ dup2(saved_fd, fileno(stdout)); close(saved_fd); fclose(tmp); r.out = buf; r.retval = rv; r.err = 0; free(mutable_line); return r; } /* Minimal environment setup helpers (operate on program globals). */ static void set_locale_basics(void) { /* Ensure a simple, predictable locale: C/POSIX */ setlocale(LC_ALL, "C"); /* The following identifiers are static globals in the program file. Since this test is included in the same translation unit, we can set them. */ extern const char *decimal_point; extern int decimal_point_length; extern const char *thousands_sep; extern int thousands_sep_length; extern unsigned char line_delim; /* Force simple decimal and no thousands separator. */ decimal_point = "."; decimal_point_length = 1; thousands_sep = ""; thousands_sep_length = 0; line_delim = '\n'; } static void reset_numfmt_state(void) { /* Reset a subset of globals to defaults that affect processing/printing. */ extern const char *delimiter; extern int auto_padding; extern intmax_t padding_width; extern int zero_padding_width; extern long int user_precision; extern const char *suffix; extern const char *unit_separator; extern int grouping; extern enum inval_type inval_style; delimiter = NULL; auto_padding = 0; padding_width = 0; zero_padding_width = 0; user_precision = -1; suffix = NULL; unit_separator = NULL; grouping = 0; inval_style = inval_abort; /* default; tests that need ignore will set it */ } /* Field range helpers: arrays to control which fields are included. */ static struct field_range_pair FRP_NONE[] = { { UINTMAX_MAX, 0 } /* terminator only: include no fields */ }; static struct field_range_pair FRP_ALL[] = { { 1, UINTMAX_MAX }, { UINTMAX_MAX, 0 } }; static struct field_range_pair FRP_FIRST_ONLY[] = { { 1, 1 }, { UINTMAX_MAX, 0 } }; void setUp(void) { set_locale_basics(); reset_numfmt_state(); } void tearDown(void) { /* nothing to clean up */ } /* Test: default whitespace delimiter, no fields included => output identical to input; newline appended. */ void test_process_line_whitespace_preserved_with_newline(void) { extern const char *delimiter; delimiter = NULL; /* whitespace mode */ frp = FRP_NONE; /* include no fields => no numeric parsing/conversion */ const char *in = " 123 456\t789 "; CapResult r = capture_process_line_call(in, true); TEST_ASSERT_EQUAL_INT_MESSAGE(0, r.err, "capture failed"); /* Expected: identical input plus a newline. */ size_t exp_len = strlen(in) + 1; char *exp = (char *)malloc(exp_len + 1); TEST_ASSERT_NOT_NULL(exp); strcpy(exp, in); exp[exp_len - 1] = '\n'; exp[exp_len] = '\0'; TEST_ASSERT_EQUAL_STRING(exp, r.out); TEST_ASSERT_TRUE(r.retval); free(exp); free(r.out); } /* Test: custom simple delimiter, no fields included => output identical; newline appended. */ void test_process_line_custom_delimiter_simple(void) { extern const char *delimiter; delimiter = ","; frp = FRP_NONE; const char *in = "a,b,c"; CapResult r = capture_process_line_call(in, true); TEST_ASSERT_EQUAL_INT_MESSAGE(0, r.err, "capture failed"); char expected[16]; snprintf(expected, sizeof expected, "%s\n", in); TEST_ASSERT_EQUAL_STRING(expected, r.out); TEST_ASSERT_TRUE(r.retval); free(r.out); } /* Test: multibyte delimiter, no newline requested. */ void test_process_line_multibyte_delimiter_no_newline(void) { extern const char *delimiter; delimiter = "\xE2\x98\x83"; /* UTF-8 snowman: ☃ */ frp = FRP_NONE; const char *in = "x\xE2\x98\x83y\xE2\x98\x83z"; /* x☃y☃z */ CapResult r = capture_process_line_call(in, false); TEST_ASSERT_EQUAL_INT_MESSAGE(0, r.err, "capture failed"); TEST_ASSERT_EQUAL_STRING(in, r.out); TEST_ASSERT_TRUE(r.retval); free(r.out); } /* Test: empty fields with delimiter are preserved exactly. */ void test_process_line_empty_fields_preserved_with_delimiter(void) { extern const char *delimiter; delimiter = ","; frp = FRP_NONE; const char *in = "a,,b,,"; CapResult r = capture_process_line_call(in, true); TEST_ASSERT_EQUAL_INT_MESSAGE(0, r.err, "capture failed"); char expected[32]; snprintf(expected, sizeof expected, "%s\n", in); TEST_ASSERT_EQUAL_STRING(expected, r.out); TEST_ASSERT_TRUE(r.retval); free(r.out); } /* Test: invalid number in included field returns false, output remains unchanged. */ void test_process_line_invalid_number_included_field_returns_false(void) { /* Include first field only using FRP_FIRST_ONLY (or simply frp=NULL behavior), ensure invalid input doesn't abort and returns false. */ extern enum inval_type inval_style; inval_style = inval_ignore; /* avoid exiting on invalid input */ frp = FRP_FIRST_ONLY; /* include first field */ extern const char *delimiter; delimiter = NULL; /* whitespace */ const char *in = "abc def"; /* 'abc' is not a number */ CapResult r = capture_process_line_call(in, true); TEST_ASSERT_EQUAL_INT_MESSAGE(0, r.err, "capture failed"); char expected[32]; snprintf(expected, sizeof expected, "%s\n", in); TEST_ASSERT_EQUAL_STRING(expected, r.out); TEST_ASSERT_FALSE(r.retval); free(r.out); } /* Test: include all fields with valid numbers, should return true and print numbers. */ void test_process_line_convert_all_fields_valid(void) { /* With default scale_to=none and no formatting overrides, integer numbers should print unchanged. */ frp = FRP_ALL; /* include all fields */ extern const char *delimiter; delimiter = NULL; const char *in = "123 456"; CapResult r = capture_process_line_call(in, true); TEST_ASSERT_EQUAL_INT_MESSAGE(0, r.err, "capture failed"); char expected[32]; snprintf(expected, sizeof expected, "%s\n", in); TEST_ASSERT_EQUAL_STRING(expected, r.out); TEST_ASSERT_TRUE(r.retval); free(r.out); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_process_line_whitespace_preserved_with_newline); RUN_TEST(test_process_line_custom_delimiter_simple); RUN_TEST(test_process_line_multibyte_delimiter_no_newline); RUN_TEST(test_process_line_empty_fields_preserved_with_delimiter); RUN_TEST(test_process_line_invalid_number_included_field_returns_false); RUN_TEST(test_process_line_convert_all_fields_valid); return UNITY_END(); }