From 4210db90bf108442fc3d732af8ae315b0308e157 Mon Sep 17 00:00:00 2001 From: Witek Krecicki Date: Thu, 5 Jun 2025 11:27:23 +0000 Subject: [PATCH] Mockable directory iterator --- .../complianceengine/src/lib/CMakeLists.txt | 1 + .../src/lib/CommonContext.cpp | 17 + .../complianceengine/src/lib/CommonContext.h | 7 +- .../src/lib/ContextInterface.h | 4 + .../src/lib/DirectoryEntry.cpp | 406 ++++++++++++++++++ .../complianceengine/src/lib/DirectoryEntry.h | 159 +++++++ .../complianceengine/tests/CMakeLists.txt | 2 + .../DirectoryIterationIntegrationTest.cpp | 166 +++++++ .../tests/DirectoryIterationTest.cpp | 156 +++++++ .../complianceengine/tests/MockContext.h | 2 + 10 files changed, 919 insertions(+), 1 deletion(-) create mode 100644 src/modules/complianceengine/src/lib/DirectoryEntry.cpp create mode 100644 src/modules/complianceengine/src/lib/DirectoryEntry.h create mode 100644 src/modules/complianceengine/tests/DirectoryIterationIntegrationTest.cpp create mode 100644 src/modules/complianceengine/tests/DirectoryIterationTest.cpp diff --git a/src/modules/complianceengine/src/lib/CMakeLists.txt b/src/modules/complianceengine/src/lib/CMakeLists.txt index 7afc4a3fef..757ce4ff45 100644 --- a/src/modules/complianceengine/src/lib/CMakeLists.txt +++ b/src/modules/complianceengine/src/lib/CMakeLists.txt @@ -28,6 +28,7 @@ add_library(complianceenginelib STATIC CommonContext.cpp ComplianceEngineInterface.cpp ContextInterface.cpp + DirectoryEntry.cpp Engine.cpp Evaluator.cpp KernelModuleTools.cpp diff --git a/src/modules/complianceengine/src/lib/CommonContext.cpp b/src/modules/complianceengine/src/lib/CommonContext.cpp index 68db63691e..fddb27abcd 100644 --- a/src/modules/complianceengine/src/lib/CommonContext.cpp +++ b/src/modules/complianceengine/src/lib/CommonContext.cpp @@ -36,4 +36,21 @@ Result CommonContext::GetFileContents(const std::string& filePath) return result; } +Result CommonContext::GetDirectoryEntries(const std::string& directoryPath, bool recursive) const +{ + try + { + auto entries = mDirectoryIterator->GetEntries(directoryPath, recursive); + return entries; + } + catch (const std::exception& e) + { + return Error("Failed to get directory entries: " + std::string(e.what())); + } + catch (...) + { + return Error("Failed to get directory entries: unknown error"); + } +} + } // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/lib/CommonContext.h b/src/modules/complianceengine/src/lib/CommonContext.h index 1a6b288575..e43438aa57 100644 --- a/src/modules/complianceengine/src/lib/CommonContext.h +++ b/src/modules/complianceengine/src/lib/CommonContext.h @@ -5,9 +5,11 @@ #define COMPLIANCEENGINE_COMMONCONTEXT_H #include "ContextInterface.h" +#include "DirectoryEntry.h" #include "Logging.h" #include "Result.h" +#include #include #include @@ -17,13 +19,15 @@ class CommonContext : public ContextInterface { public: CommonContext(OsConfigLogHandle log) - : mLog(log) + : mLog(log), + mDirectoryIterator(new FtsDirectoryIterator()) { } ~CommonContext() override; Result ExecuteCommand(const std::string& cmd) const override; Result GetFileContents(const std::string& filePath) const override; + Result GetDirectoryEntries(const std::string& directoryPath, bool recursive) const override; OsConfigLogHandle GetLogHandle() const override { return mLog; @@ -31,6 +35,7 @@ class CommonContext : public ContextInterface private: OsConfigLogHandle mLog; + std::unique_ptr mDirectoryIterator; }; } // namespace ComplianceEngine #endif // COMPLIANCEENGINE_COMMONCONTEXT_H diff --git a/src/modules/complianceengine/src/lib/ContextInterface.h b/src/modules/complianceengine/src/lib/ContextInterface.h index 6ebb75916e..1593b5d307 100644 --- a/src/modules/complianceengine/src/lib/ContextInterface.h +++ b/src/modules/complianceengine/src/lib/ContextInterface.h @@ -4,19 +4,23 @@ #ifndef COMPLIANCEENGINE_CONTEXTINTERFACE_H #define COMPLIANCEENGINE_CONTEXTINTERFACE_H +#include "DirectoryEntry.h" #include "Logging.h" #include "Result.h" +#include #include namespace ComplianceEngine { + class ContextInterface { public: virtual ~ContextInterface() = 0; virtual Result ExecuteCommand(const std::string& cmd) const = 0; virtual Result GetFileContents(const std::string& filePath) const = 0; + virtual Result GetDirectoryEntries(const std::string& directoryPath, bool recursive) const = 0; virtual OsConfigLogHandle GetLogHandle() const = 0; }; diff --git a/src/modules/complianceengine/src/lib/DirectoryEntry.cpp b/src/modules/complianceengine/src/lib/DirectoryEntry.cpp new file mode 100644 index 0000000000..32621308d5 --- /dev/null +++ b/src/modules/complianceengine/src/lib/DirectoryEntry.cpp @@ -0,0 +1,406 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "DirectoryEntry.h" + +#include +#include +#include + +namespace ComplianceEngine +{ + +// DirectoryIterator implementation +DirectoryIterator::DirectoryIterator() + : mBackendType(END_ITERATOR), + mFts(nullptr), + mCurrentEntry(nullptr), + mRecursive(false), + mCurrentDirectoryEntry("", DirectoryEntryType::Other) +{ +} + +DirectoryIterator::DirectoryIterator(FTS* fts, bool recursive) + : mBackendType(FTS_BACKEND), + mFts(fts), + mCurrentEntry(nullptr), + mRecursive(recursive), + mCurrentDirectoryEntry("", DirectoryEntryType::Other) +{ + if (mFts) + { + advanceFts(); // Get the first valid entry + } + else + { + mBackendType = END_ITERATOR; + } +} + +DirectoryIterator::DirectoryIterator(std::vector::const_iterator it) + : mBackendType(VECTOR_BACKEND), + mFts(nullptr), + mCurrentEntry(nullptr), + mRecursive(false), + mCurrentDirectoryEntry("", DirectoryEntryType::Other), + mVectorIt(it) +{ +} + +DirectoryIterator::~DirectoryIterator() +{ + // Don't close FTS here - DirectoryEntries owns it +} + +DirectoryIterator::DirectoryIterator(const DirectoryIterator& other) + : mBackendType(other.mBackendType), + mFts(other.mFts), + mCurrentEntry(other.mCurrentEntry), + mRecursive(other.mRecursive), + mCurrentDirectoryEntry(other.mCurrentDirectoryEntry), + mVectorIt(other.mVectorIt) +{ +} + +DirectoryIterator& DirectoryIterator::operator=(const DirectoryIterator& other) +{ + if (this != &other) + { + mBackendType = other.mBackendType; + mFts = other.mFts; + mCurrentEntry = other.mCurrentEntry; + mRecursive = other.mRecursive; + mCurrentDirectoryEntry = other.mCurrentDirectoryEntry; + mVectorIt = other.mVectorIt; + } + return *this; +} + +DirectoryIterator& DirectoryIterator::operator++() +{ + switch (mBackendType) + { + case FTS_BACKEND: + advanceFts(); + break; + case VECTOR_BACKEND: + ++mVectorIt; + break; + case END_ITERATOR: + // No-op + break; + } + return *this; +} + +DirectoryIterator DirectoryIterator::operator++(int) +{ + DirectoryIterator temp(*this); + operator++(); + return temp; +} + +const DirectoryEntry& DirectoryIterator::operator*() const +{ + switch (mBackendType) + { + case FTS_BACKEND: + return mCurrentDirectoryEntry; + case VECTOR_BACKEND: + return *mVectorIt; + case END_ITERATOR: + default: + return mCurrentDirectoryEntry; // Should not happen, but avoid crash + } +} + +const DirectoryEntry* DirectoryIterator::operator->() const +{ + return &(operator*()); +} + +bool DirectoryIterator::operator==(const DirectoryIterator& other) const +{ + // Different backend types are not equal unless both are end iterators + if (mBackendType != other.mBackendType) + { + return mBackendType == END_ITERATOR && other.mBackendType == END_ITERATOR; + } + + switch (mBackendType) + { + case FTS_BACKEND: + return mFts == other.mFts && mCurrentEntry == other.mCurrentEntry; + case VECTOR_BACKEND: + return mVectorIt == other.mVectorIt; + case END_ITERATOR: + return true; + default: + return false; + } +} + +bool DirectoryIterator::operator!=(const DirectoryIterator& other) const +{ + return !(*this == other); +} + +void DirectoryIterator::advanceFts() +{ + if (!mFts || mBackendType != FTS_BACKEND) + { + mBackendType = END_ITERATOR; + return; + } + + // Stream through FTS entries one at a time + while ((mCurrentEntry = fts_read(mFts)) != nullptr) + { + // Skip entries we don't want to include + if (shouldSkipEntry(mCurrentEntry)) + { + continue; + } + + // Found a valid entry - update our current entry and return + updateCurrentEntryFromFts(); + return; + } + + // No more entries - we've reached the end + mBackendType = END_ITERATOR; + mCurrentEntry = nullptr; +} + +bool DirectoryIterator::shouldSkipEntry(FTSENT* entry) const +{ + if (!entry) + return true; + + // Skip the root directory itself (level 0) + if (entry->fts_level == 0) + { + return true; + } + + // For non-recursive mode, only include immediate children (level 1) + if (!mRecursive && entry->fts_level > 1) + { + // Tell FTS to skip this entire subtree for efficiency + fts_set(mFts, entry, FTS_SKIP); + return true; + } + + // Handle directory entries in non-recursive mode + if (!mRecursive && entry->fts_info == FTS_D && entry->fts_level == 1) + { + // We want to include the directory entry itself, but not descend into it + fts_set(mFts, entry, FTS_SKIP); + return false; // Don't skip - include the directory entry + } + + // Skip post-order directory visits (FTS_DP) - we only want pre-order + if (entry->fts_info == FTS_DP) + { + return true; + } + + // Skip error entries and unreadable entries + if (entry->fts_info == FTS_ERR || entry->fts_info == FTS_NS || entry->fts_info == FTS_DNR) + { + return true; + } + + return false; +} + +DirectoryEntryType DirectoryIterator::getEntryType(int fts_info) const +{ + switch (fts_info) + { + case FTS_F: + return DirectoryEntryType::RegularFile; + case FTS_D: + return DirectoryEntryType::Directory; + case FTS_SL: + case FTS_SLNONE: + return DirectoryEntryType::SymbolicLink; + default: + return DirectoryEntryType::Other; + } +} + +void DirectoryIterator::updateCurrentEntryFromFts() +{ + if (mCurrentEntry) + { + DirectoryEntryType type = getEntryType(mCurrentEntry->fts_info); + mCurrentDirectoryEntry = DirectoryEntry(mCurrentEntry->fts_path, type); + } +} + +// DirectoryEntries implementation +DirectoryEntries::DirectoryEntries(FTS* fts, bool recursive) + : mBackendType(FTS_BACKEND), + mFts(fts), + mRecursive(recursive), + mOwnsHandle(true) +{ + // If FTS is null, we'll return empty iterators + if (!mFts) + { + mOwnsHandle = false; + } +} + +DirectoryEntries::DirectoryEntries(std::vector entries) + : mBackendType(VECTOR_BACKEND), + mFts(nullptr), + mRecursive(false), + mOwnsHandle(false), + mEntries(std::move(entries)) +{ +} + +DirectoryEntries::~DirectoryEntries() +{ + if (mOwnsHandle && mFts) + { + fts_close(mFts); + mFts = nullptr; + } +} + +DirectoryEntries::DirectoryEntries(DirectoryEntries&& other) + : mBackendType(other.mBackendType), + mFts(other.mFts), + mRecursive(other.mRecursive), + mOwnsHandle(other.mOwnsHandle), + mEntries(std::move(other.mEntries)) +{ + other.mFts = nullptr; + other.mOwnsHandle = false; +} + +DirectoryEntries& DirectoryEntries::operator=(DirectoryEntries&& other) +{ + if (this != &other) + { + if (mOwnsHandle && mFts) + { + fts_close(mFts); + } + + mBackendType = other.mBackendType; + mFts = other.mFts; + mRecursive = other.mRecursive; + mOwnsHandle = other.mOwnsHandle; + mEntries = std::move(other.mEntries); + + other.mFts = nullptr; + other.mOwnsHandle = false; + } + return *this; +} + +DirectoryIterator DirectoryEntries::begin() +{ + switch (mBackendType) + { + case FTS_BACKEND: + if (!mFts) + { + return DirectoryIterator(); // End iterator + } + return DirectoryIterator(mFts, mRecursive); + case VECTOR_BACKEND: + return DirectoryIterator(mEntries.begin()); + default: + return DirectoryIterator(); // End iterator + } +} + +DirectoryIterator DirectoryEntries::end() +{ + switch (mBackendType) + { + case FTS_BACKEND: + return DirectoryIterator(); // End iterator + case VECTOR_BACKEND: + return DirectoryIterator(mEntries.end()); + default: + return DirectoryIterator(); // End iterator + } +} + +size_t DirectoryEntries::size() const +{ + switch (mBackendType) + { + case FTS_BACKEND: + // For streaming FTS backend, size is not known until full iteration + // This is a fundamental limitation of lazy streaming + // To get size, caller would need to iterate through all entries + return 0; // Indicates unknown size + case VECTOR_BACKEND: + return mEntries.size(); + default: + return 0; + } +} + +bool DirectoryEntries::empty() const +{ + switch (mBackendType) + { + case FTS_BACKEND: + // For FTS backend, check if we have a valid FTS handle + // This is a best-effort check - true emptiness requires iteration + return !mFts; + case VECTOR_BACKEND: + return mEntries.empty(); + default: + return true; + } +} + +// FtsDirectoryIterator implementation +DirectoryEntries FtsDirectoryIterator::GetEntries(const std::string& directoryPath, bool recursive) const +{ + // Prepare paths for fts_open + char* paths[] = {const_cast(directoryPath.c_str()), nullptr}; + + // Configure FTS options for optimal streaming performance + int options = FTS_PHYSICAL | FTS_NOCHDIR; + + // Use physical traversal for better performance and to avoid symbolic link loops + // FTS_NOCHDIR prevents fts from changing the working directory + + FTS* fts = fts_open(paths, options, nullptr); + if (!fts) + { + // Return empty container if fts_open fails + return DirectoryEntries(static_cast(nullptr), recursive); + } + + // DirectoryEntries takes ownership of the FTS handle and will close it in destructor + return DirectoryEntries(fts, recursive); +} + +DirectoryEntryType FtsDirectoryIterator::GetEntryType(int fts_info) const +{ + switch (fts_info) + { + case FTS_F: + return DirectoryEntryType::RegularFile; + case FTS_D: + return DirectoryEntryType::Directory; + case FTS_SL: + case FTS_SLNONE: + return DirectoryEntryType::SymbolicLink; + default: + return DirectoryEntryType::Other; + } +} + +} // namespace ComplianceEngine diff --git a/src/modules/complianceengine/src/lib/DirectoryEntry.h b/src/modules/complianceengine/src/lib/DirectoryEntry.h new file mode 100644 index 0000000000..eb079e3409 --- /dev/null +++ b/src/modules/complianceengine/src/lib/DirectoryEntry.h @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef COMPLIANCEENGINE_DIRECTORYENTRY_H +#define COMPLIANCEENGINE_DIRECTORYENTRY_H + +#include +#include +#include +#include +#include + +namespace ComplianceEngine +{ + +enum class DirectoryEntryType +{ + RegularFile, + Directory, + SymbolicLink, + Other +}; + +struct DirectoryEntry +{ + std::string path; + DirectoryEntryType type; + + DirectoryEntry(const std::string& p, DirectoryEntryType t) + : path(p), + type(t) + { + } +}; + +// Forward declarations +class DirectoryEntries; + +// Interface for directory iteration - enables mocking +class DirectoryIteratorInterface +{ +public: + virtual ~DirectoryIteratorInterface() = default; + virtual DirectoryEntries GetEntries(const std::string& directoryPath, bool recursive) const = 0; +}; + +// Streaming iterator that advances FTS structure lazily +class DirectoryIterator +{ +public: + using iterator_category = std::forward_iterator_tag; + using value_type = DirectoryEntry; + using difference_type = std::ptrdiff_t; + using pointer = const DirectoryEntry*; + using reference = const DirectoryEntry&; + + // Constructors for different backends + DirectoryIterator(); // End iterator + DirectoryIterator(FTS* fts, bool recursive); // FTS-based streaming iterator + DirectoryIterator(std::vector::const_iterator it); // Vector-based iterator for testing + + ~DirectoryIterator(); + + // Copy constructor and assignment + DirectoryIterator(const DirectoryIterator& other); + DirectoryIterator& operator=(const DirectoryIterator& other); + + // Iterator operations + DirectoryIterator& operator++(); + DirectoryIterator operator++(int); + const DirectoryEntry& operator*() const; + const DirectoryEntry* operator->() const; + bool operator==(const DirectoryIterator& other) const; + bool operator!=(const DirectoryIterator& other) const; + +private: + enum BackendType + { + FTS_BACKEND, + VECTOR_BACKEND, + END_ITERATOR + }; + + BackendType mBackendType; + + // FTS backend data + FTS* mFts; + FTSENT* mCurrentEntry; + bool mRecursive; + DirectoryEntry mCurrentDirectoryEntry; + + // Vector backend data + std::vector::const_iterator mVectorIt; + + void AdvanceFts(); + bool ShouldSkipEntry(FTSENT* entry) const; + DirectoryEntryType GetEntryType(int fts_info) const; + void UpdateCurrentEntryFromFts(); +}; + +// Container class that provides begin/end functionality for range-based loops +class DirectoryEntries +{ +public: + using iterator = DirectoryIterator; + using const_iterator = DirectoryIterator; + + // Constructor for FTS-based streaming iteration (production) + DirectoryEntries(FTS* fts, bool recursive); + + // Constructor for vector-based iteration (testing) + explicit DirectoryEntries(std::vector entries); + + ~DirectoryEntries(); + + // Non-copyable to avoid FTS pointer issues, but movable + DirectoryEntries(const DirectoryEntries&) = delete; + DirectoryEntries& operator=(const DirectoryEntries&) = delete; + + // Move constructor and assignment + DirectoryEntries(DirectoryEntries&& other) noexcept; + DirectoryEntries& operator=(DirectoryEntries&& other) noexcept; + + iterator Begin(); + iterator End(); + size_t Size() const; + bool Empty() const; + +private: + enum BackendType + { + FTS_BACKEND, + VECTOR_BACKEND + }; + + BackendType mBackendType; + + // FTS backend data + FTS* mFts; + bool mRecursive; + bool mOwnsHandle; + + // Vector backend data + std::vector mEntries; +}; + +// Concrete implementation using fts +class FtsDirectoryIterator : public DirectoryIteratorInterface +{ +public: + DirectoryEntries GetEntries(const std::string& directoryPath, bool recursive) const override; + +private: + DirectoryEntryType GetEntryType(int fts_info) const; +}; + +} // namespace ComplianceEngine + +#endif // COMPLIANCEENGINE_DIRECTORYENTRY_H diff --git a/src/modules/complianceengine/tests/CMakeLists.txt b/src/modules/complianceengine/tests/CMakeLists.txt index 53b290bdc4..356bcd9ba4 100644 --- a/src/modules/complianceengine/tests/CMakeLists.txt +++ b/src/modules/complianceengine/tests/CMakeLists.txt @@ -13,6 +13,8 @@ add_executable(complianceenginetests Base64Test.cpp CommonContextTest.cpp ComplianceEngineTest.cpp + DirectoryIterationTest.cpp + DirectoryIterationIntegrationTest.cpp EngineTest.cpp EvaluatorTest.cpp OptionalTest.cpp diff --git a/src/modules/complianceengine/tests/DirectoryIterationIntegrationTest.cpp b/src/modules/complianceengine/tests/DirectoryIterationIntegrationTest.cpp new file mode 100644 index 0000000000..c33e00e9f0 --- /dev/null +++ b/src/modules/complianceengine/tests/DirectoryIterationIntegrationTest.cpp @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "CommonContext.h" +#include "DirectoryEntry.h" + +#include +#include +#include +#include +#include + +using ComplianceEngine::CommonContext; +using ComplianceEngine::DirectoryEntry; +using ComplianceEngine::DirectoryEntryType; + +class DirectoryIterationIntegrationTest : public ::testing::Test +{ +protected: + std::string mTestDir; + CommonContext mContext; + + DirectoryIterationIntegrationTest() + : mContext(nullptr) + { + } + + void SetUp() override + { + char tempTemplate[] = "/tmp/dir_test_XXXXXX"; + mTestDir = mkdtemp(tempTemplate); + ASSERT_FALSE(mTestDir.empty()); + + CreateTestFile(mTestDir + "/file1.txt", "content1"); + CreateTestFile(mTestDir + "/file2.txt", "content2"); + CreateTestDirectory(mTestDir + "/subdir"); + CreateTestFile(mTestDir + "/subdir/nested_file.txt", "nested content"); + CreateTestDirectory(mTestDir + "/subdir/nested_dir"); + CreateTestFile(mTestDir + "/subdir/nested_dir/deep_file.txt", "deep content"); + } + + void TearDown() override + { + if (!mTestDir.empty()) + { + system(("rm -rf " + mTestDir).c_str()); + } + } + +private: + void CreateTestFile(const std::string& path, const std::string& content) + { + std::ofstream file(path); + file << content; + file.close(); + } + + void CreateTestDirectory(const std::string& path) + { + mkdir(path.c_str(), 0755); + } +}; + +TEST_F(DirectoryIterationIntegrationTest, NonRecursiveRealDirectory) +{ + auto result = mContext.GetDirectoryEntries(mTestDir, false); + + ASSERT_TRUE(result.HasValue()); + + auto entries = std::move(result).Value(); + // Note: size() doesn't work for streaming iterators, so we count by iteration + + std::vector filenames; + for (const auto& entry : entries) + { + // Extract just the filename from the full path + size_t lastSlash = entry.path.rfind('/'); + if (lastSlash != std::string::npos) + { + filenames.push_back(entry.path.substr(lastSlash + 1)); + } + } + + // Should contain our test files and directory - at least 3 entries + EXPECT_GE(filenames.size(), 3); + bool hasFile1 = std::find(filenames.begin(), filenames.end(), std::string("file1.txt")) != filenames.end(); + bool hasFile2 = std::find(filenames.begin(), filenames.end(), std::string("file2.txt")) != filenames.end(); + bool hasSubdir = std::find(filenames.begin(), filenames.end(), std::string("subdir")) != filenames.end(); + + EXPECT_TRUE(hasFile1); + EXPECT_TRUE(hasFile2); + EXPECT_TRUE(hasSubdir); + + // Should not contain nested files for non-recursive search + bool hasNestedFile = std::find(filenames.begin(), filenames.end(), std::string("nested_file.txt")) != filenames.end(); + EXPECT_FALSE(hasNestedFile); +} + +TEST_F(DirectoryIterationIntegrationTest, RecursiveRealDirectory) +{ + auto result = mContext.GetDirectoryEntries(mTestDir, true); + + ASSERT_TRUE(result.HasValue()); + + auto entries = std::move(result).Value(); + // Note: size() doesn't work for streaming iterators, so we count manually + + bool foundNestedFile = false; + bool foundDeepFile = false; + int totalCount = 0; + + for (const auto& entry : entries) + { + totalCount++; + if (entry.path.find("nested_file.txt") != std::string::npos) + { + foundNestedFile = true; + } + if (entry.path.find("deep_file.txt") != std::string::npos) + { + foundDeepFile = true; + } + } + + EXPECT_GE(totalCount, 5); // Should have at least 5 entries in recursive mode + EXPECT_TRUE(foundNestedFile); + EXPECT_TRUE(foundDeepFile); +} + +TEST_F(DirectoryIterationIntegrationTest, NonExistentDirectory) +{ + auto result = mContext.GetDirectoryEntries("/this/path/does/not/exist", false); + + // Should return an empty result, not an error (fts behavior) + ASSERT_TRUE(result.HasValue()); + auto entries = std::move(result).Value(); + + // Count entries by iteration since empty() doesn't work reliably for streaming + int count = 0; + for (const auto& entry : entries) + { + (void)entry; // Suppress unused variable warning + count++; + } + EXPECT_EQ(count, 0); +} + +TEST_F(DirectoryIterationIntegrationTest, EmptyDirectory) +{ + std::string emptyDir = mTestDir + "/empty_subdir"; + mkdir(emptyDir.c_str(), 0755); + + auto result = mContext.GetDirectoryEntries(emptyDir, false); + + ASSERT_TRUE(result.HasValue()); + auto entries = std::move(result).Value(); + + // Count entries by iteration since empty() doesn't work reliably for streaming + int count = 0; + for (const auto& entry : entries) + { + (void)entry; // Suppress unused variable warning + count++; + } + EXPECT_EQ(count, 0); +} diff --git a/src/modules/complianceengine/tests/DirectoryIterationTest.cpp b/src/modules/complianceengine/tests/DirectoryIterationTest.cpp new file mode 100644 index 0000000000..a75c032aa2 --- /dev/null +++ b/src/modules/complianceengine/tests/DirectoryIterationTest.cpp @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "DirectoryEntry.h" +#include "MockContext.h" + +#include +#include + +using ComplianceEngine::DirectoryEntries; +using ComplianceEngine::DirectoryEntry; +using ComplianceEngine::DirectoryEntryType; +using ComplianceEngine::Result; +using ::testing::Return; + +class DirectoryIterationTest : public ::testing::Test +{ +protected: + MockContext mContext; + + void SetUp() override + { + } +}; + +TEST_F(DirectoryIterationTest, NonRecursiveDirectoryIteration) +{ + std::vector mockEntries = {DirectoryEntry("/test/file1.txt", DirectoryEntryType::RegularFile), + DirectoryEntry("/test/file2.txt", DirectoryEntryType::RegularFile), DirectoryEntry("/test/subdir", DirectoryEntryType::Directory)}; + + EXPECT_CALL(mContext, GetDirectoryEntries("/test", false)).WillOnce([mockEntries](const std::string&, bool) { + return Result(DirectoryEntries(mockEntries)); + }); + + auto result = mContext.GetDirectoryEntries("/test", false); + + ASSERT_TRUE(result.HasValue()); + + auto entries = std::move(result).Value(); + ASSERT_EQ(entries.size(), 3); + + std::vector paths; + for (const auto& entry : entries) + { + paths.push_back(entry.path); + } + + EXPECT_EQ(paths.size(), 3); + EXPECT_EQ(paths[0], "/test/file1.txt"); + EXPECT_EQ(paths[1], "/test/file2.txt"); + EXPECT_EQ(paths[2], "/test/subdir"); +} + +TEST_F(DirectoryIterationTest, RecursiveDirectoryIteration) +{ + std::vector mockEntries = {DirectoryEntry("/test/file1.txt", DirectoryEntryType::RegularFile), + DirectoryEntry("/test/subdir", DirectoryEntryType::Directory), DirectoryEntry("/test/subdir/nested_file.txt", DirectoryEntryType::RegularFile), + DirectoryEntry("/test/subdir/another_dir", DirectoryEntryType::Directory), + DirectoryEntry("/test/subdir/another_dir/deep_file.txt", DirectoryEntryType::RegularFile)}; + + EXPECT_CALL(mContext, GetDirectoryEntries("/test", true)).WillOnce([mockEntries](const std::string&, bool) { + return Result(DirectoryEntries(mockEntries)); + }); + + auto result = mContext.GetDirectoryEntries("/test", true); + + ASSERT_TRUE(result.HasValue()); + + auto entries = std::move(result).Value(); + ASSERT_EQ(entries.size(), 5); + + auto it = entries.begin(); + EXPECT_EQ(it->path, "/test/file1.txt"); + EXPECT_EQ(it->type, DirectoryEntryType::RegularFile); + + ++it; + EXPECT_EQ(it->path, "/test/subdir"); + EXPECT_EQ(it->type, DirectoryEntryType::Directory); + + int fileCount = 0; + for (const auto& entry : entries) + { + if (entry.type == DirectoryEntryType::RegularFile) + { + fileCount++; + } + } + EXPECT_EQ(fileCount, 3); +} + +TEST_F(DirectoryIterationTest, DirectoryIterationError) +{ + EXPECT_CALL(mContext, GetDirectoryEntries("/nonexistent", false)).WillOnce([](const std::string&, bool) { + return Result(ComplianceEngine::Error("Directory not found", -1)); + }); + + auto result = mContext.GetDirectoryEntries("/nonexistent", false); + + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.Error().message, "Directory not found"); + EXPECT_EQ(result.Error().code, -1); +} + +TEST_F(DirectoryIterationTest, EmptyDirectoryIteration) +{ + std::vector emptyEntries; + + EXPECT_CALL(mContext, GetDirectoryEntries("/empty", false)).WillOnce([emptyEntries](const std::string&, bool) { + return Result(DirectoryEntries(emptyEntries)); + }); + + auto result = mContext.GetDirectoryEntries("/empty", false); + + ASSERT_TRUE(result.HasValue()); + + auto entries = std::move(result).Value(); + EXPECT_TRUE(entries.empty()); + EXPECT_EQ(entries.size(), 0); + + int count = 0; + for (const auto& entry : entries) + { + (void)entry; // Suppress unused variable warning + count++; + } + EXPECT_EQ(count, 0); +} + +TEST_F(DirectoryIterationTest, DifferentFileTypes) +{ + std::vector mockEntries = {DirectoryEntry("/test/regular.txt", DirectoryEntryType::RegularFile), + DirectoryEntry("/test/subdir", DirectoryEntryType::Directory), DirectoryEntry("/test/symlink", DirectoryEntryType::SymbolicLink), + DirectoryEntry("/test/other", DirectoryEntryType::Other)}; + + EXPECT_CALL(mContext, GetDirectoryEntries("/test", false)).WillOnce([mockEntries](const std::string&, bool) { + return Result(DirectoryEntries(mockEntries)); + }); + + auto result = mContext.GetDirectoryEntries("/test", false); + + ASSERT_TRUE(result.HasValue()); + + auto entries = std::move(result).Value(); + ASSERT_EQ(entries.size(), 4); + + std::map typeCounts; + for (const auto& entry : entries) + { + typeCounts[entry.type]++; + } + + EXPECT_EQ(typeCounts[DirectoryEntryType::RegularFile], 1); + EXPECT_EQ(typeCounts[DirectoryEntryType::Directory], 1); + EXPECT_EQ(typeCounts[DirectoryEntryType::SymbolicLink], 1); + EXPECT_EQ(typeCounts[DirectoryEntryType::Other], 1); +} diff --git a/src/modules/complianceengine/tests/MockContext.h b/src/modules/complianceengine/tests/MockContext.h index 6b2a1b31de..fa1d959d8d 100644 --- a/src/modules/complianceengine/tests/MockContext.h +++ b/src/modules/complianceengine/tests/MockContext.h @@ -10,6 +10,8 @@ struct MockContext : public ComplianceEngine::ContextInterface { MOCK_METHOD(ComplianceEngine::Result, ExecuteCommand, (const std::string& cmd), (const, override)); MOCK_METHOD(ComplianceEngine::Result, GetFileContents, (const std::string& filePath), (const, override)); + MOCK_METHOD(ComplianceEngine::Result, GetDirectoryEntries, (const std::string& directoryPath, bool recursive), + (const, override)); OsConfigLogHandle GetLogHandle() const override {