[libc][realpath][test] Create test files/directories (#209369)

Follow-up PRs will add path validation etc to `realpath`, which will
require paths to exist in the filesystem.

This PR bumps the memory limit in `HermeticTestUtils.cpp`. The realpath
unit tests allocate quite a few strings, and since memory in hermetic
tests is never freed, the unit test quickly reaches the limit.
This commit is contained in:
Jackson Stogel 2026-07-17 12:12:15 -07:00 committed by GitHub
parent 93bce300db
commit 3e55f3c56e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 271 additions and 14 deletions

View file

@ -45,7 +45,7 @@ namespace {
// requires. Hence, as a work around for this problem, we use a simple allocator
// which just hands out continuous blocks from a statically allocated chunk of
// memory.
static constexpr uint64_t MEMORY_SIZE = 65336;
static constexpr uint64_t MEMORY_SIZE = 1 << 20; // 1 MiB
alignas(ALIGNMENT) static uint8_t memory[MEMORY_SIZE];
static uint8_t *ptr = memory;

View file

@ -385,13 +385,27 @@ add_libc_test(
realpath_test.cpp
DEPENDS
libc.hdr.errno_macros
libc.hdr.func.free
libc.hdr.limits_macros
libc.hdr.types.size_t
libc.src.__support.CPP.string
libc.src.__support.CPP.string_view
libc.src.__support.CPP.utility
libc.src.__support.fixedvector
libc.src.__support.libc_assert
libc.src.__support.libc_errno
libc.src.__support.macros.config
libc.src.__support.OSUtil.path
libc.src.fcntl.openat
libc.src.stdlib.realpath
libc.src.string.strdup
libc.src.sys.random.getrandom
libc.src.sys.stat.mkdirat
libc.src.unistd.close
libc.src.unistd.getpid
libc.src.unistd.unlinkat
libc.test.UnitTest.ErrnoCheckingTest
libc.test.UnitTest.ErrnoSetterMatcher
)
add_libc_test(

View file

@ -12,23 +12,164 @@
//===----------------------------------------------------------------------===//
#include "hdr/errno_macros.h"
#include "hdr/func/free.h"
#include "hdr/limits_macros.h"
#include "hdr/types/size_t.h"
#include "src/__support/CPP/string.h"
#include "src/__support/CPP/string_view.h"
#include "src/__support/CPP/utility.h"
#include "src/__support/OSUtil/path.h"
#include "src/__support/fixedvector.h"
#include "src/__support/libc_assert.h"
#include "src/__support/libc_errno.h"
#include "src/__support/macros/config.h"
#include "src/fcntl/openat.h"
#include "src/stdlib/realpath.h"
#include "src/string/strdup.h"
#include "src/sys/random/getrandom.h"
#include "src/sys/stat/mkdirat.h"
#include "src/unistd/close.h"
#include "src/unistd/getpid.h"
#include "src/unistd/unlinkat.h"
#include "test/UnitTest/ErrnoCheckingTest.h"
#include "test/UnitTest/ErrnoSetterMatcher.h"
namespace cpp = LIBC_NAMESPACE::cpp;
namespace path = LIBC_NAMESPACE::path;
using LIBC_NAMESPACE::testing::ErrnoCheckingTest;
using LIBC_NAMESPACE::FixedVector;
using LIBC_NAMESPACE::testing::tlog;
using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds;
// This test assumes the following values, so fail early if they mismatch.
static_assert(path::SEPARATOR == '/');
static_assert(path::CURRENT_DIR_COMPONENT == ".");
static_assert(path::PARENT_DIR_COMPONENT == "..");
// Size of a path separator.
constexpr size_t PATH_SEP_SIZE = 1;
// Creates the given directory if it does not exist. Returns zero on success.
[[nodiscard]] int ensure_directory_exists(int dirfd, const char *path,
mode_t mode = 0755) {
if (LIBC_NAMESPACE::mkdirat(dirfd, path, mode) == 0)
return 0;
if (libc_errno == EEXIST)
libc_errno = 0;
if (libc_errno != 0) {
tlog << "Failed to create temp directory: " << path << "\n";
return -1;
}
return 0;
}
// A test directory that removes itself on destruction.
class TestDir {
// The test directory's absolute path.
cpp::string path;
// File descriptor of the test directory.
int fd = -1;
// Files created in this directory tree.
// Use a C string instead of cpp::string because FixedVector
// only supports trivially destructable types.
FixedVector<char *, 64> files;
// Subdirectories created in this directory tree.
FixedVector<char *, 64> dirs;
public:
TestDir() = default;
// Initializes this TestDir container with the given path.
void initialize(cpp::string directory_path, int dirfd) {
LIBC_ASSERT(this->fd == -1);
this->path = directory_path;
this->fd = dirfd;
}
~TestDir() {
if (path.empty())
return;
for (size_t i = 0; i < files.size(); i++) {
LIBC_NAMESPACE::unlinkat(fd, files[i], 0);
::free(files[i]);
}
// Remove directories in reverse order so they'll be empty.
for (size_t i = dirs.size(); i > 0; i--) {
LIBC_NAMESPACE::unlinkat(fd, dirs[i - 1], AT_REMOVEDIR);
::free(dirs[i - 1]);
}
LIBC_NAMESPACE::close(fd);
LIBC_NAMESPACE::unlinkat(AT_FDCWD, path.c_str(), AT_REMOVEDIR);
}
TestDir(TestDir &other) = delete;
TestDir &operator=(TestDir &other) = delete;
TestDir(TestDir &&other) = delete;
TestDir &operator=(TestDir &&other) = delete;
// Returns the absolute path of `relative_path` in this test directory.
cpp::string absolute_path(cpp::string_view relative_path) const {
return path + "/" + relative_path;
}
// Returns this test directory path as a C string.
const char *c_str() const { return path.c_str(); }
// Returns this test directory path as a string view.
const cpp::string_view view() const { return path; }
// Creates a directory relative to TestDir. Returns zero on success.
[[nodiscard]] int mkdir(const char *relative_path, mode_t mode = 0755) {
char *path = LIBC_NAMESPACE::strdup(relative_path);
if (path == nullptr)
return -1;
if (!dirs.push_back(path)) {
tlog << "Not enough space in TestDir::dirs_\n";
return -1;
}
return ensure_directory_exists(fd, relative_path, mode);
}
// Creates an empty file relative to TestDir. Returns zero on success.
[[nodiscard]] int touch(const char *relative_path, mode_t mode = 0644) {
char *path = LIBC_NAMESPACE::strdup(relative_path);
if (path == nullptr)
return -1;
if (!files.push_back(path)) {
tlog << "Not enough space in TestDir::files_\n";
return -1;
}
int newfd =
LIBC_NAMESPACE::openat(fd, relative_path, O_RDONLY | O_CREAT, mode);
if (newfd < 0)
return -1;
return LIBC_NAMESPACE::close(newfd);
}
};
cpp::string unique_id() {
cpp::string id;
id += cpp::to_string(LIBC_NAMESPACE::getpid());
id += ".";
constexpr cpp::string_view alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
uint8_t rand_bytes[16] = {};
(void)LIBC_NAMESPACE::getrandom(rand_bytes, sizeof(rand_bytes), 0);
for (size_t i = 0; i < sizeof(rand_bytes); i++)
id += alphabet[rand_bytes[i] % alphabet.size()];
return id;
}
class LlvmLibcRealpathTest : public LIBC_NAMESPACE::testing::ErrnoCheckingTest {
public:
char *realpath_buffered(const char *path) {
@ -39,6 +180,32 @@ public:
return realpath_buffered(path.c_str());
}
// Creates a test directory in dst. Returns true if successful.
//
// While we would prefer to return cpp::optional<TestDir> here,
// LLVM-libc's optional expects types to be trivially destructible.
[[nodiscard]] bool create_test_dir(const char *name, TestDir &dst) {
// Use /tmp instead of the typical libc_make_test_file_path to ensure
// the path is absolute and does not contain symlinks.
cpp::string test_dir_path = "/tmp/LlvmLibcRealpathTest.";
test_dir_path += name;
test_dir_path += ".";
// Include a unique ID in case multiple builds of this test run at once.
test_dir_path += unique_id();
if (ensure_directory_exists(AT_FDCWD, test_dir_path.c_str()))
return false;
int fd = LIBC_NAMESPACE::openat(AT_FDCWD, test_dir_path.c_str(),
O_RDONLY | O_DIRECTORY);
if (fd < 0)
return false;
dst.initialize(cpp::move(test_dir_path), fd);
return true;
}
private:
char buf_[PATH_MAX];
};
@ -61,13 +228,62 @@ TEST_F(LlvmLibcRealpathTest, OkIfPathArgIsExactlyMaxSize) {
ASSERT_STREQ(realpath_buffered(s), "/");
}
TEST_F(LlvmLibcRealpathTest, OkIfResolvedPathIsExactlyMaxSize) {
// PATH_MAX counts null terminator, so construct a path of size PATH_MAX-1.
cpp::string s(PATH_MAX - 1, 'a');
for (size_t i = 0; i < s.size() - 1; i += 8)
s[i] = '/';
// Creates a test directory that has an absolute path with exactly
// `desired_size` characters.
[[nodiscard]] bool create_absolute_path_with_size(TestDir &test_dir,
size_t desired_size,
cpp::string &out) {
if (desired_size < test_dir.view().size() + PATH_SEP_SIZE) {
tlog << "Test directory is already too long in "
"create_absolute_path_with_size: "
<< test_dir.view().size() << "\n";
return false;
}
size_t remaining_size = desired_size - test_dir.view().size() - PATH_SEP_SIZE;
ASSERT_STREQ(realpath_buffered(s), s.c_str());
cpp::string relative_path;
relative_path.reserve(remaining_size);
while (remaining_size != 0) {
if (!relative_path.empty()) {
relative_path += '/';
remaining_size -= PATH_SEP_SIZE;
}
size_t component_size = NAME_MAX;
if (component_size > remaining_size)
component_size = remaining_size;
// If adding a component of this size would leave us in a state where
// we only have enough space for a separator, shorten the component.
if (remaining_size - component_size == PATH_SEP_SIZE)
component_size -= 1;
for (size_t i = 0; i < component_size; ++i)
relative_path += 'a';
remaining_size -= component_size;
if (test_dir.mkdir(relative_path.c_str()))
return false;
}
out = test_dir.absolute_path(relative_path);
if (out.size() != desired_size) {
tlog << "Failed to create path of size=" << desired_size << "\n";
return false;
}
return true;
}
TEST_F(LlvmLibcRealpathTest, OkIfResolvedPathIsExactlyMaxSize) {
TestDir test_dir;
ASSERT_TRUE(create_test_dir("OkIfResolvedPathIsExactlyMaxSize", test_dir));
cpp::string path;
ASSERT_TRUE(create_absolute_path_with_size(test_dir, PATH_MAX - 1, path));
ASSERT_STREQ(realpath_buffered(path), path.c_str());
}
TEST_F(LlvmLibcRealpathTest, ErrorsWithNameTooLongIfPathArgExceedsMaxSize) {
@ -89,23 +305,50 @@ TEST_F(LlvmLibcRealpathTest, RootDotDotTraversalStaysAtRoot) {
}
TEST_F(LlvmLibcRealpathTest, SimpleAbsolutePath) {
ASSERT_STREQ(realpath_buffered("/a/b/c"), "/a/b/c");
TestDir test_dir;
ASSERT_TRUE(create_test_dir("SimpleAbsolutePath", test_dir));
ASSERT_THAT(test_dir.mkdir("a"), Succeeds());
ASSERT_THAT(test_dir.mkdir("a/b"), Succeeds());
ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("a/b")),
test_dir.absolute_path("a/b").c_str());
}
TEST_F(LlvmLibcRealpathTest, DotDotTraversesParent) {
ASSERT_STREQ(realpath_buffered("/a/b/.."), "/a");
TestDir test_dir;
ASSERT_TRUE(create_test_dir("DotDotTraversesParent", test_dir));
ASSERT_THAT(test_dir.mkdir("a"), Succeeds());
ASSERT_THAT(test_dir.mkdir("a/b"), Succeeds());
ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("a/b/..")),
test_dir.absolute_path("a").c_str());
}
TEST_F(LlvmLibcRealpathTest, DotTraversalIsNop) {
ASSERT_STREQ(realpath_buffered("/a/b/./"), "/a/b");
TestDir test_dir;
ASSERT_TRUE(create_test_dir("DotTraversalIsNop", test_dir));
ASSERT_THAT(test_dir.mkdir("a"), Succeeds());
ASSERT_THAT(test_dir.mkdir("a/b"), Succeeds());
ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("a/b/./")),
test_dir.absolute_path("a/b").c_str());
}
TEST_F(LlvmLibcRealpathTest, ConsecutiveSeparatorsIgnored) {
ASSERT_STREQ(realpath_buffered("////a///..///a//"), "/a");
TestDir test_dir;
ASSERT_TRUE(create_test_dir("ConsecutiveSeparatorsIgnored", test_dir));
ASSERT_THAT(test_dir.mkdir("a"), Succeeds());
ASSERT_STREQ(realpath_buffered(test_dir.absolute_path("a//..///a//")),
test_dir.absolute_path("a").c_str());
}
TEST_F(LlvmLibcRealpathTest, AllocatesResultWhenBufferIsNull) {
char *result = LIBC_NAMESPACE::realpath("/a", nullptr);
ASSERT_STREQ(result, "/a");
char *result = LIBC_NAMESPACE::realpath("/", nullptr);
ASSERT_STREQ(result, "/");
::free(result);
}