/************************************************************************ * EOS - the CERN Disk Storage System * * Copyright (C) 2016 CERN/Switzerland * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see .* ************************************************************************/ //------------------------------------------------------------------------------ //! @author Georgios Bitzes //! @brief Various namespace tests //------------------------------------------------------------------------------ #include #include #include #include "namespace/interface/ContainerIterators.hh" #include "namespace/ns_quarkdb/explorer/NamespaceExplorer.hh" #include "namespace/ns_quarkdb/persistency/ContainerMDSvc.hh" #include "namespace/ns_quarkdb/persistency/FileMDSvc.hh" #include "namespace/ns_quarkdb/persistency/MetadataFetcher.hh" #include "namespace/ns_quarkdb/persistency/RequestBuilder.hh" #include "namespace/ns_quarkdb/views/HierarchicalView.hh" #include "namespace/ns_quarkdb/accounting/FileSystemView.hh" #include "namespace/ns_quarkdb/flusher/MetadataFlusher.hh" #include "namespace/ns_quarkdb/FileMD.hh" #include "namespace/ns_quarkdb/ContainerMD.hh" #include "namespace/ns_quarkdb/utils/FutureVectorIterator.hh" #include "namespace/ns_quarkdb/inspector/Printing.hh" #include "namespace/ns_quarkdb/persistency/FileSystemIterator.hh" #include "namespace/ns_quarkdb/inspector/AttributeExtraction.hh" #include "namespace/ns_quarkdb/inspector/FileMetadataFilter.hh" #include "namespace/ns_quarkdb/accounting/QuotaNodeCore.hh" #include "namespace/utils/Checksum.hh" #include "namespace/utils/Etag.hh" #include "namespace/utils/Attributes.hh" #include "namespace/PermissionHandler.hh" #include "namespace/Resolver.hh" #include "TestUtils.hh" #include #include "google/protobuf/util/message_differencer.h" #include using namespace eos; class VariousTests : public eos::ns::testing::NsTestsFixture {}; class NamespaceExplorerF : public eos::ns::testing::NsTestsFixture {}; class FileMDFetching : public eos::ns::testing::NsTestsFixture {}; bool validateReply(qclient::redisReplyPtr reply) { if (reply->type != REDIS_REPLY_STRING) { return false; } if (std::string(reply->str, reply->len) != "ayy-lmao") { return false; } return true; } TEST_F(VariousTests, FollyWithGloriousContinuations) { folly::Future ok = qcl().follyExec("PING", "ayy-lmao").thenValue(validateReply); ASSERT_TRUE(std::move(ok).get()); } TEST_F(VariousTests, FileCacheInvalidation) { ASSERT_THROW(view()->getFile("/dir/my-file.txt", true), eos::MDException); view()->createContainer("/dir", true); std::shared_ptr file1 = view()->createFile("/dir/my-file.txt"); ASSERT_EQ(file1->getId(), 1); mdFlusher()->synchronize(); std::cout << qclient::describeRedisReply(qcl().exec("hdel", "2:map_files", "my-file.txt").get()) << std::endl; eos::IFileMDPtr file2 = view()->getFile("/dir/my-file.txt"); // Cache not updated, view still thinks path is valid ASSERT_EQ(file1.get(), file2.get()); file1.reset(); file2.reset(); fileSvc()->dropCachedFileMD(FileIdentifier(1)); containerSvc()->dropCachedContainerMD(ContainerIdentifier(2)); // cache dropped, should no longer be able to lookup file ASSERT_THROW(view()->getFile("/dir/my-file.txt", true), eos::MDException); } TEST_F(VariousTests, CheckLocationInFsView) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); std::shared_ptr file = view()->createFile("/my-file.txt", true); ASSERT_EQ(file->getId(), 1); ASSERT_EQ(file->getNumLocation(), 0u); file->addLocation(99); file->addLocation(77); file->addLocation(11); file->addLocation(22); file->unlinkLocation(11); file->unlinkLocation(22); std::shared_ptr file2 = view()->createFile("/my-file-2.txt", true); file2->addLocation(22); mdFlusher()->synchronize(); ASSERT_TRUE(eos::MetadataFetcher::locationExistsInFsView(qcl(), FileIdentifier(1), 99, false).get()); ASSERT_TRUE(eos::MetadataFetcher::locationExistsInFsView(qcl(), FileIdentifier(1), 77, false).get()); ASSERT_FALSE(eos::MetadataFetcher::locationExistsInFsView(qcl(), FileIdentifier(1), 11, false).get()); ASSERT_FALSE(eos::MetadataFetcher::locationExistsInFsView(qcl(), FileIdentifier(1), 22, false).get()); ASSERT_FALSE(eos::MetadataFetcher::locationExistsInFsView(qcl(), FileIdentifier(1), 33, false).get()); ASSERT_TRUE(eos::MetadataFetcher::locationExistsInFsView(qcl(), FileIdentifier(1), 11, true).get()); ASSERT_TRUE(eos::MetadataFetcher::locationExistsInFsView(qcl(), FileIdentifier(1), 22, true).get()); ASSERT_FALSE(eos::MetadataFetcher::locationExistsInFsView(qcl(), FileIdentifier(1), 99, true).get()); ASSERT_FALSE(eos::MetadataFetcher::locationExistsInFsView(qcl(), FileIdentifier(1), 77, true).get()); ASSERT_FALSE(eos::MetadataFetcher::locationExistsInFsView(qcl(), FileIdentifier(1), 33, true).get()); // Try to confuse the iterator object qcl().exec("SET", "fsview:22:pickles", "123").get(); FileSystemIterator fsIter(qcl()); ASSERT_TRUE(fsIter.valid()); ASSERT_EQ(fsIter.getFileSystemID(), 11); ASSERT_TRUE(fsIter.isUnlinked()); ASSERT_EQ(fsIter.getRedisKey(), "fsview:11:unlinked"); fsIter.next(); ASSERT_TRUE(fsIter.valid()); ASSERT_EQ(fsIter.getFileSystemID(), 22); ASSERT_FALSE(fsIter.isUnlinked()); ASSERT_EQ(fsIter.getRedisKey(), "fsview:22:files"); fsIter.next(); ASSERT_TRUE(fsIter.valid()); ASSERT_EQ(fsIter.getFileSystemID(), 22); ASSERT_TRUE(fsIter.isUnlinked()); ASSERT_EQ(fsIter.getRedisKey(), "fsview:22:unlinked"); fsIter.next(); ASSERT_TRUE(fsIter.valid()); ASSERT_EQ(fsIter.getFileSystemID(), 77); ASSERT_FALSE(fsIter.isUnlinked()); ASSERT_EQ(fsIter.getRedisKey(), "fsview:77:files"); fsIter.next(); ASSERT_TRUE(fsIter.valid()); ASSERT_EQ(fsIter.getFileSystemID(), 99); ASSERT_FALSE(fsIter.isUnlinked()); ASSERT_EQ(fsIter.getRedisKey(), "fsview:99:files"); fsIter.next(); ASSERT_FALSE(fsIter.valid()); } TEST_F(VariousTests, ReconstructContainerPath) { std::shared_ptr cont = view()->createContainer("/eos/a/b/c/d/e", true); std::shared_ptr file = view()->createFile("/eos/a/b/c/d/e/my-file"); ASSERT_EQ(cont->getId(), 7); ASSERT_EQ(file->getId(), 1); mdFlusher()->synchronize(); ASSERT_EQ("/", eos::MetadataFetcher::resolveFullPath(qcl(), ContainerIdentifier(1)).get()); ASSERT_EQ("/eos/", eos::MetadataFetcher::resolveFullPath(qcl(), ContainerIdentifier(2)).get()); ASSERT_EQ("/eos/a/", eos::MetadataFetcher::resolveFullPath(qcl(), ContainerIdentifier(3)).get()); ASSERT_EQ("/eos/a/b/", eos::MetadataFetcher::resolveFullPath(qcl(), ContainerIdentifier(4)).get()); ASSERT_EQ("/eos/a/b/c/", eos::MetadataFetcher::resolveFullPath(qcl(), ContainerIdentifier(5)).get()); ASSERT_EQ("/eos/a/b/c/d/", eos::MetadataFetcher::resolveFullPath(qcl(), ContainerIdentifier(6)).get()); ASSERT_EQ("/eos/a/b/c/d/e/", eos::MetadataFetcher::resolveFullPath(qcl(), ContainerIdentifier(7)).get()); ASSERT_THROW(eos::MetadataFetcher::resolveFullPath(qcl(), ContainerIdentifier(8)).get(), eos::MDException) ; ASSERT_EQ(eos::MetadataFetcher::resolvePathToID(qcl(), "/").get(), ContainerIdentifier(1)); ASSERT_EQ(eos::MetadataFetcher::resolvePathToID(qcl(), "/eos").get(), ContainerIdentifier(2)); ASSERT_EQ(eos::MetadataFetcher::resolvePathToID(qcl(), "/eos/a").get(), ContainerIdentifier(3)); ASSERT_EQ(eos::MetadataFetcher::resolvePathToID(qcl(), "/eos/a/b").get(), ContainerIdentifier(4)); ASSERT_EQ(eos::MetadataFetcher::resolvePathToID(qcl(), "/eos/a/b/c").get(), ContainerIdentifier(5)); ASSERT_EQ(eos::MetadataFetcher::resolvePathToID(qcl(), "/eos/a/b/c/d").get(), ContainerIdentifier(6)); ASSERT_EQ(eos::MetadataFetcher::resolvePathToID(qcl(), "/eos/a/b/c/d/e").get(), ContainerIdentifier(7)); ASSERT_EQ(eos::MetadataFetcher::resolvePathToID(qcl(), "/eos/a/b/c/d/e/my-file").get(), FileIdentifier(1)); ASSERT_THROW(eos::MetadataFetcher::resolvePathToID(qcl(), "/aaaaaaa").get(), eos::MDException); ASSERT_THROW(eos::MetadataFetcher::resolvePathToID(qcl(), "/eos/aaaaaaa").get(), eos::MDException); } TEST_F(VariousTests, BasicSanity) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); ASSERT_EQ(view()->getUri(root.get()), "/"); ASSERT_EQ(view()->getUri(1), "/"); std::shared_ptr cont1 = view()->createContainer("/eos/", true); ASSERT_EQ(cont1->getId(), 2); ASSERT_THROW(view()->createFile("/eos/", true), eos::MDException); ASSERT_EQ(view()->getUri(cont1.get()), "/eos/"); ASSERT_EQ(view()->getUri(cont1->getId()), "/eos/"); ASSERT_EQ(view()->getUri(cont1->getParentId()), "/"); ASSERT_EQ(view()->getUriFut(cont1->getIdentifier()).get(), "/eos/"); std::shared_ptr file1 = view()->createFile("/eos/my-file.txt", true); ASSERT_EQ(file1->getId(), 1); ASSERT_EQ(file1->getNumLocation(), 0u); file1->addLocation(1); file1->addLocation(7); file1->setCUid(333); file1->setCGid(999); file1->setSize(555); file1->setFlags((S_IRWXU | S_IRWXG | S_IRWXO)); char buff[32]; buff[0] = 0x12; buff[1] = 0x23; buff[2] = 0x55; buff[3] = 0x99; buff[4] = 0xAA; buff[5] = 0xDD; buff[6] = 0x00; buff[7] = 0x55; file1->setChecksum(buff, 8); std::string out; ASSERT_FALSE(eos::appendChecksumOnStringAsHex(file1.get(), out)); unsigned long layout = eos::common::LayoutId::GetId( eos::common::LayoutId::kReplica, eos::common::LayoutId::kMD5, 2, eos::common::LayoutId::k4k); file1->setLayoutId(layout); ASSERT_EQ(file1->getNumLocation(), 2u); ASSERT_EQ(view()->getUri(file1.get()), "/eos/my-file.txt"); ASSERT_EQ(view()->getUriFut(file1->getIdentifier()).get(), "/eos/my-file.txt"); struct timespec ctime; ctime.tv_sec = 1999; ctime.tv_nsec = 8888; file1->setCTime(ctime); struct timespec mtime; mtime.tv_sec = 2000; mtime.tv_nsec = 999; file1->setMTime(mtime); struct timespec atime; mtime.tv_sec = 2000; mtime.tv_nsec = 999; file1->setATime(atime); ASSERT_EQ(eos::Printing::printMultiline(static_cast (file1.get())->getProto()), SSTR("ID: 1\n" "Name: my-file.txt\n" "Link name: \n" "Container ID: 2\n" "uid: 333, gid: 999\n" "Size: 555\n" "Modify: " << Printing::timespecToFileinfo(mtime) << "\n" "Change: " << Printing::timespecToFileinfo(ctime) << "\n" "Access: " << Printing::timespecToFileinfo(atime) << "\n" "Flags: 0777\n" "Checksum type: md5, checksum bytes: 12235599aadd00550000000000000000\n" "Expected number of replicas / stripes: 2\n" "Etag: \"12235599aadd00550000000000000000\"\n" "Locations: [1, 7]\n" "Unlinked locations: []\n" "Extended attributes (0):\n") ); containerSvc()->updateStore(root.get()); containerSvc()->updateStore(cont1.get()); fileSvc()->updateStore(file1.get()); shut_down_everything(); file1 = view()->getFile("/eos/my-file.txt"); ASSERT_EQ(view()->getUri(file1.get()), "/eos/my-file.txt"); ASSERT_EQ(view()->getUriFut(file1->getIdentifier()).get(), "/eos/my-file.txt"); ASSERT_EQ(file1->getId(), 1); ASSERT_EQ(file1->getNumLocation(), 2u); ASSERT_EQ(file1->getLocation(0), 1); ASSERT_EQ(file1->getLocation(1), 7); root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); FileOrContainerMD item = view()->getItem("/").get(); ASSERT_TRUE(item.container); ASSERT_FALSE(item.file); ASSERT_EQ(item.container->getId(), 1); item = view()->getItem("/eos/my-file.txt").get(); ASSERT_TRUE(item.file); ASSERT_FALSE(item.container); ASSERT_EQ(item.file->getId(), 1); // Ensure fsview for location 1 contains file1 std::shared_ptr> it = fsview()->getFileList(1); ASSERT_TRUE(it->valid()); ASSERT_EQ(it->getElement(), file1->getId()); it->next(); ASSERT_FALSE(it->valid()); // Create some subdirectories std::shared_ptr subdir1 = view()->createContainer("/eos/subdir1", true); std::shared_ptr subdir2 = view()->createContainer("/eos/subdir2", true); std::shared_ptr subdir3 = view()->createContainer("/eos/subdir3", true); ASSERT_LT(subdir1->getId(), subdir2->getId()); ASSERT_LT(subdir2->getId(), subdir3->getId()); mdFlusher()->synchronize(); ASSERT_EQ(ContainerIdentifier(subdir1->getId()), eos::MetadataFetcher::getContainerIDFromName(qcl(), ContainerIdentifier(2), "subdir1").get()); ASSERT_EQ(ContainerIdentifier(subdir2->getId()), eos::MetadataFetcher::getContainerIDFromName(qcl(), ContainerIdentifier(2), "subdir2").get()); ASSERT_EQ(ContainerIdentifier(subdir3->getId()), eos::MetadataFetcher::getContainerIDFromName(qcl(), ContainerIdentifier(2), "subdir3").get()); ASSERT_EQ(subdir1->getId(), eos::MetadataFetcher::getContainerFromName(qcl(), ContainerIdentifier(2), "subdir1").get().id()); ASSERT_EQ(subdir2->getId(), eos::MetadataFetcher::getContainerFromName(qcl(), ContainerIdentifier(2), "subdir2").get().id()); ASSERT_EQ(subdir3->getId(), eos::MetadataFetcher::getContainerFromName(qcl(), ContainerIdentifier(2), "subdir3").get().id()); IContainerMD::ContainerMap containerMap = eos::MetadataFetcher::getContainerMap( qcl(), ContainerIdentifier(subdir1->getId())).get(); IContainerMD::FileMap fileMap = eos::MetadataFetcher::getContainerMap(qcl(), ContainerIdentifier(subdir1->getId())).get(); ASSERT_TRUE(containerMap.empty()); ASSERT_TRUE(fileMap.empty()); ASSERT_THROW(view()->getFile("/"), eos::MDException); } TEST_F(VariousTests, FileMDGetEnv) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); IFileMDPtr file1 = view()->createFile("/file1", true); struct timespec mtime; mtime.tv_sec = 123; mtime.tv_nsec = 345; file1->setMTime(mtime); file1->setCUid(999); file1->setSize(1337); std::string output; file1->getEnv(output); } TEST_F(VariousTests, MkdirOnBrokenSymlink) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); IFileMDPtr file1 = view()->createFile("/file1", true); file1->setLink("/not-existing"); fileSvc()->updateStore(file1.get()); containerSvc()->updateStore(root.get()); ASSERT_THROW(view()->createContainer("/file1", true), eos::MDException); } TEST_F(VariousTests, SymlinkExtravaganza) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); // Basic symlink sanity checks. IFileMDPtr file1 = view()->createFile("/file1", true); file1->setLink("/cont1"); IContainerMDPtr cont1 = view()->createContainer("/cont1", true); IFileMDPtr awesomeFile = view()->createFile("/cont1/awesome-file", true); ASSERT_EQ(view()->getUri(cont1.get()), "/cont1/"); ASSERT_EQ(view()->getUri(cont1->getId()), "/cont1/"); ASSERT_EQ(view()->getUriFut(cont1->getIdentifier()).get(), "/cont1/"); fileSvc()->updateStore(file1.get()); fileSvc()->updateStore(awesomeFile.get()); containerSvc()->updateStore(cont1.get()); IContainerMDPtr cont2 = view()->getContainer("/file1", true); ASSERT_TRUE(cont2.get() != nullptr); ASSERT_EQ(cont1.get(), cont2.get()); ASSERT_THROW(view()->getContainer("/file1", false), MDException); ASSERT_EQ(view()->getUri(cont2.get()), "/cont1/"); ASSERT_EQ(view()->getUri(cont2->getId()), "/cont1/"); ASSERT_EQ(view()->getUriFut(cont2->getIdentifier()).get(), "/cont1/"); IFileMDPtr file2 = view()->createFile("/file2", true); file2->setLink("/file1"); fileSvc()->updateStore(file2.get()); // NOTE: The following does currently not work on citrine + old NS. IContainerMDPtr cont3 = view()->getContainer("/file2", true); ASSERT_TRUE(cont3.get() != nullptr); ASSERT_EQ(cont1.get(), cont3.get()); ASSERT_THROW(view()->getFile("/file2", true), MDException); // it actually points to a container // Retrieve awesome-file through the symlink. IFileMDPtr awesomeFile1 = view()->getFile("/file1/awesome-file", true); ASSERT_TRUE(awesomeFile1.get() != nullptr); ASSERT_EQ(awesomeFile.get(), awesomeFile1.get()); ASSERT_EQ(view()->getUri(awesomeFile.get()), "/cont1/awesome-file"); ASSERT_EQ(view()->getUriFut(awesomeFile->getIdentifier()).get(), "/cont1/awesome-file"); ASSERT_EQ(view()->getUri(awesomeFile->getContainerId()), "/cont1/"); // Retrieve awesome-file through two levels of symlinks. // NOTE: The following does currently not work on citrine + old NS. IFileMDPtr awesomeFile2 = view()->getFile("/file2/awesome-file", true); ASSERT_TRUE(awesomeFile2.get() != nullptr); ASSERT_EQ(awesomeFile.get(), awesomeFile2.get()); ASSERT_THROW(view()->getContainer("/file2/awesome-file", true), MDException); // Let's create a symlink loop, composed of four files. IFileMDPtr symlinkLoop1 = view()->createFile("/loop1", true); IFileMDPtr symlinkLoop2 = view()->createFile("/loop2", true); IFileMDPtr symlinkLoop3 = view()->createFile("/loop3", true); IFileMDPtr symlinkLoop4 = view()->createFile("/loop4", true); symlinkLoop1->setLink("/loop2"); symlinkLoop2->setLink("/loop3"); symlinkLoop3->setLink("/loop4"); symlinkLoop4->setLink("/loop1"); fileSvc()->updateStore(symlinkLoop1.get()); fileSvc()->updateStore(symlinkLoop2.get()); fileSvc()->updateStore(symlinkLoop3.get()); fileSvc()->updateStore(symlinkLoop4.get()); ASSERT_THROW(view()->getContainer("/loop1", true), MDException); ASSERT_THROW(view()->getContainer("/loop2", true), MDException); ASSERT_THROW(view()->getContainer("/loop3", true), MDException); ASSERT_THROW(view()->getContainer("/loop4", true), MDException); ASSERT_THROW(view()->getFile("/loop1", true), MDException); ASSERT_THROW(view()->getFile("/loop2", true), MDException); ASSERT_THROW(view()->getFile("/loop3", true), MDException); ASSERT_THROW(view()->getFile("/loop4", true), MDException); ASSERT_THROW(view()->getFile("/", true), MDException); // But: We should be able to retrieve the loop-files with follow = false. ASSERT_EQ(view()->getFile("/loop1", false), symlinkLoop1); ASSERT_EQ(view()->getFile("/loop2", false), symlinkLoop2); ASSERT_EQ(view()->getFile("/loop3", false), symlinkLoop3); ASSERT_EQ(view()->getFile("/loop4", false), symlinkLoop4); // Try out the following ridiculous situation: // /folder1/f2 -> /folder2 // /folder2/f3 -> /folder3 // /folder3/f4 -> /folder4 // /folder4/f1 -> /folder1 // /folder1/target-file // // We should be able to access target-file through // /folder1/f2/f3/f4/f1/target-file IContainerMDPtr folder1 = view()->createContainer("/folder1", true); IContainerMDPtr folder2 = view()->createContainer("/folder2", true); IContainerMDPtr folder3 = view()->createContainer("/folder3", true); IContainerMDPtr folder4 = view()->createContainer("/folder4", true); IFileMDPtr f2 = view()->createFile("/folder1/f2", true); f2->setLink("/folder2"); IFileMDPtr f3 = view()->createFile("/folder2/f3", true); f3->setLink("/folder3"); IFileMDPtr f4 = view()->createFile("/folder3/f4", true); f4->setLink("/folder4"); IFileMDPtr f1 = view()->createFile("/folder4/f1", true); f1->setLink("/folder1"); IFileMDPtr targetFile1 = view()->createFile("/folder1/target-file", true); fileSvc()->updateStore(f1.get()); fileSvc()->updateStore(f2.get()); fileSvc()->updateStore(f3.get()); fileSvc()->updateStore(f4.get()); fileSvc()->updateStore(targetFile1.get()); IFileMDPtr targetFile2 = view()->getFile("/folder1/f2/f3/f4/f1/target-file", true); ASSERT_TRUE(targetFile2.get() != nullptr); ASSERT_EQ(targetFile1.get(), targetFile2.get()); ASSERT_EQ(view()->getUri(targetFile2.get()), "/folder1/target-file"); ASSERT_EQ(view()->getUriFut(targetFile2->getIdentifier()).get(), "/folder1/target-file"); IFileMDPtr symlinkFile = view()->getFile("/folder1/f2/f3/f4/f1", false); ASSERT_EQ(view()->getUri(symlinkFile.get()), "/folder4/f1"); ASSERT_TRUE(symlinkFile->isLink()); ASSERT_EQ(symlinkFile->getLink(), "/folder1"); // Use relative symlinks IFileMDPtr ff1 = view()->createFile("/ff1", true); IFileMDPtr ff2 = view()->createFile("/ff2", true); ff2->setLink("./ff1"); fileSvc()->updateStore(ff1.get()); fileSvc()->updateStore(ff2.get()); ASSERT_EQ(view()->getFile("/ff2", true), ff1); ASSERT_EQ(view()->getFile("/ff2", false), ff2); IFileMDPtr ff3 = view()->createFile("/folder1/ff3", true); ff3->setLink("../ff1"); fileSvc()->updateStore(ff3.get()); ASSERT_EQ(view()->getFile("/folder1/ff3", true), ff1); ASSERT_EQ(view()->getFile("/folder1/ff3", false), ff3); // More relative symlinks containerSvc()->updateStore(view()->createContainer("/eos", true).get()); containerSvc()->updateStore(view()->createContainer("/eos/dev", true).get()); containerSvc()->updateStore(view()->createContainer("/eos/dev/test", true).get()); containerSvc()->updateStore( view()->createContainer("/eos/dev/test/instancetest", true).get()); containerSvc()->updateStore( view()->createContainer("/eos/dev/test/instancetest/ref", true).get()); IFileMDPtr touch = view()->createFile("/eos/dev/test/instancetest/ref/touch", true); IFileMDPtr symdir = view()->createFile("/eos/dev/test/instancetest/symrel2", true); symdir->setLink("../../test/instancetest/ref"); fileSvc()->updateStore(touch.get()); fileSvc()->updateStore(symdir.get()); ASSERT_EQ(view()->getFile("/eos/dev/test/instancetest/symrel2/touch", true), touch); ASSERT_EQ(view()->getRealPath("/eos/dev/test/instancetest/symrel2/touch"), "/eos/dev/test/instancetest/ref/touch"); ASSERT_EQ(view()->getRealPath("/eos/dev/test/instancetest/symrel2"), "/eos/dev/test/instancetest/symrel2"); } TEST_F(VariousTests, MoreSymlinks) { containerSvc()->updateStore(view()->createContainer("/eos/dev/user", true).get()); IFileMDPtr myFile = view()->createFile("/eos/dev/user/my-file", true); fileSvc()->updateStore(myFile.get()); IFileMDPtr link = view()->createFile("/eos/dev/user/link", true); link->setLink("my-file"); fileSvc()->updateStore(link.get()); ASSERT_EQ(view()->getFile("/eos/dev/user/link", true), myFile); ASSERT_EQ(view()->getFile("/eos/dev/user/link", false), link); containerSvc()->updateStore(view()->createContainer("/eos/dev/user/dir1", true).get()); containerSvc()->updateStore(view()->createContainer("/eos/dev/user/dir1/dir2", true).get()); IFileMDPtr myFile2 = view()->createFile("/eos/dev/user/dir1/dir2/my-file-2", true); fileSvc()->updateStore(myFile2.get()); link->setLink("dir1/dir2/my-file-2"); fileSvc()->updateStore(link.get()); ASSERT_EQ(view()->getFile("/eos/dev/user/link", true), myFile2); ASSERT_EQ(view()->getFile("/eos/dev/user/link", false), link); } TEST_F(VariousTests, createFile) { containerSvc()->updateStore(view()->createContainer("/eos/dev/user", true).get()); IFileMDPtr myFile = view()->createFile("/eos/dev/user/my-file"); fileSvc()->updateStore(myFile.get()); ASSERT_THROW(view()->createFile("/eos/dev/user/my-file"), eos::MDException); ASSERT_THROW(view()->createFile("/eos/dev/user"), eos::MDException); ASSERT_THROW(view()->createFile("/eos/dev/user/my-file/aaaa"), eos::MDException); } TEST_F(VariousTests, createContainerMadness) { containerSvc()->updateStore(view()->createContainer("/eos/dev/../dev/", true).get()); containerSvc()->updateStore(view()->createContainer( "/eos/dev/./my-dir-1/./../my-dir-2/../my-dir-3/./my-dir-4/../my-dir-5", true).get()); // MUAHAHAHAH // This is how "mkdir -p" on Linux behaves, as well. We want to be compatible. view()->getContainer("/eos"); view()->getContainer("/eos/dev"); view()->getContainer("/eos/dev/my-dir-1"); view()->getContainer("/eos/dev/my-dir-2"); view()->getContainer("/eos/dev/my-dir-3"); view()->getContainer("/eos/dev/my-dir-3/my-dir-4"); view()->getContainer("/eos/dev/my-dir-3/my-dir-5"); shut_down_everything(); view()->getContainer("/eos"); view()->getContainer("/eos/dev"); view()->getContainer("/eos/dev/my-dir-1"); view()->getContainer("/eos/dev/my-dir-2"); view()->getContainer("/eos/dev/my-dir-3"); view()->getContainer("/eos/dev/my-dir-3/my-dir-4"); view()->getContainer("/eos/dev/my-dir-3/my-dir-5"); ASSERT_THROW(view()->createContainer("/eos/dev/my-dir-1/aaa/bbb", false), eos::MDException); IFileMDPtr file1 = view()->createFile("/eos/dev/my-dir-1/link", true); file1->setLink("/eos/dev/my-dir-3/my-dir-4"); fileSvc()->updateStore(file1.get()); shut_down_everything(); ASSERT_THROW(view()->createContainer( "/eos/dev/../dev/my-dir-1/./link/../my-dir-4/what-am-i-doing/aaaaaa/../bbbbbbb/../bbbbbbb/chicken", false), eos::MDException); containerSvc()->updateStore(view()->createContainer( "/eos/dev/../dev/my-dir-1/./link/../my-dir-4/what-am-i-doing/aaaaaa/../bbbbbbb/../bbbbbbb/chicken", true).get()); view()->getContainer("/eos/dev/my-dir-3/my-dir-4/what-am-i-doing"); view()->getContainer("/eos/dev/my-dir-3/my-dir-4/what-am-i-doing/aaaaaa"); view()->getContainer("/eos/dev/my-dir-3/my-dir-4/what-am-i-doing/bbbbbbb"); auto chicken = view()->getContainer("/eos/dev/my-dir-3/my-dir-4/what-am-i-doing/bbbbbbb/chicken"); ASSERT_EQ(view()->getUri(chicken.get()), "/eos/dev/my-dir-3/my-dir-4/what-am-i-doing/bbbbbbb/chicken/"); ASSERT_EQ(view()->getUri(chicken->getId()), "/eos/dev/my-dir-3/my-dir-4/what-am-i-doing/bbbbbbb/chicken/"); ASSERT_EQ(view()->getUri(chicken->getParentId()), "/eos/dev/my-dir-3/my-dir-4/what-am-i-doing/bbbbbbb/"); } TEST_F(VariousTests, ChecksumFormatting) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); std::shared_ptr file1 = view()->createFile("/my-file.txt", true); ASSERT_EQ(file1->getId(), 1); char buff[32]; buff[0] = 0x12; buff[1] = 0x23; buff[2] = 0x55; buff[3] = 0x99; buff[4] = 0xAA; buff[5] = 0xDD; buff[6] = 0x00; buff[7] = 0x55; file1->setChecksum(buff, 8); std::string out; ASSERT_FALSE(eos::appendChecksumOnStringAsHex(file1.get(), out)); unsigned long layout = eos::common::LayoutId::GetId( eos::common::LayoutId::kReplica, eos::common::LayoutId::kMD5, 2, eos::common::LayoutId::k4k); file1->setLayoutId(layout); ASSERT_TRUE(eos::appendChecksumOnStringAsHex(file1.get(), out)); ASSERT_EQ(out, "12235599aadd00550000000000000000"); layout = eos::common::LayoutId::GetId( eos::common::LayoutId::kReplica, eos::common::LayoutId::kCRC32, 2, eos::common::LayoutId::k4k); file1->setLayoutId(layout); out.clear(); ASSERT_TRUE(eos::appendChecksumOnStringAsHex(file1.get(), out)); ASSERT_EQ(out, "12235599"); out.clear(); ASSERT_TRUE(eos::appendChecksumOnStringAsHex(file1.get(), out, ' ')); ASSERT_EQ(out, "12 23 55 99"); out.clear(); ASSERT_TRUE(eos::appendChecksumOnStringAsHex(file1.get(), out, '_')); ASSERT_EQ(out, "12_23_55_99"); out.clear(); ASSERT_TRUE(eos::appendChecksumOnStringAsHex(file1.get(), out, '_', 20)); ASSERT_EQ(out, "12_23_55_99_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00"); ASSERT_FALSE(eos::appendChecksumOnStringAsHex(nullptr, out)); } TEST(HexToByteString, EdgeCases) { std::string byteArray; ASSERT_FALSE(eos::hexArrayToByteArray("chickens", byteArray)); ASSERT_TRUE(eos::hexArrayToByteArray("", byteArray)); ASSERT_EQ(byteArray, ""); ASSERT_FALSE(eos::hexArrayToByteArray("deadbeeg", byteArray)); } TEST(HexToByteString, BasicSanity) { std::string byteArray; ASSERT_TRUE(eos::hexArrayToByteArray("deadbeef", byteArray)); ASSERT_EQ(byteArray.size(), 4); ASSERT_EQ(byteArray[0], '\xde'); ASSERT_EQ(byteArray[1], '\xad'); ASSERT_EQ(byteArray[2], '\xbe'); ASSERT_EQ(byteArray[3], '\xef'); std::string tmp; ASSERT_TRUE(eos::hexArrayToByteArray("DEADBEEF", tmp)); ASSERT_EQ(tmp, byteArray); ASSERT_TRUE(eos::hexArrayToByteArray("DeAdbEEf", tmp)); ASSERT_EQ(tmp, byteArray); } namespace eos { TEST_F(VariousTests, EtagFormatting) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); // Create a test file. std::shared_ptr file1 = view()->createFile("/my-file.txt", true); ASSERT_EQ(file1->getId(), 1); eos::IFileMD::ctime_t mtime; mtime.tv_sec = 1537360812; mtime.tv_nsec = 0; file1->setCTime(mtime); eos::QuarkFileMD* file1f = reinterpret_cast(file1.get()); file1f->mFile.set_id(4697755903ull); // File has no checksum, using inode + modification time. std::string outcome; eos::calculateEtag(file1.get(), outcome); ASSERT_EQ(outcome, "\"1261044247998496768:1537360812\""); // Force temporary etag file1->setAttribute("sys.tmp.etag", "lmao"); eos::calculateEtag(file1.get(), outcome); ASSERT_EQ(outcome, "lmao"); // Remove temporary etag file1->removeAttribute("sys.tmp.etag"); // etag based on inode + mtime char buff[4]; buff[0] = 0xa7; buff[1] = 0x25; buff[2] = 0x99; buff[3] = 0x97; file1->setChecksum(buff, 4); file1f->mFile.set_id(4697755939ull); unsigned long layout = eos::common::LayoutId::GetId( eos::common::LayoutId::kReplica, eos::common::LayoutId::kAdler, 2, eos::common::LayoutId::k4k); file1->setLayoutId(layout); eos::calculateEtag(file1.get(), outcome); ASSERT_EQ(outcome, "\"1261044257662173184:a7259997\""); char buff2[32]; buff2[0] = 0x65; buff2[1] = 0x01; buff2[2] = 0xe9; buff2[3] = 0xc7; buff2[4] = 0xbf; buff2[5] = 0x20; buff2[6] = 0xb1; buff2[7] = 0xdc; buff2[8] = 0x56; buff2[9] = 0xf0; buff2[10] = 0x15; buff2[11] = 0xe3; buff2[12] = 0x41; buff2[13] = 0xf7; buff2[14] = 0x98; buff2[15] = 0x33; file1->setChecksum(buff2, 16); layout = eos::common::LayoutId::GetId( eos::common::LayoutId::kReplica, eos::common::LayoutId::kMD5, 2, eos::common::LayoutId::k4k); file1->setLayoutId(layout); eos::calculateEtag(file1.get(), outcome); ASSERT_EQ(outcome, "\"6501e9c7bf20b1dc56f015e341f79833\""); } TEST_F(VariousTests, EtagFormattingContainer) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); // Create a test directory. std::shared_ptr cont1 = view()->createContainer("/my-file.txt", true); ASSERT_EQ(cont1->getId(), 2); eos::IFileMD::ctime_t mtime; mtime.tv_sec = 1534776794; mtime.tv_nsec = 97343404; cont1->setTMTime(mtime); eos::QuarkContainerMD* cont1c = reinterpret_cast (cont1.get()); cont1c->mCont.set_id(5734137); std::string outcome; cont1->setAttribute("sys.tmp.etag", "lmao"); eos::calculateEtag(cont1.get(), outcome); ASSERT_EQ(outcome, "lmao"); cont1->removeAttribute("sys.tmp.etag"); eos::calculateEtag(cont1.get(), outcome); ASSERT_EQ(outcome, "577ef9:1534776794.097"); } } TEST_F(FileMDFetching, ExistenceTest) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); std::shared_ptr file1 = view()->createFile("/my-file.txt", true); ASSERT_EQ(file1->getId(), 1); mdFlusher()->synchronize(); ASSERT_TRUE(MetadataFetcher::doesFileMdExist(qcl(), FileIdentifier(1)).get()); ASSERT_FALSE(MetadataFetcher::doesFileMdExist(qcl(), FileIdentifier(2)).get()); ASSERT_TRUE(fileSvc()->hasFileMD(FileIdentifier(1)).get()); ASSERT_FALSE(fileSvc()->hasFileMD(FileIdentifier(2)).get()); ASSERT_TRUE(MetadataFetcher::doesContainerMdExist(qcl(), ContainerIdentifier(1)).get()); ASSERT_FALSE(MetadataFetcher::doesContainerMdExist(qcl(), ContainerIdentifier(2)).get()); } TEST_F(FileMDFetching, FilemapToFutureVector) { populateDummyData1(); eos::IContainerMDPtr cont = view()->getContainer("/eos/d1"); ASSERT_EQ(cont->getId(), 3); IContainerMD::FileMap filemap = MetadataFetcher::getFileMap(qcl(), ContainerIdentifier(3)).get(); std::map sorted; std::map expected = { {"f1", 1}, {"f2", 2}, {"f3", 3}, {"f4", 4}, {"f5", 5} }; for (auto it = filemap.begin(); it != filemap.end(); ++it) { sorted[it->first] = it->second; } ASSERT_EQ(sorted, expected); std::vector> mdvector = MetadataFetcher::getFilesFromFilemap(qcl(), filemap); ASSERT_EQ(mdvector.size(), 5u); std::unique_ptr executor(new folly::IOThreadPoolExecutor(4)); std::vector> mdvector3 = MetadataFetcher::getFileMDsInContainer(qcl(), ContainerIdentifier(3), executor.get()).get(); ASSERT_EQ(mdvector3.size(), 5u); eos::ns::FileMdProto f1 = std::move(mdvector[0]).get(); eos::ns::FileMdProto f2 = std::move(mdvector[1]).get(); eos::ns::FileMdProto f3 = std::move(mdvector[2]).get(); eos::ns::FileMdProto f4 = std::move(mdvector[3]).get(); eos::ns::FileMdProto f5 = std::move(mdvector[4]).get(); ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(f1, std::move(mdvector3[0]).get())); ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(f2, std::move(mdvector3[1]).get())); ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(f3, std::move(mdvector3[2]).get())); ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(f4, std::move(mdvector3[3]).get())); ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(f5, std::move(mdvector3[4]).get())); ASSERT_EQ(f1.name(), "f1"); ASSERT_EQ(f1.id(), 1); ASSERT_EQ(f2.name(), "f2"); ASSERT_EQ(f2.id(), 2); ASSERT_EQ(f3.name(), "f3"); ASSERT_EQ(f3.id(), 3); ASSERT_EQ(f4.name(), "f4"); ASSERT_EQ(f4.id(), 4); ASSERT_EQ(f5.name(), "f5"); ASSERT_EQ(f5.id(), 5); IContainerMD::FileMap containermap = MetadataFetcher::getContainerMap(qcl(), ContainerIdentifier(3)).get(); std::map sorted2; std::map expected2 = { {"d2", 4}, {"d2-1", 11}, {"d2-2", 12}, {"d2-3", 13} }; for (auto it = containermap.begin(); it != containermap.end(); ++it) { sorted2[it->first] = it->second; } ASSERT_EQ(sorted2, expected2); std::vector> mdvector2 = MetadataFetcher::getContainersFromContainerMap(qcl(), containermap); ASSERT_EQ(mdvector2.size(), 4u); std::vector> mdvector5 = MetadataFetcher::getContainerMDsInContainer(qcl(), ContainerIdentifier(3), executor.get()).get(); ASSERT_EQ(mdvector5.size(), 4u); eos::ns::ContainerMdProto d0 = std::move(mdvector2[0]).get(); eos::ns::ContainerMdProto d1 = std::move(mdvector2[1]).get(); eos::ns::ContainerMdProto d2 = std::move(mdvector2[2]).get(); eos::ns::ContainerMdProto d3 = std::move(mdvector2[3]).get(); ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(d0, std::move(mdvector5[0]).get())); ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(d1, std::move(mdvector5[1]).get())); ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(d2, std::move(mdvector5[2]).get())); ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(d3, std::move(mdvector5[3]).get())); ASSERT_EQ(d0.name(), "d2"); ASSERT_EQ(d0.id(), 4); ASSERT_EQ(d1.name(), "d2-1"); ASSERT_EQ(d1.id(), 11); ASSERT_EQ(d2.name(), "d2-2"); ASSERT_EQ(d2.id(), 12); ASSERT_EQ(d3.name(), "d2-3"); ASSERT_EQ(d3.id(), 13); } TEST_F(FileMDFetching, CorruptionTest) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); std::shared_ptr file1 = view()->createFile("/my-file.txt", true); ASSERT_EQ(file1->getId(), 1); shut_down_everything(); qcl().execute(RequestBuilder::writeFileProto(FileIdentifier(1), "hint", "chicken_chicken_chicken_chicken")).get(); try { MetadataFetcher::getFileFromId(qcl(), FileIdentifier(1)).get(); FAIL(); } catch (const MDException& exc) { ASSERT_STREQ(exc.what(), "Error while deserializing FileMD #1 protobuf: FileMD object checksum mismatch"); } shut_down_everything(); qcl().exec("DEL", constants::sFileKey).get(); qcl().exec("SADD", constants::sFileKey, "zzz").get(); try { MetadataFetcher::getFileFromId(qcl(), FileIdentifier(1)).get(); FAIL(); } catch (const MDException& exc) { ASSERT_STREQ(exc.what(), "Error while fetching FileMD #1 protobuf from QDB: Received unexpected response, was expecting string: (error) ERR Invalid argument: WRONGTYPE Operation against a key holding the wrong kind of value"); } } TEST_F(NamespaceExplorerF, BasicSanity) { populateDummyData1(); ExplorationOptions options; options.depthLimit = 999; // Invalid path ASSERT_THROW(eos::NamespaceExplorer("/eos/invalid/path", options, qcl(), executor()), eos::MDException); // Find on single file - weird, but possible NamespaceExplorer explorer("/eos/d2/d3-2/my-file", options, qcl(), executor()); NamespaceItem item; ASSERT_TRUE(explorer.fetch(item)); ASSERT_EQ(item.fullPath, "/eos/d2/d3-2/my-file"); ASSERT_FALSE(explorer.fetch(item)); // Find on directory NamespaceExplorer explorer2("/eos/d2", options, qcl(), executor()); ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/"); for (size_t i = 1; i <= 3; i++) { ASSERT_TRUE(explorer2.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, SSTR("/eos/d2/asdf" << i)); } ASSERT_TRUE(explorer2.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/b"); for (size_t i = 1; i <= 6; i++) { ASSERT_TRUE(explorer2.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, SSTR("/eos/d2/zzzzz" << i)); } ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d3-1/"); ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d3-2/"); ASSERT_TRUE(explorer2.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d3-2/my-file"); ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d4/"); ASSERT_TRUE(explorer2.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d4/adsf"); std::stringstream path; path << "/eos/d2/d4/"; for (size_t i = 1; i <= 7; i++) { path << i << "/"; ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, path.str()); } ASSERT_FALSE(explorer2.fetch(item)); ASSERT_FALSE(explorer2.fetch(item)); ASSERT_FALSE(explorer2.fetch(item)); } TEST_F(NamespaceExplorerF, NoFiles) { populateDummyData1(); ExplorationOptions options; options.depthLimit = 999; options.ignoreFiles = true; // Find on directory NamespaceExplorer explorer2("/eos/d2", options, qcl(), executor()); NamespaceItem item; ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/"); ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d3-1/"); ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d3-2/"); ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d4/"); std::stringstream path; path << "/eos/d2/d4/"; for (size_t i = 1; i <= 7; i++) { path << i << "/"; ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, path.str()); } ASSERT_FALSE(explorer2.fetch(item)); ASSERT_FALSE(explorer2.fetch(item)); ASSERT_FALSE(explorer2.fetch(item)); } TEST_F(NamespaceExplorerF, LinkedAttributes) { std::shared_ptr root = view()->getContainer("/"); ASSERT_EQ(root->getId(), 1); root->setAttribute("sys.chickens", "no"); root->setAttribute("sys.qwerty", "asdf"); containerSvc()->updateStore(root.get()); std::shared_ptr file1 = view()->createFile("/my-file.txt", true); ASSERT_EQ(file1->getId(), 1); file1->setAttribute("sys.chickens", "yes"); file1->setAttribute("sys.attr.link", "/some-file"); fileSvc()->updateStore(file1.get()); mdFlusher()->synchronize(); // Find on single file - weird, but possible ExplorationOptions options; options.depthLimit = 999; options.populateLinkedAttributes = true; // attrs asked, but view not provided ASSERT_THROW(eos::NamespaceExplorer("/", options, qcl(), executor()), eos::MDException); options.view = view(); eos::NamespaceExplorer explorer("/", options, qcl(), executor()); NamespaceItem item; ASSERT_TRUE(explorer.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/"); ASSERT_TRUE(explorer.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, "/my-file.txt"); std::map predictedAttrs { {"sys.chickens", "yes" }, {"sys.attr.link", "/some-file"} }; ASSERT_EQ(item.attrs, predictedAttrs); file1->setAttribute("sys.attr.link", "/"); fileSvc()->updateStore(file1.get()); mdFlusher()->synchronize(); eos::NamespaceExplorer explorer2("/", options, qcl(), executor()); ASSERT_TRUE(explorer2.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/"); ASSERT_TRUE(explorer2.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, "/my-file.txt"); predictedAttrs = { {"sys.chickens", "yes" }, {"sys.attr.link", "/"}, {"sys.qwerty", "asdf"}, }; ASSERT_EQ(item.attrs, predictedAttrs); } class ContainerFilter : public eos::ExpansionDecider { public: virtual bool shouldExpandContainer(const eos::ns::ContainerMdProto& proto, const eos::IContainerMD::XAttrMap& attrs, const std::string& fullPath) override { if (proto.name() == "d4") { std::cerr << "INFO: Filtering out encountered container with name d4." << std::endl; return false; } return true; } }; TEST_F(NamespaceExplorerF, ExpansionDecider) { populateDummyData1(); ExplorationOptions options; options.depthLimit = 999; options.expansionDecider.reset(new ContainerFilter()); NamespaceExplorer explorer("/eos/d2", options, qcl(), executor()); NamespaceItem item; ASSERT_TRUE(explorer.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/"); ASSERT_FALSE(item.expansionFilteredOut); for (size_t i = 1; i <= 3; i++) { ASSERT_TRUE(explorer.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, SSTR("/eos/d2/asdf" << i)); ASSERT_FALSE(item.expansionFilteredOut); } ASSERT_TRUE(explorer.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/b"); ASSERT_FALSE(item.expansionFilteredOut); for (size_t i = 1; i <= 6; i++) { ASSERT_TRUE(explorer.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, SSTR("/eos/d2/zzzzz" << i)); ASSERT_FALSE(item.expansionFilteredOut); } ASSERT_TRUE(explorer.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d3-1/"); ASSERT_FALSE(item.expansionFilteredOut); ASSERT_TRUE(explorer.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d3-2/"); ASSERT_FALSE(item.expansionFilteredOut); ASSERT_TRUE(explorer.fetch(item)); ASSERT_TRUE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d3-2/my-file"); ASSERT_FALSE(item.expansionFilteredOut); ASSERT_TRUE(explorer.fetch(item)); ASSERT_FALSE(item.isFile); ASSERT_EQ(item.fullPath, "/eos/d2/d4/"); ASSERT_TRUE(item.expansionFilteredOut); ASSERT_FALSE(explorer.fetch(item)); ASSERT_FALSE(explorer.fetch(item)); ASSERT_FALSE(explorer.fetch(item)); } TEST_F(VariousTests, LinkedExtendedAttributes) { IContainerMDPtr cont1 = view()->createContainer("/eos/dir1", true); IContainerMDPtr cont2 = view()->createContainer("/eos/dir1/dir2", true); cont1->setAttribute("sys.chickens", "yes"); cont1->setAttribute("user.qwerty", "asdf"); cont2->setAttribute("sys.chickens", "no"); cont2->setAttribute("sys.attr.link", "/eos/dir4"); eos::IContainerMD::XAttrMap out; eos::listAttributes(view(), cont1.get(), out, false); ASSERT_EQ(out.size(), 2u); ASSERT_EQ(out["sys.chickens"], "yes"); ASSERT_EQ(out["user.qwerty"], "asdf"); eos::listAttributes(view(), cont2.get(), out, false); ASSERT_EQ(out.size(), 2u); ASSERT_EQ(out["sys.chickens"], "no"); ASSERT_EQ(out["sys.attr.link"], "/eos/dir4 - not found"); cont2->setAttribute("sys.attr.link", "/eos/dir1"); eos::listAttributes(view(), cont2.get(), out, false); ASSERT_EQ(out.size(), 3u); ASSERT_EQ(out["sys.chickens"], "no"); ASSERT_EQ(out["sys.attr.link"], "/eos/dir1"); ASSERT_EQ(out["user.qwerty"], "asdf"); eos::listAttributes(view(), cont2.get(), out, true); ASSERT_EQ(out.size(), 3u); ASSERT_EQ(out["sys.chickens"], "no"); ASSERT_EQ(out["sys.attr.link"], "/eos/dir1"); ASSERT_EQ(out["user.qwerty"], "asdf"); cont2->removeAttribute("sys.chickens"); eos::listAttributes(view(), cont2.get(), out, false); ASSERT_EQ(out.size(), 3u); ASSERT_EQ(out["sys.chickens"], "yes"); ASSERT_EQ(out["sys.attr.link"], "/eos/dir1"); ASSERT_EQ(out["user.qwerty"], "asdf"); eos::listAttributes(view(), cont2.get(), out, true); ASSERT_EQ(out.size(), 3u); ASSERT_EQ(out["sys.link.chickens"], "yes"); ASSERT_EQ(out["sys.attr.link"], "/eos/dir1"); ASSERT_EQ(out["user.qwerty"], "asdf"); } TEST(OctalParsing, BasicSanity) { mode_t mode; ASSERT_TRUE(PermissionHandler::parseOctalMask("0700", mode)); ASSERT_EQ(mode, 0700); ASSERT_TRUE(PermissionHandler::parseOctalMask("700", mode)); ASSERT_EQ(mode, 0700); ASSERT_TRUE(PermissionHandler::parseOctalMask("744", mode)); ASSERT_EQ(mode, 0744); ASSERT_TRUE(PermissionHandler::parseOctalMask("777", mode)); ASSERT_EQ(mode, 0777); ASSERT_TRUE(PermissionHandler::parseOctalMask("000", mode)); ASSERT_EQ(mode, 0000); ASSERT_FALSE(PermissionHandler::parseOctalMask("chicken", mode)); ASSERT_FALSE(PermissionHandler::parseOctalMask("700turtles", mode)); ASSERT_FALSE(PermissionHandler::parseOctalMask("chicken777", mode)); ASSERT_FALSE(PermissionHandler::parseOctalMask("999", mode)); ASSERT_FALSE(PermissionHandler::parseOctalMask("0789", mode)); ASSERT_FALSE(PermissionHandler::parseOctalMask("0709", mode)); ASSERT_FALSE(PermissionHandler::parseOctalMask("0x123", mode)); } TEST(SysMask, BasicSanity) { std::map xattr; xattr.emplace("chicken.chicken", "chicken chicken chicken chicken"); ASSERT_EQ(0700, PermissionHandler::filterWithSysMask(xattr, 0700)); ASSERT_EQ(0770, PermissionHandler::filterWithSysMask(xattr, 0770)); ASSERT_EQ(0774, PermissionHandler::filterWithSysMask(xattr, 0774)); xattr.emplace("sys.mask", "700"); ASSERT_EQ(0700, PermissionHandler::filterWithSysMask(xattr, 0777)); ASSERT_EQ(0700, PermissionHandler::filterWithSysMask(xattr, 0744)); ASSERT_EQ(0700, PermissionHandler::filterWithSysMask(xattr, 0755)); ASSERT_EQ(0400, PermissionHandler::filterWithSysMask(xattr, 0444)); xattr["sys.mask"] = "0700"; ASSERT_EQ(0700, PermissionHandler::filterWithSysMask(xattr, 0777)); ASSERT_EQ(0700, PermissionHandler::filterWithSysMask(xattr, 0744)); ASSERT_EQ(0700, PermissionHandler::filterWithSysMask(xattr, 0755)); ASSERT_EQ(0400, PermissionHandler::filterWithSysMask(xattr, 0444)); xattr["sys.mask"] = "0400"; ASSERT_EQ(0400, PermissionHandler::filterWithSysMask(xattr, 0777)); ASSERT_EQ(0400, PermissionHandler::filterWithSysMask(xattr, 0744)); ASSERT_EQ(0400, PermissionHandler::filterWithSysMask(xattr, 0755)); ASSERT_EQ(0400, PermissionHandler::filterWithSysMask(xattr, 0444)); xattr["sys.mask"] = "744"; ASSERT_EQ(0744, PermissionHandler::filterWithSysMask(xattr, 0744)); ASSERT_EQ(0744, PermissionHandler::filterWithSysMask(xattr, 0757)); ASSERT_EQ(0404, PermissionHandler::filterWithSysMask(xattr, 0407)); xattr["sys.mask"] = "chicken"; ASSERT_EQ(0700, PermissionHandler::filterWithSysMask(xattr, 0700)); ASSERT_EQ(0770, PermissionHandler::filterWithSysMask(xattr, 0770)); ASSERT_EQ(0774, PermissionHandler::filterWithSysMask(xattr, 0774)); } TEST(QuotaNodeCore, BasicSanity) { QuotaNodeCore qn; std::unordered_set uids; std::unordered_set gids; ASSERT_EQ(qn.getNumFilesByUser(12), 0u); ASSERT_EQ(qn.getNumFilesByGroup(12), 0u); qn.addFile(12, 13, 1024, 2048); ASSERT_EQ(qn.getNumFilesByUser(12), 1u); ASSERT_EQ(qn.getNumFilesByUser(13), 0u); ASSERT_EQ(qn.getNumFilesByGroup(12), 0u); ASSERT_EQ(qn.getNumFilesByGroup(13), 1u); ASSERT_EQ(qn.getPhysicalSpaceByUser(12), 2048); ASSERT_EQ(qn.getPhysicalSpaceByGroup(12), 0); ASSERT_EQ(qn.getPhysicalSpaceByGroup(13), 2048); uids.emplace(12); gids.emplace(13); ASSERT_EQ(qn.getUids(), uids); ASSERT_EQ(qn.getGids(), gids); qn.addFile(12, 12, 1, 2); ASSERT_EQ(qn.getPhysicalSpaceByUser(12), 2050); ASSERT_EQ(qn.getPhysicalSpaceByGroup(12), 2); ASSERT_EQ(qn.getPhysicalSpaceByGroup(13), 2048); ASSERT_EQ(qn.getNumFilesByUser(12), 2u); ASSERT_EQ(qn.getNumFilesByUser(13), 0u); ASSERT_EQ(qn.getNumFilesByGroup(12), 1u); ASSERT_EQ(qn.getNumFilesByGroup(13), 1u); gids.emplace(12); ASSERT_EQ(qn.getUids(), uids); ASSERT_EQ(qn.getGids(), gids); qn.removeFile(12, 13, 1024, 2048); ASSERT_EQ(qn.getPhysicalSpaceByUser(12), 2); ASSERT_EQ(qn.getPhysicalSpaceByGroup(12), 2); ASSERT_EQ(qn.getPhysicalSpaceByGroup(13), 0); // gids.erase(13); ASSERT_EQ(qn.getUids(), uids); ASSERT_EQ(qn.getGids(), gids); qn.removeFile(12, 12, 1, 2); ASSERT_EQ(qn.getPhysicalSpaceByUser(12), 0); ASSERT_EQ(qn.getPhysicalSpaceByGroup(12), 0); ASSERT_EQ(qn.getPhysicalSpaceByGroup(13), 0); ASSERT_EQ(qn.getNumFilesByUser(12), 0u); ASSERT_EQ(qn.getNumFilesByUser(13), 0u); ASSERT_EQ(qn.getNumFilesByGroup(12), 0u); ASSERT_EQ(qn.getNumFilesByGroup(13), 0u); // uids.clear(); // gids.clear(); ASSERT_EQ(qn.getUids(), uids); ASSERT_EQ(qn.getGids(), gids); } TEST(Resolver, FidParsing) { XrdOucString str = "fid:123"; ASSERT_EQ(FileIdentifier(123), Resolver::retrieveFileIdentifier(str)); str = "asdef234"; ASSERT_EQ(FileIdentifier(0), Resolver::retrieveFileIdentifier(str)); str = "fxid:0x12f"; ASSERT_EQ(FileIdentifier(303), Resolver::retrieveFileIdentifier(str)); str = "fxid:12f"; ASSERT_EQ(FileIdentifier(303), Resolver::retrieveFileIdentifier(str)); str = "ino:0x3e70000000"; // fid: 999, old encoding ASSERT_EQ(FileIdentifier(999), Resolver::retrieveFileIdentifier(str)); str = "ino:zzzz"; ASSERT_EQ(FileIdentifier(0), Resolver::retrieveFileIdentifier(str)); str = "ino:123"; // cid: 123 ASSERT_EQ(FileIdentifier(0), Resolver::retrieveFileIdentifier(str)); str = "ino:0x80000000000003e7"; // fid: 999, new encoding ASSERT_EQ(FileIdentifier(999), Resolver::retrieveFileIdentifier(str)); str = "ino:80000000000003e7"; // fid: 999, new encoding ASSERT_EQ(FileIdentifier(999), Resolver::retrieveFileIdentifier(str)); } TEST(FileOrContainerIdentifier, BasicSanity) { FileOrContainerIdentifier empty; ASSERT_TRUE(empty.empty()); ASSERT_FALSE(empty.isFile()); ASSERT_FALSE(empty.isContainer()); ASSERT_EQ(empty.toFileIdentifier(), FileIdentifier(0)); ASSERT_EQ(empty.toContainerIdentifier(), ContainerIdentifier(0)); FileOrContainerIdentifier file(FileIdentifier(111)); ASSERT_FALSE(file.empty()); ASSERT_TRUE(file.isFile()); ASSERT_FALSE(file.isContainer()); ASSERT_EQ(file.toFileIdentifier(), FileIdentifier(111)); ASSERT_EQ(file.toContainerIdentifier(), ContainerIdentifier(0)); FileOrContainerIdentifier container(ContainerIdentifier(222)); ASSERT_FALSE(container.empty()); ASSERT_FALSE(container.isFile()); ASSERT_TRUE(container.isContainer()); ASSERT_EQ(container.toFileIdentifier(), FileIdentifier(0)); ASSERT_EQ(container.toContainerIdentifier(), ContainerIdentifier(222)); } TEST(FutureVectorIterator, EmptyConstructor) { FutureVectorIterator fvi; ASSERT_TRUE(fvi.isReady()); ASSERT_TRUE(fvi.isMainFutureReady()); int out; ASSERT_FALSE(fvi.fetchNext(out)); } TEST(FutureVectorIterator, BasicSanity) { folly::Promise>> mainPromise; FutureVectorIterator fvi(mainPromise.getFuture()); ASSERT_FALSE(fvi.isReady()); ASSERT_FALSE(fvi.isMainFutureReady()); // Build our future vector std::vector> mainVector; folly::Promise p1; folly::Promise p2; folly::Promise p3; mainVector.emplace_back(p1.getFuture()); mainVector.emplace_back(p2.getFuture()); mainVector.emplace_back(p3.getFuture()); mainPromise.setValue(std::move(mainVector)); ASSERT_FALSE(fvi.isReady()); ASSERT_TRUE(fvi.isMainFutureReady()); ASSERT_EQ(fvi.size(), 3u); p1.setValue(9); ASSERT_TRUE(fvi.isReady()); ASSERT_TRUE(fvi.isMainFutureReady()); int val; ASSERT_TRUE(fvi.fetchNext(val)); ASSERT_EQ(val, 9); ASSERT_FALSE(fvi.isReady()); p3.setValue(999); ASSERT_FALSE(fvi.isReady()); p2.setValue(8); ASSERT_TRUE(fvi.isReady()); ASSERT_TRUE(fvi.fetchNext(val)); ASSERT_EQ(val, 8); ASSERT_TRUE(fvi.isReady()); ASSERT_TRUE(fvi.fetchNext(val)); ASSERT_EQ(val, 999); ASSERT_TRUE(fvi.isReady()); ASSERT_FALSE(fvi.fetchNext(val)); ASSERT_TRUE(fvi.isReady()); ASSERT_FALSE(fvi.fetchNext(val)); ASSERT_TRUE(fvi.isReady()); } TEST_F(VariousTests, QuotanodeCorruption) { IContainerMDPtr cont = view()->createContainer("/a/b/c/d/e/f/g", true); ASSERT_EQ(cont->getId(), 8); ASSERT_EQ(cont->getParentId(), 7); ASSERT_EQ(view()->getQuotaNode(cont.get()), nullptr); cont->setParentId(999); containerSvc()->updateStore(cont.get()); ASSERT_EQ(view()->getQuotaNode(cont.get()), nullptr); shut_down_everything(); cont = containerSvc()->getContainerMD(8); ASSERT_EQ(cont->getParentId(), 999); ASSERT_EQ(view()->getQuotaNode(cont.get()), nullptr); } TEST_F(VariousTests, UnlinkAllLocations) { std::shared_ptr file1 = view()->createFile("/my-file.txt"); ASSERT_EQ(file1->getId(), 1); file1->addLocation(13); file1->unlinkLocation(13); file1->addLocation(13); file1->unlinkAllLocations(); ASSERT_EQ(file1->getLocations().size(), 0u); ASSERT_EQ(file1->getUnlinkedLocations().size(), 1u); } TEST_F(VariousTests, CountContents) { eos::IContainerMDPtr cont1 = view()->createContainer("/dir-1/"); eos::IContainerMDPtr cont2 = view()->createContainer("/dir-2/"); ASSERT_EQ(cont1->getId(), 2); ASSERT_EQ(cont2->getId(), 3); eos::IFileMDPtr file1 = view()->createFile("/file-1"); eos::IFileMDPtr file2 = view()->createFile("/file-2"); eos::IFileMDPtr file3 = view()->createFile("/file-3"); eos::IFileMDPtr file4 = view()->createFile("/file-4"); ASSERT_EQ(file1->getId(), 1); ASSERT_EQ(file2->getId(), 2); mdFlusher()->synchronize(); std::pair, folly::Future> counts = eos::MetadataFetcher::countContents(qcl(), ContainerIdentifier(1)); ASSERT_EQ(std::move(counts.first).get(), 4u); ASSERT_EQ(std::move(counts.second).get(), 2u); counts = eos::MetadataFetcher::countContents(qcl(), ContainerIdentifier(2)); ASSERT_EQ(std::move(counts.first).get(), 0u); ASSERT_EQ(std::move(counts.second).get(), 0u); } TEST_F(VariousTests, ContainerIterator) { eos::IContainerMDPtr cont1 = view()->createContainer("/dir-1/"); ASSERT_EQ(cont1->getId(), 2); eos::IFileMDPtr file1 = view()->createFile("/dir-1/file-1"); eos::IFileMDPtr file2 = view()->createFile("/dir-1/file-2"); eos::IFileMDPtr file3 = view()->createFile("/dir-1/file-3"); eos::IFileMDPtr file4 = view()->createFile("/dir-1/file-4"); eos::IContainerMDPtr subcont1 = view()->createContainer("/dir-1/dir-1/"); eos::IContainerMDPtr subcont2 = view()->createContainer("/dir-1/dir-2/"); eos::IContainerMDPtr subcont3 = view()->createContainer("/dir-1/dir-3/"); eos::IContainerMDPtr subcont4 = view()->createContainer("/dir-1/dir-4/"); ASSERT_EQ(cont1->getNumFiles(), 4); ASSERT_EQ(cont1->getNumContainers(), 4); eos::ContainerMapIterator cit(cont1); eos::FileMapIterator fit(cont1); // file iterator test { std::vector fv; for (auto i = 5 ; i <= 1024; ++i) { std::string f = "/dir-1/file-" + std::to_string(i); fv.push_back(view()->createFile(f)); } size_t iterations = 1; do { fit.next(); if (fit.valid()) { iterations++; } } while (fit.valid()); ASSERT_EQ(iterations, 1024); eos::FileMapIterator fit1(cont1); std::string f = "/dir-1/" + fit1.key(); view()->unlinkFile(f); // delete one more file during iteration, diffrent from the 1st one if (f != "/dir-1/file-1024") { std::string f = "/dir-1/file-1024"; view()->unlinkFile(f); } else { std::string f = "/dir-1/file-512"; view()->unlinkFile(f); } iterations = 1; do { fit1.next(); if (fit1.valid()) { iterations++; } } while (fit1.valid()); ASSERT_EQ(iterations, 1023); } // container iterator test { std::vector cv; for (auto i = 5 ; i <= 1024; ++i) { std::string f = "/dir-1/dir-" + std::to_string(i); cv.push_back(view()->createContainer(f)); } size_t iterations = 1; do { cit.next(); if (cit.valid()) { iterations++; } } while (cit.valid()); ASSERT_EQ(iterations, 1024); eos::ContainerMapIterator cit1(cont1); std::string f = "/dir-1/" + cit1.key(); view()->removeContainer(f); // delete one more file during iteration, diffrent from the 1st one if (f != "/dir-1/dir-1024") { std::string f = "/dir-1/dir-1024"; view()->removeContainer(f); } else { std::string f = "/dir-1/dir-512"; view()->removeContainer(f); } iterations = 1; do { cit1.next(); if (cit1.valid()) { iterations++; } } while (cit1.valid()); ASSERT_EQ(iterations, 1023); } } TEST_F(VariousTests, FileIteratorInvalidation) { // exercises the FileMapIterator when underlying file map's densehashtable // is likely to have been reallocated at a different location but still have // the same size. eos::IContainerMDPtr cont1 = view()->createContainer("/dir-1/"); for (size_t i = 1; i <= 17; ++i) { std::string s = "/dir-1/file-" + std::to_string(i); view()->createFile(s); } // expect 64 buckets now allocated in densehashtable eos::FileMapIterator fit(cont1); for (size_t i = 0; i < 8; ++i) { ASSERT_TRUE(fit.valid()); fit.next(); } ASSERT_TRUE(fit.valid()); // remove some files, but erasing does not cause reallocation for (size_t i = 12; i <= 17; ++i) { std::string s = "/dir-1/file-" + std::to_string(i); view()->unlinkFile(s); } // expect number of buckets to be shrunk to 32 on next insert view()->createFile("/dir-1/file-12"); // allocate a few regions, each equal to the size of 64 buckets const size_t sz = 64 * sizeof(std::pair); const size_t nalloc = 32; void* p[nalloc]; for (size_t i = 0; i < nalloc; ++i) { p[i] = malloc(sz); memset(p[i], 0xcc, sz); } // add files, expect number of buckets to expand to 64 after last insert for (size_t i = 13; i <= 17; ++i) { std::string s = "/dir-1/file-" + std::to_string(i); view()->createFile(s); } for (size_t i = 0; i < nalloc; ++i) { free(p[i]); p[i] = nullptr; } while (fit.valid()) { fit.next(); } } TEST_F(NamespaceExplorerF, MissingFile) { view()->createContainer("/dir-1/"); view()->createContainer("/dir-2/"); view()->createContainer("/dir-3/"); view()->createContainer("/dir-4/"); view()->createFile("/dir-1/file-1"); view()->createFile("/dir-1/file-2"); IFileMDPtr f = view()->createFile("/dir-1/file-3"); ASSERT_EQ(f->getId(), 3); view()->createFile("/dir-1/file-4"); view()->createFile("/dir-1/file-5"); mdFlusher()->synchronize(); ASSERT_EQ( qclient::describeRedisReply(qcl().exec("lhdel", "eos-file-md", "3").get()), "(integer) 1" ); mdFlusher()->synchronize(); ExplorationOptions options; options.depthLimit = 999; NamespaceExplorer explorer("/", options, qcl(), executor()); NamespaceItem item; ASSERT_TRUE(explorer.fetch(item)); ASSERT_EQ(item.fullPath, "/"); ASSERT_TRUE(explorer.fetch(item)); ASSERT_EQ(item.fullPath, "/dir-1/"); ASSERT_TRUE(explorer.fetch(item)); ASSERT_EQ(item.fullPath, "/dir-1/file-1"); ASSERT_TRUE(explorer.fetch(item)); ASSERT_EQ(item.fullPath, "/dir-1/file-2"); ASSERT_TRUE(explorer.fetch(item)); ASSERT_EQ(item.fullPath, "/dir-1/file-4"); ASSERT_TRUE(explorer.fetch(item)); ASSERT_EQ(item.fullPath, "/dir-1/file-5"); }