The branch main has been updated by asomers: URL: https://cgit.FreeBSD.org/src/commit/?id=940142d6103746bb1158c408da443d0240b836d9
commit 940142d6103746bb1158c408da443d0240b836d9 Author: Jitendra Bhati <[email protected]> AuthorDate: 2026-05-20 15:40:51 +0000 Commit: Alan Somers <[email protected]> CommitDate: 2026-06-02 19:01:44 +0000 lib/libc/tests/gen: add fts_set() tests Add ATF test cases for fts_set(): fts_set: - invalid instruction returns non-zero with EINVAL - FTS_AGAIN revisits the current node - FTS_AGAIN consecutive visits node three times - FTS_FOLLOW on symlink to file yields FTS_F - FTS_FOLLOW on symlink to directory causes descent - FTS_FOLLOW on dead symlink yields FTS_SLNONE - FTS_SKIP prevents descent into directory - fts_set_clientptr/fts_get_clientptr round-trip - fts_get_stream returns parent FTS* from FTSENT* Sponsored by: Google LLC (GSoC 2026) Reviewed by: asomers MFC after: 2 weeks Pull Request: https://github.com/freebsd/freebsd-src/pull/2242 --- lib/libc/tests/gen/Makefile | 1 + lib/libc/tests/gen/fts_set_test.c | 361 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 362 insertions(+) diff --git a/lib/libc/tests/gen/Makefile b/lib/libc/tests/gen/Makefile index 9341fe8c9074..7213fb4d4431 100644 --- a/lib/libc/tests/gen/Makefile +++ b/lib/libc/tests/gen/Makefile @@ -14,6 +14,7 @@ ATF_TESTS_C+= fts_children_test ATF_TESTS_C+= fts_misc_test ATF_TESTS_C+= fts_open_test ATF_TESTS_C+= fts_options_test +ATF_TESTS_C+= fts_set_test ATF_TESTS_C+= ftw_test ATF_TESTS_C+= getentropy_test ATF_TESTS_C+= getmntinfo_test diff --git a/lib/libc/tests/gen/fts_set_test.c b/lib/libc/tests/gen/fts_set_test.c new file mode 100644 index 000000000000..340af648c472 --- /dev/null +++ b/lib/libc/tests/gen/fts_set_test.c @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2026 Jitendra Bhati + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/* + * Tests for fts_set(), fts_set_clientptr(), fts_get_clientptr(), + * and fts_get_stream(). + */ + +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <fts.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <atf-c.h> + +#include "fts_test.h" + +/* + * fts_set with invalid options must return non-zero with EINVAL. + * Note: fts_set returns 1 (not -1) on error. + */ +ATF_TC(invalid_options); +ATF_TC_HEAD(invalid_options, tc) +{ + atf_tc_set_md_var(tc, "descr", + "fts_set with invalid options returns non-zero with EINVAL"); +} +ATF_TC_BODY(invalid_options, tc) +{ + char *paths[] = { ".", NULL }; + FTS *fts; + FTSENT *ent; + + ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); + + ent = fts_read(fts); + ATF_REQUIRE(ent != NULL); + ATF_REQUIRE_ERRNO(EINVAL, fts_set(fts, ent, 99) != 0); + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); +} + +/* + * FTS_AGAIN causes the current node to be re-stat()ed and returned + * again on the next fts_read() call. + */ +ATF_TC(again); +ATF_TC_HEAD(again, tc) +{ + atf_tc_set_md_var(tc, "descr", + "FTS_AGAIN causes the current node to be returned once more"); +} +ATF_TC_BODY(again, tc) +{ + char *paths[] = { "dir", NULL }; + FTS *fts; + FTSENT *ent; + int revisit_count; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644))); + + ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, + fts_lexical_compar)) != NULL); + + revisit_count = 0; + while ((ent = fts_read(fts)) != NULL) { + if (ent->fts_info == FTS_F && revisit_count == 0) { + ATF_REQUIRE_EQ_MSG(0, + fts_set(fts, ent, FTS_AGAIN), + "fts_set(FTS_AGAIN): %m"); + revisit_count++; + } else if (ent->fts_info == FTS_F && revisit_count >= 1) { + revisit_count++; + } + } + ATF_CHECK_EQ_MSG(0, errno, "traversal ended with errno %d", errno); + ATF_CHECK_EQ_MSG(2, revisit_count, + "expected file visited twice via FTS_AGAIN, saw %d", + revisit_count); + + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); +} + +/* + * FTS_AGAIN set twice in a row causes the node to be visited three + * times total. Each fts_read() clears fts_options, so the caller must + * set FTS_AGAIN again explicitly each time. + */ +ATF_TC(again_consecutive); +ATF_TC_HEAD(again_consecutive, tc) +{ + atf_tc_set_md_var(tc, "descr", + "FTS_AGAIN set twice in a row visits the node three times"); +} +ATF_TC_BODY(again_consecutive, tc) +{ + char *paths[] = { "file", NULL }; + FTS *fts; + FTSENT *ent; + int visit_count; + + ATF_REQUIRE_EQ(0, close(creat("file", 0644))); + + ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); + + visit_count = 0; + while ((ent = fts_read(fts)) != NULL) { + if (ent->fts_info == FTS_F) { + visit_count++; + if (visit_count < 3) + ATF_REQUIRE_EQ(0, + fts_set(fts, ent, FTS_AGAIN)); + } + } + ATF_CHECK_EQ_MSG(3, visit_count, + "expected 3 visits with consecutive FTS_AGAIN, got %d", + visit_count); + + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); +} + +/* + * FTS_FOLLOW on an FTS_SL entry pointing to a regular file yields FTS_F. + */ +ATF_TC(follow_symlink_to_file); +ATF_TC_HEAD(follow_symlink_to_file, tc) +{ + atf_tc_set_md_var(tc, "descr", + "FTS_FOLLOW on FTS_SL to regular file yields FTS_F"); +} +ATF_TC_BODY(follow_symlink_to_file, tc) +{ + char *paths[] = { "dir", NULL }; + FTS *fts; + FTSENT *ent; + int followed; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE_EQ(0, close(creat("dir/target", 0644))); + ATF_REQUIRE_EQ(0, symlink("target", "dir/link")); + + ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, + fts_lexical_compar)) != NULL); + + followed = 0; + while ((ent = fts_read(fts)) != NULL) { + if (ent->fts_info == FTS_SL && + strcmp(ent->fts_name, "link") == 0) + ATF_REQUIRE_EQ(0, fts_set(fts, ent, FTS_FOLLOW)); + else if (ent->fts_info == FTS_F && + strcmp(ent->fts_name, "link") == 0) + followed = 1; + } + ATF_CHECK_MSG(followed != 0, + "FTS_FOLLOW on symlink-to-file must yield FTS_F"); + + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); +} + +/* + * FTS_FOLLOW on an FTS_SL entry pointing to a directory causes descent + * into the target directory. + */ +ATF_TC(follow_symlink_to_dir); +ATF_TC_HEAD(follow_symlink_to_dir, tc) +{ + atf_tc_set_md_var(tc, "descr", + "FTS_FOLLOW on FTS_SL to directory causes descent"); +} +ATF_TC_BODY(follow_symlink_to_dir, tc) +{ + char *paths[] = { "dir", NULL }; + FTS *fts; + FTSENT *ent; + int saw_inside; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE_EQ(0, mkdir("dir/real", 0755)); + ATF_REQUIRE_EQ(0, close(creat("dir/real/inside", 0644))); + ATF_REQUIRE_EQ(0, symlink("real", "dir/link")); + + ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, + fts_lexical_compar)) != NULL); + + saw_inside = 0; + while ((ent = fts_read(fts)) != NULL) { + if (ent->fts_info == FTS_SL && + strcmp(ent->fts_name, "link") == 0) + ATF_REQUIRE_EQ(0, fts_set(fts, ent, FTS_FOLLOW)); + if (ent->fts_info == FTS_F && + strcmp(ent->fts_name, "inside") == 0 && + strcmp(ent->fts_path, "dir/link/inside") == 0) + saw_inside = 1; + } + ATF_CHECK_MSG(saw_inside != 0, + "FTS_FOLLOW on symlink-to-dir should descend and visit 'inside'"); + + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); +} + +/* + * FTS_FOLLOW on a dangling symlink (FTS_SLNONE) yields FTS_SLNONE again. + * FTS_SLNONE requires FTS_LOGICAL — under FTS_PHYSICAL a dangling + * symlink is reported as FTS_SL. + */ +ATF_TC(follow_dead_symlink); +ATF_TC_HEAD(follow_dead_symlink, tc) +{ + atf_tc_set_md_var(tc, "descr", + "FTS_FOLLOW on dead symlink yields FTS_SLNONE"); +} +ATF_TC_BODY(follow_dead_symlink, tc) +{ + char *paths[] = { "dead", NULL }; + FTS *fts; + FTSENT *ent; + + ATF_REQUIRE_EQ(0, symlink("no-such-target", "dead")); + + ATF_REQUIRE((fts = fts_open(paths, FTS_LOGICAL, NULL)) != NULL); + + ent = fts_read(fts); + ATF_REQUIRE(ent != NULL); + ATF_REQUIRE_EQ_MSG(FTS_SLNONE, ent->fts_info, + "expected FTS_SLNONE for dead symlink, got %d", ent->fts_info); + + ATF_REQUIRE_EQ(0, fts_set(fts, ent, FTS_FOLLOW)); + ent = fts_read(fts); + ATF_REQUIRE(ent != NULL); + ATF_CHECK_EQ_MSG(FTS_SLNONE, ent->fts_info, + "FTS_FOLLOW on dead symlink should still be FTS_SLNONE, got %d", + ent->fts_info); + + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); +} + +/* + * FTS_SKIP on an FTS_D node prevents descent into that directory. + * The next fts_read() converts the node to FTS_DP without visiting + * any children. + */ +ATF_TC(skip); +ATF_TC_HEAD(skip, tc) +{ + atf_tc_set_md_var(tc, "descr", + "FTS_SKIP prevents descent into a directory"); +} +ATF_TC_BODY(skip, tc) +{ + char *paths[] = { "dir", NULL }; + FTS *fts; + FTSENT *ent; + int saw_inside; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE_EQ(0, mkdir("dir/skip_me", 0755)); + ATF_REQUIRE_EQ(0, close(creat("dir/skip_me/inside", 0644))); + ATF_REQUIRE_EQ(0, close(creat("dir/sibling", 0644))); + + ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, + fts_lexical_compar)) != NULL); + + saw_inside = 0; + while ((ent = fts_read(fts)) != NULL) { + if (ent->fts_info == FTS_D && + strcmp(ent->fts_name, "skip_me") == 0) + ATF_REQUIRE_EQ(0, fts_set(fts, ent, FTS_SKIP)); + if (strcmp(ent->fts_name, "inside") == 0) + saw_inside = 1; + } + ATF_CHECK_MSG(saw_inside == 0, + "FTS_SKIP: 'inside' must not have been visited"); + + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); +} + +/* + * fts_set_clientptr() and fts_get_clientptr() store and retrieve an + * arbitrary pointer on the FTS stream. + */ +ATF_TC(clientptr_roundtrip); +ATF_TC_HEAD(clientptr_roundtrip, tc) +{ + atf_tc_set_md_var(tc, "descr", + "fts_set_clientptr / fts_get_clientptr round-trip"); +} +ATF_TC_BODY(clientptr_roundtrip, tc) +{ + char *paths[] = { ".", NULL }; + FTS *fts; + int value = 42; + + ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); + + ATF_CHECK_EQ(NULL, fts_get_clientptr(fts)); + + fts_set_clientptr(fts, &value); + ATF_CHECK_EQ_MSG(&value, fts_get_clientptr(fts), + "fts_get_clientptr did not return the stored pointer"); + + fts_set_clientptr(fts, NULL); + ATF_CHECK_EQ(NULL, fts_get_clientptr(fts)); + + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); +} + +/* + * fts_get_stream() returns the parent FTS* from any FTSENT* returned + * by fts_read(). + */ +ATF_TC(get_stream_backpointer); +ATF_TC_HEAD(get_stream_backpointer, tc) +{ + atf_tc_set_md_var(tc, "descr", + "fts_get_stream returns the parent FTS* from an FTSENT*"); +} +ATF_TC_BODY(get_stream_backpointer, tc) +{ + char *paths[] = { "dir", NULL }; + FTS *fts; + FTSENT *ent; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644))); + + ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL); + + while ((ent = fts_read(fts)) != NULL) { + ATF_CHECK_EQ_MSG(fts, fts_get_stream(ent), + "fts_get_stream(ent) must return the parent FTS*, " + "entry: %s info: %d", + ent->fts_name, ent->fts_info); + } + + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); +} + +ATF_TP_ADD_TCS(tp) +{ + fts_check_debug(); + ATF_TP_ADD_TC(tp, invalid_options); + ATF_TP_ADD_TC(tp, again); + ATF_TP_ADD_TC(tp, again_consecutive); + ATF_TP_ADD_TC(tp, follow_symlink_to_file); + ATF_TP_ADD_TC(tp, follow_symlink_to_dir); + ATF_TP_ADD_TC(tp, follow_dead_symlink); + ATF_TP_ADD_TC(tp, skip); + ATF_TP_ADD_TC(tp, clientptr_roundtrip); + ATF_TP_ADD_TC(tp, get_stream_backpointer); + + return (atf_no_error()); +}
