Files
test/source/blender/blenlib/tests/BLI_fileops_test.cc
Sybren A. Stüvel 7f4096be41 Core: treat already-existing directory as 'ok' in recursive mkdir
When creating a directory recursively (via
`BLI_file_ensure_parent_dir_exists()` or `BLI_dir_create_recursive()`,
filter out errors that indicate that the directory already exists.
This is now also covered by a unit test.

I've also removed `BLI_assert(BLI_exists(dirname) == 0);` from
`dir_create_recursive()` (the workhorse for the above-mentioned
functions), for two reasons:

- The function is only used in an "ensure this directory exist"
  context, so the directory existing should be fine, and
- the assertion doesn't guarantee that the subsequent call to
  `mkdir()` actually succeeds. Race conditions between Blender and
  other processes (potentially on other computers, when using
  networked filesystems) will make such a precondition test
  unreliable.

This is a followup of 00abaa571a.

Pull Request: https://projects.blender.org/blender/blender/pulls/145173
2025-08-26 18:30:46 +02:00

310 lines
11 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include <fcntl.h>
#include "testing/testing.h"
#include "BLI_fileops.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_system.h"
#include "BLI_tempfile.h"
#include "BLI_threads.h"
#include BLI_SYSTEM_PID_H
namespace blender::tests {
/*
* General `BLI_fileops.h` tests.
*/
class FileOpsTest : public testing::Test {
public:
/* The base temp directory for all tests using this helper class. Absolute path. */
std::string temp_dir;
void SetUp() override
{
char temp_dir_c[FILE_MAX];
BLI_temp_directory_path_get(temp_dir_c, sizeof(temp_dir_c));
temp_dir = std::string(temp_dir_c) + SEP_STR + "blender_fileops_test_" +
std::to_string(getpid());
if (!BLI_exists(temp_dir.c_str())) {
BLI_dir_create_recursive(temp_dir.c_str());
}
}
void TearDown() override
{
if (BLI_exists(temp_dir.c_str())) {
BLI_delete(temp_dir.c_str(), true, true);
}
}
};
TEST_F(FileOpsTest, rename)
{
const std::string file_name_src = "test_file_src.txt";
const std::string file_name_dst = "test_file_dst.txt";
const std::string test_filepath_src = temp_dir + SEP_STR + file_name_src;
const std::string test_filepath_dst = temp_dir + SEP_STR + file_name_dst;
ASSERT_FALSE(BLI_exists(test_filepath_src.c_str()));
ASSERT_FALSE(BLI_exists(test_filepath_dst.c_str()));
BLI_file_touch(test_filepath_src.c_str());
ASSERT_TRUE(BLI_exists(test_filepath_src.c_str()));
/* `test_filepath_dst` does not exist, so regular rename should succeed. */
ASSERT_EQ(0, BLI_rename(test_filepath_src.c_str(), test_filepath_dst.c_str()));
ASSERT_FALSE(BLI_exists(test_filepath_src.c_str()));
ASSERT_TRUE(BLI_exists(test_filepath_dst.c_str()));
BLI_file_touch(test_filepath_src.c_str());
ASSERT_TRUE(BLI_exists(test_filepath_src.c_str()));
/* `test_filepath_dst` does exist now, so regular rename should fail. */
ASSERT_NE(0, BLI_rename(test_filepath_src.c_str(), test_filepath_dst.c_str()));
ASSERT_TRUE(BLI_exists(test_filepath_src.c_str()));
ASSERT_TRUE(BLI_exists(test_filepath_dst.c_str()));
BLI_file_touch(test_filepath_src.c_str());
ASSERT_TRUE(BLI_exists(test_filepath_src.c_str()));
/* `test_filepath_dst` does exist now, but overwrite rename should succeed on all systems. */
ASSERT_EQ(0, BLI_rename_overwrite(test_filepath_src.c_str(), test_filepath_dst.c_str()));
ASSERT_FALSE(BLI_exists(test_filepath_src.c_str()));
ASSERT_TRUE(BLI_exists(test_filepath_dst.c_str()));
BLI_file_touch(test_filepath_src.c_str());
ASSERT_TRUE(BLI_exists(test_filepath_src.c_str()));
/* Keep `test_filepath_dst` read-open before attempting to rename `test_filepath_src` to
* `test_filepath_dst`.
*
* This is expected to succeed on Unix, but fail on Windows. */
int fd_dst = BLI_open(test_filepath_dst.c_str(), O_BINARY | O_RDONLY, 0);
#ifdef WIN32
ASSERT_NE(0, BLI_rename_overwrite(test_filepath_src.c_str(), test_filepath_dst.c_str()));
ASSERT_TRUE(BLI_exists(test_filepath_src.c_str()));
#else
ASSERT_EQ(0, BLI_rename_overwrite(test_filepath_src.c_str(), test_filepath_dst.c_str()));
ASSERT_FALSE(BLI_exists(test_filepath_src.c_str()));
#endif
ASSERT_TRUE(BLI_exists(test_filepath_dst.c_str()));
close(fd_dst);
/*
* Check directory renaming.
*/
const std::string dir_name_src = "test_dir_src";
const std::string dir_name_dst = "test_dir_dst";
const std::string test_dirpath_src = temp_dir + SEP_STR + dir_name_src;
const std::string test_dirpath_dst = temp_dir + SEP_STR + dir_name_dst;
BLI_dir_create_recursive(test_dirpath_src.c_str());
ASSERT_TRUE(BLI_exists(test_dirpath_src.c_str()));
/* `test_dirpath_dst` does not exist, so regular rename should succeed. */
ASSERT_EQ(0, BLI_rename(test_dirpath_src.c_str(), test_dirpath_dst.c_str()));
ASSERT_FALSE(BLI_exists(test_dirpath_src.c_str()));
ASSERT_TRUE(BLI_exists(test_dirpath_dst.c_str()));
BLI_dir_create_recursive(test_dirpath_src.c_str());
ASSERT_TRUE(BLI_exists(test_dirpath_src.c_str()));
/* `test_dirpath_dst` now exists, so regular rename should fail. */
ASSERT_NE(0, BLI_rename(test_dirpath_src.c_str(), test_dirpath_dst.c_str()));
ASSERT_TRUE(BLI_exists(test_dirpath_src.c_str()));
ASSERT_TRUE(BLI_exists(test_dirpath_dst.c_str()));
/* `test_dirpath_dst` now exists, but is empty, so overwrite rename should succeed. */
ASSERT_EQ(0, BLI_rename_overwrite(test_dirpath_src.c_str(), test_dirpath_dst.c_str()));
ASSERT_FALSE(BLI_exists(test_dirpath_src.c_str()));
ASSERT_TRUE(BLI_exists(test_dirpath_dst.c_str()));
BLI_dir_create_recursive(test_dirpath_src.c_str());
ASSERT_TRUE(BLI_exists(test_dirpath_src.c_str()));
const std::string test_dir_filepath_src = test_dirpath_src + SEP_STR + file_name_src;
const std::string test_dir_filepath_dst = test_dirpath_dst + SEP_STR + file_name_dst;
ASSERT_FALSE(BLI_exists(test_dir_filepath_src.c_str()));
ASSERT_FALSE(BLI_exists(test_dir_filepath_dst.c_str()));
BLI_file_touch(test_dir_filepath_src.c_str());
ASSERT_TRUE(BLI_exists(test_dir_filepath_src.c_str()));
/* `test_dir_filepath_src` does not exist, so regular rename should succeed. */
ASSERT_EQ(0, BLI_rename(test_dir_filepath_src.c_str(), test_dir_filepath_dst.c_str()));
ASSERT_FALSE(BLI_exists(test_dir_filepath_src.c_str()));
ASSERT_TRUE(BLI_exists(test_dir_filepath_dst.c_str()));
/* `test_dirpath_dst` exists and is not empty, so regular rename should fail. */
ASSERT_NE(0, BLI_rename(test_dirpath_src.c_str(), test_dirpath_dst.c_str()));
ASSERT_TRUE(BLI_exists(test_dirpath_src.c_str()));
ASSERT_TRUE(BLI_exists(test_dirpath_dst.c_str()));
/* `test_dirpath_dst` exists and is not empty, so even overwrite rename should fail. */
ASSERT_NE(0, BLI_rename_overwrite(test_dirpath_src.c_str(), test_dirpath_dst.c_str()));
ASSERT_TRUE(BLI_exists(test_dirpath_src.c_str()));
ASSERT_TRUE(BLI_exists(test_dirpath_dst.c_str()));
}
TEST_F(FileOpsTest, dir_create_recursive)
{
const std::string dir_path = this->temp_dir + SEP_STR + "dir-to-create";
const std::string subdir_path = dir_path + SEP_STR + "subdir";
ASSERT_FALSE(BLI_exists(dir_path.c_str()));
ASSERT_TRUE(BLI_dir_create_recursive(subdir_path.c_str()));
ASSERT_TRUE(BLI_exists(subdir_path.c_str()));
ASSERT_TRUE(BLI_dir_create_recursive(subdir_path.c_str()))
<< "Creating an already-existing directory should be fine";
const std::string subfile_path = dir_path + SEP_STR + "some_file.txt";
ASSERT_TRUE(BLI_file_touch(subfile_path.c_str()));
ASSERT_FALSE(BLI_dir_create_recursive(subfile_path.c_str()))
<< "Creating a directory that already exists as file should return an error status";
}
/*
* blender::fstream tests.
*/
TEST(fileops, fstream_open_string_filename)
{
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
if (test_files_dir.empty()) {
FAIL();
}
const std::string filepath = test_files_dir + "/asset_library/новый/blender_assets.cats.txt";
fstream in(filepath, std::ios_base::in);
ASSERT_TRUE(in.is_open()) << "could not open " << filepath;
in.close(); /* This should not crash. */
/* Reading the file not tested here. That's deferred to `std::fstream` anyway. */
}
TEST(fileops, fstream_open_charptr_filename)
{
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
if (test_files_dir.empty()) {
FAIL();
}
const std::string filepath_str = test_files_dir + "/asset_library/новый/blender_assets.cats.txt";
const char *filepath = filepath_str.c_str();
fstream in(filepath, std::ios_base::in);
ASSERT_TRUE(in.is_open()) << "could not open " << filepath;
in.close(); /* This should not crash. */
/* Reading the file not tested here. That's deferred to `std::fstream` anyway. */
}
/*
* Current Directory operations tests.
*/
class ChangeWorkingDirectoryTest : public testing::Test {
public:
std::string test_temp_dir;
void SetUp() override
{
/* Must use because BLI_change_working_dir() checks that we are on the main thread. */
BLI_threadapi_init();
}
void TearDown() override
{
if (!test_temp_dir.empty()) {
BLI_delete(test_temp_dir.c_str(), true, false);
}
BLI_threadapi_exit();
}
/* Make a pseudo-unique file name file within the temp directory in a cross-platform manner. */
static std::string make_pseudo_unique_temp_filename()
{
char temp_dir[FILE_MAX];
BLI_temp_directory_path_get(temp_dir, sizeof(temp_dir));
const std::string directory_name = "blender_test_" + std::to_string(getpid());
char filepath[FILE_MAX];
BLI_path_join(filepath, sizeof(filepath), temp_dir, directory_name.c_str());
return filepath;
}
};
TEST_F(ChangeWorkingDirectoryTest, change_working_directory)
{
char original_cwd_buff[FILE_MAX];
char *original_cwd = BLI_current_working_dir(original_cwd_buff, sizeof(original_cwd_buff));
ASSERT_FALSE(original_cwd == nullptr) << "Unable to get the current working directory.";
/* While some implementation of `getcwd` (or similar) may return allocated memory in some cases,
* in the context of `BLI_current_working_dir` usages, this is not expected and should not
* happen. */
ASSERT_TRUE(original_cwd == original_cwd_buff)
<< "Returned CWD path unexpectedly different than given char buffer.";
std::string temp_file_name = make_pseudo_unique_temp_filename();
test_temp_dir = temp_file_name + "овый";
if (BLI_exists(test_temp_dir.c_str())) {
BLI_delete(test_temp_dir.c_str(), true, false);
}
ASSERT_FALSE(BLI_change_working_dir(test_temp_dir.c_str()))
<< "changing directory to a non-existent directory is expected to fail.";
ASSERT_TRUE(BLI_dir_create_recursive(test_temp_dir.c_str()))
<< "temporary directory should have been created successfully.";
ASSERT_TRUE(BLI_change_working_dir(test_temp_dir.c_str()))
<< "temporary directory should succeed changing directory.";
char new_cwd_buff[FILE_MAX];
char *new_cwd = BLI_current_working_dir(new_cwd_buff, sizeof(new_cwd_buff));
ASSERT_FALSE(new_cwd == nullptr) << "Unable to get the current working directory.";
ASSERT_TRUE(new_cwd == new_cwd_buff)
<< "Returned CWD path unexpectedly different than given char buffer.";
#ifdef __APPLE__
/* The name returned by `std::tmpnam` is fine but the Apple OS method
* picks the true var folder, not the alias, meaning the below
* comparison always fails unless we prepend with the correct value. */
test_temp_dir = "/private" + test_temp_dir;
#endif // #ifdef __APPLE__
ASSERT_EQ(BLI_path_cmp_normalized(new_cwd, test_temp_dir.c_str()), 0)
<< "the path of the current working directory should equal the path of the temporary "
"directory that was created.";
ASSERT_TRUE(BLI_change_working_dir(original_cwd))
<< "changing directory back to the original working directory should succeed.";
char final_cwd_buff[FILE_MAX];
char *final_cwd = BLI_current_working_dir(final_cwd_buff, sizeof(final_cwd_buff));
ASSERT_FALSE(final_cwd == nullptr) << "Unable to get the current working directory.";
ASSERT_TRUE(final_cwd == final_cwd_buff)
<< "Returned CWD path unexpectedly different than given char buffer.";
ASSERT_EQ(BLI_path_cmp_normalized(final_cwd, original_cwd), 0)
<< "The final CWD path should be the same as the original CWD path.";
}
} // namespace blender::tests