#include "../../unity/unity.h" #include #include #include #include #include #include #include #include #include #include #include #include #include /* The tests are included into the same translation unit as ln.c, so we can access atomic_link and the static flags: - symbolic_link - relative - beware_hard_dir_link - logical */ /* ---------- Test helpers ---------- */ static char *test_dir = NULL; static int test_dfd = -1; /* Join directory and base into a newly-allocated path string. */ static char *test_join(const char *dir, const char *base) { size_t len_dir = strlen(dir); size_t len_base = strlen(base); int need_slash = (len_dir > 0 && dir[len_dir - 1] != '/'); size_t total = len_dir + need_slash + len_base + 1; char *res = (char *)malloc(total); if (!res) return NULL; memcpy(res, dir, len_dir); if (need_slash) res[len_dir++] = '/'; memcpy(res + len_dir, base, len_base); res[len_dir + len_base] = '\0'; return res; } /* Create a temporary directory for each test. */ static char *make_temp_dir(void) { char tmpl[] = "ln_atomic_link_test.XXXXXX"; char *buf = (char *)malloc(sizeof(tmpl)); if (!buf) return NULL; memcpy(buf, tmpl, sizeof(tmpl)); char *d = mkdtemp(buf); if (!d) { free(buf); return NULL; } return buf; /* buf now contains the directory path */ } /* Recursively remove a path. */ static int rm_rf(const char *path) { struct stat st; if (lstat(path, &st) != 0) return (errno == ENOENT) ? 0 : -1; if (S_ISDIR(st.st_mode)) { DIR *dir = opendir(path); if (!dir) return -1; struct dirent *de; while ((de = readdir(dir)) != NULL) { if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue; char *child = test_join(path, de->d_name); if (!child) { closedir(dir); return -1; } if (rm_rf(child) != 0) { free(child); closedir(dir); return -1; } free(child); } closedir(dir); return rmdir(path); } else { return unlink(path); } } /* Create a file with optional content. */ static int create_file(const char *path, const char *content) { int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (fd < 0) return -1; if (content) { size_t len = strlen(content); ssize_t wr = write(fd, content, len); if (wr < 0 || (size_t)wr != len) { int e = errno; close(fd); errno = e; return -1; } } return close(fd); } /* Assert that a path does not exist. */ static void assert_path_not_exists(const char *path) { struct stat st; int r = lstat(path, &st); TEST_ASSERT_TRUE_MESSAGE(r != 0, "lstat unexpectedly succeeded"); TEST_ASSERT_EQUAL_INT(ENOENT, errno); } /* ---------- Unity hooks ---------- */ void setUp(void) { test_dir = make_temp_dir(); TEST_ASSERT_NOT_NULL_MESSAGE(test_dir, "Failed to create temp directory"); test_dfd = open(test_dir, O_RDONLY | O_DIRECTORY); TEST_ASSERT_TRUE_MESSAGE(test_dfd >= 0, "Failed to open temp directory"); /* Ensure flags are in a known state before each test. */ symbolic_link = false; relative = false; beware_hard_dir_link = false; logical = false; } void tearDown(void) { if (test_dfd >= 0) close(test_dfd); test_dfd = -1; if (test_dir) { rm_rf(test_dir); free(test_dir); test_dir = NULL; } } /* ---------- Tests ---------- */ /* Hard link success: should return 0 and create a hard link with same inode/dev. */ static void test_atomic_link_hard_success(void) { char *src = test_join(test_dir, "src1"); char *dst_full = test_join(test_dir, "dst1"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_NOT_NULL(dst_full); TEST_ASSERT_EQUAL_INT(0, create_file(src, "hello")); symbolic_link = false; beware_hard_dir_link = false; logical = false; int ret = atomic_link(src, test_dfd, "dst1"); TEST_ASSERT_EQUAL_INT(0, ret); struct stat s1, s2; TEST_ASSERT_EQUAL_INT(0, stat(src, &s1)); TEST_ASSERT_EQUAL_INT(0, stat(dst_full, &s2)); TEST_ASSERT_EQUAL_INT(s1.st_dev, s2.st_dev); TEST_ASSERT_EQUAL_INT(s1.st_ino, s2.st_ino); free(src); free(dst_full); } /* Hard link failure when destination exists: expect positive errno EEXIST and no change. */ static void test_atomic_link_hard_eexist(void) { char *src = test_join(test_dir, "src2"); char *dst_full = test_join(test_dir, "dst2"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_NOT_NULL(dst_full); TEST_ASSERT_EQUAL_INT(0, create_file(src, "hello")); TEST_ASSERT_EQUAL_INT(0, create_file(dst_full, "preexisting")); struct stat before; TEST_ASSERT_EQUAL_INT(0, lstat(dst_full, &before)); symbolic_link = false; beware_hard_dir_link = false; logical = false; int ret = atomic_link(src, test_dfd, "dst2"); TEST_ASSERT_EQUAL_INT(EEXIST, ret); struct stat after; TEST_ASSERT_EQUAL_INT(0, lstat(dst_full, &after)); TEST_ASSERT_EQUAL_INT(before.st_dev, after.st_dev); TEST_ASSERT_EQUAL_INT(before.st_ino, after.st_ino); free(src); free(dst_full); } /* Hard link when beware_hard_dir_link is true: should return -1 and not create destination. */ static void test_atomic_link_beware_returns_minus1(void) { char *src = test_join(test_dir, "src3"); char *dst_full = test_join(test_dir, "dst3"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_NOT_NULL(dst_full); TEST_ASSERT_EQUAL_INT(0, create_file(src, "hello")); symbolic_link = false; beware_hard_dir_link = true; /* triggers -1 path */ logical = false; int ret = atomic_link(src, test_dfd, "dst3"); TEST_ASSERT_EQUAL_INT(-1, ret); assert_path_not_exists(dst_full); free(src); free(dst_full); } /* Symbolic link with relative=true: should return -1 and not create destination. */ static void test_atomic_link_symbolic_relative_returns_minus1(void) { char *src = test_join(test_dir, "src4"); char *dst_full = test_join(test_dir, "symrel"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_NOT_NULL(dst_full); /* Source need not exist for symlinkat, but create it anyway. */ TEST_ASSERT_EQUAL_INT(0, create_file(src, "hello")); symbolic_link = true; relative = true; /* triggers -1 for atomic_link */ int ret = atomic_link(src, test_dfd, "symrel"); TEST_ASSERT_EQUAL_INT(-1, ret); assert_path_not_exists(dst_full); free(src); free(dst_full); } /* Symbolic link success: should return 0 and create a symlink pointing to the given source string. */ static void test_atomic_link_symbolic_success(void) { char *src = test_join(test_dir, "src5"); char *dst_full = test_join(test_dir, "sym1"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_NOT_NULL(dst_full); TEST_ASSERT_EQUAL_INT(0, create_file(src, "world")); symbolic_link = true; relative = false; int ret = atomic_link(src, test_dfd, "sym1"); TEST_ASSERT_EQUAL_INT(0, ret); struct stat lst; TEST_ASSERT_EQUAL_INT(0, lstat(dst_full, &lst)); TEST_ASSERT_TRUE_MESSAGE(S_ISLNK(lst.st_mode), "Destination is not a symlink"); char buf[PATH_MAX]; ssize_t n = readlink(dst_full, buf, sizeof(buf) - 1); TEST_ASSERT_TRUE_MESSAGE(n >= 0, "readlink failed"); buf[n] = '\0'; TEST_ASSERT_EQUAL_STRING(src, buf); free(src); free(dst_full); } /* Symbolic link failure when destination exists: expect EEXIST and no change. */ static void test_atomic_link_symbolic_eexist(void) { char *src = test_join(test_dir, "src6"); char *dst_full = test_join(test_dir, "sym2"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_NOT_NULL(dst_full); TEST_ASSERT_EQUAL_INT(0, create_file(src, "data")); /* Precreate destination as a regular file. */ TEST_ASSERT_EQUAL_INT(0, create_file(dst_full, "preexisting")); struct stat before; TEST_ASSERT_EQUAL_INT(0, lstat(dst_full, &before)); symbolic_link = true; relative = false; int ret = atomic_link(src, test_dfd, "sym2"); TEST_ASSERT_EQUAL_INT(EEXIST, ret); struct stat after; TEST_ASSERT_EQUAL_INT(0, lstat(dst_full, &after)); TEST_ASSERT_EQUAL_INT(before.st_dev, after.st_dev); TEST_ASSERT_EQUAL_INT(before.st_ino, after.st_ino); free(src); free(dst_full); } /* ---------- Unity main ---------- */ int main(void) { UNITY_BEGIN(); RUN_TEST(test_atomic_link_hard_success); RUN_TEST(test_atomic_link_hard_eexist); RUN_TEST(test_atomic_link_beware_returns_minus1); RUN_TEST(test_atomic_link_symbolic_relative_returns_minus1); RUN_TEST(test_atomic_link_symbolic_success); RUN_TEST(test_atomic_link_symbolic_eexist); return UNITY_END(); }