#include "../../unity/unity.h" #include #include #include #include #include #include /* Helper: run expand() with given input string. If initial_only is true, simulate -i by setting convert_entire_line = false. Returns a newly malloc'd string with the captured output, or NULL on error. */ static char* run_expand_on_input(const char* input, bool initial_only) { int saved_stdin = -1; int saved_stdout = -1; FILE* in_file = NULL; FILE* out_file = NULL; int in_fd, out_fd; char* out_buf = NULL; /* Create temp files for input and output */ in_file = tmpfile(); if (!in_file) goto fail; out_file = tmpfile(); if (!out_file) goto fail; /* Write input to in_file and rewind */ if (input) { if (fwrite(input, 1, strlen(input), in_file) != strlen(input)) goto fail; } if (fflush(in_file) != 0) goto fail; if (fseek(in_file, 0, SEEK_SET) != 0) goto fail; in_fd = fileno(in_file); out_fd = fileno(out_file); if (in_fd < 0 || out_fd < 0) goto fail; /* Save original fds */ saved_stdin = dup(STDIN_FILENO); saved_stdout = dup(STDOUT_FILENO); if (saved_stdin < 0 || saved_stdout < 0) goto fail; /* Redirect stdin/stdout */ if (dup2(in_fd, STDIN_FILENO) < 0) goto fail; if (dup2(out_fd, STDOUT_FILENO) < 0) goto fail; /* Configure initial-only behavior via global (declared in expand-common.h) */ extern bool convert_entire_line; convert_entire_line = !initial_only; /* Call the function under test */ expand(); /* Ensure all output is flushed to out_file before restoring stdout */ fflush(stdout); /* Restore stdout and stdin before any assertions/logging */ if (dup2(saved_stdout, STDOUT_FILENO) < 0) goto fail; if (dup2(saved_stdin, STDIN_FILENO) < 0) goto fail; /* No longer need the saved fds */ close(saved_stdout); saved_stdout = -1; close(saved_stdin); saved_stdin = -1; /* Read captured output */ if (fflush(out_file) != 0) goto fail; if (fseek(out_file, 0, SEEK_END) != 0) goto fail; long sz = ftell(out_file); if (sz < 0) goto fail; if (fseek(out_file, 0, SEEK_SET) != 0) goto fail; out_buf = (char*)malloc((size_t)sz + 1); if (!out_buf) goto fail; if (sz > 0) { size_t rd = fread(out_buf, 1, (size_t)sz, out_file); if (rd != (size_t)sz) goto fail; } out_buf[sz] = '\0'; /* Cleanup temp files */ fclose(in_file); fclose(out_file); return out_buf; fail: { int e = errno; /* preserve */ if (saved_stdout >= 0) { dup2(saved_stdout, STDOUT_FILENO); close(saved_stdout); } if (saved_stdin >= 0) { dup2(saved_stdin, STDIN_FILENO); close(saved_stdin); } if (in_file) fclose(in_file); if (out_file) fclose(out_file); errno = e; } return NULL; } void setUp(void) { /* Default to full-line conversion before each test */ extern bool convert_entire_line; convert_entire_line = true; } void tearDown(void) { /* Nothing to clean up */ } /* Test: empty input -> empty output */ void test_expand_empty_input(void) { char* out = run_expand_on_input("", false); TEST_ASSERT_NOT_NULL_MESSAGE(out, "run_expand_on_input failed"); TEST_ASSERT_EQUAL_STRING("", out); free(out); } /* Test: basic single tab expansion with default tab size 8: "ab\n" */ void test_expand_basic_single_tab(void) { const char* in = "a\tb\n"; const char* expected = "a b\n"; /* 7 spaces between a and b */ char* out = run_expand_on_input(in, false); TEST_ASSERT_NOT_NULL_MESSAGE(out, "run_expand_on_input failed"); TEST_ASSERT_EQUAL_STRING(expected, out); free(out); } /* Test: two leading tabs expand to 16 spaces before newline */ void test_expand_two_leading_tabs(void) { const char* in = "\t\t\n"; const char* expected = " \n"; /* 16 spaces then newline */ char* out = run_expand_on_input(in, false); TEST_ASSERT_NOT_NULL_MESSAGE(out, "run_expand_on_input failed"); TEST_ASSERT_EQUAL_STRING(expected, out); free(out); } /* Test: backspace affects column but is preserved in output */ void test_expand_backspace_behavior(void) { /* After "abc", backspace moves back to col 2; tab then expands to col 8: 6 spaces inserted, and backspace is preserved in output. */ const char* in = "abc\b\tZ\n"; const char* expected = "abc\b Z\n"; /* backspace + 6 spaces */ char* out = run_expand_on_input(in, false); TEST_ASSERT_NOT_NULL_MESSAGE(out, "run_expand_on_input failed"); TEST_ASSERT_EQUAL_STRING(expected, out); free(out); } /* Test: initial-only mode (-i) on a single line */ void test_expand_initial_only_simple(void) { /* Leading space + tab should expand to 8 spaces; after 'X' a following tab must remain */ const char* in = " \tX\tY\n"; const char* expected = " X\tY\n"; /* 8 spaces, then X, then literal tab, then Y, nl */ char* out = run_expand_on_input(in, true /* initial_only */); TEST_ASSERT_NOT_NULL_MESSAGE(out, "run_expand_on_input failed"); TEST_ASSERT_EQUAL_STRING(expected, out); free(out); } /* Test: initial-only mode resets at each new line */ void test_expand_initial_only_resets_each_line(void) { /* First line: leading tab converts; second line: leading tab converts again */ const char* in = "\tA\n\tB\n"; const char* expected = " A\n B\n"; char* out = run_expand_on_input(in, true /* initial_only */); TEST_ASSERT_NOT_NULL_MESSAGE(out, "run_expand_on_input failed"); TEST_ASSERT_EQUAL_STRING(expected, out); free(out); } /* Test: no trailing newline in input */ void test_expand_no_trailing_newline(void) { const char* in = "A\tB"; const char* expected = "A B"; /* 7 spaces between A and B */ char* out = run_expand_on_input(in, false); TEST_ASSERT_NOT_NULL_MESSAGE(out, "run_expand_on_input failed"); TEST_ASSERT_EQUAL_STRING(expected, out); free(out); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_expand_empty_input); RUN_TEST(test_expand_basic_single_tab); RUN_TEST(test_expand_two_leading_tabs); RUN_TEST(test_expand_backspace_behavior); RUN_TEST(test_expand_initial_only_simple); RUN_TEST(test_expand_initial_only_resets_each_line); RUN_TEST(test_expand_no_trailing_newline); return UNITY_END(); }