diff --git a/.gitignore b/.gitignore index 25185dc..ebe1990 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ CMakeUserPresets.json ### VCPKG ### # Temporary measure which fixes build agent issue vcpkg_registry_cache/ + +### Ignored Test Run Files ### +/tests/testData/IgnoredTestOutputFiles/ diff --git a/CMakeLists.txt b/CMakeLists.txt index ce48c23..59710f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ include(cmake/CcpPackageConfigHelpers.cmake) include(cmake/CcpBuildConfigurations.cmake) find_package(yaml-cpp CONFIG REQUIRED) +find_package(unofficial-inih CONFIG REQUIRED) # Add subdirectory for resource tools static library add_subdirectory(tools) @@ -66,7 +67,7 @@ endif () target_compile_definitions(resources PUBLIC CARBON_RESOURCES_STATIC) -target_link_libraries(resources PRIVATE $ yaml-cpp::yaml-cpp) +target_link_libraries(resources PRIVATE $ yaml-cpp::yaml-cpp unofficial::inih::inireader) target_include_directories(resources PUBLIC diff --git a/cli/src/CreateResourceGroupCliOperation.cpp b/cli/src/CreateResourceGroupCliOperation.cpp index d4b567a..6bc907f 100644 --- a/cli/src/CreateResourceGroupCliOperation.cpp +++ b/cli/src/CreateResourceGroupCliOperation.cpp @@ -15,7 +15,8 @@ CreateResourceGroupCliOperation::CreateResourceGroupCliOperation() : m_createResourceGroupSkipCompressionCalculationId( "--skip-compression" ), m_createResourceGroupExportResourcesId( "--export-resources" ), m_createResourceGroupExportResourcesDestinationTypeId( "--export-resources-destination-type" ), - m_createResourceGroupExportResourcesDestinationPathId( "--export-resources-destination-path" ) + m_createResourceGroupExportResourcesDestinationPathId( "--export-resources-destination-path" ), + m_createResourceGroupIniFilterFilesArgumentId( "--filter-file" ) { AddRequiredPositionalArgument( m_createResourceGroupPathArgumentId, "Base directory to create resource group from." ); @@ -32,25 +33,27 @@ CreateResourceGroupCliOperation::CreateResourceGroupCliOperation() : AddArgument( m_createResourceGroupDocumentVersionArgumentId, "Document version for created resource group.", false, false, VersionToString( defaultImportParams.outputDocumentVersion ) ); AddArgument( m_createResourceGroupResourcePrefixArgumentId, R"(Optional resource path prefix, such as "res" or "app")", false, false, "" ); - - AddArgumentFlag( m_createResourceGroupSkipCompressionCalculationId, "Set skip compression calculations on resources." ); - AddArgumentFlag( m_createResourceGroupExportResourcesId, "Export resources after processing. see --export-resources-destination-type and --export-resources-destination-path" ); + AddArgumentFlag( m_createResourceGroupSkipCompressionCalculationId, "Set skip compression calculations on resources." ); - AddArgument( m_createResourceGroupExportResourcesDestinationTypeId, "Represents the type of repository where exported resources will be saved. Requires --export-resources", false, false, DestinationTypeToString( defaultImportParams.exportResourcesDestinationSettings.destinationType ), ResourceDestinationTypeChoicesAsString() ); + AddArgumentFlag( m_createResourceGroupExportResourcesId, "Export resources after processing. see --export-resources-destination-type and --export-resources-destination-path" ); + + AddArgument( m_createResourceGroupExportResourcesDestinationTypeId, "Represents the type of repository where exported resources will be saved. Requires --export-resources", false, false, DestinationTypeToString( defaultImportParams.exportResourcesDestinationSettings.destinationType ), ResourceDestinationTypeChoicesAsString() ); AddArgument( m_createResourceGroupExportResourcesDestinationPathId, "Represents the base path where the exported resources will be saved. Requires --export-resources", false, false, defaultImportParams.exportResourcesDestinationSettings.basePath.string() ); + + AddArgument( m_createResourceGroupIniFilterFilesArgumentId, "Path to INI file(s) for resource filtering.", false, true, "" ); } bool CreateResourceGroupCliOperation::Execute( std::string& returnErrorMessage ) const { CarbonResources::CreateResourceGroupFromDirectoryParams createResourceGroupParams; - CarbonResources::ResourceGroupExportToFileParams exportParams; + CarbonResources::ResourceGroupExportToFileParams exportParams; - createResourceGroupParams.directory = m_argumentParser->get( m_createResourceGroupPathArgumentId ); + createResourceGroupParams.directory = m_argumentParser->get( m_createResourceGroupPathArgumentId ); - bool versionIsValid = ParseDocumentVersion( m_argumentParser->get( m_createResourceGroupDocumentVersionArgumentId ), createResourceGroupParams.outputDocumentVersion ); + bool versionIsValid = ParseDocumentVersion( m_argumentParser->get( m_createResourceGroupDocumentVersionArgumentId ), createResourceGroupParams.outputDocumentVersion ); if( !versionIsValid ) { @@ -63,7 +66,7 @@ bool CreateResourceGroupCliOperation::Execute( std::string& returnErrorMessage ) createResourceGroupParams.calculateCompressions = !m_argumentParser->get( m_createResourceGroupSkipCompressionCalculationId ); - createResourceGroupParams.exportResources = m_argumentParser->get( m_createResourceGroupExportResourcesId ); + createResourceGroupParams.exportResources = m_argumentParser->get( m_createResourceGroupExportResourcesId ); if( createResourceGroupParams.exportResources ) { @@ -79,17 +82,36 @@ bool CreateResourceGroupCliOperation::Execute( std::string& returnErrorMessage ) createResourceGroupParams.exportResourcesDestinationSettings.basePath = m_argumentParser->get( m_createResourceGroupExportResourcesDestinationPathId ); } + exportParams.filename = m_argumentParser->get( m_createResourceGroupOutputFileArgumentId ); - exportParams.filename = m_argumentParser->get( m_createResourceGroupOutputFileArgumentId ); + exportParams.outputDocumentVersion = createResourceGroupParams.outputDocumentVersion; - exportParams.outputDocumentVersion = createResourceGroupParams.outputDocumentVersion; + if( m_argumentParser->is_used( m_createResourceGroupIniFilterFilesArgumentId )) + { + std::vector filterIniFilePaths; + auto iniFileStringVector = m_argumentParser->get>( m_createResourceGroupIniFilterFilesArgumentId ); + + for( const auto& iniPathStr : iniFileStringVector ) + { + if( !iniPathStr.empty() ) + { + filterIniFilePaths.push_back( iniPathStr ); + } + } + if ( !filterIniFilePaths.empty() ) + { + createResourceGroupParams.resourceFilterIniFiles = filterIniFilePaths; + } + } PrintStartBanner( createResourceGroupParams, exportParams ); return CreateResourceGroup( createResourceGroupParams, exportParams ); } -void CreateResourceGroupCliOperation::PrintStartBanner( CarbonResources::CreateResourceGroupFromDirectoryParams& createResourceGroupFromDirectoryParams, CarbonResources::ResourceGroupExportToFileParams& ResourceGroupExportToFileParams ) const +void CreateResourceGroupCliOperation::PrintStartBanner( + CarbonResources::CreateResourceGroupFromDirectoryParams& createResourceGroupFromDirectoryParams, + CarbonResources::ResourceGroupExportToFileParams& ResourceGroupExportToFileParams ) const { if( s_verbosityLevel == CarbonResources::StatusLevel::OFF ) { @@ -104,41 +126,57 @@ void CreateResourceGroupCliOperation::PrintStartBanner( CarbonResources::CreateR std::cout << "Output File: " << ResourceGroupExportToFileParams.filename << std::endl; - std::cout << "Output Document Version: " << VersionToString(ResourceGroupExportToFileParams.outputDocumentVersion) << std::endl; + std::cout << "Output Document Version: " << VersionToString( ResourceGroupExportToFileParams.outputDocumentVersion ) << std::endl; std::cout << "Resource Prefix: " << createResourceGroupFromDirectoryParams.resourcePrefix << std::endl; - if( createResourceGroupFromDirectoryParams.calculateCompressions) - { + if( createResourceGroupFromDirectoryParams.calculateCompressions ) + { std::cout << "Calculate Compression: On" << std::endl; - } + } else { std::cout << "Calculate Compression: Off" << std::endl; } - if( createResourceGroupFromDirectoryParams.exportResources ) + if( createResourceGroupFromDirectoryParams.exportResources ) { std::cout << "Export Resources: On" << std::endl; - std::cout << "Export Resources Type: " << DestinationTypeToString( createResourceGroupFromDirectoryParams.exportResourcesDestinationSettings.destinationType ) << std::endl; + std::cout << "Export Resources Type: " << DestinationTypeToString( createResourceGroupFromDirectoryParams.exportResourcesDestinationSettings.destinationType ) << std::endl; - std::cout << "Export Resources Base Path: " << createResourceGroupFromDirectoryParams.exportResourcesDestinationSettings.basePath << std::endl; + std::cout << "Export Resources Base Path: " << createResourceGroupFromDirectoryParams.exportResourcesDestinationSettings.basePath << std::endl; } else { std::cout << "Export Resources: Off" << std::endl; } + if( createResourceGroupFromDirectoryParams.resourceFilterIniFiles.size() > 0 ) + { + std::cout << "Resource Filter INI File(s) used: " << std::endl; + + for( const auto& iniPath : createResourceGroupFromDirectoryParams.resourceFilterIniFiles ) + { + std::cout << " - " << iniPath.generic_string() << std::endl; + } + } + else + { + std::cout << "Resource Filter INI File(s) used: None" << std::endl; + } + std::cout << "----------------------------\n" << std::endl; } -bool CreateResourceGroupCliOperation::CreateResourceGroup( CarbonResources::CreateResourceGroupFromDirectoryParams& createResourceGroupFromDirectoryParams, CarbonResources::ResourceGroupExportToFileParams& ResourceGroupExportToFileParams ) const +bool CreateResourceGroupCliOperation::CreateResourceGroup( + CarbonResources::CreateResourceGroupFromDirectoryParams& createResourceGroupFromDirectoryParams, + CarbonResources::ResourceGroupExportToFileParams& ResourceGroupExportToFileParams ) const { CarbonResources::ResourceGroup resourceGroup; - createResourceGroupFromDirectoryParams.statusCallback = GetStatusCallback(); + createResourceGroupFromDirectoryParams.statusCallback = GetStatusCallback(); if( createResourceGroupFromDirectoryParams.statusCallback ) { diff --git a/cli/src/CreateResourceGroupCliOperation.h b/cli/src/CreateResourceGroupCliOperation.h index ca9cee1..d40f982 100644 --- a/cli/src/CreateResourceGroupCliOperation.h +++ b/cli/src/CreateResourceGroupCliOperation.h @@ -18,9 +18,13 @@ class CreateResourceGroupCliOperation : public CliOperation virtual bool Execute( std::string& returnErrorMessage ) const final; private: - void PrintStartBanner( CarbonResources::CreateResourceGroupFromDirectoryParams& createResourceGroupFromDirectoryParams, CarbonResources::ResourceGroupExportToFileParams& ResourceGroupExportToFileParams ) const; + void PrintStartBanner( + CarbonResources::CreateResourceGroupFromDirectoryParams& createResourceGroupFromDirectoryParams, + CarbonResources::ResourceGroupExportToFileParams& ResourceGroupExportToFileParams ) const; - bool CreateResourceGroup( CarbonResources::CreateResourceGroupFromDirectoryParams& createResourceGroupFromDirectoryParams, CarbonResources::ResourceGroupExportToFileParams& ResourceGroupExportToFileParams ) const; + bool CreateResourceGroup( + CarbonResources::CreateResourceGroupFromDirectoryParams& createResourceGroupFromDirectoryParams, + CarbonResources::ResourceGroupExportToFileParams& ResourceGroupExportToFileParams ) const; private: std::string m_createResourceGroupPathArgumentId; @@ -30,14 +34,16 @@ class CreateResourceGroupCliOperation : public CliOperation std::string m_createResourceGroupDocumentVersionArgumentId; std::string m_createResourceGroupResourcePrefixArgumentId; - - std::string m_createResourceGroupSkipCompressionCalculationId; - std::string m_createResourceGroupExportResourcesId; + std::string m_createResourceGroupSkipCompressionCalculationId; - std::string m_createResourceGroupExportResourcesDestinationTypeId; - - std::string m_createResourceGroupExportResourcesDestinationPathId; + std::string m_createResourceGroupExportResourcesId; + + std::string m_createResourceGroupExportResourcesDestinationTypeId; + + std::string m_createResourceGroupExportResourcesDestinationPathId; + + std::string m_createResourceGroupIniFilterFilesArgumentId; }; #endif // CreateResourceGroupCliOperation_H \ No newline at end of file diff --git a/include/Enums.h b/include/Enums.h index 119f94c..6923c81 100644 --- a/include/Enums.h +++ b/include/Enums.h @@ -135,6 +135,10 @@ using StatusCallback = std::function resourceFilterIniFiles = {}; }; /** @struct ResourceGroupMergeParams diff --git a/src/Enums.cpp b/src/Enums.cpp index a7a303b..677ebf2 100644 --- a/src/Enums.cpp +++ b/src/Enums.cpp @@ -156,6 +156,14 @@ bool ResultTypeToString( ResultType resultType, std::string& output ) case ResultType::REQUIRED_INPUT_PARAMETER_NOT_SET: output = "A required parameter was not set"; return true; + + case ResultType::FAILED_TO_INITIALIZE_RESOURCE_FILTER: + output = "Failed to initialize ResourceFilter from .ini file"; + return true; + + case ResultType::FAILED_TO_APPLY_RESOURCE_FILTER_RULES: + output = "Unable to decide on include/exclude filtering rules for resource"; + return true; } output = "Error code unrecognised. This is an internal library error which shouldn't be encountered. If you encounter this error contact API addministrators."; diff --git a/src/ResourceGroupImpl.cpp b/src/ResourceGroupImpl.cpp index fa74bd1..5ff17f8 100644 --- a/src/ResourceGroupImpl.cpp +++ b/src/ResourceGroupImpl.cpp @@ -19,6 +19,7 @@ #include "BundleResourceGroupImpl.h" #include "ChunkIndex.h" #include "ResourceGroupFactory.h" +#include namespace CarbonResources { @@ -66,6 +67,21 @@ Result ResourceGroup::ResourceGroupImpl::CreateFromDirectory( const CreateResour return Result{ ResultType::DOCUMENT_VERSION_UNSUPPORTED }; } + // Initialize ResourceFilter if .ini files are supplied + ResourceTools::ResourceFilter resourceFilter; + if( !params.resourceFilterIniFiles.empty() ) + { + try + { + resourceFilter.Initialize( params.resourceFilterIniFiles ); + } + catch( const std::exception& e ) + { + std::string errorMsg = "Unable to create ResourceFilter - because of: " + std::string( e.what() ); + return Result{ ResultType::FAILED_TO_INITIALIZE_RESOURCE_FILTER, errorMsg }; + } + } + // Walk directory and create a resource from each file using data auto recursiveDirectoryIter = std::filesystem::recursive_directory_iterator( params.directory ); @@ -73,6 +89,25 @@ Result ResourceGroup::ResourceGroupImpl::CreateFromDirectory( const CreateResour { if( entry.is_regular_file() ) { + // Apply Resource filtering (in case any filters are supplied) + if( resourceFilter.HasFilters() ) + { + try + { + // Resource filtering: + // Check if the file, i.e. entry.path() should be included or excluded based on filtering rules + if( !resourceFilter.ShouldInclude( entry.path() ) ) + { + continue; + } + } + catch( const std::exception& e ) + { + std::string errorMsg = "Unable to decide on include/exclude filtering for: " + entry.path().generic_string() + " - because of: " + std::string( e.what() ); + return Result{ ResultType::FAILED_TO_APPLY_RESOURCE_FILTER_RULES, errorMsg }; + } + } + // Update status if( params.statusCallback ) { @@ -126,22 +161,22 @@ Result ResourceGroup::ResourceGroupImpl::CreateFromDirectory( const CreateResour return addResourceResult; } - // If resources are set to be exported, then export as specified - if (params.exportResources) - { + // If resources are set to be exported, then export as specified + if( params.exportResources ) + { ResourcePutDataParams putDataParams; - putDataParams.resourceDestinationSettings = params.exportResourcesDestinationSettings; + putDataParams.resourceDestinationSettings = params.exportResourcesDestinationSettings; - putDataParams.data = &resourceData; + putDataParams.data = &resourceData; Result putDataResult = resource->PutData( putDataParams ); - if( putDataResult.type != ResultType::SUCCESS ) + if( putDataResult.type != ResultType::SUCCESS ) { return putDataResult; } - } + } } else { @@ -153,13 +188,13 @@ Result ResourceGroup::ResourceGroupImpl::CreateFromDirectory( const CreateResour ResourceTools::FileDataStreamIn fileStreamIn( params.resourceStreamThreshold ); - if (params.calculateCompressions) - { + if( params.calculateCompressions ) + { if( !compressionStream.Start() ) { return Result{ ResultType::FAILED_TO_COMPRESS_DATA }; } - } + } if( !fileStreamIn.StartRead( entry.path() ) ) { @@ -252,31 +287,31 @@ Result ResourceGroup::ResourceGroupImpl::CreateFromDirectory( const CreateResour return addResourceResult; } - // If resources are set to be exported, then export as specified. - // This is slow with large files as each need to be streamed again - // The problem is that checksum of the whole file needs to be calculated first - // in order to get the correct destination CDN path - // If compression is not skipped and REMOTE_CDN is chosen as destination then - // compression will also be calculated twice. - // This can be improved with a refactor but currently this code path not + // If resources are set to be exported, then export as specified. + // This is slow with large files as each need to be streamed again + // The problem is that checksum of the whole file needs to be calculated first + // in order to get the correct destination CDN path + // If compression is not skipped and REMOTE_CDN is chosen as destination then + // compression will also be calculated twice. + // This can be improved with a refactor but currently this code path not // likely to be relied upon often if( params.exportResources ) { ResourcePutDataStreamParams putDataStreamParams; - // Create the correct file data streaming for the desination - std::unique_ptr resourceDataStreamOut; + // Create the correct file data streaming for the desination + std::unique_ptr resourceDataStreamOut; - if (params.exportResourcesDestinationSettings.destinationType == ResourceDestinationType::REMOTE_CDN) - { - // REMOTE_CDN requires compression + if( params.exportResourcesDestinationSettings.destinationType == ResourceDestinationType::REMOTE_CDN ) + { + // REMOTE_CDN requires compression resourceDataStreamOut = std::make_unique(); - } - else - { - // Else just stream out uncompressed + } + else + { + // Else just stream out uncompressed resourceDataStreamOut = std::make_unique(); - } + } putDataStreamParams.resourceDestinationSettings = params.exportResourcesDestinationSettings; @@ -289,44 +324,42 @@ Result ResourceGroup::ResourceGroupImpl::CreateFromDirectory( const CreateResour return putDataStreamResult; } - // Export resource using streaming + // Export resource using streaming ResourceTools::FileDataStreamIn fileStreamIn( params.resourceStreamThreshold ); - if( !fileStreamIn.StartRead( entry.path() ) ) + if( !fileStreamIn.StartRead( entry.path() ) ) { return Result{ ResultType::FAILED_TO_OPEN_FILE_STREAM }; } - while( !fileStreamIn.IsFinished() ) + while( !fileStreamIn.IsFinished() ) { std::string data = ""; - if (!(fileStreamIn >> data)) - { + if( !( fileStreamIn >> data ) ) + { return Result{ ResultType::FAILED_TO_READ_FROM_STREAM }; - } + } - if (!(resourceDataStreamOut->operator<<(data))) - { + if( !( resourceDataStreamOut->operator<<( data ) ) ) + { return Result{ ResultType::FAILED_TO_SAVE_TO_STREAM }; - } + } } - if (!resourceDataStreamOut->Finish()) - { + if( !resourceDataStreamOut->Finish() ) + { return Result{ ResultType::FAILED_TO_SAVE_TO_STREAM }; - } - + } } - } } } - if (!params.calculateCompressions) - { + if( !params.calculateCompressions ) + { m_totalResourcesSizeCompressed.Reset(); - } + } if( params.statusCallback ) { @@ -877,14 +910,13 @@ Result ResourceGroup::ResourceGroupImpl::ExportYaml( const VersionInternal& outp out << YAML::Key << m_numberOfResources.GetTag(); out << YAML::Value << m_numberOfResources.GetValue(); - if (m_totalResourcesSizeCompressed.HasValue()) - { + if( m_totalResourcesSizeCompressed.HasValue() ) + { uintmax_t compressedSize = m_totalResourcesSizeCompressed.GetValue(); out << YAML::Key << m_totalResourcesSizeCompressed.GetTag(); out << YAML::Value << compressedSize; - - } + } out << YAML::Key << m_totalResourcesSizeUncompressed.GetTag(); out << YAML::Value << m_totalResourcesSizeUncompressed.GetValue(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 00e15d1..c0f8453 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,8 @@ set(SRC_FILES src/ResourcesLibraryTest.cpp src/ResourcesCliTest.cpp src/ResourceToolsLibraryTest.cpp + src/ResourceFilterTest.cpp + src/ResourceFilterTest.h ) add_executable(resources-test ${SRC_FILES}) diff --git a/tests/src/CliTestFixture.cpp b/tests/src/CliTestFixture.cpp index 95e6ec2..9034863 100644 --- a/tests/src/CliTestFixture.cpp +++ b/tests/src/CliTestFixture.cpp @@ -21,4 +21,15 @@ int CliTestFixture::RunCli( std::vector& arguments, std::string& ou output = processOutput; return exit_status; -} \ No newline at end of file +} + +void CliTestFixture::CleanupTestOutputFiles( const std::vector& filesToRemove ) +{ + for( const auto& filePath : filesToRemove ) + { + if( std::filesystem::exists( filePath ) ) + { + std::filesystem::remove( filePath ); + } + } +} diff --git a/tests/src/CliTestFixture.h b/tests/src/CliTestFixture.h index be936e7..541ea3f 100644 --- a/tests/src/CliTestFixture.h +++ b/tests/src/CliTestFixture.h @@ -13,6 +13,8 @@ struct CliTestFixture : public ResourcesTestFixture { int RunCli( std::vector& arguments, std::string& output ); + + void CleanupTestOutputFiles( const std::vector& filesToRemove ); }; #endif // CliTestFixture_H \ No newline at end of file diff --git a/tests/src/ResourceFilterTest.cpp b/tests/src/ResourceFilterTest.cpp new file mode 100644 index 0000000..1a2e64a --- /dev/null +++ b/tests/src/ResourceFilterTest.cpp @@ -0,0 +1,2174 @@ +// Copyright © 2025 CCP ehf. + +#include "ResourceFilterTest.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TEST_F( ResourceFilterTest, Example1IniParsing ) +{ + // Use the test fixture's helper to get the absolute path + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/example1.ini" ); + INIReader reader( iniPath.string() ); + ASSERT_EQ( reader.ParseError(), 0 ) << "Failed to parse example1.ini"; + + // There should only be 2 sections + EXPECT_EQ( reader.Sections().size(), 2 ); + + // Check [default] section + ASSERT_TRUE( reader.HasSection( "DEFAULT" ) ); + EXPECT_EQ( reader.Get( "DEFAULT", "prefixmap", "" ), "res:./Indicies;./resourcesOnBranch res2:./ResourceGroups" ); + EXPECT_EQ( reader.Get( "DEFAULT", "version", "" ), "1.2" ); + EXPECT_EQ( reader.Keys( "DEFAULT" ).size(), 2 ); + + // Check [testyamlfilesovermultilinerespaths] section + ASSERT_TRUE( reader.HasSection( "testYamlFilesOverMultiLineResPathsWithEmptyLines" ) ); + EXPECT_EQ( reader.Get( "testYamlFilesOverMultiLineResPathsWithEmptyLines", "filter", "" ), "[ .yaml ]" ); + EXPECT_EQ( reader.Get( "testYamlFilesOverMultiLineResPathsWithEmptyLines", "resfile", "" ), "res:/binaryFileIndex_v0_0_0.txt" ); + auto respathValueGet = reader.Get( "testYamlFilesOverMultiLineResPathsWithEmptyLines", "respaths", "" ); + std::string respathValueGetString = reader.Get( "testYamlFilesOverMultiLineResPathsWithEmptyLines", "respaths", "" ); + EXPECT_EQ( respathValueGet, respathValueGetString ); + EXPECT_EQ( respathValueGet, "res:/firstLine/...\nres:/secondLine/*\nres2:/thirdLine/..." ); // Note: Under the hood, INIReader converts multi-empty-lines to a single \n line breaks + EXPECT_EQ( reader.Keys( "testYamlFilesOverMultiLineResPathsWithEmptyLines" ).size(), 3 ); +} + +// ----------------------------------------- + +TEST_F( ResourceFilterTest, FilterResourceFilter_OnlyIncludeFilter ) +{ + ResourceTools::FilterResourceFilter filter( "[ .this .is .included ]" ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + EXPECT_EQ( includes.size(), 3 ); + EXPECT_EQ( includes[0], ".this" ); + EXPECT_EQ( includes[1], ".is" ); + EXPECT_EQ( includes[2], ".included" ); + EXPECT_TRUE( excludes.empty() ); +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_OnlyExcludeFilter_Toplevel ) +{ + ResourceTools::FilterResourceFilter filter( "![ .excluded .extension ]", true ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + + EXPECT_EQ( excludes.size(), 2 ); + EXPECT_EQ( excludes[0], ".excluded" ); + EXPECT_EQ( excludes[1], ".extension" ); + + EXPECT_EQ( includes.size(), 1 ); + EXPECT_EQ( includes[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_OnlyExcludeFilter_Inline ) +{ + ResourceTools::FilterResourceFilter filter( "![ .excluded .extension ]" ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + + EXPECT_EQ( excludes.size(), 2 ); + EXPECT_EQ( excludes[0], ".excluded" ); + EXPECT_EQ( excludes[1], ".extension" ); + + EXPECT_EQ( includes.size(), 0 ); + EXPECT_TRUE( includes.empty() ); // No wild-card added when no include filter specified INLINE +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_ComplexIncludeExcludeFilter ) +{ + ResourceTools::FilterResourceFilter filter( "[ .red .gr2 .dds .png .yaml ] [ .txt ] ![ .csv .xls ] [ .bat .sh ] ![ .blk .yel ]" ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + std::vector expectedIncludes = { ".red", ".gr2", ".dds", ".png", ".yaml", ".txt", ".bat", ".sh" }; + std::vector expectedExcludes = { ".csv", ".xls", ".blk", ".yel" }; + EXPECT_EQ( includes, expectedIncludes ); + EXPECT_EQ( excludes, expectedExcludes ); +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_SimpleIncludeFilter ) +{ + ResourceTools::FilterResourceFilter filter( "[ .red ]" ); + const auto& includes = filter.GetIncludeFilter(); + EXPECT_EQ( includes.size(), 1 ); + EXPECT_EQ( includes[0], ".red" ); +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_SimpleExcludeFilter ) +{ + ResourceTools::FilterResourceFilter filter( "![ .blk ]" ); + const auto& excludes = filter.GetExcludeFilter(); + EXPECT_EQ( excludes.size(), 1 ); + EXPECT_EQ( excludes[0], ".blk" ); +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_IncludeExcludeInclude ) +{ + // Include .in1 and .in2 + // Exclude .in2, .ex1, and .ex2 (removes .in2 from include) + // Include .ex1, .in3 and .in1 (removes .ex1 from exclude, adds .in3, keeps .in1) + // Resulting include: .in1, .ex1, .in3 + // Resulting exclude: .in2, .ex2 + ResourceTools::FilterResourceFilter filter( "[ .in1 .in2 ] ![ .in2 .ex1 .ex2 ] [ .ex1 .in3 .in1 ]" ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + std::vector expectedIncludes = { ".in1", ".ex1", ".in3" }; + std::vector expectedExcludes = { ".in2", ".ex2" }; + EXPECT_EQ( includes, expectedIncludes ); + EXPECT_EQ( excludes, expectedExcludes ); +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_MissingClosingIncludeBracketBeforeNextOpenExcludeBracket ) +{ + try + { + // This test filter has a missing closing bracket for the first include filter, before the next exclude filter starts + ResourceTools::FilterResourceFilter filter( "[ .in1 ! [ .ex1 ]" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid filter format: matching end bracket ']' not present before the next start bracket '['" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterResourceFilter"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_ExcludeMarkerWithoutBracket_v1 ) +{ + try + { + ResourceTools::FilterResourceFilter filter( "! .ex1 ]" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid filter format: missing '['" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterResourceFilter"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_ExcludeMarkerWithoutBracket_v2 ) +{ + try + { + ResourceTools::FilterResourceFilter filter( " [ .in1 ] ! .ex1 ]" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid filter format: missing '['" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterResourceFilter"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_ExcludeMarkerWithoutBracket_v3 ) +{ + try + { + ResourceTools::FilterResourceFilter filter( " [ .in1 ] ![ .ex1 ] ! " ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid filter format: exclude filter marker found without a [ token ] section" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterResourceFilter"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_MissingOpeningBracket_v1 ) +{ + try + { + ResourceTools::FilterResourceFilter filter( ".in1 .in2 ]" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid filter format: missing '['" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterResourceFilter"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_MissingOpeningBracket_v2 ) +{ + try + { + ResourceTools::FilterResourceFilter filter( " [ .in1 .in2 ] .in3 ] " ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid filter format: missing '['" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterResourceFilter"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_MissingClosingBracket_v1 ) +{ + try + { + ResourceTools::FilterResourceFilter filter( "[ .in1 .in2 " ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid filter format: missing ']'" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterResourceFilter"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_MissingClosingBracket_v2 ) +{ + try + { + ResourceTools::FilterResourceFilter filter( "[ .in1 .in2 ] [ .in3 " ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid filter format: missing ']'" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterResourceFilter"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_MissingClosingBracket_v3 ) +{ + try + { + ResourceTools::FilterResourceFilter filter( "[ .in1 .in2 ] [ .in3 [ .in4 ]" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid filter format: matching end bracket ']' not present before the next start bracket '['" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterResourceFilter"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_CondensedValidFilterStringv1 ) +{ + ResourceTools::FilterResourceFilter filter( "[inToken1 inToken2]![exToken1 exToken2][inToken3]" ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + std::vector expectedIncludes = { "inToken1", "inToken2", "inToken3" }; + std::vector expectedExcludes = { "exToken1", "exToken2" }; + EXPECT_EQ( includes, expectedIncludes ); + EXPECT_EQ( excludes, expectedExcludes ); +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_CondensedValidFilterStringv2 ) +{ + ResourceTools::FilterResourceFilter filter( "![exToken1][inToken1 inToken2]![exToken2][inToken3]" ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + std::vector expectedIncludes = { "inToken1", "inToken2", "inToken3" }; + std::vector expectedExcludes = { "exToken1", "exToken2" }; + EXPECT_EQ( includes, expectedIncludes ); + EXPECT_EQ( excludes, expectedExcludes ); +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_EmptyFilterString_TopLevel ) +{ + ResourceTools::FilterResourceFilter filter( "", true ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + + EXPECT_EQ( includes.size(), 1 ); + EXPECT_EQ( includes[0], "*" ); // Wild-card added when no include filter specified on TOP-LEVEL filter + + EXPECT_TRUE( excludes.empty() ); +} + +TEST_F( ResourceFilterTest, FilterResourceFilter_EmptyFilterString_Inline ) +{ + ResourceTools::FilterResourceFilter filter( "" ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + + EXPECT_EQ( includes.size(), 0 ); + EXPECT_TRUE( includes.empty() ); // No wild-card added when no include filter specified INLINE + + EXPECT_TRUE( excludes.empty() ); +} + +// ----------------------------------------- + +TEST_F( ResourceFilterTest, FilterPrefixMap_SinglePrefixMultiplePaths ) +{ + ResourceTools::FilterPrefixMap map( "prefix1:/somePath;../otherPath" ); + const auto& prefixMapEntries = map.GetMapEntries(); + ASSERT_EQ( prefixMapEntries.size(), 1 ) << "There should only be 1 prefix in the map"; + + // If iterator is at end, the prefix was not found + auto it = prefixMapEntries.find( "prefix1" ); + ASSERT_NE( it, prefixMapEntries.end() ) << "Prefix 'prefix1' not found in the map"; + + std::set expected = { "/somePath", "../otherPath" }; + EXPECT_EQ( it->second.GetPrefix(), "prefix1" ) << "Prefix should be 'prefix1'"; + EXPECT_EQ( it->first, it->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; + EXPECT_EQ( it->second.GetPaths(), expected ) << "Paths do not match expected values"; +} + +TEST_F( ResourceFilterTest, FilterPrefixMap_MultiplePrefixes ) +{ + ResourceTools::FilterPrefixMap map( "prefix1:/path1;/path2 prefix2:/newPath1" ); + const auto& prefixMapEntries = map.GetMapEntries(); + ASSERT_EQ( prefixMapEntries.size(), 2 ) << "There should be 2 prefixes in the map"; + + // Make sure both prefixes exist + auto it1 = prefixMapEntries.find( "prefix1" ); + auto it2 = prefixMapEntries.find( "prefix2" ); + ASSERT_NE( it1, prefixMapEntries.end() ) << "Prefix 'prefix1' not found in the map"; + ASSERT_NE( it2, prefixMapEntries.end() ) << "Prefix 'prefix2' not found in the map"; + + std::set expected1 = { "/path1", "/path2" }; + std::set expected2 = { "/newPath1" }; + EXPECT_EQ( it1->second.GetPrefix(), "prefix1" ) << "Prefix should be 'prefix1'"; + EXPECT_EQ( it2->second.GetPrefix(), "prefix2" ) << "Prefix should be 'prefix2'"; + EXPECT_EQ( it1->first, it1->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; + EXPECT_EQ( it2->first, it2->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; + EXPECT_EQ( it1->second.GetPaths(), expected1 ) << "Paths do not match expected values"; + EXPECT_EQ( it2->second.GetPaths(), expected2 ) << "Paths do not match expected values"; +} + +TEST_F( ResourceFilterTest, FilterPrefixMap_DuplicateSamePrefixPathsInDifferentOrder ) +{ + ResourceTools::FilterPrefixMap map( "prefix1:/path1;/path2 prefix1:/path2;/path1" ); + + // There should only be one prefix (prefix1) + const auto& prefixMapEntries = map.GetMapEntries(); + ASSERT_EQ( prefixMapEntries.size(), 1 ) << "There should only be 1 prefix in the map"; + auto it = prefixMapEntries.find( "prefix1" ); + ASSERT_NE( it, prefixMapEntries.end() ) << "Prefix 'prefix1' not found in the map"; + + // There should be only 2 paths, sorted in set + std::set expected_a = { "/path1", "/path2" }; + std::set expected_b = { "/path2", "/path1" }; + EXPECT_EQ( it->second.GetPrefix(), "prefix1" ) << "Prefix should be 'prefix1'"; + EXPECT_EQ( it->second.GetPaths(), expected_a ) << "Paths do not match expected values - inserted in alphabetical order"; + EXPECT_EQ( it->second.GetPaths(), expected_b ) << "Paths do not match expected values - inserted in reverse alphabetical order"; + EXPECT_EQ( it->first, it->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; +} + +TEST_F( ResourceFilterTest, FilterPrefixMap_MultiplePrefixesAppendToPaths ) +{ + ResourceTools::FilterPrefixMap map( "prefix1:/path2;/path1 prefix2:/otherPath1;/otherPath2 prefix1:/path3;/path1" ); + const auto& prefixMapEntries = map.GetMapEntries(); + ASSERT_EQ( prefixMapEntries.size(), 2 ) << "There should be 2 prefixes in the map"; + auto it1 = prefixMapEntries.find( "prefix1" ); + auto it2 = prefixMapEntries.find( "prefix2" ); + ASSERT_NE( it1, prefixMapEntries.end() ) << "Prefix 'prefix1' not found in the map"; + ASSERT_NE( it2, prefixMapEntries.end() ) << "Prefix 'prefix2' not found in the map"; + + // Prefix1 should have 3 paths and prefix2 should have 2 + std::set prefix1Paths = { "/path1", "/path2", "/path3" }; + std::set prefix2Paths = { "/otherPath1", "/otherPath2" }; + EXPECT_EQ( it1->second.GetPrefix(), "prefix1" ) << "Prefix should be 'prefix1'"; + EXPECT_EQ( it2->second.GetPrefix(), "prefix2" ) << "Prefix should be 'prefix2'"; + EXPECT_EQ( it1->second.GetPaths(), prefix1Paths ) << "Paths do not match expected values"; + EXPECT_EQ( it2->second.GetPaths(), prefix2Paths ) << "Paths do not match expected values"; + EXPECT_EQ( it1->first, it1->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; + EXPECT_EQ( it2->first, it2->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; +} + +TEST_F( ResourceFilterTest, FilterPrefixMap_DifferentWhitespacesBetweenPrefixes ) +{ + std::string input = "prefix1:/path1\tprefixTab:/path2\nprefixNewLine:/path3"; + ResourceTools::FilterPrefixMap map( input ); + const auto& prefixMapEntries = map.GetMapEntries(); + ASSERT_EQ( prefixMapEntries.size(), 3 ) << "There should only be 3 prefix in the map"; + auto it1 = prefixMapEntries.find( "prefix1" ); + auto it2 = prefixMapEntries.find( "prefixTab" ); + auto it3 = prefixMapEntries.find( "prefixNewLine" ); + EXPECT_NE( it1, prefixMapEntries.end() ) << "Prefix 'prefix1' not found in the map"; + EXPECT_NE( it2, prefixMapEntries.end() ) << "Prefix 'prefixTab' not found in the map"; + EXPECT_NE( it3, prefixMapEntries.end() ) << "Prefix 'prefixNewLine' not found in the map"; + + std::set prefix1Paths = { "/path1" }; + std::set prefixTabPaths = { "/path2" }; + std::set prefixNewLinePaths = { "/path3" }; + EXPECT_EQ( it1->second.GetPrefix(), "prefix1" ) << "Prefix should be 'prefix1'"; + EXPECT_EQ( it2->second.GetPrefix(), "prefixTab" ) << "Prefix should be 'prefixTab'"; + EXPECT_EQ( it3->second.GetPrefix(), "prefixNewLine" ) << "Prefix should be 'prefixNewLine'"; + EXPECT_EQ( it1->second.GetPaths(), prefix1Paths ) << "Paths do not match expected values"; + EXPECT_EQ( it2->second.GetPaths(), prefixTabPaths ) << "Paths do not match expected values"; + EXPECT_EQ( it3->second.GetPaths(), prefixNewLinePaths ) << "Paths do not match expected values"; + EXPECT_EQ( it1->first, it1->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; + EXPECT_EQ( it2->first, it2->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; + EXPECT_EQ( it3->first, it3->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; +} + +TEST_F( ResourceFilterTest, FilterPrefixMap_Invalid_MissingColon ) +{ + try + { + ResourceTools::FilterPrefixMap prefixmap( "prefix1/path1" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid prefixmap format: missing ':'" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterPrefixMap"; + } +} + +TEST_F( ResourceFilterTest, FilterPrefixMap_Invalid_EmptyPrefix ) +{ + try + { + ResourceTools::FilterPrefixMap prefixmap( ":/path1" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid prefixmap format: empty prefix" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterPrefixMap"; + } +} + +TEST_F( ResourceFilterTest, FilterPrefixMap_Invalid_NoPaths ) +{ + try + { + ResourceTools::FilterPrefixMap prefixmap( "prefix1:" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid prefixmap format: No paths defined for prefix: prefix1" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterPrefixMap"; + } +} + +TEST_F( ResourceFilterTest, FilterPrefixMapEntry_PrefixMismatchOnAppend ) +{ + try + { + ResourceTools::FilterPrefixMapEntry entry( "prefix1", "/path1" ); + entry.AppendPaths( "prefix2", "/path2" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Prefix mismatch while appending path(s): prefix2 (incoming) != prefix1 (existing)" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when appending paths to FilterPrefixMapEntry"; + } +} + +TEST_F( ResourceFilterTest, FilterPrefixMapEntry_InvalidNoPathsOnAppend ) +{ + try + { + ResourceTools::FilterPrefixMapEntry entry( "prefix1", "" ); // Empty string for paths + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid prefixmap format: No paths appended for prefix: prefix1" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterPrefixMapEntry with no paths"; + } +} + +// ----------------------------------------- + +TEST_F( ResourceFilterTest, FilterDefaultSection_InitializeValid ) +{ + std::string input = "prefix1:/path1;../path2 prefix2:/path3"; + ResourceTools::FilterDefaultSection defaultSection( input ); + const auto& prefixMapEntries = defaultSection.GetPrefixMap().GetMapEntries(); + ASSERT_EQ( prefixMapEntries.size(), 2 ) << "There should be 2 prefixes in the map"; + auto it1 = prefixMapEntries.find( "prefix1" ); + auto it2 = prefixMapEntries.find( "prefix2" ); + ASSERT_NE( it1, prefixMapEntries.end() ) << "Prefix 'prefix1' not found in the map"; + ASSERT_NE( it2, prefixMapEntries.end() ) << "Prefix 'prefix2' not found in the map"; + + std::set prefix1Paths = { "/path1", "../path2" }; + std::set prefix2Paths = { "/path3" }; + EXPECT_EQ( it1->second.GetPrefix(), "prefix1" ) << "Prefix should be 'prefix1'"; + EXPECT_EQ( it2->second.GetPrefix(), "prefix2" ) << "Prefix should be 'prefix2'"; + EXPECT_EQ( it1->second.GetPaths(), prefix1Paths ) << "Paths do not match expected values"; + EXPECT_EQ( it2->second.GetPaths(), prefix2Paths ) << "Paths do not match expected values"; + EXPECT_EQ( it1->first, it1->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; + EXPECT_EQ( it2->first, it2->second.GetPrefix() ) << "Value of FilterPrefixMap.m_prefixMap key does not match associated FilterPrefixMapEntry.m_prefix"; +} + +TEST_F( ResourceFilterTest, FilterDefaultSection_Initialize_InvalidMissingColon ) +{ + try + { + ResourceTools::FilterDefaultSection defaultSection( "prefix1/path1" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid prefixmap format: missing ':'" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterDefaultSection"; + } +} + +TEST_F( ResourceFilterTest, FilterDefaultSection_Initialize_InvalidEmptyPrefix ) +{ + try + { + ResourceTools::FilterDefaultSection defaultSection( ":/path1" ); + FAIL() << "Expected std::invalid_argument to be thrown"; + } + catch( const std::invalid_argument& e ) + { + EXPECT_STREQ( e.what(), "Invalid prefixmap format: empty prefix" ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when constructing FilterDefaultSection"; + } +} + +// ----------------------------------------- + +void MapContainsPaths( const std::set& allExpectedPaths, + const std::map& resolvedMap, + const std::string messagePrefix = "" ) +{ + for( const auto& path : allExpectedPaths ) + { + EXPECT_TRUE( resolvedMap.find( path ) != resolvedMap.end() ) << messagePrefix << " - Expected path not found in resolved map: " << path; + } + if( resolvedMap.size() != allExpectedPaths.size() ) + { + FAIL() << messagePrefix << " - Resolved map size (" << resolvedMap.size() << ") does not match expected paths size (" << allExpectedPaths.size() << ")"; + } +} + +void ValidatePathMap( const std::set& expectedPaths, + const std::map& resolvedPathMap, + const std::vector& expectedIncludes, + const std::vector& expectedExcludes, + const std::string messagePrefix = "" ) +{ + for( const auto& p : expectedPaths ) + { + EXPECT_TRUE( resolvedPathMap.count( p ) ) << messagePrefix << " - Expected path not found in resolved path map: " << p; + } + + for( const auto& kv : resolvedPathMap ) + { + // Ignore resolved paths that are not in the expectedPaths set (useful when checking partial expectedPaths, because of include/exclude overrides from default) + if( expectedPaths.find( kv.first ) == expectedPaths.end() ) + { + continue; + } + EXPECT_EQ( kv.second.GetIncludeFilter(), expectedIncludes ) << messagePrefix << " - Include filter does not match expected for path: " << kv.first; + EXPECT_EQ( kv.second.GetExcludeFilter(), expectedExcludes ) << messagePrefix << " - Exclude filter does not match expected for path: " << kv.first; + } +} + + +TEST_F( ResourceFilterTest, FilterResourcePathFile_SingleLine_NoFilter ) +{ + std::string prefixMapStr = "prefix1:/path1;../path2"; + std::string parentFilterStr = "[ .in1 .in2 ] ![ .ex1 ]"; + ResourceTools::FilterPrefixMap prefixMap( prefixMapStr ); + ResourceTools::FilterResourceFilter parentFilter( parentFilterStr ); + + std::string rawResPathAttrib = "prefix1:/foo/bar"; + ResourceTools::FilterResourcePathFile pathFile( rawResPathAttrib, prefixMap, parentFilter ); + const auto& resolvedPathMap = pathFile.GetResolvedPathMap(); + + // Check the resolved path and filters against expected + std::set expectedPaths = { "/path1/foo/bar", "../path2/foo/bar" }; + std::vector expectedIncludes = { ".in1", ".in2" }; + std::vector expectedExcludes = { ".ex1" }; + + ValidatePathMap( expectedPaths, resolvedPathMap, expectedIncludes, expectedExcludes ); +} + +TEST_F( ResourceFilterTest, FilterResourcePathFile_SingleLine_InlineIncludeExclude ) +{ + std::string prefixMapStr = "prefix1:/path1"; + std::string parentFilterStr = "[ .in1 .in2 ] ![ .ex1 ]"; + ResourceTools::FilterPrefixMap prefixMap( prefixMapStr ); + ResourceTools::FilterResourceFilter parentFilter( parentFilterStr ); + + std::string rawPathFileAttrib = "prefix1:/foo/bar [ .inLine1 ] ![ .exLine1 ]"; + ResourceTools::FilterResourcePathFile pathFile( rawPathFileAttrib, prefixMap, parentFilter ); + const auto& resolvedPathMap = pathFile.GetResolvedPathMap(); + + // Check the resolved path and filters against expected + std::set expectedPaths = { "/path1/foo/bar" }; + std::vector expectedIncludes = { ".in1", ".in2", ".inLine1" }; + std::vector expectedExcludes = { ".ex1", ".exLine1" }; + + ValidatePathMap( expectedPaths, resolvedPathMap, expectedIncludes, expectedExcludes ); +} + +TEST_F( ResourceFilterTest, FilterResourcePathFile_SingleLine_InlineOverridesParentFilter ) +{ + std::string prefixMapStr = "prefix1:/path1;../subPath2;/path3"; + std::string parentFilterStr = "[ .parIn1 .parIn2 ] ![ .parEx1 ]"; + ResourceTools::FilterPrefixMap prefixMap( prefixMapStr ); + ResourceTools::FilterResourceFilter parentFilter( parentFilterStr ); + + // Override the "location" of the parent include filter (.parIn2) and exclude filter (.parEx1), moving them to opposite filter side + std::string rawPathFileAttrib = "prefix1:/foo [ .lineIn1 .parEx1 ] ![ .parIn2 .lineEx1 ]"; + ResourceTools::FilterResourcePathFile pathFile( rawPathFileAttrib, prefixMap, parentFilter ); + const auto& resolvedPathMap = pathFile.GetResolvedPathMap(); + + // Check the resolved path and filters against expected (some filters switched around) + std::set expectedPaths = { "/path1/foo", "../subPath2/foo", "/path3/foo" }; + std::vector expectedIncludes = { ".parIn1", ".lineIn1", ".parEx1" }; + std::vector expectedExcludes = { ".parIn2", ".lineEx1" }; + + ValidatePathMap( expectedPaths, resolvedPathMap, expectedIncludes, expectedExcludes ); +} + +TEST_F( ResourceFilterTest, FilterResourcePathFile_MultiLine_MixedFiltersWithOverrides ) +{ + std::string prefixMapStr = "prefix1:/path1;../path2 prefix2:/path3"; + std::string parentFilterStr = "[ .parIn1 .parIn2 ] ![ .parEx1 ]"; + ResourceTools::FilterPrefixMap prefixMap( prefixMapStr ); + ResourceTools::FilterResourceFilter parentFilter( parentFilterStr ); + + std::string rawPathFileAttrib = + "prefix1:/firstLine [ .inLine1 ] ![ .parIn1 ]\n" // Add .inLine1 to include and move .parIn1 from include to exclude filter + "prefix2:/secondLine\n" // Keep parent filters unchanged + "prefix1:/thirdLine ![ .exLine3 ] [ .parEx1 ]\n" // Add .exLine3 to exclude filter and move .parEx1 from exclude to include + "prefix2:/fourthLine [ .inLine4 ] ![ .exLine4 ]"; // Add .inLine4 to include and .exLine4 to exclude filter + ResourceTools::FilterResourcePathFile pathFile( rawPathFileAttrib, prefixMap, parentFilter ); + const auto& resolved = pathFile.GetResolvedPathMap(); + + // Check the resolved path and filters against expected (multiple switching of filters and overrides) + std::set expectedPaths = { "/path1/firstLine", "../path2/firstLine", "/path3/secondLine", "/path1/thirdLine", "../path2/thirdLine", "/path3/fourthLine" }; + for( const auto& p : expectedPaths ) + { + EXPECT_TRUE( resolved.count( p ) ); + } + + for( const auto& kv : resolved ) + { + if( kv.first == "/path1/firstLine" || kv.first == "../path2/firstLine" ) + { + // Add .inLine1 to include and move .parIn1 from include to exclude filter + EXPECT_EQ( kv.second.GetIncludeFilter(), std::vector( { ".parIn2", ".inLine1" } ) ); + EXPECT_EQ( kv.second.GetExcludeFilter(), std::vector( { ".parEx1", ".parIn1" } ) ); + } + else if( kv.first == "/path3/secondLine" ) + { + // Keep parent filters unchanged + EXPECT_EQ( kv.second.GetIncludeFilter(), std::vector( { ".parIn1", ".parIn2" } ) ); + EXPECT_EQ( kv.second.GetExcludeFilter(), std::vector( { ".parEx1" } ) ); + } + else if( kv.first == "/path1/thirdLine" || kv.first == "../path2/thirdLine" ) + { + // Add .exLine3 to exclude filter and move .parEx1 from exclude to include + EXPECT_EQ( kv.second.GetIncludeFilter(), std::vector( { ".parIn1", ".parIn2", ".parEx1" } ) ); + EXPECT_EQ( kv.second.GetExcludeFilter(), std::vector( { ".exLine3" } ) ); + } + else if( kv.first == "/path3/fourthLine" ) + { + // Add .inLine4 to include and .exLine4 to exclude filter + EXPECT_EQ( kv.second.GetIncludeFilter(), std::vector( { ".parIn1", ".parIn2", ".inLine4" } ) ); + EXPECT_EQ( kv.second.GetExcludeFilter(), std::vector( { ".parEx1", ".exLine4" } ) ); + } + } +} + +TEST_F( ResourceFilterTest, FilterResourcePathFile_SingleLine_DuplicateOverrides ) +{ + std::string prefixMapStr = "prefix1:/path1"; + std::string parentFilterStr = "[ .parIn1 .parIn2 ] ![ .parEx1 ]"; + ResourceTools::FilterPrefixMap prefixMap( prefixMapStr ); + ResourceTools::FilterResourceFilter parentFilter( parentFilterStr ); + + // Make sure we DUPLICATE the inline with the same as the parents (should NOT result in combined duplicates) + std::string rawPathFileAttrib = "prefix1:/foo/bar [ .parIn2 .parIn1 .lineIn1 .parIn1 .parIn2 ] ![ .lineEx1 .parEx1 ]"; + ResourceTools::FilterResourcePathFile pathFile( rawPathFileAttrib, prefixMap, parentFilter ); + const auto& resolvedPathMap = pathFile.GetResolvedPathMap(); + + // Check the resolved path and filters against expected (NO duplicates in final filters list) + std::set expectedPaths = { "/path1/foo/bar" }; + std::vector expectedIncludes = { ".parIn1", ".parIn2", ".lineIn1" }; + std::vector expectedExcludes = { ".parEx1", ".lineEx1" }; + + ValidatePathMap( expectedPaths, resolvedPathMap, expectedIncludes, expectedExcludes ); +} + +TEST_F( ResourceFilterTest, FilterResourcePathFile_Invalid_MissingPrefix ) +{ + std::string prefixMapStr = "prefix1:/path1;../path2 prefix2:/path3"; + std::string parentFilterStr = "[ .in1 .in2 ] ![ .ex1 ]"; + ResourceTools::FilterPrefixMap prefixMap( prefixMapStr ); + ResourceTools::FilterResourceFilter parentFilter( parentFilterStr ); + + std::string rawPathFileAttrib = "/foo/bar"; // respath is missing the prefix: + EXPECT_THROW( ResourceTools::FilterResourcePathFile pathFile( rawPathFileAttrib, prefixMap, parentFilter ), std::invalid_argument ); +} + +TEST_F( ResourceFilterTest, FilterResourcePathFile_Invalid_UnknownPrefix ) +{ + std::string prefixMapStr = "prefix1:/path1;../path2 prefix2:/path3"; + std::string parentFilterStr = "[ .in1 .in2 ] ![ .ex1 ]"; + ResourceTools::FilterPrefixMap prefixMap( prefixMapStr ); + ResourceTools::FilterResourceFilter parentFilter( parentFilterStr ); + + std::string rawPathFileAttrib = "prefixNotInPrefixMap:/foo/bar"; // unknown prefix + EXPECT_THROW( ResourceTools::FilterResourcePathFile pathFile( rawPathFileAttrib, prefixMap, parentFilter ), std::invalid_argument ); +} + +TEST_F( ResourceFilterTest, FilterResourcePathFile_Invalid_MalformedInlineFilter ) +{ + std::string prefixMapStr = "prefix1:/path1;../path2 prefix2:/path3"; + std::string parentFilterStr = "[ .in1 .in2 ] ![ .ex1 ]"; + ResourceTools::FilterPrefixMap prefixMap( prefixMapStr ); + ResourceTools::FilterResourceFilter parentFilter( parentFilterStr ); + + std::string rawPathFileAttrib = "prefix1:/foo/bar [ .yaml "; // missing closing bracket of inline include filter + EXPECT_THROW( ResourceTools::FilterResourcePathFile pathFile( rawPathFileAttrib, prefixMap, parentFilter ), std::invalid_argument ); +} + +// ----------------------------------------- + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_SingleLineRespath ) +{ + std::string sectionName = "FilterNamedSection_Valid_SingleLineRespath"; + std::string filter = "[ .in1 .in2 ] ![ .ex1 ]"; + std::string respaths = "testPrefix:/foo/bar"; + std::string defaultParentPrefixMapStr = "testPrefix:/myPath"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, "", defaultPrefixMap ); + + // Expected values: + std::set expectedPaths = { "/myPath/foo/bar" }; + std::vector expectedIncludes = { ".in1", ".in2" }; + std::vector expectedExcludes = { ".ex1" }; + + const auto& resolvedRespathMap = namedSection.GetResolvedRespathsMap(); + const auto& resolvedResfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ValidatePathMap( expectedPaths, resolvedRespathMap, expectedIncludes, expectedExcludes, "ResolvedRespathsMap" ); + EXPECT_TRUE( resolvedResfileMap.empty() ); + ValidatePathMap( expectedPaths, combinedMap, expectedIncludes, expectedExcludes, "CombinedResolvedPathMap" ); +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_EmptyFilter_TopLevel ) +{ + std::string sectionName = "FilterNamedSection_Valid_EmptyFilter_TopLevel"; + std::string defaultParentPrefixMapStr = "testPrefix:/myPath"; + std::string filter = ""; // Empty filter string at top-level should add wildcard include + std::string respaths = "testPrefix:/foo/bar"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, "", defaultPrefixMap ); + + // Expected values: + std::set expectedPaths = { "/myPath/foo/bar" }; + std::vector expectedIncludes = { "*" }; + std::vector expectedExcludes = {}; + + const auto& resolvedRespathMap = namedSection.GetResolvedRespathsMap(); + const auto& resolvedResfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ValidatePathMap( expectedPaths, resolvedRespathMap, expectedIncludes, expectedExcludes, "ResolvedRespathsMap" ); + EXPECT_TRUE( resolvedResfileMap.empty() ); + ValidatePathMap( expectedPaths, combinedMap, expectedIncludes, expectedExcludes, "CombinedResolvedPathMap" ); +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_OnlyExcludeFilter_TopLevel ) +{ + std::string sectionName = "FilterNamedSection_Valid_OnlyExcludeFilter_TopLevel"; + std::string defaultParentPrefixMapStr = "testPrefix:/myPath"; + std::string filter = "![ .ex1 ]"; // When there is only an exclude filter at top-level, it should add wildcard include + std::string respaths = "testPrefix:/foo/bar"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, "", defaultPrefixMap ); + + // Expected values: + std::set expectedPaths = { "/myPath/foo/bar" }; + std::vector expectedIncludes = { "*" }; + std::vector expectedExcludes = { ".ex1" }; + + const auto& resolvedRespathMap = namedSection.GetResolvedRespathsMap(); + const auto& resolvedResfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ValidatePathMap( expectedPaths, resolvedRespathMap, expectedIncludes, expectedExcludes, "ResolvedRespathsMap" ); + EXPECT_TRUE( resolvedResfileMap.empty() ); + ValidatePathMap( expectedPaths, combinedMap, expectedIncludes, expectedExcludes, "CombinedResolvedPathMap" ); +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_MultiLineRespath ) +{ + std::string sectionName = "FilterNamedSection_Valid_MultiLineRespath"; + std::string filter = "[ .in1 .in2 ] ![ .ex1 ]"; + std::string respaths = + "prefix1:/firstLine [ .inLine1 ] ![ .exLine1 ]\n" // Add entries to both include and exclude filters + "prefix2:/secondLine"; // Just using vanilla parent filter + std::string defaultParentPrefixMapStr = "prefix1:/pathA;/pathB prefix2:/path2"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, "", defaultPrefixMap ); + + // Expected values: + std::set allExpectedPaths = { "/pathA/firstLine", "/pathB/firstLine", "/path2/secondLine" }; + std::set firstLinePaths = { "/pathA/firstLine", "/pathB/firstLine" }; + std::set secondLinePaths = { "/path2/secondLine" }; + std::vector defaultIncludes = { ".in1", ".in2" }; + std::vector defaultExcludes = { ".ex1" }; + std::vector firstLineIncludes = { ".in1", ".in2", ".inLine1" }; + std::vector firstLineExcludes = { ".ex1", ".exLine1" }; + + const auto& resolvedRespathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resolvedResfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + MapContainsPaths( allExpectedPaths, resolvedRespathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( firstLinePaths, resolvedRespathsMap, firstLineIncludes, firstLineExcludes, "FirstLine ResolvedRespathsMap" ); + ValidatePathMap( secondLinePaths, resolvedRespathsMap, defaultIncludes, defaultExcludes, "SecondLine ResolvedRespathsMap" ); + EXPECT_TRUE( resolvedResfileMap.empty() ); + MapContainsPaths( allExpectedPaths, combinedMap, "CombinedResolvedMap" ); + ValidatePathMap( firstLinePaths, combinedMap, firstLineIncludes, firstLineExcludes, "FirstLine ResolvedRespathsMap" ); + ValidatePathMap( secondLinePaths, combinedMap, defaultIncludes, defaultExcludes, "SecondLine ResolvedRespathsMap" ); +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_RespathAndResfile ) +{ + std::string sectionName = "FilterNamedSection_Valid_RespathAndResfile"; + std::string defaultParentPrefixMapStr = "prefix1:/pathA;/pathB prefix2:/pathC"; + std::string filter = "[ .in1 .in2 ] ![ .ex1 ]"; + std::string respaths = "prefix1:/respaths"; + std::string resfile = "prefix2:/resfile"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set allExpectedPaths = { "/pathA/respaths", "/pathB/respaths", "/pathC/resfile" }; + std::set respathsPaths = { "/pathA/respaths", "/pathB/respaths" }; + std::set resfilesPaths = { "/pathC/resfile" }; + std::vector defaultIncludes = { ".in1", ".in2" }; + std::vector defaultExcludes = { ".ex1" }; + + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ASSERT_EQ( respathsMap.size(), 2 ); // prefix1 has two paths + MapContainsPaths( respathsPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( respathsPaths, respathsMap, defaultIncludes, defaultExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 1 ); // prefix2 has one path + MapContainsPaths( resfilesPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( resfilesPaths, resfileMap, defaultIncludes, defaultExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 3 ); // 2 from respaths, 1 from resfile + MapContainsPaths( allExpectedPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( allExpectedPaths, combinedMap, defaultIncludes, defaultExcludes, "ResolvedCombinedMap" ); +} + + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_RespathSet_ResfileEmpty ) +{ + std::string sectionName = "FilterNamedSection_Valid_RespathSet_ResfileEmpty"; + std::string parentPrefixMapStr = "prefix1:/pathA"; + std::string filter = "[ .in1 .in2 ] ![ .ex1 ]"; + std::string respaths = "prefix1:/foo/bar"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( parentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, "", defaultPrefixMap ); + + // Expected values: + std::set onlyValidPaths = { "/pathA/foo/bar" }; + std::vector defaultIncludes = { ".in1", ".in2" }; + std::vector defaultExcludes = { ".ex1" }; + + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ASSERT_EQ( respathsMap.size(), 1 ); + MapContainsPaths( onlyValidPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( onlyValidPaths, respathsMap, defaultIncludes, defaultExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 0 ); // Nothing in resfile + EXPECT_TRUE( resfileMap.empty() ); + + ASSERT_EQ( combinedMap.size(), 1 ); // 1 from respaths, 0 from resfile + MapContainsPaths( onlyValidPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( onlyValidPaths, combinedMap, defaultIncludes, defaultExcludes, "ResolvedCombinedMap" ); +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Invalid_RespathMissing ) +{ + std::string sectionName = "FilterNamedSection_Invalid_RespathMissing"; + std::string defaultParentPrefixMapStr = "prefix1:/path1"; + std::string filter = "[ .in1 ]"; + std::string resfile = "prefix1:/foo/bar"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + + // TODO: Should change code to throw defined error code/type + try + { + ResourceTools::FilterNamedSection namedSection( sectionName, filter, "", resfile, defaultPrefixMap ); + FAIL() << "Expected std::invalid_argument when constructing FilterNamedSection with missing respaths"; + } + catch( const std::invalid_argument& e ) + { + std::string errorString = "Respaths attribute is empty for section: " + sectionName; + EXPECT_STREQ( e.what(), errorString.c_str() ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument to be thrown"; + } +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_CombinedResolvedMap ) +{ + std::string sectionName = "FilterNamedSection_Valid_CombinedResolvedMap"; + std::string defaultParentPrefixMapStr = "prefixA:/path1"; + std::string filter = "[ .in1 ]"; + std::string respaths = "prefixA:/foo/bar"; + std::string resfile = "prefixA:/loo/car"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set combinedPaths = { "/path1/foo/bar", "/path1/loo/car" }; + std::set respathsPaths = { "/path1/foo/bar" }; + std::set resfilesPaths = { "/path1/loo/car" }; + std::vector defaultIncludes = { ".in1" }; + std::vector defaultExcludes = {}; + + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ASSERT_EQ( respathsMap.size(), 1 ); // "/path1/foo/bar" + MapContainsPaths( respathsPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( respathsPaths, respathsMap, defaultIncludes, defaultExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 1 ); // "/path1/loo/bar" + MapContainsPaths( resfilesPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( resfilesPaths, resfileMap, defaultIncludes, defaultExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // both + MapContainsPaths( combinedPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( combinedPaths, combinedMap, defaultIncludes, defaultExcludes, "ResolvedCombinedMap" ); +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_CombinedResolvedMap_EmptyTopLevelFilter ) +{ + std::string sectionName = "FilterNamedSection_Valid_CombinedResolvedMap_EmptyTopLevelFilter"; + std::string defaultParentPrefixMapStr = "prefixA:/path1"; + std::string filter = ""; // Empty top-level filter should add wildcard ("*") include + std::string respaths = "prefixA:/foo/bar"; + std::string resfile = "prefixA:/loo/car"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set combinedPaths = { "/path1/foo/bar", "/path1/loo/car" }; + std::set respathsPaths = { "/path1/foo/bar" }; + std::set resfilesPaths = { "/path1/loo/car" }; + std::vector defaultIncludes = { "*" }; + std::vector defaultExcludes = {}; + + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ASSERT_EQ( respathsMap.size(), 1 ); // "/path1/foo/bar" + MapContainsPaths( respathsPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( respathsPaths, respathsMap, defaultIncludes, defaultExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 1 ); // "/path1/loo/bar" + MapContainsPaths( resfilesPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( resfilesPaths, resfileMap, defaultIncludes, defaultExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // both + MapContainsPaths( combinedPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( combinedPaths, combinedMap, defaultIncludes, defaultExcludes, "ResolvedCombinedMap" ); + for( const auto& kv : combinedMap ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetIncludeFilter()[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + EXPECT_TRUE( kv.second.GetExcludeFilter().empty() ); + } +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_CombinedResolvedMap_OnlyExcludeTopLevelFilter ) +{ + std::string sectionName = "FilterNamedSection_Valid_CombinedResolvedMap_OnlyExcludeTopLevelFilter"; + std::string defaultParentPrefixMapStr = "prefixA:/path1"; + std::string filter = " ![ .ex1 ] "; // Only exclude filter at top-level should add wildcard ("*") include as well + std::string respaths = "prefixA:/foo/bar"; + std::string resfile = "prefixA:/loo/car"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set combinedPaths = { "/path1/foo/bar", "/path1/loo/car" }; + std::set respathsPaths = { "/path1/foo/bar" }; + std::set resfilesPaths = { "/path1/loo/car" }; + std::vector defaultIncludes = { "*" }; // Default added + std::vector defaultExcludes = { ".ex1" }; + + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ASSERT_EQ( respathsMap.size(), 1 ); // "/path1/foo/bar" + MapContainsPaths( respathsPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( respathsPaths, respathsMap, defaultIncludes, defaultExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 1 ); // "/path1/loo/bar" + MapContainsPaths( resfilesPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( resfilesPaths, resfileMap, defaultIncludes, defaultExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // both + MapContainsPaths( combinedPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( combinedPaths, combinedMap, defaultIncludes, defaultExcludes, "ResolvedCombinedMap" ); + for( const auto& kv : combinedMap ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetIncludeFilter()[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], ".ex1" ); + } +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_DiffCombinedResolvedMap_EmptyTopLevelFilter_OverrideRespath ) +{ + std::string sectionName = "FilterNamedSection_Valid_DiffCombinedResolvedMap_EmptyTopLevelFilter_OverrideRespath"; + std::string defaultParentPrefixMapStr = "prefixA:/path1"; + std::string filter = ""; // Empty top-level filter should add wildcard ("*") include filter + std::string respaths = "prefixA:/foo/bar [ .inlineInclude ] ![ .inlineExclude ]"; // Inline filters to override + std::string resfile = "prefixA:/loo/car"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set combinedPaths = { "/path1/foo/bar", "/path1/loo/car" }; + std::set respathsPaths = { "/path1/foo/bar" }; + std::set resfilesPaths = { "/path1/loo/car" }; + std::vector defaultIncludes = { "*" }; // Default "*" added to top-level include + std::vector defaultExcludes = {}; + std::vector overrideIncludes = { "*", ".inlineInclude" }; + std::vector overrideExcludes = { ".inlineExclude" }; + + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ASSERT_EQ( respathsMap.size(), 1 ); // "/path1/foo/bar" + "[ *, .inlineInclude ] ![ .inlineExclude ]" + MapContainsPaths( respathsPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( respathsPaths, respathsMap, overrideIncludes, overrideExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 1 ); // "/path1/loo/bar" + "[ * ]" + MapContainsPaths( resfilesPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( resfilesPaths, resfileMap, defaultIncludes, defaultExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // both + MapContainsPaths( combinedPaths, combinedMap, "ResolvedCombinedMap" ); + // Manually validate combined map since it combines both overrides and defaults + for( const auto& kv : combinedMap ) + { + // The "respaths" part: + if( kv.first == "/path1/foo/bar" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), "*" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".inlineInclude" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], ".inlineExclude" ); + } + // The "resfile" part: + else if( kv.first == "/path1/loo/car" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetIncludeFilter()[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + } + } +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_DiffCombinedResolvedMap_EmptyTopLevelFilter_OverrideResfile ) +{ + std::string sectionName = "FilterNamedSection_Valid_DiffCombinedResolvedMap_EmptyTopLevelFilter_OverrideResfile"; + std::string defaultParentPrefixMapStr = "prefixA:/path1"; + std::string filter = ""; // Empty top-level filter should add wildcard ("*") include filter + std::string respaths = "prefixA:/foo/bar"; + std::string resfile = "prefixA:/loo/car [ .inlineInclude ] ![ .inlineExclude ]"; // Inline filters to override + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set combinedPaths = { "/path1/foo/bar", "/path1/loo/car" }; + std::set respathsPaths = { "/path1/foo/bar" }; + std::set resfilesPaths = { "/path1/loo/car" }; + std::vector defaultIncludes = { "*" }; // Default "*" added to top-level include + std::vector defaultExcludes = {}; + std::vector overrideIncludes = { "*", ".inlineInclude" }; + std::vector overrideExcludes = { ".inlineExclude" }; + + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ASSERT_EQ( respathsMap.size(), 1 ); // "/path1/foo/bar" + "[ * ]" + MapContainsPaths( respathsPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( respathsPaths, respathsMap, defaultIncludes, defaultExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 1 ); // "/path1/loo/bar" + "[ *, .inlineInclude ] ![ .inlineExclude ]" + MapContainsPaths( resfilesPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( resfilesPaths, resfileMap, overrideIncludes, overrideExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // both + MapContainsPaths( combinedPaths, combinedMap, "ResolvedCombinedMap" ); + // Manually validate combined map since it combines both overrides and defaults + for( const auto& kv : combinedMap ) + { + // The "resfile" part: + if( kv.first == "/path1/loo/car" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), "*" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".inlineInclude" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], ".inlineExclude" ); + } + // The "respaths" part: + else if( kv.first == "/path1/foo/bar" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetIncludeFilter()[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + } + } +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_DiffCombinedResolvedMap_OnlyExcludeTopLevelFilter_OverrideRespath ) +{ + std::string sectionName = "FilterNamedSection_Valid_DiffCombinedResolvedMap_OnlyExcludeTopLevelFilter_OverrideRespath"; + std::string defaultParentPrefixMapStr = "prefixA:/path1"; + std::string filter = "![ .toplevelExclude ]"; // Only exclude filter at top-level should add wildcard ("*") include as well + std::string respaths = "prefixA:/foo/bar [ .inlineInclude ] ![ .inlineExclude ]"; // Inline filters to override + std::string resfile = "prefixA:/loo/car"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set combinedPaths = { "/path1/foo/bar", "/path1/loo/car" }; + std::set respathsPaths = { "/path1/foo/bar" }; + std::set resfilesPaths = { "/path1/loo/car" }; + std::vector defaultIncludes = { "*" }; // Default "*" added to top-level include + std::vector defaultExcludes = { ".toplevelExclude" }; + std::vector overrideIncludes = { "*", ".inlineInclude" }; + std::vector overrideExcludes = { ".toplevelExclude", ".inlineExclude" }; + + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ASSERT_EQ( respathsMap.size(), 1 ); // "/path1/foo/bar" + "[ *, .inlineInclude ] ![ .toplevelExclude .inlineExclude ]" + MapContainsPaths( respathsPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( respathsPaths, respathsMap, overrideIncludes, overrideExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 1 ); // "/path1/loo/bar" + "[ * ]" + MapContainsPaths( resfilesPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( resfilesPaths, resfileMap, defaultIncludes, defaultExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // both + MapContainsPaths( combinedPaths, combinedMap, "ResolvedCombinedMap" ); + // Manually validate combined map since it combines both overrides and defaults + for( const auto& kv : combinedMap ) + { + // The "respaths" part: + if( kv.first == "/path1/foo/bar" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), "*" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".inlineInclude" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetExcludeFilter().begin(), kv.second.GetExcludeFilter().end(), ".toplevelExclude" ) != kv.second.GetExcludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetExcludeFilter().begin(), kv.second.GetExcludeFilter().end(), ".inlineExclude" ) != kv.second.GetExcludeFilter().end() ); + } + // The "resfile" part: + else if( kv.first == "/path1/loo/car" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetIncludeFilter()[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], ".toplevelExclude" ); // Top-level exclude filter + } + } +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_DiffCombinedResolvedMap_OnlyExcludeTopLevelFilter_OverrideResfile ) +{ + std::string sectionName = "FilterNamedSection_Valid_DiffCombinedResolvedMap_OnlyExcludeTopLevelFilter_OverrideRespath"; + std::string defaultParentPrefixMapStr = "prefixA:/path1"; + std::string filter = "![ .toplevelExclude ]"; // Only exclude filter at top-level should add wildcard ("*") include as well + std::string respaths = "prefixA:/foo/bar"; + std::string resfile = "prefixA:/loo/car [ .inlineInclude ] ![ .inlineExclude ]"; // Inline filters to override"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set combinedPaths = { "/path1/foo/bar", "/path1/loo/car" }; + std::set respathsPaths = { "/path1/foo/bar" }; + std::set resfilesPaths = { "/path1/loo/car" }; + std::vector defaultIncludes = { "*" }; // Default "*" added to top-level include + std::vector defaultExcludes = { ".toplevelExclude" }; + std::vector overrideIncludes = { "*", ".inlineInclude" }; + std::vector overrideExcludes = { ".toplevelExclude", ".inlineExclude" }; + + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + + ASSERT_EQ( respathsMap.size(), 1 ); // "/path1/foo/bar" + "[ * ]" + MapContainsPaths( respathsPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( respathsPaths, respathsMap, defaultIncludes, defaultExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 1 ); // "/path1/loo/bar" + "[ *, .inlineInclude ] ![ .toplevelExclude .inlineExclude ]" + MapContainsPaths( resfilesPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( resfilesPaths, resfileMap, overrideIncludes, overrideExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // both + MapContainsPaths( combinedPaths, combinedMap, "ResolvedCombinedMap" ); + // Manually validate combined map since it combines both overrides and defaults + for( const auto& kv : combinedMap ) + { + // The "resfile" part: + if( kv.first == "/path1/loo/car" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), "*" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".inlineInclude" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetExcludeFilter().begin(), kv.second.GetExcludeFilter().end(), ".toplevelExclude" ) != kv.second.GetExcludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetExcludeFilter().begin(), kv.second.GetExcludeFilter().end(), ".inlineExclude" ) != kv.second.GetExcludeFilter().end() ); + } + // The "respaths" part: + else if( kv.first == "/path1/foo/bar" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetIncludeFilter()[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], ".toplevelExclude" ); // Top-level exclude filter + } + } +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_CombinedResolvedMap_OverwrittenByResfileMap ) +{ + std::string sectionName = "FilterNamedSection_Valid_CombinedResolvedMap_OverwrittenByResfileMap"; + std::string defaultParentPrefixMapStr = "prefix1:/pathA;/pathB"; + std::string filter = "[ .in1 .in2 ] ![ .ex1 ]"; + std::string respaths = "prefix1:/foo/bar"; + std::string resfile = "prefix1:/foo/bar [ .extra ]"; // Same path, extra include filter + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set allPaths = { "/pathA/foo/bar", "/pathB/foo/bar" }; + std::vector defaultIncludes = { ".in1", ".in2" }; + std::vector allExcludes = { ".ex1" }; + std::vector overrideIncludes = { ".in1", ".in2", ".extra" }; + + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + + ASSERT_EQ( respathsMap.size(), 2 ); + MapContainsPaths( allPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( allPaths, respathsMap, defaultIncludes, allExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 2 ); + MapContainsPaths( allPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( allPaths, resfileMap, overrideIncludes, allExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // Both, same count but now with overrides + MapContainsPaths( allPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( allPaths, combinedMap, overrideIncludes, allExcludes, "ResolvedCombinedMap" ); + + // Re-validate that RespathsMap is unchanged + const auto& respathsAgainMap = namedSection.GetResolvedRespathsMap(); + MapContainsPaths( allPaths, respathsAgainMap, "ResolvedRespathsMap-Again" ); + ValidatePathMap( allPaths, respathsAgainMap, defaultIncludes, allExcludes, "ResolvedRespathsMap-Again" ); +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_SameCombinedResolvedMap_EmptyTopLevelFilter_OverwrittenByResfileMap ) +{ + std::string sectionName = "FilterNamedSection_Valid_SameCombinedResolvedMap_EmptyTopLevelFilter_OverwrittenByResfileMap"; + std::string defaultParentPrefixMapStr = "prefix1:/pathA;/pathB"; + std::string filter = ""; // Empty top-level filter should add wildcard ("*") include + std::string respaths = "prefix1:/foo/bar"; + std::string resfile = "prefix1:/foo/bar [ .extra ]"; // Same path, extra include filter + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set allPaths = { "/pathA/foo/bar", "/pathB/foo/bar" }; + std::vector defaultIncludes = { "*" }; + std::vector allExcludes = {}; + std::vector overrideIncludes = { "*", ".extra" }; + + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + + ASSERT_EQ( respathsMap.size(), 2 ); + MapContainsPaths( allPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( allPaths, respathsMap, defaultIncludes, allExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 2 ); + MapContainsPaths( allPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( allPaths, resfileMap, overrideIncludes, allExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // Both, same count but now with overrides + MapContainsPaths( allPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( allPaths, combinedMap, overrideIncludes, allExcludes, "ResolvedCombinedMap" ); + for( const auto& kv : combinedMap ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_EQ( kv.second.GetIncludeFilter()[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter + EXPECT_EQ( kv.second.GetIncludeFilter()[1], ".extra" ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + EXPECT_TRUE( kv.second.GetExcludeFilter().empty() ); + } + + // Re-validate that RespathsMap is unchanged + const auto& respathsAgainMap = namedSection.GetResolvedRespathsMap(); + MapContainsPaths( allPaths, respathsAgainMap, "ResolvedRespathsMap-Again" ); + ValidatePathMap( allPaths, respathsAgainMap, defaultIncludes, allExcludes, "ResolvedRespathsMap-Again" ); +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_SameCombinedResolvedMap_EmptyTopLevelFilter_OverwrittenByRespathMap ) +{ + std::string sectionName = "FilterNamedSection_Valid_SameCombinedResolvedMap_EmptyTopLevelFilter_OverwrittenByRespathMap"; + std::string defaultParentPrefixMapStr = "prefix1:/pathA;/pathB"; + std::string filter = ""; // Empty top-level filter should add wildcard ("*") include + std::string respaths = "prefix1:/foo/bar [ .extra ]"; // Same path, extra include filter + std::string resfile = "prefix1:/foo/bar"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set allPaths = { "/pathA/foo/bar", "/pathB/foo/bar" }; + std::vector defaultIncludes = { "*" }; + std::vector allExcludes = {}; + std::vector overrideIncludes = { "*", ".extra" }; + + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + + ASSERT_EQ( respathsMap.size(), 2 ); + MapContainsPaths( allPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( allPaths, respathsMap, overrideIncludes, allExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 2 ); + MapContainsPaths( allPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( allPaths, resfileMap, defaultIncludes, allExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // Both, same count but now with overrides + MapContainsPaths( allPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( allPaths, combinedMap, overrideIncludes, allExcludes, "ResolvedCombinedMap" ); + for( const auto& kv : combinedMap ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_EQ( kv.second.GetIncludeFilter()[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter + EXPECT_EQ( kv.second.GetIncludeFilter()[1], ".extra" ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + EXPECT_TRUE( kv.second.GetExcludeFilter().empty() ); + } + + // Re-validate original Respaths/files Maps + const auto& respathsAgainMap = namedSection.GetResolvedRespathsMap(); + MapContainsPaths( allPaths, respathsAgainMap, "ResolvedRespathsMap-Again" ); + ValidatePathMap( allPaths, respathsAgainMap, overrideIncludes, allExcludes, "ResolvedRespathsMap-Again" ); + + const auto& resfileAgainMap = namedSection.GetResolvedResfileMap(); + MapContainsPaths( allPaths, resfileAgainMap, "ResolvedResfileMap-Again" ); + ValidatePathMap( allPaths, resfileAgainMap, defaultIncludes, allExcludes, "ResolvedResfileMap-Again" ); +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_SameCombinedResolvedMap_ExcludeOnlyTopLevelFilter_OverwrittenByResfileMap ) +{ + std::string sectionName = "FilterNamedSection_Valid_SameCombinedResolvedMap_ExcludeOnlyTopLevelFilter_OverwrittenByResfileMap"; + std::string defaultParentPrefixMapStr = "prefix1:/pathA;/pathB"; + std::string filter = " ![ .topLevelExclude ]"; // Only exclude filter at top-level should add wildcard ("*") include + std::string respaths = "prefix1:/foo/bar"; + std::string resfile = "prefix1:/foo/bar [ .extra ]"; // Same path, extra include filter + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set allPaths = { "/pathA/foo/bar", "/pathB/foo/bar" }; + std::vector defaultIncludes = { "*" }; + std::vector allExcludes = { ".topLevelExclude" }; + std::vector overrideIncludes = { "*", ".extra" }; + + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + + ASSERT_EQ( respathsMap.size(), 2 ); + MapContainsPaths( allPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( allPaths, respathsMap, defaultIncludes, allExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 2 ); + MapContainsPaths( allPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( allPaths, resfileMap, overrideIncludes, allExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // Both, same count but now with overrides + MapContainsPaths( allPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( allPaths, combinedMap, overrideIncludes, allExcludes, "ResolvedCombinedMap" ); + for( const auto& kv : combinedMap ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_EQ( kv.second.GetIncludeFilter()[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter + EXPECT_EQ( kv.second.GetIncludeFilter()[1], ".extra" ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], ".topLevelExclude" ); + } + + // Re-validate that RespathsMap is unchanged + const auto& respathsAgainMap = namedSection.GetResolvedRespathsMap(); + MapContainsPaths( allPaths, respathsAgainMap, "ResolvedRespathsMap-Again" ); + ValidatePathMap( allPaths, respathsAgainMap, defaultIncludes, allExcludes, "ResolvedRespathsMap-Again" ); +} + +TEST_F( ResourceFilterTest, FilterNamedSection_Valid_SameCombinedResolvedMap_ExcludeOnlyTopLevelFilter_OverwrittenByRespathMap ) +{ + std::string sectionName = "FilterNamedSection_Valid_SameCombinedResolvedMap_ExcludeOnlyTopLevelFilter_OverwrittenByRespathMap"; + std::string defaultParentPrefixMapStr = "prefix1:/pathA;/pathB"; + std::string filter = " ![ .topLevelExclude ]"; // Only exclude filter at top-level should add wildcard ("*") include + std::string respaths = "prefix1:/foo/bar [ .extra ]"; // Same path, extra include filter + std::string resfile = "prefix1:/foo/bar"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + ResourceTools::FilterNamedSection namedSection( sectionName, filter, respaths, resfile, defaultPrefixMap ); + + // Expected values: + std::set allPaths = { "/pathA/foo/bar", "/pathB/foo/bar" }; + std::vector defaultIncludes = { "*" }; + std::vector allExcludes = { ".topLevelExclude" }; + std::vector overrideIncludes = { "*", ".extra" }; + + const auto& combinedMap = namedSection.GetCombinedResolvedPathMap(); + const auto& respathsMap = namedSection.GetResolvedRespathsMap(); + const auto& resfileMap = namedSection.GetResolvedResfileMap(); + + ASSERT_EQ( respathsMap.size(), 2 ); + MapContainsPaths( allPaths, respathsMap, "ResolvedRespathsMap" ); + ValidatePathMap( allPaths, respathsMap, overrideIncludes, allExcludes, "ResolvedRespathsMap" ); + + ASSERT_EQ( resfileMap.size(), 2 ); + MapContainsPaths( allPaths, resfileMap, "ResolvedResfileMap" ); + ValidatePathMap( allPaths, resfileMap, defaultIncludes, allExcludes, "ResolvedResfileMap" ); + + ASSERT_EQ( combinedMap.size(), 2 ); // Both, same count but now with overrides + MapContainsPaths( allPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( allPaths, combinedMap, overrideIncludes, allExcludes, "ResolvedCombinedMap" ); + for( const auto& kv : combinedMap ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_EQ( kv.second.GetIncludeFilter()[0], "*" ); // Wild-card added when no include filter specified for TOP-LEVEL filter + EXPECT_EQ( kv.second.GetIncludeFilter()[1], ".extra" ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], ".topLevelExclude" ); + } + + // Re-validate original Respaths/files Maps + const auto& respathsAgainMap = namedSection.GetResolvedRespathsMap(); + MapContainsPaths( allPaths, respathsAgainMap, "ResolvedRespathsMap-Again" ); + ValidatePathMap( allPaths, respathsAgainMap, overrideIncludes, allExcludes, "ResolvedRespathsMap-Again" ); + + const auto& resfileAgainMap = namedSection.GetResolvedResfileMap(); + MapContainsPaths( allPaths, resfileAgainMap, "ResolvedResfileMap-Again" ); + ValidatePathMap( allPaths, resfileAgainMap, defaultIncludes, allExcludes, "ResolvedResfileMap-Again" ); +} + +// ------------------------------------------ + +TEST_F( ResourceFilterTest, FilterResourceFile_Load_example1_ini ) +{ + // Use the test fixture's helper to get the absolute path + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/example1.ini" ); + + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.string() ); + const auto& iniFilePathMap = resourceFile.GetIniFileResolvedPathMap(); + + // Validate the paths: + std::set expectedPaths = { + // From the respaths attribute: + "./Indicies/firstLine/...", // "res:/firstLine/..." + "./resourcesOnBranch/firstLine/...", // "res:/firstLine/..." + "./Indicies/secondLine/*", // "res:/secondLine/*" + "./resourcesOnBranch/secondLine/*", // "res:/secondLine/*" + "./ResourceGroups/thirdLine/...", // "res2:/thirdLine/..." + // From the resfile attribute: + "./Indicies/binaryFileIndex_v0_0_0.txt", // "res:/binaryFileIndex_v0_0_0.txt" + "./resourcesOnBranch/binaryFileIndex_v0_0_0.txt" // "res:/binaryFileIndex_v0_0_0.txt" + }; + std::vector expectedIncludes = { ".yaml" }; + std::vector expectedExcludes = {}; + + MapContainsPaths( expectedPaths, iniFilePathMap, "FullResolvedPathMap from example1.ini" ); + ValidatePathMap( expectedPaths, iniFilePathMap, expectedIncludes, expectedExcludes, "FullResolvedPathMap from example1.ini" ); + } + catch( const std::exception& e ) + { + FAIL() << "Exception thrown while loading example1.ini: " << e.what(); + } + catch( ... ) + { + FAIL() << "Unknown exception thrown while loading example1.ini"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFile_Load_invalidMissingDefaultSection_ini ) +{ + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidMissingDefaultSection.ini" ); + + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.string() ); + FAIL() << "Expected std::invalid_argument when loading ini file missing [DEFAULT] section"; + } + catch( const std::invalid_argument& e ) + { + std::string expectedError = "Missing [DEFAULT] section in INI file: " + iniPath.string(); + EXPECT_STREQ( e.what(), expectedError.c_str() ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when loading ini file missing [DEFAULT] section"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFile_Load_invalidMissingNamedSection_ini ) +{ + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidMissingNamedSection.ini" ); + + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.string() ); + FAIL() << "Expected std::invalid_argument when loading ini file missing [NamedSection] section"; + } + catch( const std::invalid_argument& e ) + { + std::string expectedError = "No namedSections defined in INI file: " + iniPath.string(); + EXPECT_STREQ( e.what(), expectedError.c_str() ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when loading ini file missing [NamedSection] section"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFile_Load_iniFileNotFound ) +{ + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/iniFileNotFound.ini" ); + + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.string() ); + FAIL() << "Expected std::runtime_error when loading non-existent ini file"; + } + catch( const std::runtime_error& e ) + { + std::string expectedError = "Failed to parse INI file: " + iniPath.string() + " - unable to open file"; + EXPECT_STREQ( e.what(), expectedError.c_str() ); + } + catch( ... ) + { + FAIL() << "Expected std::runtime_error when loading non-existent ini file"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFile_Load_invalidPrefixmap_ini ) +{ + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidPrefixmap.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.string() ); + FAIL() << "Expected std::invalid_argument when loading ini file with invalid prefixmap"; + } + catch( const std::invalid_argument& e ) + { + std::string expectedError = "Invalid prefixmap format: No paths defined for prefix: prefix1"; + EXPECT_STREQ( e.what(), expectedError.c_str() ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when loading invalidPrefixmap.ini file"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFile_Load_invalidSectionFilter_ini ) +{ + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidSectionFilter.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.string() ); + FAIL() << "Expected std::invalid_argument when loading ini file with invalid section filter"; + } + catch( const std::invalid_argument& e ) + { + std::string expectedError = "Invalid filter format: missing ']'"; + EXPECT_STREQ( e.what(), expectedError.c_str() ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when loading invalidSectionFilter.ini file"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFile_Load_invalidInlineFilter_ini ) +{ + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidInlineFilter.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.string() ); + FAIL() << "Expected std::invalid_argument when loading ini file with invalid inline filter"; + } + catch( const std::invalid_argument& e ) + { + std::string expectedError = "Invalid filter format: missing '['"; + EXPECT_STREQ( e.what(), expectedError.c_str() ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when loading invalidInlineFilter.ini file"; + } +} + +TEST_F( ResourceFilterTest, FilterResourceFile_Load_invalidPrefixMismatch_ini ) +{ + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidPrefixMismatch.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.string() ); + FAIL() << "Expected std::invalid_argument when loading ini file with prefix mismatch"; + } + catch( const std::invalid_argument& e ) + { + std::string expectedError = "Prefix 'prefixDoesNotExist' not present in prefixMap for line: prefixDoesNotExist:/firstLine/*"; + EXPECT_STREQ( e.what(), expectedError.c_str() ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when loading invalidPrefixMismatch.ini file"; + } +} + +// ------------------------------------------ + +TEST_F( ResourceFilterTest, ResourceFilter_Load_SingleFile_ThatDoesNotExist ) +{ + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/noSuchFile.ini" ); + std::vector paths = { iniPath }; + + ResourceTools::ResourceFilter resourceFilter; + + try + { + resourceFilter.Initialize( paths ); + FAIL() << "Expected this test to fail: ResourceFilter_Load_SingleFile_ThatDoesNotExist"; + } + catch( const std::exception& e ) + { + std::string errorMessage = e.what(); + ASSERT_NE( errorMessage.find( "Unable to create ResourceFilter for:" ), std::string::npos ); + ASSERT_NE( errorMessage.find( "Failed to parse INI file" ), std::string::npos ); + ASSERT_NE( errorMessage.find( "unable to open file" ), std::string::npos ); + } + catch( ... ) + { + FAIL() << "Unknown exception thrown while initializing ResourceFilter with example1.ini"; + } +} + +TEST_F( ResourceFilterTest, ResourceFilter_Load_MultipleFiles_OneThatDoesNotExist ) +{ + const std::filesystem::path iniPath1 = GetTestFileFileAbsolutePath( "ExampleIniFiles/example1.ini" ); + const std::filesystem::path iniPath2 = GetTestFileFileAbsolutePath( "ExampleIniFiles/noSuchFile.ini" ); + std::vector paths = { iniPath1, iniPath2 }; + + ResourceTools::ResourceFilter resourceFilter; + + try + { + resourceFilter.Initialize( paths ); + FAIL() << "Expected this test to fail: ResourceFilter_Load_MultipleFiles_OneThatDoesNotExist"; + } + catch( const std::exception& e ) + { + std::string errorMessage = e.what(); + ASSERT_NE( errorMessage.find( "Unable to create ResourceFilter for:" ), std::string::npos ); + ASSERT_NE( errorMessage.find( "Failed to parse INI file" ), std::string::npos ); + ASSERT_NE( errorMessage.find( "unable to open file" ), std::string::npos ); + } + catch( ... ) + { + FAIL() << "Unknown exception thrown while initializing ResourceFilter with example1.ini"; + } +} + +TEST_F( ResourceFilterTest, ResourceFilter_JustLoad_example1_ini ) +{ + const std::filesystem::path iniPath1 = GetTestFileFileAbsolutePath( "ExampleIniFiles/example1.ini" ); + std::vector paths = { iniPath1 }; + + ResourceTools::ResourceFilter resourceFilter; + + try + { + resourceFilter.Initialize( paths ); + } + catch( const std::exception& e ) + { + FAIL() << "Exception in test, should not have failed: " << e.what(); + } + catch( ... ) + { + FAIL() << "Unknown exception thrown while initializing ResourceFilter with example1.ini"; + } +} + +TEST_F( ResourceFilterTest, ResourceFilter_Test_CurrentWorkingDirectoryChanger ) +{ + // RAII class to change the current working directory for the duration of this test + // Needed so both relative paths in the .ini file resolve correctly, based on paths to the location of the example .ini files + CurrentWorkingDirectoryChanger cwdRAII( TEST_DATA_BASE_PATH ); + + const std::filesystem::path iniPath1 = "ExampleIniFiles/example1.ini"; + std::vector paths = { iniPath1 }; + + ResourceTools::ResourceFilter resourceFilter; + try + { + resourceFilter.Initialize( paths ); + ASSERT_EQ( resourceFilter.HasFilters(), true ); + ASSERT_EQ( resourceFilter.GetFullResolvedPathMap().size(), 7 ); + + // Check that the "binaryFileIndex_v0_0_0.txt" file (and it's path) is included correctly + std::filesystem::path oneValidRelativePath = "./Indicies/binaryFileIndex_v0_0_0.txt"; + ASSERT_EQ( resourceFilter.ShouldInclude( oneValidRelativePath ), true ); + } + catch( const std::exception& e ) + { + FAIL() << "Exception in test, should not have failed: " << e.what(); + } + catch( ... ) + { + FAIL() << "Unknown exception thrown while initializing ResourceFilter with example1.ini"; + } +} + +TEST_F( ResourceFilterTest, ResourceFilter_Load_validSimpleExample1_ini_usingRelativePaths ) +{ + // Alter the current working directory for the duration of this test + CurrentWorkingDirectoryChanger cwdRAII( TEST_DATA_BASE_PATH ); + + try + { + const std::filesystem::path iniPath1 = "ExampleIniFiles/validSimpleExample1.ini"; + std::vector paths = { iniPath1 }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( paths ); + + // Validate correct included paths via the resourceFilter: + std::set validResolvedRelativePaths = { + "resourcesOnBranch/introMovie.txt", + "resourcesOnBranch/videoCardCategories.yaml" + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedRelativePath : validResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.ShouldInclude( resolvedRelativePath ), true ) << "Should have included relative path: " << resolvedRelativePath.generic_string(); + } + + // Additional check to make sure the FullResolvedPathMap contains correct data (either include or exclude): + const auto& fullPathMap = resourceFilter.GetFullResolvedPathMap(); + ASSERT_EQ( fullPathMap.size(), 1 ); + + std::set expectedPaths = { + "./resourcesOnBranch/*" + }; + std::vector expectedIncludes = { ".yaml", ".txt" }; + std::vector expectedExcludes = {}; + + MapContainsPaths( expectedPaths, fullPathMap, "FullResolvedPathMap from validSimpleExample1.ini" ); + ValidatePathMap( expectedPaths, fullPathMap, expectedIncludes, expectedExcludes, "FullResolvedPathMap from validSimpleExample1.ini" ); + } + catch( ... ) + { + FAIL() << "Test [ResourceFilter_Load_validSimpleExample1_ini] failed when it should have passed."; + } +} + +TEST_F( ResourceFilterTest, ResourceFilter_Load_validSimpleExample1_ini_usingAbsolutePaths ) +{ + // Alter the current working directory for the duration of this test + CurrentWorkingDirectoryChanger cwdRAII( TEST_DATA_BASE_PATH ); + + try + { + const std::filesystem::path iniPath1 = "ExampleIniFiles/validSimpleExample1.ini"; + std::filesystem::path iniPath1Abs = std::filesystem::absolute( iniPath1 ); + std::vector paths = { iniPath1Abs }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( paths ); + + // Validate correct included paths via the resourceFilter: + std::set validResolvedAbsolutePaths = { + std::filesystem::absolute( "resourcesOnBranch/introMovie.txt" ), + std::filesystem::absolute( "resourcesOnBranch/videoCardCategories.yaml" ) + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedAbsPath : validResolvedAbsolutePaths ) + { + ASSERT_EQ( resourceFilter.ShouldInclude( resolvedAbsPath ), true ) << "Should have included absolute path: " << resolvedAbsPath.generic_string(); + } + + // Additional check to make sure the FullResolvedPathMap contains correct data (either include or exclude): + const auto& fullPathMap = resourceFilter.GetFullResolvedPathMap(); + ASSERT_EQ( fullPathMap.size(), 1 ); + + std::set expectedPaths = { + "./resourcesOnBranch/*" + }; + std::vector expectedIncludes = { ".yaml", ".txt" }; + std::vector expectedExcludes = {}; + + MapContainsPaths( expectedPaths, fullPathMap, "FullResolvedPathMap from validSimpleExample1.ini" ); + ValidatePathMap( expectedPaths, fullPathMap, expectedIncludes, expectedExcludes, "FullResolvedPathMap from validSimpleExample1.ini" ); + } + catch( ... ) + { + FAIL() << "Test [ResourceFilter_Load_validSimpleExample1_ini_usingAbsolutePaths] failed when it should have passed."; + } +} + +TEST_F( ResourceFilterTest, ResourceFilter_Load_validComplexExample1_ini_usingRelativePaths ) +{ + // Alter the current working directory for the duration of this test + CurrentWorkingDirectoryChanger cwdRAII( TEST_DATA_BASE_PATH ); + + try + { + const std::filesystem::path iniPath1 = "ExampleIniFiles/validComplexExample1.ini"; + std::vector paths = { iniPath1 }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( paths ); + + // Validate correct included paths via the resourceFilter: + std::set validResolvedRelativePaths = { + //"PatchWithInputChunk/NextBuildResources/introMovie.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + "PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + resLocalCDN:/../NextBuildResources/introMoviePrefixed.txt + //"PatchWithInputChunk/NextBuildResources/introMovieSomewhatChanged.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + "PatchWithInputChunk/NextBuildResources/testResource2.txt", + "PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml", + "PatchWithInputChunk/PreviousBuildResources/introMovie.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + "PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + "PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + //"PatchWithInputChunk/PreviousBuildResources/testResource.txt", // resRoot:/PatchWithInputChunk/PreviousBuildResources/* ![ testResource.txt ] + "PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml", + "PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml", + "PatchWithInputChunk/resFileIndexShort_build_next.txt", + "PatchWithInputChunk/resFileIndexShort_build_previous.txt", + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedRelativePath : validResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.ShouldInclude( resolvedRelativePath ), true ) << "Should have included relative path: " << resolvedRelativePath.generic_string(); + } + + // Additional check to make sure the FullResolvedPathMap contains correct data (either include or exclude): + std::set expectedPaths = { + "PatchWithInputChunk/...", + "./PatchWithInputChunk/...", + "PatchWithInputChunk/PreviousBuildResources/*", + "PatchWithInputChunk/LocalCDNPatches/../NextBuildResources/introMoviePrefixed.txt", + "./PatchWithInputChunk/PreviousBuildResources/*" + }; + const auto& fullPathMap = resourceFilter.GetFullResolvedPathMap(); + ASSERT_EQ( fullPathMap.size(), expectedPaths.size() ); + MapContainsPaths( expectedPaths, fullPathMap, "FullResolvedPathMap from validSimpleExample1.ini" ); + + // Manually validate the fullPathMap, as it has several different prefixPathCombos + some inline filter overrides + for( const auto& kv : fullPathMap ) + { + if( kv.first == "PatchWithInputChunk/..." ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + } + else if( kv.first == "./PatchWithInputChunk/..." ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], "Movie" ); + } + else if( kv.first == "PatchWithInputChunk/PreviousBuildResources/*" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 3 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), "Movie" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + } + else if( kv.first == "PatchWithInputChunk/LocalCDNPatches/../NextBuildResources/introMoviePrefixed.txt" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + } + else if( kv.first == "./PatchWithInputChunk/PreviousBuildResources/*" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], "testResource.txt" ); + } + else + { + FAIL() << "Unexpected path found in FullResolvedPathMap: " << kv.first; + } + } + } + catch( const std::exception& e ) + { + FAIL() << "Test [ResourceFilter_Load_validComplexExample1_ini_usingRelativePaths] failed with: " << e.what(); + } + catch( ... ) + { + FAIL() << "Test [ResourceFilter_Load_validComplexExample1_ini_usingRelativePaths] failed when it should have passed."; + } +} + +TEST_F( ResourceFilterTest, ResourceFilter_Load2iniFiles_validComplexExample1_and_validSimpleExample1 ) +{ + // Alter the current working directory for the duration of this test + CurrentWorkingDirectoryChanger cwdRAII( TEST_DATA_BASE_PATH ); + + try + { + std::vector paths = { + "ExampleIniFiles/validComplexExample1.ini", + "ExampleIniFiles/validSimpleExample1.ini" + }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( paths ); + + // Validate correct included paths via the resourceFilter: + std::set validResolvedRelativePaths = { + // From validComplexExample1: + //"PatchWithInputChunk/NextBuildResources/introMovie.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + "PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + resLocalCDN:/../NextBuildResources/introMoviePrefixed.txt + //"PatchWithInputChunk/NextBuildResources/introMovieSomewhatChanged.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + "PatchWithInputChunk/NextBuildResources/testResource2.txt", + "PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml", + "PatchWithInputChunk/PreviousBuildResources/introMovie.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + "PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + "PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + //"PatchWithInputChunk/PreviousBuildResources/testResource.txt", // resRoot:/PatchWithInputChunk/PreviousBuildResources/* ![ testResource.txt ] + "PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml", + "PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml", + "PatchWithInputChunk/resFileIndexShort_build_next.txt", + "PatchWithInputChunk/resFileIndexShort_build_previous.txt", + // From validSimpleExample1.ini: + "resourcesOnBranch/introMovie.txt", + "resourcesOnBranch/videoCardCategories.yaml" + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedRelativePath : validResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.ShouldInclude( resolvedRelativePath ), true ) << "Should have included relative path: " << resolvedRelativePath.generic_string(); + } + + // Additional check to make sure the FullResolvedPathMap contains correct data (either include or exclude): + std::set expectedPaths = { + "PatchWithInputChunk/...", + "./PatchWithInputChunk/...", + "PatchWithInputChunk/PreviousBuildResources/*", + "PatchWithInputChunk/LocalCDNPatches/../NextBuildResources/introMoviePrefixed.txt", + "./PatchWithInputChunk/PreviousBuildResources/*", + "./resourcesOnBranch/*" + }; + const auto& fullPathMap = resourceFilter.GetFullResolvedPathMap(); + ASSERT_EQ( fullPathMap.size(), expectedPaths.size() ); + MapContainsPaths( expectedPaths, fullPathMap, "FullResolvedPathMap from two ini files" ); + + // Manually validate the fullPathMap, as it has several different prefixPathCombos + some inline filter overrides + for( const auto& kv : fullPathMap ) + { + if( kv.first == "PatchWithInputChunk/..." ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + } + else if( kv.first == "./PatchWithInputChunk/..." ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], "Movie" ); + } + else if( kv.first == "PatchWithInputChunk/PreviousBuildResources/*" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 3 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), "Movie" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + } + else if( kv.first == "PatchWithInputChunk/LocalCDNPatches/../NextBuildResources/introMoviePrefixed.txt" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + } + else if( kv.first == "./PatchWithInputChunk/PreviousBuildResources/*" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 1 ); + EXPECT_EQ( kv.second.GetExcludeFilter()[0], "testResource.txt" ); + } + else if( kv.first == "./resourcesOnBranch/*" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetIncludeFilter().begin(), kv.second.GetIncludeFilter().end(), ".txt" ) != kv.second.GetIncludeFilter().end() ); + + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + } + else + { + FAIL() << "Unexpected path found in FullResolvedPathMap: " << kv.first; + } + } + } + catch( const std::exception& e ) + { + FAIL() << "Test [ResourceFilter_Load2iniFiles_validComplexExample1_and_validSimpleExample1] failed with: " << e.what(); + } + catch( ... ) + { + FAIL() << "Test [ResourceFilter_Load2iniFiles_validComplexExample1_and_validSimpleExample1] failed when it should have passed."; + } +} \ No newline at end of file diff --git a/tests/src/ResourceFilterTest.h b/tests/src/ResourceFilterTest.h new file mode 100644 index 0000000..66694a9 --- /dev/null +++ b/tests/src/ResourceFilterTest.h @@ -0,0 +1,15 @@ +// Copyright © 2025 CCP ehf. + +#pragma once +#ifndef ResourceFilterTest_H +#define ResourceFilterTest_H + +#include "ResourcesTestFixture.h" +#include + +// Inherit from ResourcesTestFixture to gain access to file and directory helper functions +class ResourceFilterTest : public ResourcesTestFixture +{ +}; + +#endif // ResourceFilterTest_H \ No newline at end of file diff --git a/tests/src/ResourcesCliTest.cpp b/tests/src/ResourcesCliTest.cpp index bd8d133..3946658 100644 --- a/tests/src/ResourcesCliTest.cpp +++ b/tests/src/ResourcesCliTest.cpp @@ -308,6 +308,166 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryOldDocumentFormatWithP EXPECT_TRUE( FilesMatch( goldFile, outputFile ) ); } +//--------------------------------------- + +TEST_F( ResourcesCliTest, CreateGroup_UsingFilter_validSimpleExample1 ) +{ + // Alter the current working directory for the duration of this test + CurrentWorkingDirectoryChanger cwdRAII( TEST_DATA_BASE_PATH ); + + // Setup test parameters + std::string output; + std::vector arguments; + std::filesystem::path inputDirectoryPath = "."; // The base testData directory + std::filesystem::path outputFilePath = "./IgnoredTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1.yaml"; + std::filesystem::path filterIniFilePath = "./ExampleIniFiles/validSimpleExample1.ini"; + + // Ensure any previous test output files are removed + CleanupTestOutputFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + + arguments.push_back( std::filesystem::absolute( inputDirectoryPath ).generic_string() ); + + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + + arguments.push_back( "--filter-file" ); + arguments.push_back( std::filesystem::absolute( filterIniFilePath ).generic_string() ); + + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.generic_string() ); + + int res = RunCli( arguments, output ); + std::cout << "Test RunCli output: " << std::endl; + std::cout << "----------------------------------" << std::endl; + std::cout << output << std::endl; + std::cout << "----------------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, output: " << output; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = std::filesystem::absolute( "./ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.yaml" ); +#elif __APPLE__ + std::filesystem::path goldFile = std::filesystem::absolute( "./ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.yaml" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ); + + // Cleanup test output files + CleanupTestOutputFiles( { outputFilePath } ); +} + +TEST_F( ResourcesCliTest, CreateGroup_UsingFilter_validComplexExample1 ) +{ + // Alter the current working directory for the duration of this test + CurrentWorkingDirectoryChanger cwdRAII( TEST_DATA_BASE_PATH ); + + // Setup test parameters + std::string output; + std::vector arguments; + std::filesystem::path inputDirectoryPath = "."; // The base testData directory + std::filesystem::path outputFilePath = "./IgnoredTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1.yaml"; + std::filesystem::path filterIniFilePath = "./ExampleIniFiles/validComplexExample1.ini"; + + // Ensure any previous test output files are removed + CleanupTestOutputFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + + arguments.push_back( std::filesystem::absolute( inputDirectoryPath ).generic_string() ); + + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + + arguments.push_back( "--filter-file" ); + arguments.push_back( std::filesystem::absolute( filterIniFilePath ).generic_string() ); + + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.generic_string() ); + + int res = RunCli( arguments, output ); + std::cout << "Test RunCli output: " << std::endl; + std::cout << "----------------------------------" << std::endl; + std::cout << output << std::endl; + std::cout << "----------------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, output: " << output; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = std::filesystem::absolute( "./ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_Windows.yaml" ); +#elif __APPLE__ + std::filesystem::path goldFile = std::filesystem::absolute( "./ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_macOS.yaml" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ); + + // Cleanup test output files + CleanupTestOutputFiles( { outputFilePath } ); +} + +TEST_F( ResourcesCliTest, CreateGroup_UsingFilter_validSimpleAndComplexExample1 ) +{ + // Alter the current working directory for the duration of this test + CurrentWorkingDirectoryChanger cwdRAII( TEST_DATA_BASE_PATH ); + + // Setup test parameters + std::string output; + std::vector arguments; + std::filesystem::path inputDirectoryPath = "."; // The base testData directory + std::filesystem::path outputFilePath = "./IgnoredTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1.yaml"; + std::vector filterIniFilePaths = { + "./ExampleIniFiles/validSimpleExample1.ini", + "./ExampleIniFiles/validComplexExample1.ini" + }; + + // Ensure any previous test output files are removed + CleanupTestOutputFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + + arguments.push_back( std::filesystem::absolute( inputDirectoryPath ).generic_string() ); + + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + + for( auto filterFilePath : filterIniFilePaths ) + { + arguments.push_back( "--filter-file" ); + arguments.push_back( std::filesystem::absolute( filterFilePath ).generic_string() ); + } + + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.generic_string() ); + + int res = RunCli( arguments, output ); + std::cout << "Test RunCli output: " << std::endl; + std::cout << "----------------------------------" << std::endl; + std::cout << output << std::endl; + std::cout << "----------------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, output: " << output; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = std::filesystem::absolute( "./ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_Windows.yaml" ); +#elif __APPLE__ + std::filesystem::path goldFile = std::filesystem::absolute( "./ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_macOS.yaml" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ); + + // Cleanup test output files + CleanupTestOutputFiles( { outputFilePath } ); +} + +//--------------------------------------- + TEST_F( ResourcesCliTest, CreateBundle ) { std::string output; diff --git a/tests/src/ResourcesTestFixture.h b/tests/src/ResourcesTestFixture.h index 94fbf0e..f8b5987 100644 --- a/tests/src/ResourcesTestFixture.h +++ b/tests/src/ResourcesTestFixture.h @@ -25,4 +25,50 @@ struct ResourcesTestFixture : public ::testing::Test bool DirectoryIsSubset( const std::filesystem::path& dir1, const std::filesystem::path& dir2 ); // Test that all files in dir1 exist in dir2, and the contents of the files in both directories are the same. }; +// RAII helper class to change the current working directory temporarily (within a scope) +class CurrentWorkingDirectoryChanger { +public: + // Constructor acquires the current path and changes it + explicit CurrentWorkingDirectoryChanger(const std::filesystem::path& new_path) : + original_path_(std::filesystem::current_path()) + { + try + { + std::cout << "CurrentWorkingDirectoryChanger - Original directory: " << original_path_.generic_string() << std::endl; + std::filesystem::current_path(new_path); // Change to new path + std::cout << "CurrentWorkingDirectoryChanger - Changed directory to: " << std::filesystem::current_path().generic_string() << std::endl; + } + catch (const std::filesystem::filesystem_error& e) + { + std::cerr << "CurrentWorkingDirectoryChanger - Error changing directory: " << e.what() << std::endl; + } + } + + // Destructor restores the original path + ~CurrentWorkingDirectoryChanger() + { + try + { + std::filesystem::current_path(original_path_); // Restore original path + std::cout << "CurrentWorkingDirectoryChanger - Restored directory to: " << std::filesystem::current_path().generic_string() << std::endl; + } + catch (const std::filesystem::filesystem_error& e) + { + std::cerr << "CurrentWorkingDirectoryChanger - Error restoring directory: " << e.what() << std::endl; + } + } + + // Disable copy and move operations + CurrentWorkingDirectoryChanger(const CurrentWorkingDirectoryChanger&) = delete; + + CurrentWorkingDirectoryChanger& operator=(const CurrentWorkingDirectoryChanger&) = delete; + + CurrentWorkingDirectoryChanger(CurrentWorkingDirectoryChanger&&) = delete; + + CurrentWorkingDirectoryChanger& operator=(CurrentWorkingDirectoryChanger&&) = delete; + +private: + std::filesystem::path original_path_; +}; + #endif // CarbonResourcesTestFixture_H \ No newline at end of file diff --git a/tests/testData/ExampleIniFiles/example1.ini b/tests/testData/ExampleIniFiles/example1.ini new file mode 100644 index 0000000..84a45bb --- /dev/null +++ b/tests/testData/ExampleIniFiles/example1.ini @@ -0,0 +1,23 @@ +# First line of comment (Unix style) +; Second line of comment (regular ini file comment style) + +[DEFAULT] +version = 1.2 +prefixmap = res:./Indicies;./resourcesOnBranch res2:./ResourceGroups + +#============================================================================= +# example1.ini - test file +# - This file is intended to test generic ini file parsing. +# - Ini file parser of this file should handle both regular ini comments (;) and Unix style (#) comments. +; - It should handle multi-line values too (as in the respaths values below, with empties inbetween). +;============================================================================= + +[testYamlFilesOverMultiLineResPathsWithEmptyLines] +filter = [ .yaml ] +respaths = res:/firstLine/... + res:/secondLine/* + + + res2:/thirdLine/... +resfile = res:/binaryFileIndex_v0_0_0.txt + diff --git a/tests/testData/ExampleIniFiles/invalidInlineFilter.ini b/tests/testData/ExampleIniFiles/invalidInlineFilter.ini new file mode 100644 index 0000000..fa31010 --- /dev/null +++ b/tests/testData/ExampleIniFiles/invalidInlineFilter.ini @@ -0,0 +1,8 @@ +[DEFAULT] +prefixmap = prefix1:okPath + +[invalidInlineFilterSection] +filter = [ .txt ] +# This INI file is invalid because the inline filter is missing an opening bracket "[" +respaths = prefix1:/firstLine/* .csv ] + prefix2:/secondLine/... \ No newline at end of file diff --git a/tests/testData/ExampleIniFiles/invalidMissingDefaultSection.ini b/tests/testData/ExampleIniFiles/invalidMissingDefaultSection.ini new file mode 100644 index 0000000..7d52b24 --- /dev/null +++ b/tests/testData/ExampleIniFiles/invalidMissingDefaultSection.ini @@ -0,0 +1,6 @@ +# This INI file is invalid because it lacks a DEFAULT section. + +[iniFileWithNoDEFAULTSection_andMissingPrefixMap] +filter = [ .in1 ] ![ .ex1 ] +respaths = res:/firstLine/... + res:/secondLine/... diff --git a/tests/testData/ExampleIniFiles/invalidMissingNamedSection.ini b/tests/testData/ExampleIniFiles/invalidMissingNamedSection.ini new file mode 100644 index 0000000..5cb561c --- /dev/null +++ b/tests/testData/ExampleIniFiles/invalidMissingNamedSection.ini @@ -0,0 +1,4 @@ +# This INI file is invalid because it lacks a named section. + +[DEFAULT] +prefixmap = res:ThereIsNoNamedSection diff --git a/tests/testData/ExampleIniFiles/invalidPrefixMismatch.ini b/tests/testData/ExampleIniFiles/invalidPrefixMismatch.ini new file mode 100644 index 0000000..ed43f75 --- /dev/null +++ b/tests/testData/ExampleIniFiles/invalidPrefixMismatch.ini @@ -0,0 +1,8 @@ +[DEFAULT] +prefixmap = prefix1:okPath + +[invalidPrefixmapMismatchSection] +filter = [ .txt ] +# This INI file is invalid because the prefixDoesNotExist prefix does not exist in the parent prefixmap +respaths = prefixDoesNotExist:/firstLine/* + prefix2:/secondLine/... \ No newline at end of file diff --git a/tests/testData/ExampleIniFiles/invalidPrefixmap.ini b/tests/testData/ExampleIniFiles/invalidPrefixmap.ini new file mode 100644 index 0000000..b2c5610 --- /dev/null +++ b/tests/testData/ExampleIniFiles/invalidPrefixmap.ini @@ -0,0 +1,8 @@ +[DEFAULT] +# This INI file is invalid because the prefixmap entry "prefix1:" has no actual paths defined +prefixmap = prefix1: prefix2:someValidPrefix + +[invalidPrefixMapSection] +filter = [ .txt ] +respaths = prefix1:/firstLine/... + prefix2:/secondLine/... diff --git a/tests/testData/ExampleIniFiles/invalidSectionFilter.ini b/tests/testData/ExampleIniFiles/invalidSectionFilter.ini new file mode 100644 index 0000000..2007f6e --- /dev/null +++ b/tests/testData/ExampleIniFiles/invalidSectionFilter.ini @@ -0,0 +1,8 @@ +[DEFAULT] +prefixmap = prefix1:okPath prefix2:anotherPath + +[invalidSectionFilterSection] +# This INI file is invalid because the filter is missing a closing bracket "]" +filter = [ .txt +respaths = prefix1:/firstLine/... + prefix2:/secondLine/... diff --git a/tests/testData/ExampleIniFiles/validComplexExample1.ini b/tests/testData/ExampleIniFiles/validComplexExample1.ini new file mode 100644 index 0000000..59db777 --- /dev/null +++ b/tests/testData/ExampleIniFiles/validComplexExample1.ini @@ -0,0 +1,29 @@ +[DEFAULT] +prefixmap = resRoot:. resPatch:PatchWithInputChunk resLocalCDN:PatchWithInputChunk/LocalCDNPatches resPrevious:PatchWithInputChunk/PreviousBuildResources + +#============================================================================= +# validComplexExample1.ini - test file +# - The prefixmap contains four entries: +# * resRoot: => The testData (root) folder, in order to have access to resourceOnBranch folder +# * resPatch: => A folder containing 3 sub folders and both .yaml and .txt files in its root +# * resLocalCDN: => A subfolder that we can use to create different relative patsh to same actual path from either resRoot or resPatch +# * resPrevious: => A subfolder to be used to create different priority levels for same actual path from either resRoot or resPatch +;============================================================================= + +# This test should: +# A. In resRoot/PatchWithInputChunk/.., Exclude ![ Movie ] ==> Should exclude *Movie* files in the two sub folders +# B. In resPrevious/*, Include [ Movie ] ==> Should include *Movie* files in the previous folder only +# C. In resPatch/NextBuildResources/introMoviePrefixed.txt, ==> Should include only this file +# D. In resRoot/PatchWithInputChunk/PreviousBuildResources/*, Exclude ![ testResource.txt ] ==> Should exclude testResource.txt only + + +[resPatch_EllipseIncludeBothYamlAndTxtFiles] +filter = [ .yaml .txt ] +respaths = resPatch:/... + resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + resLocalCDN:/../NextBuildResources/introMoviePrefixed.txt + resRoot:/PatchWithInputChunk/PreviousBuildResources/* ![ testResource.txt ] + + + diff --git a/tests/testData/ExampleIniFiles/validSimpleExample1.ini b/tests/testData/ExampleIniFiles/validSimpleExample1.ini new file mode 100644 index 0000000..27f3f8e --- /dev/null +++ b/tests/testData/ExampleIniFiles/validSimpleExample1.ini @@ -0,0 +1,15 @@ +[DEFAULT] +prefixmap = res:. + +#============================================================================= +# validSimpleExample1.ini - test file +# - Wildcard include any .txt and .yaml files found (expected 2) +# - introMovie.txt +# - videoCardCategories.yaml +# - Note the prefixmap is set to the current folder (.) +;============================================================================= + +[allFilesInResourcesOnBranchFolderExcludingSubfolders] +filter = [ .yaml .txt ] +respaths = res:/resourcesOnBranch/* + diff --git a/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_Windows.yaml b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_Windows.yaml new file mode 100644 index 0000000..6e049fe --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_Windows.yaml @@ -0,0 +1,76 @@ +Version: 0.1.0 +Type: ResourceGroup +NumberOfResources: 10 +TotalResourcesSizeCompressed: 24573 +TotalResourcesSizeUnCompressed: 107508 +Resources: + - RelativePath: PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt + Type: Resource + Location: dc/dca8056adced9237_0fb2bed9d164ad014bcb060b95df7ba1 + Checksum: 0fb2bed9d164ad014bcb060b95df7ba1 + UncompressedSize: 9425 + CompressedSize: 3409 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/NextBuildResources/testResource2.txt + Type: Resource + Location: 82/8264e4640cf94b94_271c8036e4eae5515053e924a0a39e0f + Checksum: 271c8036e4eae5515053e924a0a39e0f + UncompressedSize: 29 + CompressedSize: 47 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml + Type: Resource + Location: 02/02b2b2627ca28b62_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml + Type: Resource + Location: 44/446d5f683db0d467_b7431ad6d859bfd67a4c3fae337b0b6e + Checksum: b7431ad6d859bfd67a4c3fae337b0b6e + UncompressedSize: 3902 + CompressedSize: 765 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMovie.txt + Type: Resource + Location: fd/fd2a47b3d5faa494_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt + Type: Resource + Location: 5d/5d2aadfa1344ac63_5e631fd37d3350e30095d1251b178f2c + Checksum: 5e631fd37d3350e30095d1251b178f2c + UncompressedSize: 9117 + CompressedSize: 3259 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt + Type: Resource + Location: 60/6054e5e0cf24763e_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml + Type: Resource + Location: fb/fbc1cb8895f6c396_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/resFileIndexShort_build_next.txt + Type: Resource + Location: 1f/1f23be4b8bf6d2ae_4d398902b75611f7ae19903e6b461514 + Checksum: 4d398902b75611f7ae19903e6b461514 + UncompressedSize: 612 + CompressedSize: 324 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/resFileIndexShort_build_previous.txt + Type: Resource + Location: 32/326b0449af10b716_849821e0c98e37de4edc5f7156cf5887 + Checksum: 849821e0c98e37de4edc5f7156cf5887 + UncompressedSize: 611 + CompressedSize: 299 + BinaryOperation: 33206 \ No newline at end of file diff --git a/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_Windows.yaml b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_Windows.yaml new file mode 100644 index 0000000..a6632d2 --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_Windows.yaml @@ -0,0 +1,90 @@ +Version: 0.1.0 +Type: ResourceGroup +NumberOfResources: 12 +TotalResourcesSizeCompressed: 32808 +TotalResourcesSizeUnCompressed: 149414 +Resources: + - RelativePath: PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt + Type: Resource + Location: dc/dca8056adced9237_0fb2bed9d164ad014bcb060b95df7ba1 + Checksum: 0fb2bed9d164ad014bcb060b95df7ba1 + UncompressedSize: 9425 + CompressedSize: 3409 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/NextBuildResources/testResource2.txt + Type: Resource + Location: 82/8264e4640cf94b94_271c8036e4eae5515053e924a0a39e0f + Checksum: 271c8036e4eae5515053e924a0a39e0f + UncompressedSize: 29 + CompressedSize: 47 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml + Type: Resource + Location: 02/02b2b2627ca28b62_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml + Type: Resource + Location: 44/446d5f683db0d467_b7431ad6d859bfd67a4c3fae337b0b6e + Checksum: b7431ad6d859bfd67a4c3fae337b0b6e + UncompressedSize: 3902 + CompressedSize: 765 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMovie.txt + Type: Resource + Location: fd/fd2a47b3d5faa494_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt + Type: Resource + Location: 5d/5d2aadfa1344ac63_5e631fd37d3350e30095d1251b178f2c + Checksum: 5e631fd37d3350e30095d1251b178f2c + UncompressedSize: 9117 + CompressedSize: 3259 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt + Type: Resource + Location: 60/6054e5e0cf24763e_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml + Type: Resource + Location: fb/fbc1cb8895f6c396_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/resFileIndexShort_build_next.txt + Type: Resource + Location: 1f/1f23be4b8bf6d2ae_4d398902b75611f7ae19903e6b461514 + Checksum: 4d398902b75611f7ae19903e6b461514 + UncompressedSize: 612 + CompressedSize: 324 + BinaryOperation: 33206 + - RelativePath: PatchWithInputChunk/resFileIndexShort_build_previous.txt + Type: Resource + Location: 32/326b0449af10b716_849821e0c98e37de4edc5f7156cf5887 + Checksum: 849821e0c98e37de4edc5f7156cf5887 + UncompressedSize: 611 + CompressedSize: 299 + BinaryOperation: 33206 + - RelativePath: resourcesOnBranch/introMovie.txt + Type: Resource + Location: 1c/1c6368ffb440041a_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33206 + - RelativePath: resourcesOnBranch/videoCardCategories.yaml + Type: Resource + Location: ff/ffe4c396c9c935d4_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33206 \ No newline at end of file diff --git a/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.yaml b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.yaml new file mode 100644 index 0000000..75c0d30 --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.yaml @@ -0,0 +1,20 @@ +Version: 0.1.0 +Type: ResourceGroup +NumberOfResources: 2 +TotalResourcesSizeCompressed: 8235 +TotalResourcesSizeUnCompressed: 41906 +Resources: + - RelativePath: resourcesOnBranch/introMovie.txt + Type: Resource + Location: 1c/1c6368ffb440041a_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33206 + - RelativePath: resourcesOnBranch/videoCardCategories.yaml + Type: Resource + Location: ff/ffe4c396c9c935d4_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33206 \ No newline at end of file diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 06b8025..bc71deb 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -20,6 +20,15 @@ set(SRC_FILES include/RollingChecksum.h include/ScopedFile.h include/StatusCallback.h + include/FilterResourceFile.h + include/FilterDefaultSection.h + include/FilterNamedSection.h + include/FilterPrefixMap.h + include/FilterResourceFilter.h + include/FilterResourcePathFile.h + include/FilterPrefixMapEntry.h + include/FilterResourcePathFileEntry.h + include/ResourceFilter.h src/BundleStreamIn.cpp src/BundleStreamOut.cpp @@ -35,6 +44,15 @@ set(SRC_FILES src/ScopedFile.cpp src/Patching.cpp src/RollingChecksum.cpp + src/FilterResourceFile.cpp + src/FilterDefaultSection.cpp + src/FilterNamedSection.cpp + src/FilterPrefixMap.cpp + src/FilterResourceFilter.cpp + src/FilterResourcePathFile.cpp + src/FilterPrefixMapEntry.cpp + src/FilterResourcePathFileEntry.cpp + src/ResourceFilter.cpp ) add_library(resources-tools STATIC ${SRC_FILES}) diff --git a/tools/include/FilterDefaultSection.h b/tools/include/FilterDefaultSection.h new file mode 100644 index 0000000..3000b8d --- /dev/null +++ b/tools/include/FilterDefaultSection.h @@ -0,0 +1,27 @@ +// Copyright © 2025 CCP ehf. + +#ifndef FILTERDEFAULTSECTION_H +#define FILTERDEFAULTSECTION_H + +#include +#include + +namespace ResourceTools +{ + +class FilterDefaultSection +{ +public: + FilterDefaultSection() = default; + + explicit FilterDefaultSection( const std::string& prefixmapStr ); + + const FilterPrefixMap& GetPrefixMap() const; + +private: + FilterPrefixMap m_prefixMap; +}; + +} + +#endif // FILTERDEFAULTSECTION_H diff --git a/tools/include/FilterNamedSection.h b/tools/include/FilterNamedSection.h new file mode 100644 index 0000000..9702634 --- /dev/null +++ b/tools/include/FilterNamedSection.h @@ -0,0 +1,52 @@ +// Copyright © 2025 CCP ehf. + +#ifndef FILTERNAMEDSECTION_H +#define FILTERNAMEDSECTION_H + +#include +#include +#include +#include +#include +#include +#include + +namespace ResourceTools +{ + +class FilterNamedSection +{ +public: + explicit FilterNamedSection( std::string sectionName, + const std::string& filter, + const std::string& respaths, + const std::string& resfile, + const FilterPrefixMap& parentPrefixMap ); + + const std::string& GetSectionName() const; + + // Return combined resolved path map from both respaths and optional resfile + const std::map& GetCombinedResolvedPathMap(); + + const std::map& GetResolvedRespathsMap() const; + + const std::map& GetResolvedResfileMap() const; + +private: + std::string m_sectionName; + + const FilterPrefixMap& m_parentPrefixMap; // The "parent" prefix map from the [DEFAULT] section + + FilterResourceFilter m_filter; + + FilterResourcePathFile m_respaths; + + std::optional m_resfile; + + // Combined map of fully resolved respaths and resfile FilterResourceFilter objects + std::map m_resolvedCombinedPathMap; +}; + +} + +#endif // FILTERNAMEDSECTION_H diff --git a/tools/include/FilterPrefixMapEntry.h b/tools/include/FilterPrefixMapEntry.h new file mode 100644 index 0000000..ee04cba --- /dev/null +++ b/tools/include/FilterPrefixMapEntry.h @@ -0,0 +1,34 @@ +// Copyright © 2025 CCP ehf. + +#ifndef FILTERPREFIXMAPENTRY_H +#define FILTERPREFIXMAPENTRY_H + +#include +#include + +namespace ResourceTools +{ + +// Class representing all (one or more) path entries for a given prefix identifier. +class FilterPrefixMapEntry +{ +public: + explicit FilterPrefixMapEntry( const std::string& prefix, const std::string& rawPaths ); + + // Parse rawPaths and appends to existing paths if needed. + void AppendPaths( const std::string& prefix, const std::string& rawPaths ); + + const std::string& GetPrefix() const; + + const std::set& GetPaths() const; + +private: + std::string m_prefix; + + // The set of parsed paths (sorted). + std::set m_paths; +}; + +} + +#endif // FILTERPREFIXMAPENTRY_H diff --git a/tools/include/FilterPrefixmap.h b/tools/include/FilterPrefixmap.h new file mode 100644 index 0000000..bd066c9 --- /dev/null +++ b/tools/include/FilterPrefixmap.h @@ -0,0 +1,33 @@ +// Copyright © 2025 CCP ehf. + +#ifndef FILTERPREFIXMAP_H +#define FILTERPREFIXMAP_H + +#include +#include +#include +#include + +namespace ResourceTools +{ + +// Class representing the prefixmap attribute. +class FilterPrefixMap +{ +public: + FilterPrefixMap() = default; + + explicit FilterPrefixMap( const std::string& rawPrefixMap ); + + const std::map& GetMapEntries() const; + +private: + // Map of prefixes to FilterPrefixMapEntry objects. + std::map m_prefixMapEntries; + + void ParsePrefixMap( const std::string& rawPrefixMap ); +}; + +} + +#endif // FILTERPREFIXMAP_H diff --git a/tools/include/FilterResourceFile.h b/tools/include/FilterResourceFile.h new file mode 100644 index 0000000..03ddbba --- /dev/null +++ b/tools/include/FilterResourceFile.h @@ -0,0 +1,38 @@ +// Copyright © 2025 CCP ehf. + +#ifndef FILTERRESOURCEFILE_H +#define FILTERRESOURCEFILE_H + +#include +#include +#include +#include + +namespace ResourceTools +{ + +class FilterResourceFile +{ +public: + explicit FilterResourceFile( const std::filesystem::path& iniFilePath ); + + // Returns the full resolved PathMaps for all named sections defined in the resource .ini file + // Key is the "resolved path", Value is the associated FilterResourceFilter (include and exclude filters) + const std::map& GetIniFileResolvedPathMap(); + +private: + std::filesystem::path m_iniFilePath; + + FilterDefaultSection m_defaultSection; + + std::vector m_namedSections; + + // Resolved PathMap for all named sections defined in a resource .ini file + std::map m_iniFileResolvedPathMap; + + void ParseIniFile(); +}; + +} + +#endif // FILTERRESOURCEFILE_H diff --git a/tools/include/FilterResourceFilter.h b/tools/include/FilterResourceFilter.h new file mode 100644 index 0000000..cf31f81 --- /dev/null +++ b/tools/include/FilterResourceFilter.h @@ -0,0 +1,45 @@ +// Copyright © 2025 CCP ehf. + +#ifndef FILTERRESOURCEFILTER_H +#define FILTERRESOURCEFILTER_H + +#include +#include + +namespace ResourceTools +{ + +// Class representing a resource filter with include and exclude filters. +// - This is for filter attribute of a NamedSection AND the "combined resolved" filter for each respaths/resfile line +class FilterResourceFilter +{ +public: + FilterResourceFilter() = default; + + explicit FilterResourceFilter( const std::string& rawFilter, bool isToplevelFilter = false ); + + // Used as input when constructing a combined resolved filter for a respaths/resfile line. + const std::string& GetRawFilter() const; + + const std::vector& GetIncludeFilter() const; + + const std::vector& GetExcludeFilter() const; + +private: + bool m_isToplevelFilter = false; + + std::string m_rawFilter; + + std::vector m_includeFilter; + + std::vector m_excludeFilter; + + void ParseFilters(); + + // Static helper placing tokens in the correct vector, moving it if need be. + static void PlaceTokenInCorrectVector( const std::string& token, std::vector& fromVector, std::vector& toVector ); +}; + +} + +#endif // FILTERRESOURCEFILTER_H diff --git a/tools/include/FilterResourcePathFile.h b/tools/include/FilterResourcePathFile.h new file mode 100644 index 0000000..1c00b02 --- /dev/null +++ b/tools/include/FilterResourcePathFile.h @@ -0,0 +1,42 @@ +// Copyright © 2025 CCP ehf. + +#ifndef FILTERRESOURCEPATHFILE_H +#define FILTERRESOURCEPATHFILE_H + +#include +#include +#include + +namespace ResourceTools +{ + +// Class representing a resfile/respaths attribute. +class FilterResourcePathFile +{ +public: + explicit FilterResourcePathFile( std::string rawPathFileAttrib, + const FilterPrefixMap& parentPrefixMap, + const FilterResourceFilter& parentSectionFilter ); + + // Get the map of fully resolved paths to their combined FilterResourceFilter objects. + const std::map& GetResolvedPathMap() const; + +private: + // The raw (multiline) respath attribute (same for resfile). + std::string m_rawPathFileAttrib; + + // The "parent" prefix map from the [DEFAULT] section + const FilterPrefixMap& m_parentPrefixMap; + + // The "parent" filter from the [namedSection] containing this resfile/respaths attribute + const FilterResourceFilter& m_parentSectionFilter; + + // Map of fully resolved paths to their combined FilterResourceFilter objects. + std::map m_resolvedPathMap; + + void ParseRawPathFileAttribute(); +}; + +} + +#endif // FILTERRESOURCEPATHFILE_H diff --git a/tools/include/FilterResourcePathFileEntry.h b/tools/include/FilterResourcePathFileEntry.h new file mode 100644 index 0000000..993fa0a --- /dev/null +++ b/tools/include/FilterResourcePathFileEntry.h @@ -0,0 +1,45 @@ +// Copyright © 2025 CCP ehf. + +#ifndef FILTERRESOURCEPATHFILEENTRY_H +#define FILTERRESOURCEPATHFILEENTRY_H + +#include +#include +#include + +namespace ResourceTools +{ + +class FilterResourcePathFileEntry +{ +public: + explicit FilterResourcePathFileEntry( std::string rawPathLine, + const FilterPrefixMap& parentPrefixMap, + const FilterResourceFilter& parentSectionFilter ); + + const FilterResourceFilter& GetEntryFilter() const; + + const std::set& GetResolvedPaths() const; + +private: + std::string m_rawPathLine; + + // The "parent" prefix map from the [DEFAULT] section + const FilterPrefixMap& m_parentPrefixMap; + + // The "parent" filter from the [namedSection] + const FilterResourceFilter& m_parentSectionFilter; + + // The combined filter for this resfile/respaths attribute line, built from the parentSectionFilter and any inline filter. + FilterResourceFilter m_entryFilter; + + // The set of resolved paths (sorted). + std::set m_resolvedPaths; + + // Parse the m_rawPathLine by constructing the combined m_entryFilter and append paths to m_resolvedPaths based on the m_parentPrefixMap + void ParseRawPathLine(); +}; + +} + +#endif //FILTERRESOURCEPATHFILEENTRY_H diff --git a/tools/include/ResourceFilter.h b/tools/include/ResourceFilter.h new file mode 100644 index 0000000..c4a9129 --- /dev/null +++ b/tools/include/ResourceFilter.h @@ -0,0 +1,49 @@ +// Copyright © 2026 CCP ehf. + +#ifndef RESOURCEFILTER_H +#define RESOURCEFILTER_H + +#include +#include +#include +#include + +namespace ResourceTools +{ + +class ResourceFilter +{ +public: + ResourceFilter() = default; + + explicit ResourceFilter( const std::vector& iniFilePaths ); + + void Initialize( const std::vector& iniFilePaths ); + + bool HasFilters() const + { + return !m_filterFiles.empty(); + } + + // Returns the full relative resolved PathMaps from all resource .ini file + // Key is the "relative resolved path", Value is the associated FilterResourceFilter (include and exclude filters) + const std::map& GetFullResolvedPathMap(); + + // Check if the inFilePath should be included or excluded based on filtering rules + bool ShouldInclude( const std::filesystem::path& inFilePath ); + +private: + bool m_initialized{ false }; + + std::vector> m_filterFiles; + + // Resolved PathMap for all .ini files + std::map m_fullResolvedPathMap; + + // Helper function for wildcard matching paths (supports "*" and "...") + static bool WildcardMatch( const std::string& pattern, const std::string& checkStr ); +}; + +} + +#endif // RESOURCEFILTER_H diff --git a/tools/src/FilterDefaultSection.cpp b/tools/src/FilterDefaultSection.cpp new file mode 100644 index 0000000..9e10caa --- /dev/null +++ b/tools/src/FilterDefaultSection.cpp @@ -0,0 +1,20 @@ +// Copyright © 2025 CCP ehf. + +#include +#include + +namespace ResourceTools +{ + +FilterDefaultSection::FilterDefaultSection( const std::string& prefixmapStr ) : + m_prefixMap( prefixmapStr ) +{ +} + +const FilterPrefixMap& FilterDefaultSection::GetPrefixMap() const +{ + return m_prefixMap; +} + + +} diff --git a/tools/src/FilterNamedSection.cpp b/tools/src/FilterNamedSection.cpp new file mode 100644 index 0000000..aa20076 --- /dev/null +++ b/tools/src/FilterNamedSection.cpp @@ -0,0 +1,87 @@ +// Copyright © 2025 CCP ehf. + +#include +#include + +namespace ResourceTools +{ + +FilterNamedSection::FilterNamedSection( std::string sectionName, + const std::string& filter, + const std::string& respaths, + const std::string& resfile, + const FilterPrefixMap& parentPrefixMap ) : + m_sectionName( std::move( sectionName ) ), + m_parentPrefixMap( parentPrefixMap ), + m_filter( filter, true ), // isToplevelFilter = true + m_respaths( respaths, parentPrefixMap, m_filter ), + m_resfile( resfile.empty() ? std::nullopt : std::make_optional( resfile, parentPrefixMap, m_filter ) ) +{ + m_resolvedCombinedPathMap.clear(); + + if( respaths.empty() ) + { + throw std::invalid_argument( "Respaths attribute is empty for section: " + m_sectionName ); + } +} + +const std::string& FilterNamedSection::GetSectionName() const +{ + return m_sectionName; +} + +const std::map& FilterNamedSection::GetCombinedResolvedPathMap() +{ + // Only populate the Combined map if not already done so. + if( m_resolvedCombinedPathMap.empty() ) + { + // Populate the combined map. + for( const auto& kv : m_respaths.GetResolvedPathMap() ) + { + m_resolvedCombinedPathMap.insert_or_assign( kv.first, kv.second ); + } + + // Add resfile to the combined map + if( m_resfile ) + { + // Allow "resfile" to contain multiple entries (future proofing) + for( const auto& kv : m_resfile->GetResolvedPathMap() ) + { + // Combine filters of both if same key already exists + auto it = m_resolvedCombinedPathMap.find( kv.first ); + if( it != m_resolvedCombinedPathMap.end() ) + { + // Combine the filters (using raw filter strings) + std::string combinedRawFilter = it->second.GetRawFilter() + " " + kv.second.GetRawFilter(); + FilterResourceFilter combinedFilter( combinedRawFilter ); + m_resolvedCombinedPathMap.insert_or_assign( kv.first, combinedFilter ); + } + else + { + m_resolvedCombinedPathMap.insert_or_assign( kv.first, kv.second ); + } + } + } + } + + return m_resolvedCombinedPathMap; +} + +const std::map& FilterNamedSection::GetResolvedRespathsMap() const +{ + return m_respaths.GetResolvedPathMap(); +} + +const std::map& FilterNamedSection::GetResolvedResfileMap() const +{ + if( m_resfile ) + { + return m_resfile->GetResolvedPathMap(); + } + + // Return empty map if no resfile present + static const std::map emptyResfileMap; + return emptyResfileMap; +} + +} diff --git a/tools/src/FilterPrefixMapEntry.cpp b/tools/src/FilterPrefixMapEntry.cpp new file mode 100644 index 0000000..351f8a2 --- /dev/null +++ b/tools/src/FilterPrefixMapEntry.cpp @@ -0,0 +1,53 @@ +// Copyright © 2025 CCP ehf. + +#include +#include + +namespace ResourceTools +{ + +FilterPrefixMapEntry::FilterPrefixMapEntry( const std::string& prefix, const std::string& rawPaths ) : + m_prefix( prefix ) +{ + m_paths.clear(); + + AppendPaths( prefix, rawPaths ); +} + +void FilterPrefixMapEntry::AppendPaths( const std::string& prefix, const std::string& rawPaths ) +{ + if( prefix != m_prefix ) + { + throw std::invalid_argument( "Prefix mismatch while appending path(s): " + prefix + " (incoming) != " + m_prefix + " (existing)" ); + } + + std::size_t pos = 0; + while( pos < rawPaths.size() ) + { + // Split the string up by semicolons (in case of multiple paths) + std::size_t semicolon = rawPaths.find( ';', pos ); + std::string path = ( semicolon == std::string::npos ) ? rawPaths.substr( pos ) : rawPaths.substr( pos, semicolon - pos ); + if( !path.empty() ) + m_paths.insert( path ); + + if( semicolon == std::string::npos ) + break; + pos = semicolon + 1; + } + if( m_paths.empty() ) + { + throw std::invalid_argument( "Invalid prefixmap format: No paths appended for prefix: " + m_prefix ); + } +} + +const std::string& FilterPrefixMapEntry::GetPrefix() const +{ + return m_prefix; +} + +const std::set& FilterPrefixMapEntry::GetPaths() const +{ + return m_paths; +} + +} diff --git a/tools/src/FilterPrefixmap.cpp b/tools/src/FilterPrefixmap.cpp new file mode 100644 index 0000000..90b2735 --- /dev/null +++ b/tools/src/FilterPrefixmap.cpp @@ -0,0 +1,76 @@ +// Copyright © 2025 CCP ehf. + +#include +#include +#include +#include + +namespace ResourceTools +{ + +FilterPrefixMap::FilterPrefixMap( const std::string& rawPrefixMap ) +{ + m_prefixMapEntries.clear(); + + ParsePrefixMap( rawPrefixMap ); +} + +const std::map& FilterPrefixMap::GetMapEntries() const +{ + return m_prefixMapEntries; +} + +void FilterPrefixMap::ParsePrefixMap( const std::string& rawPrefixMap ) +{ + std::size_t pos = 0; + while( pos < rawPrefixMap.size() ) + { + // Find the prefix (or error out if missing a colon) + std::size_t colon = rawPrefixMap.find( ':', pos ); + if( colon == std::string::npos ) + { + throw std::invalid_argument( "Invalid prefixmap format: missing ':'" ); + } + + std::string prefix = rawPrefixMap.substr( pos, colon - pos ); + if( prefix.empty() ) + { + throw std::invalid_argument( "Invalid prefixmap format: empty prefix" ); + } + + // Move position past the colon + pos = colon + 1; + + // Find end of paths (next whitespace or end of string) + std::size_t nextSpace = rawPrefixMap.find_first_of( " \t\r\n", pos ); + std::string rawPaths = ( nextSpace == std::string::npos ) ? rawPrefixMap.substr( pos ) : rawPrefixMap.substr( pos, nextSpace - pos ); + + if( rawPaths.empty() ) + { + throw std::invalid_argument( "Invalid prefixmap format: No paths defined for prefix: " + prefix ); + } + + auto it = m_prefixMapEntries.find( prefix ); + if( it == m_prefixMapEntries.end() ) + { + // Prefix doesn't exist, create a map entry for it. + m_prefixMapEntries.insert_or_assign( prefix, FilterPrefixMapEntry( prefix, rawPaths ) ); + } + else + { + // The same prefix has been found again, appending paths to the existing entry + it->second.AppendPaths( prefix, rawPaths ); + } + + // Go to the next token in the rawPrefixMap (or break if at end) + if( nextSpace == std::string::npos ) + break; + pos = nextSpace + 1; + + // There was a whitespace, skip any additional spaces as well + while( pos < rawPrefixMap.size() && std::isspace( static_cast( rawPrefixMap[pos] ) ) ) + ++pos; + } +} + +} \ No newline at end of file diff --git a/tools/src/FilterResourceFile.cpp b/tools/src/FilterResourceFile.cpp new file mode 100644 index 0000000..d657e78 --- /dev/null +++ b/tools/src/FilterResourceFile.cpp @@ -0,0 +1,95 @@ +// Copyright © 2025 CCP ehf. + +#include +#include +#include + +namespace ResourceTools +{ + +FilterResourceFile::FilterResourceFile( const std::filesystem::path& iniFilePath ) : + m_iniFilePath( iniFilePath ) +{ + m_iniFileResolvedPathMap.clear(); + + ParseIniFile(); +} + +const std::map& FilterResourceFile::GetIniFileResolvedPathMap() +{ + if( m_iniFileResolvedPathMap.empty() ) + { + // Populate the full resolved path map from all named sections in this INI file + for( auto& namedSection : m_namedSections ) + { + auto& sectionPathMap = namedSection.GetCombinedResolvedPathMap(); + for( const auto& kv : sectionPathMap ) + { + // Combine filters if the same path already exists + auto it = m_iniFileResolvedPathMap.find( kv.first ); + if( it != m_iniFileResolvedPathMap.end() ) + { + // Combine the filters (using raw filter strings) + std::string combinedRawFilter = it->second.GetRawFilter() + " " + kv.second.GetRawFilter(); + FilterResourceFilter combinedFilter( combinedRawFilter ); + m_iniFileResolvedPathMap.insert_or_assign( kv.first, combinedFilter ); + } + else + { + m_iniFileResolvedPathMap.insert_or_assign( kv.first, kv.second ); + } + } + } + } + + return m_iniFileResolvedPathMap; +} + +void FilterResourceFile::ParseIniFile() +{ + // Open, read and parse the resource INI file. + INIReader reader( m_iniFilePath.string() ); + if( reader.ParseError() != 0 ) + { + throw std::runtime_error( "Failed to parse INI file: " + m_iniFilePath.string() + " - " + reader.ParseErrorMessage() ); + } + + // Parse the [DEFAULT] section + if( !reader.HasSection( "DEFAULT" ) ) + { + throw std::invalid_argument( "Missing [DEFAULT] section in INI file: " + m_iniFilePath.string() ); + } + m_defaultSection = FilterDefaultSection( reader.Get( "DEFAULT", "prefixmap", "" ) ); + + + // Validate that non-DEFAULT section(s) exist + std::vector allSections = reader.Sections(); + if( allSections.size() <= 1 ) + { + // No namedSections defined + throw std::invalid_argument( "No namedSections defined in INI file: " + m_iniFilePath.string() ); + } + + // Parse all other named sections + for( const auto& sectionName : reader.Sections() ) + { + if( sectionName == "default" || sectionName == "DEFAULT" ) + { + continue; // Already loaded, skip it + } + + std::string filter = reader.Get( sectionName, "filter", "" ); + std::string respaths = reader.Get( sectionName, "respaths", "" ); + std::string resfile = reader.Get( sectionName, "resfile", "" ); + + if( respaths.empty() ) + { + throw std::invalid_argument( "Respaths attribute is empty for section: " + sectionName ); + } + + FilterNamedSection namedSection( sectionName, filter, respaths, resfile, m_defaultSection.GetPrefixMap() ); + m_namedSections.push_back( namedSection ); + } +} + +} diff --git a/tools/src/FilterResourceFilter.cpp b/tools/src/FilterResourceFilter.cpp new file mode 100644 index 0000000..0afe944 --- /dev/null +++ b/tools/src/FilterResourceFilter.cpp @@ -0,0 +1,143 @@ +// Copyright © 2025 CCP ehf. + +#include +#include +#include +#include + +namespace ResourceTools +{ + +FilterResourceFilter::FilterResourceFilter( const std::string& rawFilter, bool isToplevelFilter /* = false */ ) : + m_rawFilter( rawFilter ), + m_isToplevelFilter( isToplevelFilter ) +{ + ParseFilters(); +} + +const std::string& FilterResourceFilter::GetRawFilter() const +{ + return m_rawFilter; +} + +const std::vector& FilterResourceFilter::GetIncludeFilter() const +{ + return m_includeFilter; +} + +const std::vector& FilterResourceFilter::GetExcludeFilter() const +{ + return m_excludeFilter; +} + +void FilterResourceFilter::PlaceTokenInCorrectVector( const std::string& token, std::vector& fromVector, std::vector& toVector ) +{ + // Remove token from the fromVector if present + auto it = std::find( fromVector.begin(), fromVector.end(), token ); + if( it != fromVector.end() ) + { + fromVector.erase( it ); + } + + // Add token to the toVector if not already present in it. + if( std::find( toVector.begin(), toVector.end(), token ) == toVector.end() ) + { + toVector.push_back( token ); + } +} + +void FilterResourceFilter::ParseFilters() +{ + m_includeFilter.clear(); + m_excludeFilter.clear(); + + std::string s = m_rawFilter; + size_t pos = 0; + while( pos < s.size() ) + { + // Skip whitespace + while( pos < s.size() && std::isspace( static_cast( s[pos] ) ) ) + { + ++pos; + } + if( pos >= s.size() ) + { + break; + } + + // Check for exclude filter marker '!' + bool isExclude = false; + if( s[pos] == '!' ) + { + // We have an exclude filter, advance the position by one and skip whitespace(s) + isExclude = true; + ++pos; + while( pos < s.size() && std::isspace( static_cast( s[pos] ) ) ) + { + ++pos; + } + if( pos >= s.size() ) + { + throw std::invalid_argument( "Invalid filter format: exclude filter marker found without a [ token ] section" ); + } + } + + if( pos >= s.size() || s[pos] != '[' ) + { + throw std::invalid_argument( "Invalid filter format: missing '['" ); + } + ++pos; // skip '[' + + size_t endBracket = s.find( ']', pos ); + size_t nextStartBracket = s.find( '[', pos ); + if( nextStartBracket != std::string::npos && nextStartBracket < endBracket ) + { + throw std::invalid_argument( "Invalid filter format: matching end bracket ']' not present before the next start bracket '['" ); + } + + if( endBracket == std::string::npos ) + { + throw std::invalid_argument( "Invalid filter format: missing ']'" ); + } + + std::string entries = s.substr( pos, endBracket - pos ); + std::istringstream iss( entries ); + std::string token; + while( iss >> token ) + { + // Trim whitespace from token + size_t start = token.find_first_not_of( " \t\r\n" ); + size_t end = token.find_last_not_of( " \t\r\n" ); + if( start == std::string::npos || end == std::string::npos ) + { + continue; + } + token = token.substr( start, end - start + 1 ); + + if( token.empty() ) + { + continue; + } + + PlaceTokenInCorrectVector( token, + isExclude ? m_includeFilter : m_excludeFilter, + isExclude ? m_excludeFilter : m_includeFilter ); + } + pos = endBracket + 1; + } + + // Make sure that we have a wild-card ("*") in the TOP-LEVEL include filter if the include filter is empty + if( m_isToplevelFilter && m_includeFilter.empty() ) + { + m_includeFilter.push_back( "*" ); + + // Also make sure we add the wild-card include to the raw filter (in case filters are concatenated later) + if( !m_rawFilter.empty() ) + { + m_rawFilter += " "; + } + m_rawFilter += "[ * ]"; + } +} + +} diff --git a/tools/src/FilterResourcePathFile.cpp b/tools/src/FilterResourcePathFile.cpp new file mode 100644 index 0000000..08a71a1 --- /dev/null +++ b/tools/src/FilterResourcePathFile.cpp @@ -0,0 +1,58 @@ +// Copyright © 2025 CCP ehf. + +#include +#include +#include +#include + +namespace ResourceTools +{ + +FilterResourcePathFile::FilterResourcePathFile( std::string rawPathFileAttrib, + const FilterPrefixMap& parentPrefixMap, + const FilterResourceFilter& parentSectionFilter ) : + m_rawPathFileAttrib( std::move( rawPathFileAttrib ) ), + m_parentPrefixMap( parentPrefixMap ), + m_parentSectionFilter( parentSectionFilter ) +{ + m_resolvedPathMap.clear(); + + ParseRawPathFileAttribute(); +} + +const std::map& FilterResourcePathFile::GetResolvedPathMap() const +{ + return m_resolvedPathMap; +} + +void FilterResourcePathFile::ParseRawPathFileAttribute() +{ + // Split m_rawPathFileAttrib into lines (in case of multiline attribute) + std::istringstream stream( m_rawPathFileAttrib ); + std::string line; + while( std::getline( stream, line ) ) + { + // Trim whitespace from both ends + size_t first = line.find_first_not_of( " \t\r" ); + if( first == std::string::npos ) + continue; // skip if empty line + + size_t last = line.find_last_not_of( " \t\r" ); + std::string rawPathLine = line.substr( first, last - first + 1 ); + + // Skip commented out lines (in case there is "inline" comment within the .ini file attribute value) + if( rawPathLine.empty() || rawPathLine[0] == '#' || rawPathLine[0] == ';' ) + continue; + + // Add entries to the resolved path map + auto lineEntry = FilterResourcePathFileEntry( rawPathLine, m_parentPrefixMap, m_parentSectionFilter ); + const auto resolvedPaths = lineEntry.GetResolvedPaths(); + const auto entryFilter = lineEntry.GetEntryFilter(); + for( const auto& path : resolvedPaths ) + { + m_resolvedPathMap.insert_or_assign( path, entryFilter ); + } + } +} + +} diff --git a/tools/src/FilterResourcePathFileEntry.cpp b/tools/src/FilterResourcePathFileEntry.cpp new file mode 100644 index 0000000..d82c4c4 --- /dev/null +++ b/tools/src/FilterResourcePathFileEntry.cpp @@ -0,0 +1,97 @@ +// Copyright © 2025 CCP ehf. + +#include +#include +#include + +namespace ResourceTools +{ + +FilterResourcePathFileEntry::FilterResourcePathFileEntry( std::string rawPathLine, + const FilterPrefixMap& parentPrefixMap, + const FilterResourceFilter& parentSectionFilter ) : + m_rawPathLine( std::move( rawPathLine ) ), + m_parentPrefixMap( parentPrefixMap ), + m_parentSectionFilter( parentSectionFilter ) +{ + ParseRawPathLine(); +} + + +const FilterResourceFilter& FilterResourcePathFileEntry::GetEntryFilter() const +{ + return m_entryFilter; +} + +const std::set& FilterResourcePathFileEntry::GetResolvedPaths() const +{ + return m_resolvedPaths; +} + +void FilterResourcePathFileEntry::ParseRawPathLine() +{ + // Split on whitespace: first token is pathPart, rest is (optional) filterPart + std::string rawPathToken; + std::string rawOptionalFilterPart; + std::string combinedRawFilter; + + std::istringstream iss( m_rawPathLine ); + iss >> rawPathToken; + if( !iss.eof() ) + { + // There is an optional filter part. Construct it, will error out if wrong format + std::getline( iss, rawOptionalFilterPart ); + FilterResourceFilter inlineFilter = FilterResourceFilter( rawOptionalFilterPart ); + + // Combine parent filter and inline filter + combinedRawFilter = m_parentSectionFilter.GetRawFilter() + " " + inlineFilter.GetRawFilter(); + } + else + { + // No inline filter, use parent section filter as is + combinedRawFilter = m_parentSectionFilter.GetRawFilter(); + } + m_entryFilter = FilterResourceFilter( combinedRawFilter ); + + // Validate the rawPathToken + size_t colon = rawPathToken.find( ':' ); + if( colon == std::string::npos ) + { + throw std::invalid_argument( std::string( "Missing prefix in path for: " ) + m_rawPathLine ); + } + std::string prefix = rawPathToken.substr( 0, colon ); + std::string rest = rawPathToken.substr( colon + 1 ); + + const auto& prefixMapEntries = m_parentPrefixMap.GetMapEntries(); + auto it = prefixMapEntries.find( prefix ); + if( it == prefixMapEntries.end() ) + { + throw std::invalid_argument( std::string( "Prefix '" ) + prefix + "' not present in prefixMap for line: " + m_rawPathLine ); + } + + // Each FilterPrefixMapEntry may have multiple paths, combine/resolve all of them + const auto& prefixEntry = it->second; + const auto& prefixPaths = prefixEntry.GetPaths(); + for( const auto& basePrefixPath : prefixPaths ) + { + // Ensure only one '/' at the join point + bool baseEndsWithSlash = !basePrefixPath.empty() && basePrefixPath.back() == '/'; + bool restStartsWithSlash = !rest.empty() && rest.front() == '/'; + std::string resolvedPath = basePrefixPath; + if( baseEndsWithSlash && restStartsWithSlash ) + { + resolvedPath += rest.substr( 1 ); + } + else if( !baseEndsWithSlash && !restStartsWithSlash ) + { + resolvedPath += '/' + rest; + } + else + { + resolvedPath = basePrefixPath + rest; + } + m_resolvedPaths.insert( resolvedPath ); + } +} + +} \ No newline at end of file diff --git a/tools/src/ResourceFilter.cpp b/tools/src/ResourceFilter.cpp new file mode 100644 index 0000000..691f503 --- /dev/null +++ b/tools/src/ResourceFilter.cpp @@ -0,0 +1,226 @@ +// Copyright © 2026 CCP ehf. + +#include +#include +#include + +namespace ResourceTools +{ + +ResourceFilter::ResourceFilter( const std::vector& iniFilePaths ) +{ + Initialize( iniFilePaths ); +} + +void ResourceFilter::Initialize( const std::vector& iniFilePaths ) +{ + m_fullResolvedPathMap.clear(); + + if( m_initialized ) + { + throw std::runtime_error( "ResourceFilter is already initialized." ); + } + + std::set uniquePaths( iniFilePaths.begin(), iniFilePaths.end() ); + for( const auto& path : uniquePaths ) + { + try + { + m_filterFiles.emplace_back( std::make_unique( path ) ); + } + catch( const std::exception& e ) + { + // Optionally log or handle error + std::string errorMsg = "Unable to create ResourceFilter for: " + path.string() + " - because of: " + e.what(); + throw std::runtime_error( errorMsg ); + } + } + + m_initialized = true; +} + +const std::map& ResourceFilter::GetFullResolvedPathMap() +{ + if( m_fullResolvedPathMap.empty() ) + { + // Populate the full resolved path map from all Filter INI files + for( auto& iniFile : m_filterFiles ) + { + auto& iniFilePathMap = iniFile->GetIniFileResolvedPathMap(); + for( const auto& kv : iniFilePathMap ) + { + // Combine filters if the same path already exists + auto it = m_fullResolvedPathMap.find( kv.first ); + if( it != m_fullResolvedPathMap.end() ) + { + // Combine the filters (using raw filter strings) + std::string combinedRawFilter = it->second.GetRawFilter() + " " + kv.second.GetRawFilter(); + FilterResourceFilter combinedFilter( combinedRawFilter ); + m_fullResolvedPathMap.insert_or_assign( kv.first, combinedFilter ); + } + else + { + m_fullResolvedPathMap.insert_or_assign( kv.first, kv.second ); + } + } + } + } + + return m_fullResolvedPathMap; +} + +bool ResourceFilter::ShouldInclude( const std::filesystem::path& inFilePath ) +{ + // Make sure we work with the absolute path representation of the input file + std::filesystem::path inFilePathAbs = std::filesystem::absolute( inFilePath ); + std::string inFilePathAbsStr = inFilePathAbs.generic_string(); + + // Priority: lower is higher priority: + // -1 = exact match on filename (or folder) + // 0 = wildcard match on same folder level + // 1 = wildcard match, 1 folder up, etc... + int bestIncludePriority = std::numeric_limits::max(); + int bestExcludePriority = std::numeric_limits::max(); + + // Get the full resolved path map and iterate through it (contains relative paths) + const auto& resolvedPathMap = GetFullResolvedPathMap(); + + for( const auto& [resolvedRelativePathStr, filter] : resolvedPathMap ) + { + // Make sure to work with absolute paths for comparison + std::filesystem::path resolvedRelativePath( resolvedRelativePathStr ); + std::filesystem::path resolvedPathAbs = std::filesystem::absolute( resolvedRelativePath ); + std::string resolvedPathAbsStr = resolvedPathAbs.generic_string(); + + if( resolvedPathAbsStr == inFilePathAbsStr ) + { + // If there is an exact match on the full filename path, this means highest priority and + // SHOULD BE considered an "INCLUDE" even though resolvedPath has filters that might say otherwise. + bestIncludePriority = -1; + continue; + } + + // std::filesystem::path does not support "..." (recursive wildcard). + // We need to append it to the absolute path (if specified) before WildcardMatching + if( resolvedRelativePathStr.find( "..." ) != std::string::npos ) + { + if( resolvedPathAbsStr.back() != '/' ) + { + resolvedPathAbsStr += '/'; + } + resolvedPathAbsStr += "..."; + } + if( !WildcardMatch( resolvedPathAbsStr, inFilePathAbsStr ) ) + { + // There was NO wildcard match on paths, ignore this resolvedRelativePath entry + continue; + } + + // There is a Wildcard match - determine the folder depth difference + auto inFileIt = inFilePathAbs.begin(); + auto resolvedIt = resolvedPathAbs.begin(); + while( inFileIt != inFilePathAbs.end() && resolvedIt != resolvedPathAbs.end() && *inFileIt == *resolvedIt ) + { + ++inFileIt; + ++resolvedIt; + } + + std::filesystem::path remainingPath; + int folderDiffDepthPriority = -1; // Start at -1 (making first iteration priority 0 = same folder level) + while( inFileIt != inFilePathAbs.end() ) + { + ++folderDiffDepthPriority; + remainingPath /= *inFileIt; + ++inFileIt; + } + std::string remainingPathStr = remainingPath.generic_string(); + + // Next step is to check the include/exclude filters: + for( const auto& includeToken : filter.GetIncludeFilter() ) + { + if( includeToken == "*" || remainingPathStr.find( includeToken ) != std::string::npos ) + { + if( folderDiffDepthPriority < bestIncludePriority ) + bestIncludePriority = folderDiffDepthPriority; + } + } + + for( const auto& excludeToken : filter.GetExcludeFilter() ) + { + if( excludeToken == "*" || remainingPathStr.find( excludeToken ) != std::string::npos ) + { + if( folderDiffDepthPriority < bestExcludePriority ) + bestExcludePriority = folderDiffDepthPriority; + } + } + } + + // Apply priority rules: + if( bestIncludePriority == std::numeric_limits::max() ) + { + return false; // No include match found => Exclude the file + } + if( bestExcludePriority == std::numeric_limits::max() ) + { + return true; // No exclude match found (but includePriority less than max => Include the file + } + if( bestIncludePriority < bestExcludePriority ) + { + return true; // Include priority is lower => Include the file + } + if( bestExcludePriority < bestIncludePriority ) + { + return false; // Exclude priority is lower => Exclude the file + } + // Both include and exclude have same priority => Exclude the file + return false; +} + + +// pattern = The resolved path from the .ini file (can contain wildcards) +// checkStr = The input file path to check against the pattern +bool ResourceFilter::WildcardMatch( const std::string& pattern, const std::string& checkStr ) +{ + // Replace ... with a unique token, then process * + std::string pat = pattern; + std::string token = "\x01"; + size_t pos; + while( ( pos = pat.find( "..." ) ) != std::string::npos ) + { + pat.replace( pos, 3, token ); + } + std::string regexPat; + for( size_t i = 0; i < pat.size(); ++i ) + { + if( pat[i] == '*' ) + { + regexPat += "[^/]*"; + } + else if( pat[i] == '\x01' ) + { + regexPat += ".*"; + } + else if( std::string( ".^$|()[]{}+?\\" ).find( pat[i] ) != std::string::npos ) + { + // Regex special characters that need escaping + regexPat += '\\'; + regexPat += pat[i]; + } + else + { + regexPat += pat[i]; + } + } + try + { + std::regex re( regexPat, std::regex::ECMAScript | std::regex::icase ); + bool regexResult = std::regex_match( checkStr, re ); + return regexResult; + } + catch( ... ) + { + return false; + } +} + +} // namespace ResourceTools diff --git a/vcpkg.json b/vcpkg.json index fee8d3f..ac300ed 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -16,6 +16,10 @@ "name": "curl", "version>=": "8.11.1#1" }, + { + "name": "inih", + "version>=": "62" + }, { "name": "yaml-cpp", "version>=": "0.8.0#1"