Improvements to Env::GetChildren (#7819)

Summary:
The main improvement here is to not include `.` or `..` in the results of `Env::GetChildren`. The occurrence of `.` or `..`; it is non-portable, dependent on the Operating System and the File System. See: https://www.gnu.org/software/libc/manual/html_node/Reading_002fClosing-Directory.html

There were lots of duplicate checks spread through the RocksDB codebase previously to skip `.` and `..`. This new removes the need for those at the source.

Also some minor fixes to `Env::GetChildren`:
* Improve error handling in POSIX implementation
* Remove unnecessary array allocation on Windows
* Fix struct name for Windows Non-UTF-8 API

Pull Request resolved: https://github.com/facebook/rocksdb/pull/7819

Reviewed By: ajkr

Differential Revision: D25837394

Pulled By: jay-zhuang

fbshipit-source-id: 1e137e7218d38b450af9c083f73d5357abcbba2e
This commit is contained in:
Adam Retter 2021-01-09 09:42:21 -08:00 committed by Facebook GitHub Bot
parent 8ed680bdb0
commit 4926b33742
17 changed files with 104 additions and 186 deletions

View File

@ -30,6 +30,7 @@
### Public API Change
* Deprecated public but rarely-used FilterBitsBuilder::CalculateNumEntry, which is replaced with ApproximateNumEntries taking a size_t parameter and returning size_t.
* To improve portability the functions `Env::GetChildren` and `Env::GetChildrenFileAttributes` will no longer return entries for the special directories `.` or `..`.
## 6.15.0 (11/13/2020)
### Bug Fixes

View File

@ -898,10 +898,8 @@ TEST_P(ColumnFamilyTest, IgnoreRecoveredLog) {
std::vector<std::string> old_files;
ASSERT_OK(env_->GetChildren(backup_logs, &old_files));
for (auto& file : old_files) {
if (file != "." && file != "..") {
ASSERT_OK(env_->DeleteFile(backup_logs + "/" + file));
}
}
column_family_options_.merge_operator =
MergeOperators::CreateUInt64AddOperator();
@ -929,10 +927,8 @@ TEST_P(ColumnFamilyTest, IgnoreRecoveredLog) {
std::vector<std::string> logs;
ASSERT_OK(env_->GetChildren(db_options_.wal_dir, &logs));
for (auto& log : logs) {
if (log != ".." && log != ".") {
CopyFile(db_options_.wal_dir + "/" + log, backup_logs + "/" + log);
}
}
// recover the DB
Close();
@ -956,13 +952,11 @@ TEST_P(ColumnFamilyTest, IgnoreRecoveredLog) {
if (iter == 0) {
// copy the logs from backup back to wal dir
for (auto& log : logs) {
if (log != ".." && log != ".") {
CopyFile(backup_logs + "/" + log, db_options_.wal_dir + "/" + log);
}
}
}
}
}
#ifndef ROCKSDB_LITE // TEST functions used are not supported
TEST_P(ColumnFamilyTest, FlushTest) {

View File

@ -44,7 +44,7 @@ TEST_F(DBEncryptionTest, CheckEncrypted) {
Env* target = GetTargetEnv();
int hits = 0;
for (auto it = fileNames.begin() ; it != fileNames.end(); ++it) {
if ((*it == "..") || (*it == ".") || (*it == "LOCK")) {
if (*it == "LOCK") {
continue;
}
auto filePath = dbname_ + "/" + *it;

View File

@ -41,10 +41,8 @@ TEST_F(DBTest2, OpenForReadOnly) {
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname, &files));
for (auto& f : files) {
if (f != "." && f != "..") {
ASSERT_OK(env_->DeleteFile(dbname + "/" + f));
}
}
// <dbname> should be empty now and we should be able to delete it
ASSERT_OK(env_->DeleteDir(dbname));
options.create_if_missing = false;
@ -75,10 +73,8 @@ TEST_F(DBTest2, OpenForReadOnlyWithColumnFamilies) {
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname, &files));
for (auto& f : files) {
if (f != "." && f != "..") {
ASSERT_OK(env_->DeleteFile(dbname + "/" + f));
}
}
// <dbname> should be empty now and we should be able to delete it
ASSERT_OK(env_->DeleteDir(dbname));
options.create_if_missing = false;

View File

@ -506,10 +506,8 @@ TEST_F(DBWALTest, IgnoreRecoveredLog) {
std::vector<std::string> old_files;
ASSERT_OK(env_->GetChildren(backup_logs, &old_files));
for (auto& file : old_files) {
if (file != "." && file != "..") {
ASSERT_OK(env_->DeleteFile(backup_logs + "/" + file));
}
}
Options options = CurrentOptions();
options.create_if_missing = true;
options.merge_operator = MergeOperators::CreateUInt64AddOperator();
@ -528,10 +526,8 @@ TEST_F(DBWALTest, IgnoreRecoveredLog) {
std::vector<std::string> logs;
ASSERT_OK(env_->GetChildren(options.wal_dir, &logs));
for (auto& log : logs) {
if (log != ".." && log != ".") {
CopyFile(options.wal_dir + "/" + log, backup_logs + "/" + log);
}
}
// recover the DB
Reopen(options);
@ -541,10 +537,8 @@ TEST_F(DBWALTest, IgnoreRecoveredLog) {
// copy the logs from backup back to wal dir
for (auto& log : logs) {
if (log != ".." && log != ".") {
CopyFile(backup_logs + "/" + log, options.wal_dir + "/" + log);
}
}
// this should ignore the log files, recovery should not happen again
// if the recovery happens, the same merge operator would be called twice,
// leading to incorrect results
@ -559,10 +553,8 @@ TEST_F(DBWALTest, IgnoreRecoveredLog) {
// copy the logs from backup back to wal dir
ASSERT_OK(env_->CreateDirIfMissing(options.wal_dir));
for (auto& log : logs) {
if (log != ".." && log != ".") {
CopyFile(backup_logs + "/" + log, options.wal_dir + "/" + log);
}
}
// assert that we successfully recovered only from logs, even though we
// destroyed the DB
Reopen(options);
@ -574,12 +566,10 @@ TEST_F(DBWALTest, IgnoreRecoveredLog) {
// copy the logs from backup back to wal dir
ASSERT_OK(env_->CreateDirIfMissing(options.wal_dir));
for (auto& log : logs) {
if (log != ".." && log != ".") {
CopyFile(backup_logs + "/" + log, options.wal_dir + "/" + log);
// we won't be needing this file no more
ASSERT_OK(env_->DeleteFile(backup_logs + "/" + log));
}
}
Status s = TryReopen(options);
ASSERT_NOK(s);
Destroy(options);

93
env/env_basic_test.cc vendored
View File

@ -10,6 +10,7 @@
#include <vector>
#include "env/mock_env.h"
#include "file/file_util.h"
#include "rocksdb/convenience.h"
#include "rocksdb/env.h"
#include "rocksdb/env_encryption.h"
@ -17,46 +18,6 @@
namespace ROCKSDB_NAMESPACE {
// Normalizes trivial differences across Envs such that these test cases can
// run on all Envs.
class NormalizingEnvWrapper : public EnvWrapper {
private:
std::unique_ptr<Env> base_;
public:
explicit NormalizingEnvWrapper(std::unique_ptr<Env>&& base)
: EnvWrapper(base.get()), base_(std::move(base)) {}
explicit NormalizingEnvWrapper(Env* base) : EnvWrapper(base) {}
// Removes . and .. from directory listing
Status GetChildren(const std::string& dir,
std::vector<std::string>* result) override {
Status status = EnvWrapper::GetChildren(dir, result);
if (status.ok()) {
result->erase(std::remove_if(result->begin(), result->end(),
[](const std::string& s) {
return s == "." || s == "..";
}),
result->end());
}
return status;
}
// Removes . and .. from directory listing
Status GetChildrenFileAttributes(
const std::string& dir, std::vector<FileAttributes>* result) override {
Status status = EnvWrapper::GetChildrenFileAttributes(dir, result);
if (status.ok()) {
result->erase(std::remove_if(result->begin(), result->end(),
[](const FileAttributes& fa) {
return fa.name == "." || fa.name == "..";
}),
result->end());
}
return status;
}
};
class EnvBasicTestWithParam : public testing::Test,
public ::testing::WithParamInterface<Env*> {
public:
@ -68,32 +29,17 @@ class EnvBasicTestWithParam : public testing::Test,
test_dir_ = test::PerThreadDBPath(env_, "env_basic_test");
}
void SetUp() override {
env_->CreateDirIfMissing(test_dir_).PermitUncheckedError();
}
void SetUp() override { ASSERT_OK(env_->CreateDirIfMissing(test_dir_)); }
void TearDown() override {
std::vector<std::string> files;
env_->GetChildren(test_dir_, &files).PermitUncheckedError();
for (const auto& file : files) {
// don't know whether it's file or directory, try both. The tests must
// only create files or empty directories, so one must succeed, else the
// directory's corrupted.
Status s = env_->DeleteFile(test_dir_ + "/" + file);
if (!s.ok()) {
ASSERT_OK(env_->DeleteDir(test_dir_ + "/" + file));
}
}
}
void TearDown() override { ASSERT_OK(DestroyDir(env_, test_dir_)); }
};
class EnvMoreTestWithParam : public EnvBasicTestWithParam {};
static std::unique_ptr<Env> def_env(new NormalizingEnvWrapper(Env::Default()));
INSTANTIATE_TEST_CASE_P(EnvDefault, EnvBasicTestWithParam,
::testing::Values(def_env.get()));
::testing::Values(Env::Default()));
INSTANTIATE_TEST_CASE_P(EnvDefault, EnvMoreTestWithParam,
::testing::Values(def_env.get()));
::testing::Values(Env::Default()));
static std::unique_ptr<Env> mock_env(new MockEnv(Env::Default()));
INSTANTIATE_TEST_CASE_P(MockEnv, EnvBasicTestWithParam,
@ -104,8 +50,7 @@ static Env* NewTestEncryptedEnv(Env* base, const std::string& provider_id) {
std::shared_ptr<EncryptionProvider> provider;
EXPECT_OK(EncryptionProvider::CreateFromString(ConfigOptions(), provider_id,
&provider));
std::unique_ptr<Env> encrypted(NewEncryptedEnv(base, provider));
return new NormalizingEnvWrapper(std::move(encrypted));
return NewEncryptedEnv(base, provider);
}
// next statements run env test against default encryption code.
@ -374,6 +319,32 @@ TEST_P(EnvMoreTestWithParam, GetChildren) {
ASSERT_EQ(0U, children.size());
}
TEST_P(EnvMoreTestWithParam, GetChildrenIgnoresDotAndDotDot) {
auto* env = Env::Default();
ASSERT_OK(env->CreateDirIfMissing(test_dir_));
// Create a single file
std::string path = test_dir_;
const EnvOptions soptions;
#ifdef OS_WIN
path.append("\\test_file");
#else
path.append("/test_file");
#endif
std::string data("test data");
std::unique_ptr<WritableFile> file;
ASSERT_OK(env->NewWritableFile(path, &file, soptions));
ASSERT_OK(file->Append("test data"));
// get the children
std::vector<std::string> result;
ASSERT_OK(env->GetChildren(test_dir_, &result));
// expect only one file named `test_data`, i.e. no `.` or `..` names
ASSERT_EQ(result.size(), 1);
ASSERT_EQ(result.at(0), "test_file");
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);

28
env/fs_posix.cc vendored
View File

@ -607,6 +607,7 @@ class PosixFileSystem : public FileSystem {
std::vector<std::string>* result,
IODebugContext* /*dbg*/) override {
result->clear();
DIR* d = opendir(dir.c_str());
if (d == nullptr) {
switch (errno) {
@ -618,11 +619,34 @@ class PosixFileSystem : public FileSystem {
return IOError("While opendir", dir, errno);
}
}
const auto pre_read_errno = errno; // errno may be modified by readdir
struct dirent* entry;
while ((entry = readdir(d)) != nullptr) {
while ((entry = readdir(d)) != nullptr && errno == pre_read_errno) {
// filter out '.' and '..' directory entries
// which appear only on some platforms
const bool ignore =
entry->d_type == DT_DIR &&
(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0);
if (!ignore) {
result->push_back(entry->d_name);
}
closedir(d);
}
// always attempt to close the dir
const auto pre_close_errno = errno; // errno may be modified by closedir
const int close_result = closedir(d);
if (pre_close_errno != pre_read_errno) {
// error occured during readdir
return IOError("While readdir", dir, pre_close_errno);
}
if (close_result != 0) {
// error occured during closedir
return IOError("While closedir", dir, errno);
}
return IOStatus::OK();
}

View File

@ -57,7 +57,7 @@ class DeleteSchedulerTest : public testing::Test {
int normal_cnt = 0;
for (auto& f : files_in_dir) {
if (!DeleteScheduler::IsTrashFile(f) && f != "." && f != "..") {
if (!DeleteScheduler::IsTrashFile(f)) {
normal_cnt++;
}
}

View File

@ -230,9 +230,6 @@ Status DestroyDir(Env* env, const std::string& dir) {
s = env->GetChildren(dir, &files_in_dir);
if (s.ok()) {
for (auto& file_in_dir : files_in_dir) {
if (file_in_dir == "." || file_in_dir == "..") {
continue;
}
std::string path = dir + "/" + file_in_dir;
bool is_dir = false;
s = env->IsDirectory(path, &is_dir);

View File

@ -510,10 +510,6 @@ SstFileManager* NewSstFileManager(Env* env, std::shared_ptr<FileSystem> fs,
s = fs->GetChildren(trash_dir, IOOptions(), &files_in_trash, nullptr);
if (s.ok()) {
for (const std::string& trash_file : files_in_trash) {
if (trash_file == "." || trash_file == "..") {
continue;
}
std::string path_in_trash = trash_dir + "/" + trash_file;
res->OnAddFile(path_in_trash);
Status file_delete =

View File

@ -283,7 +283,8 @@ class Env {
virtual Status FileExists(const std::string& fname) = 0;
// Store in *result the names of the children of the specified directory.
// The names are relative to "dir".
// The names are relative to "dir", and shall never include the
// names `.` or `..`.
// Original contents of *results are dropped.
// Returns OK if "dir" exists and "*result" contains its children.
// NotFound if "dir" does not exist, the calling process does not have
@ -296,7 +297,8 @@ class Env {
// In case the implementation lists the directory prior to iterating the files
// and files are concurrently deleted, the deleted files will be omitted from
// result.
// The name attributes are relative to "dir".
// The name attributes are relative to "dir", and shall never include the
// names `.` or `..`.
// Original contents of *results are dropped.
// Returns OK if "dir" exists and "*result" contains its children.
// NotFound if "dir" does not exist, the calling process does not have

View File

@ -645,7 +645,6 @@ IOStatus WinFileSystem::GetChildren(const std::string& dir,
IODebugContext* /*dbg*/) {
IOStatus status;
result->clear();
std::vector<std::string> output;
RX_WIN32_FIND_DATA data;
memset(&data, 0, sizeof(data));
@ -677,16 +676,20 @@ IOStatus WinFileSystem::GetChildren(const std::string& dir,
UniqueFindClosePtr fc(handle, FindCloseFunc);
if (result->capacity() > 0) {
output.reserve(result->capacity());
}
// For safety
data.cFileName[MAX_PATH - 1] = 0;
while (true) {
// filter out '.' and '..' directory entries
// which appear only on some platforms
const bool ignore =
((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) &&
(strcmp(data.cFileName, ".") == 0 || strcmp(data.cFileName, "..") == 0);
if (!ignore) {
auto x = RX_FILESTRING(data.cFileName, RX_FNLEN(data.cFileName));
output.emplace_back(FN_TO_RX(x));
result->push_back(FN_TO_RX(x));
}
BOOL ret = -RX_FindNextFile(handle, &data);
// If the function fails the return value is zero
// and non-zero otherwise. Not TRUE or FALSE.
@ -696,7 +699,6 @@ IOStatus WinFileSystem::GetChildren(const std::string& dir,
}
data.cFileName[MAX_PATH - 1] = 0;
}
output.swap(*result);
return status;
}

View File

@ -388,7 +388,7 @@ extern void SetCpuPriority(ThreadId id, CpuPriority priority);
#define RX_FindFirstFileEx FindFirstFileExA
#define RX_CreateDirectory CreateDirectoryA
#define RX_FindNextFile FindNextFileA
#define RX_WIN32_FIND_DATA WIN32_FIND_DATA
#define RX_WIN32_FIND_DATA WIN32_FIND_DATAA
#define RX_CreateDirectory CreateDirectoryA
#define RX_RemoveDirectory RemoveDirectoryA
#define RX_GetFileAttributesEx GetFileAttributesExA

View File

@ -743,9 +743,6 @@ Status BackupEngineImpl::Initialize() {
}
// create backups_ structure
for (auto& file : backup_meta_files) {
if (file == "." || file == "..") {
continue;
}
ROCKS_LOG_INFO(options_.info_log, "Detected backup %s", file.c_str());
BackupID backup_id = 0;
sscanf(file.c_str(), "%u", &backup_id);
@ -1985,9 +1982,6 @@ Status BackupEngineImpl::GarbageCollect() {
}
}
for (auto& child : shared_children) {
if (child == "." || child == "..") {
continue;
}
std::string rel_fname;
if (with_checksum) {
rel_fname = GetSharedFileWithChecksumRel(child);
@ -2024,10 +2018,6 @@ Status BackupEngineImpl::GarbageCollect() {
}
}
for (auto& child : private_children) {
if (child == "." || child == "..") {
continue;
}
BackupID backup_id = 0;
bool tmp_dir = child.find(".tmp") != std::string::npos;
sscanf(child.c_str(), "%u", &backup_id);
@ -2042,9 +2032,6 @@ Status BackupEngineImpl::GarbageCollect() {
std::vector<std::string> subchildren;
if (backup_env_->GetChildren(full_private_path, &subchildren).ok()) {
for (auto& subchild : subchildren) {
if (subchild == "." || subchild == "..") {
continue;
}
Status s = backup_env_->DeleteFile(full_private_path + subchild);
ROCKS_LOG_INFO(options_.info_log, "Deleting %s -- %s",
(full_private_path + subchild).c_str(),

View File

@ -417,12 +417,10 @@ class FileManager : public EnvWrapper {
assert(fname != nullptr);
while (true) {
int i = rnd_.Next() % children.size();
if (children[i].name != "." && children[i].name != "..") {
fname->assign(dir + "/" + children[i].name);
*fsize = children[i].size_bytes;
return Status::OK();
}
}
// should never get here
assert(false);
return Status::NotFound("");
@ -433,15 +431,11 @@ class FileManager : public EnvWrapper {
Status s = GetChildren(dir, &children);
if (!s.ok()) {
return s;
} else if (children.size() <= 2) { // . and ..
return Status::NotFound("");
}
while (true) {
int i = rnd_.Next() % children.size();
if (children[i] != "." && children[i] != "..") {
return DeleteFile(dir + "/" + children[i]);
}
}
// should never get here
assert(false);
return Status::NotFound("");
@ -453,15 +447,11 @@ class FileManager : public EnvWrapper {
Status s = GetChildren(dir, &children);
if (!s.ok()) {
return s;
} else if (children.size() <= 2) {
return Status::NotFound("");
}
while (true) {
int i = rnd_.Next() % children.size();
if (children[i] != "." && children[i] != "..") {
return WriteToFile(dir + "/" + children[i], data);
}
}
// should never get here
assert(false);
return Status::NotFound("");
@ -828,9 +818,6 @@ class BackupableDBTest : public testing::Test {
ASSERT_OK(file_manager_->GetChildrenFileAttributes(dir, &children));
int found_count = 0;
for (const auto& child : children) {
if (child.name == "." || child.name == "..") {
continue;
}
const std::string match("match");
ASSERT_EQ(match, std::regex_replace(child.name, pattern, match));
++found_count;
@ -844,9 +831,6 @@ class BackupableDBTest : public testing::Test {
ASSERT_OK(file_manager_->GetChildrenFileAttributes(dir, &children));
int found_count = 0;
for (const auto& child : children) {
if (child.name == "." || child.name == "..") {
continue;
}
auto last_underscore = child.name.find_last_of('_');
auto last_dot = child.name.find_last_of('.');
ASSERT_NE(child.name, child.name.substr(0, last_underscore));
@ -1351,7 +1335,7 @@ TEST_F(BackupableDBTest, CorruptFileMaintainSize) {
const std::string dir = backupdir_ + "/shared_checksum";
ASSERT_OK(file_manager_->GetChildrenFileAttributes(dir, &children));
for (const auto& child : children) {
if (child.name == "." || child.name == ".." || child.size_bytes == 0) {
if (child.size_bytes == 0) {
continue;
}
// corrupt the file by replacing its content by file_size random bytes
@ -1575,7 +1559,7 @@ TEST_F(BackupableDBTest, FlushCompactDuringBackupCheckpoint) {
const std::string dir = backupdir_ + "/shared_checksum";
ASSERT_OK(file_manager_->GetChildrenFileAttributes(dir, &children));
for (const auto& child : children) {
if (child.name == "." || child.name == ".." || child.size_bytes == 0) {
if (child.size_bytes == 0) {
continue;
}
const std::string match("match");
@ -2061,7 +2045,7 @@ TEST_F(BackupableDBTest, FileSizeForIncremental) {
// Corrupt backup SST
ASSERT_OK(file_manager_->GetChildrenFileAttributes(shared_dir, &children));
ASSERT_EQ(children.size(), 3U); // ".", "..", one sst
ASSERT_EQ(children.size(), 1U); // one sst
for (const auto& child : children) {
if (child.name.size() > 4 && child.size_bytes > 0) {
ASSERT_OK(
@ -2121,7 +2105,7 @@ TEST_F(BackupableDBTest, FileSizeForIncremental) {
// Count backup SSTs
children.clear();
ASSERT_OK(file_manager_->GetChildrenFileAttributes(shared_dir, &children));
ASSERT_EQ(children.size(), 4U); // ".", "..", two sst
ASSERT_EQ(children.size(), 2U); // two sst
// Try create backup 3
s = backup_engine_->CreateNewBackup(db_.get(), true /*flush*/);
@ -2134,18 +2118,18 @@ TEST_F(BackupableDBTest, FileSizeForIncremental) {
// Acceptable to call it corruption if size is not in name and
// db session id collision is practically impossible.
EXPECT_TRUE(s.IsCorruption());
EXPECT_EQ(children.size(), 4U); // no SST added
EXPECT_EQ(children.size(), 2U); // no SST added
} else if (option == share_no_checksum) {
// Good to call it corruption if both backups cannot be
// accommodated.
EXPECT_TRUE(s.IsCorruption());
EXPECT_EQ(children.size(), 4U); // no SST added
EXPECT_EQ(children.size(), 2U); // no SST added
} else {
// Since opening a DB seems sufficient for detecting size corruption
// on the DB side, this should be a good thing, ...
EXPECT_OK(s);
// ... as long as we did actually treat it as a distinct SST file.
EXPECT_EQ(children.size(), 5U); // Another SST added
EXPECT_EQ(children.size(), 3U); // Another SST added
}
CloseDBAndBackupEngine();
ASSERT_OK(DestroyDB(dbname_, options_));

View File

@ -325,13 +325,7 @@ TEST_F(CheckpointTest, ExportColumnFamilyWithLinks) {
ASSERT_EQ(metadata.files.size(), num_files_expected);
std::vector<std::string> subchildren;
ASSERT_OK(env_->GetChildren(export_path_, &subchildren));
int num_children = 0;
for (const auto& child : subchildren) {
if (child != "." && child != "..") {
++num_children;
}
}
ASSERT_EQ(num_children, num_files_expected);
ASSERT_EQ(subchildren.size(), num_files_expected);
};
// Test DefaultColumnFamily

View File

@ -14,6 +14,7 @@
#include <memory>
#include <thread>
#include "file/file_util.h"
#include "utilities/persistent_cache/block_cache_tier.h"
namespace ROCKSDB_NAMESPACE {
@ -38,30 +39,9 @@ static void OnOpenForWrite(void* arg) {
}
#endif
static void RemoveDirectory(const std::string& folder) {
std::vector<std::string> files;
Status status = Env::Default()->GetChildren(folder, &files);
if (!status.ok()) {
// we assume the directory does not exist
return;
}
// cleanup files with the patter :digi:.rc
for (auto file : files) {
if (file == "." || file == "..") {
continue;
}
status = Env::Default()->DeleteFile(folder + "/" + file);
assert(status.ok());
}
status = Env::Default()->DeleteDir(folder);
assert(status.ok());
}
static void OnDeleteDir(void* arg) {
char* dir = static_cast<char*>(arg);
RemoveDirectory(std::string(dir));
ASSERT_OK(DestroyDir(Env::Default(), std::string(dir)));
}
//