#include "../../unity/unity.h" #include #include #include #include #include #include #include #include /* The following globals and functions come from du.c and its includes: - static bool process_file (FTS *fts, FTSENT *ent); - struct di_set *di_files; (static in du.c, but we can assign to it) - di_set_alloc() - many option globals like opt_all, apparent_size, etc. - xfts_open(), fts_read(), fts_close(), FTS_xxx flags - duinfo_init(&tot_dui), tot_dui, prev_level, max_depth, etc. */ /* Helpers for filesystem setup/teardown and stdout capture */ static char g_tmpdir[PATH_MAX]; static void reset_du_state_defaults(void) { /* Ensure predictable printing and accounting. */ apparent_size = true; /* Use st_size in bytes */ opt_count_all = false; hash_all = false; opt_all = false; opt_nul_terminate_output = false; print_grand_total = false; opt_separate_dirs = false; max_depth = IDX_MAX; opt_threshold = 0; human_output_opts = 0; opt_inodes = false; opt_time = false; time_type = time_mtime; time_style = NULL; time_format = NULL; output_block_size = 1; /* bytes */ exclude = NULL; /* Reset running totals and traversal level. */ duinfo_init(&tot_dui); prev_level = 0; /* Fresh hard-link detection set. */ di_files = di_set_alloc(); } static int ensure_dir(const char *path) { if (mkdir(path, 0700) == 0) return 0; if (errno == EEXIST) return 0; return -1; } static int create_file_with_size(const char *path, size_t nbytes) { int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (fd < 0) return -1; if (nbytes > 0) { /* Write deterministic content */ size_t remaining = nbytes; char buf[4096]; memset(buf, 'X', sizeof buf); while (remaining > 0) { size_t chunk = remaining < sizeof buf ? remaining : sizeof buf; ssize_t w = write(fd, buf, chunk); if (w < 0) { close(fd); return -1; } remaining -= (size_t)w; } } if (fsync(fd) != 0) { /* best-effort */ } return close(fd); } static int remove_tree(const char *root) { char *argv[2]; argv[0] = (char *)root; argv[1] = NULL; FTS *fts = xfts_open(argv, FTS_PHYSICAL | FTS_NOCHDIR, NULL); if (!fts) return -1; int ret = 0; while (1) { FTSENT *e = fts_read(fts); if (!e) break; switch (e->fts_info) { case FTS_DP: if (rmdir(e->fts_path) != 0) ret = -1; break; case FTS_F: case FTS_SL: case FTS_SLNONE: case FTS_DEFAULT: case FTS_NS: case FTS_NSOK: case FTS_ERR: if (unlink(e->fts_path) != 0) ret = -1; break; default: break; } } if (fts_close(fts) != 0) ret = -1; return ret; } /* Simple stdout capture using a temporary FILE backed by an anonymous file */ typedef struct { int saved_fd; FILE *tmpf; } capture_ctx_t; static int begin_capture_stdout(capture_ctx_t *ctx) { fflush(stdout); ctx->saved_fd = dup(fileno(stdout)); if (ctx->saved_fd < 0) return -1; ctx->tmpf = tmpfile(); if (!ctx->tmpf) { close(ctx->saved_fd); ctx->saved_fd = -1; return -1; } if (dup2(fileno(ctx->tmpf), fileno(stdout)) < 0) { fclose(ctx->tmpf); close(ctx->saved_fd); ctx->tmpf = NULL; ctx->saved_fd = -1; return -1; } return 0; } static char *end_capture_stdout(capture_ctx_t *ctx, size_t *out_len) { /* Restore stdout first, then read and assert afterwards. */ fflush(stdout); dup2(ctx->saved_fd, fileno(stdout)); close(ctx->saved_fd); ctx->saved_fd = -1; fflush(ctx->tmpf); long len; char *buf = NULL; if (fseek(ctx->tmpf, 0, SEEK_END) == 0) { len = ftell(ctx->tmpf); if (len < 0) len = 0; if (fseek(ctx->tmpf, 0, SEEK_SET) == 0) { buf = (char *)malloc((size_t)len + 1); if (buf) { size_t rd = fread(buf, 1, (size_t)len, ctx->tmpf); buf[rd] = '\0'; if (out_len) *out_len = rd; } } } fclose(ctx->tmpf); ctx->tmpf = NULL; return buf; } /* Unity hooks */ void setUp(void) { /* Create a unique temporary directory for each test */ snprintf(g_tmpdir, sizeof g_tmpdir, "/tmp/du_test_%ld_XXXXXX", (long)getpid()); char *p = mkdtemp(g_tmpdir); TEST_ASSERT_MESSAGE(p != NULL, "mkdtemp failed"); } void tearDown(void) { /* Remove the entire temporary directory tree */ if (g_tmpdir[0]) { remove_tree(g_tmpdir); /* The root directory is removed by remove_tree via FTS_DP at the end */ g_tmpdir[0] = '\0'; } } /* Tests */ static FTS *open_fts_single(const char *path) { char *files[2]; files[0] = (char *)path; /* cast acceptable: xfts_open does not modify string literal */ files[1] = NULL; return xfts_open(files, FTS_PHYSICAL | FTS_NOCHDIR, NULL); } void test_process_file_regular_file_basic(void) { reset_du_state_defaults(); char path[PATH_MAX]; snprintf(path, sizeof path, "%s/file_basic.bin", g_tmpdir); TEST_ASSERT_EQUAL_INT(0, create_file_with_size(path, 7)); FTS *fts = open_fts_single(path); TEST_ASSERT_NOT_NULL(fts); FTSENT *ent = fts_read(fts); TEST_ASSERT_NOT_NULL(ent); capture_ctx_t cap; memset(&cap, 0, sizeof cap); TEST_ASSERT_EQUAL_INT(0, begin_capture_stdout(&cap)); /* Do not use TEST_ASSERT while stdout is redirected */ bool ok = process_file(fts, ent); size_t out_len = 0; char *out = end_capture_stdout(&cap, &out_len); TEST_ASSERT_TRUE(ok); TEST_ASSERT_NOT_NULL(out); /* Expected exact output: "7\t\n" */ char expected[PATH_MAX + 64]; snprintf(expected, sizeof expected, "7\t%s\n", path); TEST_ASSERT_EQUAL_STRING(expected, out); /* Check the grand total increased appropriately */ TEST_ASSERT_EQUAL_UINT64(7u, (unsigned long long)tot_dui.size); free(out); TEST_ASSERT_EQUAL_INT(0, fts_close(fts)); } void test_process_file_threshold_suppresses_output(void) { reset_du_state_defaults(); char path[PATH_MAX]; snprintf(path, sizeof path, "%s/file_thresh.bin", g_tmpdir); TEST_ASSERT_EQUAL_INT(0, create_file_with_size(path, 10)); /* Set threshold above size so nothing is printed */ opt_threshold = 11; /* only print if >= 11 */ FTS *fts = open_fts_single(path); TEST_ASSERT_NOT_NULL(fts); FTSENT *ent = fts_read(fts); TEST_ASSERT_NOT_NULL(ent); capture_ctx_t cap; memset(&cap, 0, sizeof cap); TEST_ASSERT_EQUAL_INT(0, begin_capture_stdout(&cap)); bool ok = process_file(fts, ent); size_t out_len = 0; char *out = end_capture_stdout(&cap, &out_len); TEST_ASSERT_TRUE(ok); TEST_ASSERT_NOT_NULL(out); TEST_ASSERT_EQUAL_UINT(0u, (unsigned int)out_len); /* Totals should still be updated */ TEST_ASSERT_EQUAL_UINT64(10u, (unsigned long long)tot_dui.size); free(out); TEST_ASSERT_EQUAL_INT(0, fts_close(fts)); } void test_process_file_nul_terminated_and_inodes(void) { reset_du_state_defaults(); char path[PATH_MAX]; snprintf(path, sizeof path, "%s/file_inodes.bin", g_tmpdir); TEST_ASSERT_EQUAL_INT(0, create_file_with_size(path, 123)); /* Configure to print inodes and with NUL terminator */ opt_inodes = true; opt_nul_terminate_output = true; FTS *fts = open_fts_single(path); TEST_ASSERT_NOT_NULL(fts); FTSENT *ent = fts_read(fts); TEST_ASSERT_NOT_NULL(ent); capture_ctx_t cap; memset(&cap, 0, sizeof cap); TEST_ASSERT_EQUAL_INT(0, begin_capture_stdout(&cap)); bool ok = process_file(fts, ent); size_t out_len = 0; char *out = end_capture_stdout(&cap, &out_len); TEST_ASSERT_TRUE(ok); TEST_ASSERT_NOT_NULL(out); /* Expect: "1\t\0" (NUL terminator included in captured bytes) */ size_t expected_len = strlen("1\t") + strlen(path) + 1; /* +1 for NUL byte at end of line */ TEST_ASSERT_EQUAL_UINT((unsigned int)expected_len, (unsigned int)out_len); /* Verify contents byte-by-byte since it contains NUL */ /* First part should be "1\t" */ TEST_ASSERT_EQUAL_INT('1', out[0]); TEST_ASSERT_EQUAL_INT('\t', out[1]); /* Next should be the path */ TEST_ASSERT_EQUAL_INT(0, strncmp(out + 2, path, strlen(path))); /* Final byte should be NUL, not newline */ TEST_ASSERT_EQUAL_INT('\0', out[out_len - 1]); free(out); TEST_ASSERT_EQUAL_INT(0, fts_close(fts)); } void test_process_file_hash_all_skips_duplicate(void) { reset_du_state_defaults(); char path[PATH_MAX]; snprintf(path, sizeof path, "%s/file_dup.bin", g_tmpdir); TEST_ASSERT_EQUAL_INT(0, create_file_with_size(path, 3)); /* Enable hashing of all files so duplicates are skipped */ hash_all = true; FTS *fts = open_fts_single(path); TEST_ASSERT_NOT_NULL(fts); FTSENT *ent = fts_read(fts); TEST_ASSERT_NOT_NULL(ent); capture_ctx_t cap; memset(&cap, 0, sizeof cap); TEST_ASSERT_EQUAL_INT(0, begin_capture_stdout(&cap)); /* First call prints once */ bool ok1 = process_file(fts, ent); /* Second call on the same entry should be skipped */ bool ok2 = process_file(fts, ent); size_t out_len = 0; char *out = end_capture_stdout(&cap, &out_len); TEST_ASSERT_TRUE(ok1); TEST_ASSERT_TRUE(ok2); TEST_ASSERT_NOT_NULL(out); /* Expect exactly one line ending with '\n' */ unsigned nl_count = 0; for (size_t i = 0; i < out_len; i++) if (out[i] == '\n') nl_count++; TEST_ASSERT_EQUAL_UINT(1u, nl_count); /* Totals should reflect only a single accounting */ TEST_ASSERT_EQUAL_UINT64(3u, (unsigned long long)tot_dui.size); free(out); TEST_ASSERT_EQUAL_INT(0, fts_close(fts)); } void test_process_file_opt_all_prints_child_file_line(void) { reset_du_state_defaults(); /* Create a directory with a child file */ char dir[PATH_MAX]; snprintf(dir, sizeof dir, "%s/dirA", g_tmpdir); TEST_ASSERT_EQUAL_INT(0, ensure_dir(dir)); char child[PATH_MAX]; snprintf(child, sizeof child, "%s/child.txt", dir); TEST_ASSERT_EQUAL_INT(0, create_file_with_size(child, 42)); opt_all = true; /* ensure files at level > 0 are also printed */ char *files[2]; files[0] = dir; files[1] = NULL; FTS *fts = xfts_open(files, FTS_PHYSICAL | FTS_NOCHDIR, NULL); TEST_ASSERT_NOT_NULL(fts); capture_ctx_t cap; memset(&cap, 0, sizeof cap); TEST_ASSERT_EQUAL_INT(0, begin_capture_stdout(&cap)); /* Traverse and call process_file on each entry */ FTSENT *ent; while ((ent = fts_read(fts)) != NULL) { (void)process_file(fts, ent); } size_t out_len = 0; char *out = end_capture_stdout(&cap, &out_len); TEST_ASSERT_NOT_NULL(out); /* There should be a line for the child file: "42\t\n" */ char expected[PATH_MAX + 64]; snprintf(expected, sizeof expected, "42\t%s\n", child); TEST_ASSERT_NOT_NULL(strstr(out, expected)); free(out); TEST_ASSERT_EQUAL_INT(0, fts_close(fts)); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_process_file_regular_file_basic); RUN_TEST(test_process_file_threshold_suppresses_output); RUN_TEST(test_process_file_nul_terminated_and_inodes); RUN_TEST(test_process_file_hash_all_skips_duplicate); RUN_TEST(test_process_file_opt_all_prints_child_file_line); return UNITY_END(); }