diff --git a/.teamcity/MacOS/Project.kt b/.teamcity/MacOS/Project.kt index 712e116..db04686 100644 --- a/.teamcity/MacOS/Project.kt +++ b/.teamcity/MacOS/Project.kt @@ -150,7 +150,7 @@ class CarbonBuildMacOS(buildName: String, configType: String, preset: String, ag vcsRootExtId = "${DslContext.settingsRootId.id}" provider = github { authType = token { - token = "%GITHUB_TEAMCITY_TOKEN%" + token = "%GITHUB_CARBON_PAT%" } filterAuthorRole = PullRequests.GitHubRoleFilter.MEMBER } @@ -159,7 +159,7 @@ class CarbonBuildMacOS(buildName: String, configType: String, preset: String, ag publisher = github { githubUrl = "https://api.github.com" authType = personalToken { - token = "%GITHUB_TEAMCITY_TOKEN%" + token = "%GITHUB_CARBON_PAT%" } } } diff --git a/.teamcity/Windows/Project.kt b/.teamcity/Windows/Project.kt index 5795f06..6433401 100644 --- a/.teamcity/Windows/Project.kt +++ b/.teamcity/Windows/Project.kt @@ -208,7 +208,7 @@ class CarbonBuildWindows(buildName: String, configType: String, preset: String) vcsRootExtId = "${DslContext.settingsRootId.id}" provider = github { authType = token { - token = "%GITHUB_TEAMCITY_TOKEN%" + token = "%GITHUB_CARBON_PAT%" } filterAuthorRole = PullRequests.GitHubRoleFilter.MEMBER } @@ -217,7 +217,7 @@ class CarbonBuildWindows(buildName: String, configType: String, preset: String) publisher = github { githubUrl = "https://api.github.com" authType = personalToken { - token = "%GITHUB_TEAMCITY_TOKEN%" + token = "%GITHUB_CARBON_PAT%" } } } diff --git a/CMakeLists.txt b/CMakeLists.txt index c1ccadf..c15c0f9 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) @@ -68,7 +69,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 3cb68b1..79f3b11 100644 --- a/cli/src/CreateResourceGroupCliOperation.cpp +++ b/cli/src/CreateResourceGroupCliOperation.cpp @@ -15,7 +15,9 @@ 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" ), + m_createResourceGroupIniFilterFilesPrefixmapBasePathArgumentId( "--filter-file-prefixmap-basepath" ) { AddRequiredPositionalArgument( m_createResourceGroupPathArgumentId, "Base directory to create resource group from." ); @@ -32,25 +34,29 @@ 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 for resource filtering.", false, true, "" ); + + AddArgument( m_createResourceGroupIniFilterFilesPrefixmapBasePathArgumentId, "Base directory for resolving relative paths contained within filter INI file(s) prefixmap attribute.", false, false, "" ); } 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 +69,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,20 +85,37 @@ 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 ); - if (ShowCliStatusUpdates()) - { + for( const auto& iniPathStr : iniFileStringVector ) + { + if( !iniPathStr.empty() ) + { + createResourceGroupParams.resourceFilterIniFiles.emplace_back( iniPathStr ); + } + } + + createResourceGroupParams.resourceFilterIniFilesPrefixmapBaseDirectory = m_argumentParser->get( m_createResourceGroupIniFilterFilesPrefixmapBasePathArgumentId ); + } + + if (ShowCliStatusUpdates()) + { 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 { std::cout << "---Creating Resource Group---" << std::endl; @@ -102,37 +125,55 @@ 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.empty() ) + { + std::cout << "Resource Filter INI File(s) used: " << std::endl; + + for( const auto& iniPath : createResourceGroupFromDirectoryParams.resourceFilterIniFiles ) + { + std::cout << " - " << iniPath.generic_string() << std::endl; + } + + std::cout << "Base Directory for Resolving Relative Paths in Filter INI File(s): " << createResourceGroupFromDirectoryParams.resourceFilterIniFilesPrefixmapBaseDirectory << 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; diff --git a/cli/src/CreateResourceGroupCliOperation.h b/cli/src/CreateResourceGroupCliOperation.h index ca9cee1..6a4f39a 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,18 @@ 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; + + std::string m_createResourceGroupIniFilterFilesPrefixmapBasePathArgumentId; }; #endif // CreateResourceGroupCliOperation_H \ No newline at end of file diff --git a/customized_toolchain/toolchains/arm64-osx-carbon.cmake b/customized_toolchain/toolchains/arm64-osx-carbon.cmake index adb6dc9..9e39308 100644 --- a/customized_toolchain/toolchains/arm64-osx-carbon.cmake +++ b/customized_toolchain/toolchains/arm64-osx-carbon.cmake @@ -3,7 +3,7 @@ if (NOT _CCP_TOOLCHAIN_FILE_LOADED) set(_CCP_TOOLCHAIN_FILE_LOADED 1) set (VCPKG_USE_HOST_TOOLS ON CACHE STRING "") - set (CMAKE_CXX_STANDARD 20 CACHE STRING "") + set (CMAKE_CXX_STANDARD 17 CACHE STRING "") set (CMAKE_CXX_STANDARD_REQUIRED ON CACHE STRING "") set (CMAKE_CXX_EXTENSIONS OFF CACHE STRING "") set (CMAKE_POSITION_INDEPENDENT_CODE ON CACHE STRING "") diff --git a/doc/source/DesignDocuments/filterIniFiles.rst b/doc/source/DesignDocuments/filterIniFiles.rst new file mode 100644 index 0000000..e5b0a62 --- /dev/null +++ b/doc/source/DesignDocuments/filterIniFiles.rst @@ -0,0 +1,80 @@ +Filter .ini File Format +========================== + +When generating Resource Groups it is possible to control the included/excluded files and folders, with the help of a resource filter .ini file(s). + + +Example Filter .ini file +------------------------ + +.. code-block:: ini + + # =============== This is a comment =============== + # Every filter .ini file needs the following: + # - [DEFAULT] section containing: + # - "prefixmap" attribute (mandatory): + # This is a space separated list of prefixes and their associated relative paths (semicolon separated). + # Format is: prefixA:path1;path2 prefixB:path3 (see actual example below) + # + # - [OneOrMoreNamedSections] containing: + # - "filter" attribute (optional): + # A top-level include/exclude filter ruleset that is applied to any + # "respaths" and "resfile" attribute element within this [NamedSection]. + # Format is: [ .includeExtension1 IncludeFullOrPartialFileOrFolderName ] + # ![ .excludeExtension1 ExcludeFullOrPartialFileOrFolderName ] + # If there is no filter attribute specified (i.e. optional), then wild-card include all is implied ([ * ]). + # - "respaths" attribute (mandatory, multi-line): + # A multi-line list of resource path entries, with or without wild-cards (* or ...). + # The paths are relative to their basepath prefix from the "prefixmap". + # Each line is a separate resource path entry, with or without an optional in-line filter. + # Search paths are resolved for each entry using a lookup from the "prefixmap". + # Supported wildcards are: + # - * to match any file within the current folder + # - ... to match any file or folder recursively from the current folder + # Format is: prefix:some/path/possible_wildcard [ OptionalExtraInclude ] ![ OptionalExtraExclude ] + # - "resfile" attribute (optional, single-line): + # Identical to a "respaths" entry, but for a single file path entry only. + # Only supported for backwards compatibility of older filter .ini files. + # Any single, specific file entries can (and should) be represented in the "respaths" attribute instead. + ; =============== This is also a comment =============== + + [DEFAULT] + prefixmap = res:res;../common/res resbin:../bin + + [NamedSection1] + filter = [ .yaml .txt ] + respaths = res:/... + res:/SomeFolder/... ![ AlsoExclude ] + res:/SomeOtherFolder/* [ .red ] + resbin:/* [ .dll ] ![ .yaml .txt ] + res:/SomeFolder/FolderWithAlsoExcludeName/includeThisFile.csv ] + + # =============== For this example CLI create-group call =============== + # resources.exe + # create-group C:\Build + # --output-file ResourceGroup.yaml + # --document-version 0.1.0 + # --filter-file C:\Build\Resources\FolderForFilters\filterRules1.ini + # --filter-file-prefixmap-basepath C:\Build\Resources + # ================ The following rules would be applied ================ + # 1. Any .yaml or .txt file within: + # "res:/..." + # - C:\Build\Resources\res (and its subfolders "...") would be included. + # - C:\Build\common\res (and its subfolders "...") would be included. + # 2. Unless the .yaml or .txt file is within: + # "res:/SomeFolder/..." and "AlsoExclude" is part of its name, then it would be excluded. + # - C:\Build\Resources\res\SomeFolder (and its subfolders "...") + # - C:\Build\common\res\SomeFolder (and its subfolders "...") + # 3. Additionally, any file within: + # "res:/SomeOtherFolder/*" would be included, but only if it is a .red file. + # - C:\Build\Resources\res\SomeOtherFolder (only this folder) + # - C:\Build\common\res\SomeOtherFolder (only this folder) + # 4. Then any .dll file within: + # "resbin:/*" only .dll files would be included, excluding .yaml and .txt files. + # - C:\Build\bin (only this folder) + # 5. Finally, we should include this specific file rule: + # "res:/SomeFolder/FolderWithAlsoExcludeName/includeThisFile.csv" + # - C:\Build\Resources\res\SomeFolder\FolderWithAlsoExcludeName\includeThisFile.csv + # - C:\Build\common\res\SomeFolder\FolderWithAlsoExcludeName\includeThisFile.csv + # ====================================================================== + ... diff --git a/doc/source/Guides/HowToCreateAResourceGroup.rst b/doc/source/Guides/HowToCreateAResourceGroup.rst index aa8a67a..a3b0c85 100644 --- a/doc/source/Guides/HowToCreateAResourceGroup.rst +++ b/doc/source/Guides/HowToCreateAResourceGroup.rst @@ -5,16 +5,25 @@ Resource Groups can be created via the lib or CLI. This example uses the CLI. .. code:: - .\resources.exe create-group C:\Build --output-file ResourceGroup.yaml + .\resources.exe create-group C:\Build --output-file ResourceGroup.yaml --document-version 0.1.0 --filter-file C:\Build\Resources\FolderForFilters\filterRules1.ini --filter-file-prefixmap-basepath C:\Build\Resources **Arguments:** 1. Positional argument - Base directory to create resource group from. 2. ``--output-file`` - Filename for created resource group. +3. ``--document-version`` - The type of resource group to create/output. Valid values are: default=0.1.0 (yaml) and 0.0.0 (csv). +4. ``--filter-file`` - Absolute path to a .ini file containing include/exclude resource filtering rules. If not set = no filtering. Can be specified multiple times to combine filter rules from multiple files. See :doc:`../DesignDocuments/filterIniFiles` for more information on resource filter .ini files. +5. ``--filter-file-prefixmap-basepath`` - The absolute base directory for resolving relative paths contained within the supplied filter .ini file(s) **prefixmap** attribute. Ignored if the filter-file argument is not supplied. .. note:: See CLI help for more information regarding options. -This will create a ``ResourceGroup.yaml`` file representing the input directory ``C:\Build``. +The example CLI operation from the above example will: -The resource group files are human readable yaml files and quite self explanatory. For more information see :doc:`../DesignDocuments/filesystemDesign` \ No newline at end of file +* Create an output ``ResourceGroup.yaml`` file of the default document version 0.1.0 (yaml) format. +* Representing the contents of the input directory ``C:\Build`` +* Limiting it based on include/exclude filter rules specified in ``C:\Build\Resources\FolderForFilters\filterRules1.ini``. +* While resolving relative **prefixmap** paths within it using the supplied ``C:\Build\Resources`` base path. + + +The resource group files (in document-version 0.1.0) are human readable yaml files and quite self explanatory. For more information see :doc:`../DesignDocuments/filesystemDesign` diff --git a/doc/source/designDocuments.rst b/doc/source/designDocuments.rst index 3ab23d2..d1c7f88 100644 --- a/doc/source/designDocuments.rst +++ b/doc/source/designDocuments.rst @@ -8,3 +8,4 @@ Documents contained in this section detail design approaches in resources. DesignDocuments/filesystemDesign DesignDocuments/resourceGroupFileFormat + DesignDocuments/filterIniFiles diff --git a/include/Enums.h b/include/Enums.h index fb7ce9a..874240c 100644 --- a/include/Enums.h +++ b/include/Enums.h @@ -124,6 +124,10 @@ using StatusCallback = std::function resourceFilterIniFiles = {}; - ResourceDestinationSettings exportResourcesDestinationSettings = { CarbonResources::ResourceDestinationType::LOCAL_CDN, "ExportedResources" }; + std::filesystem::path resourceFilterIniFilesPrefixmapBaseDirectory = ""; }; /** @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 0bbb0a6..c55ceeb 100644 --- a/src/ResourceGroupImpl.cpp +++ b/src/ResourceGroupImpl.cpp @@ -19,6 +19,8 @@ #include "BundleResourceGroupImpl.h" #include "ChunkIndex.h" #include "ResourceGroupFactory.h" +#include +#include namespace CarbonResources { @@ -63,6 +65,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, params.resourceFilterIniFilesPrefixmapBaseDirectory ); + } + 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 ); @@ -70,10 +87,35 @@ Result ResourceGroup::ResourceGroupImpl::CreateFromDirectory( const CreateResour StatusSettings fileProcessingInnerStatusSettings; statusSettings.Update( CarbonResources::StatusProgressType::PERCENTAGE, 10, 90, "Processing Files", &fileProcessingInnerStatusSettings ); + int fileSkipCount = 0; for( const std::filesystem::directory_entry& entry : recursiveDirectoryIter ) { 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.FilePathMatchesIncludeFilterRules( entry.path() ) ) + { + if( fileSkipCount++ % 25 == 0 ) + { + std::string skipMessage = "Skipping file [" + std::to_string( fileSkipCount - 1 ) + "] as it doesn't match filters: " + entry.path().string(); + fileProcessingInnerStatusSettings.Update( CarbonResources::StatusProgressType::UNBOUNDED, 0, 0, skipMessage ); + } + 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 fileProcessingInnerStatusSettings.Update( CarbonResources::StatusProgressType::UNBOUNDED, 0, 0, "Processing File: " + entry.path().string() ); @@ -896,14 +938,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(); @@ -967,7 +1008,7 @@ Result ResourceGroup::ResourceGroupImpl::ExportYaml( const VersionInternal& outp data = out.c_str(); } - + return Result{ ResultType::SUCCESS }; } @@ -1464,7 +1505,6 @@ Result ResourceGroup::ResourceGroupImpl::CreatePatch( const PatchCreateParams& p return subtractionResult; } } - // Ensure that the diff results have the same number of members if( resourceGroupSubtractionPrevious->m_resourcesParameter.GetSize() != resourceGroupSubtractionNext->m_resourcesParameter.GetSize() ) @@ -1772,7 +1812,7 @@ Result ResourceGroup::ResourceGroupImpl::CreatePatch( const PatchCreateParams& p } } } - + patchResourceGroup.SetRemovedResourceRelativePaths( resourceGroupSubtractionParams.removedResources ); @@ -1822,7 +1862,7 @@ Result ResourceGroup::ResourceGroupImpl::CreatePatch( const PatchCreateParams& p return setResourceGroupResult; } } - + std::string patchResourceGroupData; { StatusSettings exportPatchResourceGroupStatusSettings; @@ -2019,7 +2059,6 @@ Result ResourceGroup::ResourceGroupImpl::Merge( const ResourceGroupMergeParams& std::back_inserter( unionResources ), []( const ResourceInfo* a, const ResourceInfo* b ) { return *a < *b; } ); - { StatusSettings nestedStatusSettings; statusSettings.Update( CarbonResources::StatusProgressType::PERCENTAGE, 20, 80, "Merging resource groups.", &nestedStatusSettings ); @@ -2109,11 +2148,11 @@ Result ResourceGroup::ResourceGroupImpl::DiffChangesAsLists( const ResourceGroup subtractionsStatusSettings.Update( StatusProgressType::PERCENTAGE, percentage, step, removedResource.string() ); i++; } - + params.subtractions->push_back( removedResource ); } } - + { StatusSettings additionsStatusSettings; statusSettings.Update( CarbonResources::StatusProgressType::PERCENTAGE, 90, 10, "Collating additions.", &additionsStatusSettings ); @@ -2121,7 +2160,7 @@ Result ResourceGroup::ResourceGroupImpl::DiffChangesAsLists( const ResourceGroup int i = 0; for( auto resource : result1.m_impl->m_resourcesParameter ) { - + std::filesystem::path relativePath; Result getRelativePathResult = resource->GetRelativePath( relativePath ); @@ -2260,7 +2299,7 @@ Result ResourceGroup::ResourceGroupImpl::Diff( ResourceGroupSubtractionParams& p } } } - + { StatusSettings addResourceStatusSettings; statusSettings.Update( CarbonResources::StatusProgressType::PERCENTAGE, 40, 20, "Calculating diff between two resource groups.", &addResourceStatusSettings ); @@ -2315,7 +2354,7 @@ Result ResourceGroup::ResourceGroupImpl::Diff( ResourceGroupSubtractionParams& p params.result1->AddResource( dummyResource ); } } - + { StatusSettings removeResourceStatusSettings; statusSettings.Update( CarbonResources::StatusProgressType::PERCENTAGE, 60, 40, "Calculating diff between two resource groups.", &removeResourceStatusSettings ); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 00e15d1..1cb7b6c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,13 +7,15 @@ find_package(tiny-process-library CONFIG REQUIRED) include(GoogleTest) set(SRC_FILES - src/ResourcesTestFixture.cpp - src/ResourcesTestFixture.h src/CliTestFixture.cpp src/CliTestFixture.h - src/ResourcesLibraryTest.cpp src/ResourcesCliTest.cpp + src/ResourcesLibraryTest.cpp + src/ResourcesTestFixture.cpp + src/ResourcesTestFixture.h + src/ResourceToolsFilterTest.cpp src/ResourceToolsLibraryTest.cpp + src/ResourceToolsTest.h ) add_executable(resources-test ${SRC_FILES}) @@ -21,7 +23,8 @@ add_executable(resources-test ${SRC_FILES}) target_compile_definitions(resources-test PRIVATE TEST_DATA_BASE_PATH="${CMAKE_SOURCE_DIR}/tests/testData" - CARBON_RESOURCES_CLI_EXE_NAME=\"$\") + CARBON_RESOURCES_CLI_EXE_NAME=\"$\" + CARBON_RESOURCES_CLI_EXE_FULLPATH=\"$\") target_include_directories(resources-test PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include) diff --git a/tests/src/CliTestFixture.cpp b/tests/src/CliTestFixture.cpp index 95e6ec2..43fcdd2 100644 --- a/tests/src/CliTestFixture.cpp +++ b/tests/src/CliTestFixture.cpp @@ -6,19 +6,48 @@ #include -int CliTestFixture::RunCli( std::vector& arguments, std::string& output ) +int CliTestFixture::RunCli( std::vector& arguments, + std::string* standardOutput /* = nullptr */, + std::string* errorOutput /* = nullptr */ ) { - std::string processOutput; - - arguments.insert( arguments.begin(), CARBON_RESOURCES_CLI_EXE_NAME ); - - TinyProcessLib::Process process1a( arguments, "", [&processOutput]( const char* bytes, size_t n ) { - processOutput += std::string( bytes, n ); - } ); + arguments.insert( arguments.begin(), CARBON_RESOURCES_CLI_EXE_FULLPATH ); + + std::cout << "--- RunCli() arguments: ---" << std::endl; + for( const auto& arg : arguments ) + { + std::cout << " " << arg << std::endl; + } + std::cout << "---------------------------" << std::endl; + + // Only populate the output and errorOutput if the caller provided non-nullptr for them, otherwise discard them + TinyProcessLib::Process process1a( + arguments, + "", + [standardOutput]( const char* bytes, size_t n ) { if (standardOutput != nullptr) { *standardOutput += std::string( bytes, n ); } }, + [errorOutput]( const char* bytes, size_t n ) { if (errorOutput != nullptr) { *errorOutput += std::string( bytes, n ); } } + ); auto exit_status = process1a.get_exit_status(); - output = processOutput; - return exit_status; -} \ No newline at end of file +} + +// ------------------------------------------------------------- +// Description: +// Helper function to remove intermediate files generated as +// part of a test being run. +// Arguments: +// filesToRemove - Vector of file paths to remove. +// Return Value: +// Nothing (void) +// ------------------------------------------------------------- +void CliTestFixture::RemoveFiles( 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..d904fe2 100644 --- a/tests/src/CliTestFixture.h +++ b/tests/src/CliTestFixture.h @@ -12,7 +12,10 @@ struct CliTestFixture : public ResourcesTestFixture { - int RunCli( std::vector& arguments, std::string& output ); + int RunCli( std::vector& arguments, std::string* standardOutput = nullptr, std::string* errorOutput = nullptr ); + + // Helper to remove files as part of a test run and cleanup + void RemoveFiles( const std::vector& filesToRemove ); }; #endif // CliTestFixture_H \ No newline at end of file diff --git a/tests/src/ResourceToolsFilterTest.cpp b/tests/src/ResourceToolsFilterTest.cpp new file mode 100644 index 0000000..ba5be46 --- /dev/null +++ b/tests/src/ResourceToolsFilterTest.cpp @@ -0,0 +1,2592 @@ +// Copyright © 2025 CCP ehf. + +#include "ResourceToolsTest.h" + +#include +#include +#include +#include + +#include + +#include "INIReader.h" + +#include "FilterDefaultSection.h" +#include "FilterNamedSection.h" +#include "FilterResourceFilter.h" +#include "FilterPrefixmap.h" +#include "FilterPrefixMapEntry.h" +#include "FilterResourceFile.h" +#include "FilterResourcePathFile.h" +#include "ResourceFilter.h" + +TEST_F( ResourceToolsTest, LoadAndParseIniFile_example1Ini_UsingIniReader ) +{ + // Use the test fixture's helper to get the absolute path + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/example1.ini" ); + INIReader reader( iniPath.generic_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( ResourceToolsTest, FilterResourceFilter_ApplyRule_IncludeFilterOnly ) +{ + 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( ResourceToolsTest, FilterResourceFilter_ApplyRule_TopLevelExcludeFilterOnly ) +{ + // Top-level filter refers to the "filter" attribute of a NamedSection. + // When there is no include filter specified at the top-level "filter" attribute, + // a wild-card "*" should be added as default include. + 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" ); + + // Include filter should contain wildcard when no explicit include filter specified at top-level ("filter" attribute) + EXPECT_EQ( includes.size(), 1 ); + EXPECT_EQ( includes[0], "*" ); +} + +TEST_F( ResourceToolsTest, FilterResourceFilter_ApplyRule_InLineExcludeFilterOnly ) +{ + // "InLine" filter refers to an optional filter element at the end of a + // respaths/resfile attribute line (class FilterResourcePathFileEntry) + // The InLine filter applies only to that line (in addition to any parent filter present, if applicable). + 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" ); + + // No wildcard should be added to include filter at in-line level + EXPECT_EQ( includes.size(), 0 ); + EXPECT_TRUE( includes.empty() ); +} + +TEST_F( ResourceToolsTest, FilterResourceFilter_ApplyRules_UseComplexIncludeExcludeFilters ) +{ + 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 ) << "Include filters do not match expected values"; + EXPECT_EQ( excludes, expectedExcludes ) << "Exclude filters do not match expected values"; +} + +TEST_F( ResourceToolsTest, FilterResourceFilter_ApplyRule_SimpleIncludeFilterOnly ) +{ + ResourceTools::FilterResourceFilter filter( "[ .red ]" ); + const auto& includes = filter.GetIncludeFilter(); + + EXPECT_EQ( includes.size(), 1 ); + EXPECT_EQ( includes[0], ".red" ); +} + +TEST_F( ResourceToolsTest, FilterResourceFilter_ApplyRule_SimpleExcludeFilterOnly ) +{ + ResourceTools::FilterResourceFilter filter( "![ .blk ]" ); + const auto& excludes = filter.GetExcludeFilter(); + + EXPECT_EQ( excludes.size(), 1 ); + EXPECT_EQ( excludes[0], ".blk" ); +} + +TEST_F( ResourceToolsTest, FilterResourceFilter_ApplyRule_CombineIncludeExcludeIncludeFilters ) +{ + // This test, combines multiple include and exclude filters, to test the logic of adding/removing filter elements: + // 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( ResourceToolsTest, FilterResourceFilter_CheckFailure_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( ResourceToolsTest, FilterResourceFilter_CheckFailure_ExcludeMarkerWithoutOpenBracket ) +{ + try + { + // This test filter has an exclude marker "!" but is missing the open bracket + // for the exclude filter. + 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( ResourceToolsTest, FilterResourceFilter_CheckFailure_ExcludeMarkerWithoutOpenBracketAfterValidIncludeFilter ) +{ + try + { + // This test filter has a valid include filter, followed by an exclude marker "!" + // but is missing the open bracket for the exclude filter. + 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( ResourceToolsTest, FilterResourceFilter_CheckFailure_ExcludeMarkerWithoutBracketAfterValidIncludeAndExcludeFilters ) +{ + try + { + // This test filter has a valid include and exclude filters, + // followed by an exclude marker "!" and no filter definition after that. + 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( ResourceToolsTest, FilterResourceFilter_CheckFailure_MissingOpeningIncludeBracketAtStart ) +{ + try + { + // This test filter is missing the opening bracket for the include filter. + 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( ResourceToolsTest, FilterResourceFilter_CheckFailure_MissingOpeningIncludeBracketForSecondIncludeFilter ) +{ + try + { + // This test filter is missing the opening bracket for the second include filter, + // after a valid first include filter. + 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( ResourceToolsTest, FilterResourceFilter_CheckFailure_MissingClosingIncludeBracket ) +{ + try + { + // This test filter is missing the closing bracket for the first and only include filter. + 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( ResourceToolsTest, FilterResourceFilter_CheckFailure_MissingClosingIncludeBracketForSecondIncludeFilter ) +{ + try + { + // This test filter is missing the closing bracket for the second include filter. + 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( ResourceToolsTest, FilterResourceFilter_CheckFailure_MissingClosingIncludeBracketForMiddleIncludeFilter ) +{ + try + { + // This test filter is missing the closing bracket for the middle include filter. + 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( ResourceToolsTest, FilterResourceFilter_ApplyRule_UseCondensedValidIncludeExcludeIncludeFilters ) +{ + // This test filter combines multiple include and exclude filters without extra whitespaces. + // Done to test the logic of adding/removing filter elements and that the parsing logic is not dependent on whitespaces. + 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( ResourceToolsTest, FilterResourceFilter_ApplyRule_UseCondensedValidExcludeIncludeExcludeFilters ) +{ + // This test filter combines multiple include and exclude filters without extra whitespaces. + // Done to test the logic of adding/removing filter elements and that the parsing logic is not dependent on whitespaces. + 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( ResourceToolsTest, FilterResourceFilter_ApplyRule_EmptyTopLevelFilter ) +{ + // When the top-level "filter" attribute of a NamedSection is empty, + // a wild-card "*" should be added to the include filter by default. + // Exclude filter should be empty though. + ResourceTools::FilterResourceFilter filter( "", true ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + + // Wildcard "*" should be added when no include filter is specified for top-level "filter" attribute + EXPECT_EQ( includes.size(), 1 ); + EXPECT_EQ( includes[0], "*" ); + + EXPECT_TRUE( excludes.empty() ); +} + +TEST_F( ResourceToolsTest, FilterResourceFilter_ApplyRule_EmptyInLineFilter ) +{ + // When an in-line filter of (respaths/resfile line) is empty (the default behavior), + // no wildcard "*" should be added to the include filter. + ResourceTools::FilterResourceFilter filter( "" ); + const auto& includes = filter.GetIncludeFilter(); + const auto& excludes = filter.GetExcludeFilter(); + + // No wildcard should be added to an in-line include filter + EXPECT_EQ( includes.size(), 0 ); + EXPECT_TRUE( includes.empty() ); + + EXPECT_TRUE( excludes.empty() ); +} + +// ----------------------------------------- + +TEST_F( ResourceToolsTest, FilterPrefixMap_Validate_SinglePrefixWithMultiplePathsIsAllowed ) +{ + // This test validates that a single prefix (prefix1) with multiple different paths + // is correctly parsed and stored in the map. + 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( ResourceToolsTest, FilterPrefixMap_Validate_MultipleDifferentPrefixesAreAllowed ) +{ + // This test validates that multiple different prefixes (prefix1 & prefix2) + // with their associated paths are correctly parsed and stored in the map. + 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( ResourceToolsTest, FilterPrefixMap_Validate_DuplicateSamePrefixWithPathsInDifferentOrderIsAllowed ) +{ + // This test validates that if the same prefix (prefix1) is defined multiple times, + // with same paths but in different order (path1+path2 & path2+path1). + // That the paths are combined and stored in the map without duplicates. + 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( ResourceToolsTest, FilterPrefixMap_Validate_MultipleSamePrefixesCanAppendToPaths ) +{ + // This test validates that if the same prefix (prefix1) is defined multiple times (first and last), + // with different paths (path2+path1 & path3+path1), that the paths are combined and stored in the map without duplicates. + 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( ResourceToolsTest, FilterPrefixMap_Validate_DifferentWhitespacesBetweenPrefixesAreAllowed ) +{ + // This test validates that different whitespaces (space, tab, new line) + // between prefix definitions are handled correctly. + 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( ResourceToolsTest, FilterPrefixMap_CheckFailure_MissingColonAfterPrefixBeforePaths ) +{ + // This test validates that if a prefix definition is missing a colon ":" + // after the prefix and before the paths section, that an exception is thrown. + 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( ResourceToolsTest, FilterPrefixMap_CheckFailure_MissingPrefixBeforePaths ) +{ + // This test validates that if a prefix definition is missing the prefix itself + // (lhs of colon) before the paths section, that an exception is thrown. + 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( ResourceToolsTest, FilterPrefixMap_CheckFailure_MissingPathsAfterPrefix ) +{ + // This test validates that if a prefix definition is missing the paths section + // (rhs of colon) after the prefix, that an exception is thrown. + 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( ResourceToolsTest, FilterPrefixMapEntry_CheckFailure_PrefixMissingInMapWhenAppendingPath ) +{ + // This test validates that when trying to append paths to a FilterPrefixMapEntry + // with a different prefix than the existing one of the mapEntry that an exception is thrown. + 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( ResourceToolsTest, FilterPrefixMapEntry_CheckFailure_EmptyPathWhenAppendingPathToPrefix ) +{ + // This test validates that when trying to append an empty path to a FilterPrefixMapEntry + // that an exception is thrown, since empty paths are not allowed. + 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( ResourceToolsTest, FilterDefaultSection_Validate_DefaultSectionWithMultiplePrefixesIsAllowed ) +{ + // This test validates that a FilterDefaultSection can be initialized + // with multiple different prefixes and their associated paths. + 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( ResourceToolsTest, FilterDefaultSection_CheckFailure_InitializeWithMissingColonInPrefixmap ) +{ + // This test validates that when trying to initialize a FilterDefaultSection + // with a prefixmap string missing a colon ":" after the prefix definition + // that an exception is thrown. + 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( ResourceToolsTest, FilterDefaultSection_CheckFailure_InitializeWithEmptyPrefixInPrefixmap ) +{ + // This test validates that when trying to initialize a FilterDefaultSection + // with a prefixmap string missing the prefix definition (empty prefix) + // that an exception is thrown. + 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"; + } +} + +// ------------------------------------------------------------- +// Description: +// Helper function, for tests in this file, to verify that all +// expected paths are present in the resolved map. +// Arguments: +// allExpectedPaths - Set of all expected paths to be found in the map +// resolvedMap - Map of resolved paths +// messagePrefix - Optional prefix to add to the failure message +// Return Value: +// None (void) +// ------------------------------------------------------------- +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() << ")"; + } +} + +// ------------------------------------------------------------- +// Description: +// Helper function, for tests in this file, to validate that the resolved +// path map contains all expected paths with the correct include/exclude filters. +// Arguments: +// expectedPaths - Set of expected paths to be found in the map +// resolvedPathMap - Map of resolved paths with their filters +// expectedIncludes - Vector of expected include filters for each path +// expectedExcludes - Vector of expected exclude filters for each path +// messagePrefix - Optional prefix to add to the failure message +// Return Value: +// None (void) +// ------------------------------------------------------------- +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( ResourceToolsTest, FilterResourcePathFile_Validate_SingleLineAttribute_WithNoInlineFilterIsAllowed ) +{ + // This test validates that a FilterResourcePathFile can be initialized with a single line attribute + // that contains a prefix and path, but no in-line filter. + 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( ResourceToolsTest, FilterResourcePathFile_Validate_SingleLineAttribute_WithInlineFilterNoOverridesIsAllowed ) +{ + // This test validates that a FilterResourcePathFile can be initialized with a single line attribute + // that contains a prefix and path, with an in-line filter that does not override any of the parent filters. + 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( ResourceToolsTest, FilterResourcePathFile_Validate_SingleLineAttribute_WithInlineFilterOverridingParentFilterIsAllowed ) +{ + // This test validates that a FilterResourcePathFile can be initialized with a single + // line attribute with an in-line filter that overrides some of the parent filters by + // moving some filters from include to exclude and vice versa. + 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( ResourceToolsTest, FilterResourcePathFile_Validate_MultiLineAttribute_WithMixedInlineFilterOverridesIsAllowed ) +{ + // This test validates that a FilterResourcePathFile can be initialized with a multi-line attribute + // that contains multiple prefixes and paths. + // The in-line filters may override parent filters in different ways. + // Some with no overrides, others with include/exclude filters switched around, etc. + 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( ResourceToolsTest, FilterResourcePathFile_Validate_SingleLineAttribute_WithDuplicateIncludeExcludeInlineOverridesIsAllowed ) +{ + // This test validates that a FilterResourcePathFile can be initialized with a single line attribute. + // With an in-line filter that has duplicate include and exclude entries (some same as parent filters). + // The final resolved filters should not contain any duplicates. + 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( ResourceToolsTest, FilterResourcePathFile_CheckFailure_MissingPrefixThrowsException ) +{ + // This test validates that when trying to initialize a FilterResourcePathFile with an + // invalid raw attribute string (missing prefix), that an exception is thrown. + 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( ResourceToolsTest, FilterResourcePathFile_CheckFailure_UnknownPrefixThrowsException ) +{ + // This test validates that when trying to initialize a FilterResourcePathFile with an + // invalid raw attribute string (unknown prefix, not in the prefix map), that an exception is thrown. + 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( ResourceToolsTest, FilterResourcePathFile_CheckFailure_MalformedInlineFilterThrowsException ) +{ + // This test validates that when trying to initialize a FilterResourcePathFile with an + // invalid raw attribute string (malformed in-line filter), that an exception is thrown. + 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( ResourceToolsTest, FilterNamedSection_Validate_SingleLineRespathIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with a single line respath. + std::string sectionName = "FilterNamedSection_Validate_SingleLineRespathIsAllowed"; + 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 || resolvedResfileMap->empty() ); + ValidatePathMap( expectedPaths, combinedMap, expectedIncludes, expectedExcludes, "CombinedResolvedPathMap" ); +} + +TEST_F( ResourceToolsTest, FilterNamedSection_Validate_EmptyTopLevelFilterIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with an empty filter at top-level, + // which should add a wildcard "*" include filter. + std::string sectionName = "FilterNamedSection_Validate_EmptyTopLevelFilterIsAllowed"; + 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 || resolvedResfileMap->empty() ); + ValidatePathMap( expectedPaths, combinedMap, expectedIncludes, expectedExcludes, "CombinedResolvedPathMap" ); +} + +TEST_F( ResourceToolsTest, FilterNamedSection_Validate_OnlyTopLevelExcludeFilterIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with only an exclude filter at top-level, + // which should add a wildcard "*" include filter. + std::string sectionName = "FilterNamedSection_Validate_OnlyTopLevelExcludeFilterIsAllowed"; + 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 || resolvedResfileMap->empty() ); + ValidatePathMap( expectedPaths, combinedMap, expectedIncludes, expectedExcludes, "CombinedResolvedPathMap" ); +} + +TEST_F( ResourceToolsTest, FilterNamedSection_Validate_MultiLineRespathWithSomeInlineOverridesIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with a multi-line respath attribute. + // Some lines may have in-line filters that override the top-level filter, while others just use the top-level filter as-is. + std::string sectionName = "FilterNamedSection_Validate_MultiLineRespathWithSomeInlineOverridesIsAllowed"; + 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 || resolvedResfileMap->empty() ); + MapContainsPaths( allExpectedPaths, combinedMap, "CombinedResolvedMap" ); + ValidatePathMap( firstLinePaths, combinedMap, firstLineIncludes, firstLineExcludes, "FirstLine ResolvedRespathsMap" ); + ValidatePathMap( secondLinePaths, combinedMap, defaultIncludes, defaultExcludes, "SecondLine ResolvedRespathsMap" ); +} + +TEST_F( ResourceToolsTest, FilterNamedSection_Validate_CombinedSingleLineRespathAndResfileAttributesIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both respath and resfile attributes. + // The combined resolved map contains entries from both attributes. + std::string sectionName = "FilterNamedSection_Validate_CombinedSingleLineRespathAndResfileAttributesIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_RespathWithoutResfileAttributeIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with only + // a respath attribute and no resfile attribute. + std::string sectionName = "FilterNamedSection_Validate_RespathWithoutResfileAttributeIsAllowed"; + 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_TRUE( !resfileMap || resfileMap->empty() ); // Nothing in resfile + + ASSERT_EQ( combinedMap.size(), 1 ); // 1 from respaths, 0 from resfile + MapContainsPaths( onlyValidPaths, combinedMap, "ResolvedCombinedMap" ); + ValidatePathMap( onlyValidPaths, combinedMap, defaultIncludes, defaultExcludes, "ResolvedCombinedMap" ); +} + +TEST_F( ResourceToolsTest, FilterNamedSection_CheckFailure_MissingRespathAttributeThrowsException ) +{ + // This test validates that when trying to initialize a FilterNamedSection + // with a missing respath attribute, that an exception is thrown. + std::string sectionName = "FilterNamedSection_CheckFailure_MissingRespathAttributeThrowsException"; + std::string defaultParentPrefixMapStr = "prefix1:/path1"; + std::string filter = "[ .in1 ]"; + std::string resfile = "prefix1:/foo/bar"; + + ResourceTools::FilterPrefixMap defaultPrefixMap( defaultParentPrefixMapStr ); + + 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( ResourceToolsTest, FilterNamedSection_Validate_CombinedResolvedMapOnSamePrefixIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both + // respath and resfile attributes using the same prefix, and that the combined + // resolved map contains entries from both attributes. + std::string sectionName = "FilterNamedSection_Validate_CombinedResolvedMapOnSamePrefixIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_CombinedResolvedMapWithEmptyTopLevelFilterIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both + // respath and resfile attributes using the same prefix, and an empty top-level filter. + // The combined resolved map contains entries from both attributes, with wildcard "*" include filter. + std::string sectionName = "FilterNamedSection_Validate_CombinedResolvedMapWithEmptyTopLevelFilterIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_CombinedResolvedMapWithOnlyExcludeTopLevelFilterIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both + // respath and resfile attributes using the same prefix, and only an exclude filter at top-level. + // The combined resolved map contains entries from both attributes, with wildcard "*" include filter. + std::string sectionName = "FilterNamedSection_Validate_CombinedResolvedMapWithOnlyExcludeTopLevelFilterIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_EmptyTopLevelFilterWithRespathOverrideIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both + // respath and resfile attributes using the same prefix, and an empty top-level filter. + // The respath has in-line filters that override the top-level filter while the + // resfile uses the default "*" wildcard top-level filter. + std::string sectionName = "FilterNamedSection_Validate_EmptyTopLevelFilterWithRespathOverrideIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_EmptyTopLevelFilterWithResfileOverrideIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both + // respath and resfile attributes using the same prefix, and an empty top-level filter. + // The resfile has in-line filters that override the top-level filter while the + // respath uses the default "*" wildcard top-level filter. + std::string sectionName = "FilterNamedSection_Validate_EmptyTopLevelFilterWithResfileOverrideIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_OnlyExcludeTopLevelFilterWithRespathOverrideIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both + // respath and resfile attributes using the same prefix, and only an exclude filter at top-level. + // The respath has in-line filters that override the top-level filter while the + // resfile uses the default "*" wildcard top-level filter. + std::string sectionName = "FilterNamedSection_Validate_OnlyExcludeTopLevelFilterWithRespathOverrideIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_OnlyExcludeTopLevelFilterWithResfileOverrideIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both + // respath and resfile attributes using the same prefix, and only an exclude filter at top-level. + // The resfile has in-line filters that override the top-level filter while the + // respath uses the default "*" wildcard top-level filter. + std::string sectionName = "FilterNamedSection_Validate_OnlyExcludeTopLevelFilterWithResfileOverrideIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_CombinedMapWithResfileOverrideIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both + // respath and resfile attributes using the same prefix, and that the combined + // resolved map contains entries from both attributes, with the resfile filters + // overriding the top-level filters. + std::string sectionName = "FilterNamedSection_Validate_CombinedMapWithResfileOverrideIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_NoTopLevelFilterSameCombinedMapWithResfileOverrideIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both + // respath and resfile attributes using the same prefix and only default "*" top-level filter. + // The combined resolved map contains entries from both attributes, with the resfile + // filters overriding the top-level filter. + std::string sectionName = "FilterNamedSection_Validate_NoTopLevelFilterSameCombinedMapWithResfileOverrideIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_NoTopLevelFilterSameCombinedMapWithRespathOverrideIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both + // respath and resfile attributes using the same prefix and only default "*" top-level filter. + // The combined resolved map contains entries from both attributes, with the respath + // filters overriding the top-level filter. + std::string sectionName = "FilterNamedSection_Validate_NoTopLevelFilterSameCombinedMapWithRespathOverrideIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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(); + ASSERT_TRUE( resfileAgainMap != nullptr ); + ASSERT_EQ( resfileAgainMap->size(), 2 ); + MapContainsPaths( allPaths, *resfileAgainMap, "ResolvedResfileMap-Again" ); + ValidatePathMap( allPaths, *resfileAgainMap, defaultIncludes, allExcludes, "ResolvedResfileMap-Again" ); +} + +TEST_F( ResourceToolsTest, FilterNamedSection_Validate_OnlyExcludeFilterSameCombinedMapWithResfileOverrideIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both respath and resfile + // attributes using the same prefix, and only an exclude filter at top-level (and default "*" include). + // The combined resolved map contains entries from both attributes, with the resfile filters + // overriding the top-level filter. + std::string sectionName = "FilterNamedSection_Validate_OnlyExcludeFilterSameCombinedMapWithResfileOverrideIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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( ResourceToolsTest, FilterNamedSection_Validate_OnlyExcludeFilterSameCombinedMapWithRespathsOverrideIsAllowed ) +{ + // This test validates that a FilterNamedSection can be initialized with both respath and resfile + // attributes using the same prefix, and only an exclude filter at top-level (and default "*" include). + // The combined resolved map contains entries from both attributes, with the respaths filters + // overriding the top-level filter. + std::string sectionName = "FilterNamedSection_Validate_OnlyExcludeFilterSameCombinedMapWithRespathsOverrideIsAllowed"; + 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_TRUE( resfileMap != nullptr ); + 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(); + ASSERT_TRUE( resfileAgainMap != nullptr ); + ASSERT_EQ( resfileAgainMap->size(), 2 ); + MapContainsPaths( allPaths, *resfileAgainMap, "ResolvedResfileMap-Again" ); + ValidatePathMap( allPaths, *resfileAgainMap, defaultIncludes, allExcludes, "ResolvedResfileMap-Again" ); +} + +// ------------------------------------------ + +TEST_F( ResourceToolsTest, FilterResourceFile_ValidateSuccessfulFileLoad_example1_ini ) +{ + // This test validates that the example1.ini file can be loaded successfully + // and that the resolved paths match the expected values. + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/example1.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.generic_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( ResourceToolsTest, FilterResourceFile_ConfirmFileLoadFailure_invalidMissingDefaultSection_ini ) +{ + // This test validates that loading an ini file missing the required [DEFAULT] + // section throws an exception. + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidMissingDefaultSection.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.generic_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.generic_string(); + EXPECT_STREQ( e.what(), expectedError.c_str() ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when loading ini file missing [DEFAULT] section"; + } +} + +TEST_F( ResourceToolsTest, FilterResourceFile_ConfirmFileLoadFailure_invalidMissingNamedSection_ini ) +{ + // This test validates that loading an ini file missing any named sections + // throws an exception since at least one named section is required. + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidMissingNamedSection.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.generic_string() ); + FAIL() << "Expected std::invalid_argument when loading ini file missing [NamedSection] section"; + } + catch( const std::invalid_argument& e ) + { + std::string expectedError = "No [namedSection] defined in INI file: " + iniPath.generic_string(); + EXPECT_STREQ( e.what(), expectedError.c_str() ); + } + catch( ... ) + { + FAIL() << "Expected std::invalid_argument when loading ini file missing [NamedSection] section"; + } +} + +TEST_F( ResourceToolsTest, FilterResourceFile_ConfirmFileLoadFailure_iniFileDoesNotExist ) +{ + // This test validates that loading a non-existent ini file throws an exception. + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/iniFileNotFound.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.generic_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.generic_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( ResourceToolsTest, FilterResourceFile_ConfirmFileLoadFailure_invalidPrefixmap_ini ) +{ + // This test validates that loading an ini file with an invalid prefixmap format throws an exception. + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidPrefixmap.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.generic_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( ResourceToolsTest, FilterResourceFile_ConfirmFileLoadFailure_invalidSectionFilter_ini ) +{ + // This test validates that loading an ini file with an invalid section filter format throws an exception. + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidSectionFilter.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.generic_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( ResourceToolsTest, FilterResourceFile_ConfirmFileLoadFailure_invalidInlineFilter_ini ) +{ + // This test validates that loading an ini file with an invalid inline filter format throws an exception. + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidInlineFilter.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.generic_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( ResourceToolsTest, FilterResourceFile_ConfirmFileLoadFailure_invalidPrefixMismatch_ini ) +{ + // This test validates that loading an ini file with a prefix in the respaths/resfile + // attributes that does not match any prefix in the prefixmap throws an exception. + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidPrefixMismatch.ini" ); + try + { + ResourceTools::FilterResourceFile resourceFile( iniPath.generic_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( ResourceToolsTest, ResourceFilter_ConfirmFileLoadFailure_SingleIniFileThatDoesNotExist ) +{ + // This test validates that initializing a ResourceFilter with a non-existent ini file throws an exception. + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/noSuchFile.ini" ); + std::vector paths = { iniPath }; + ResourceTools::ResourceFilter resourceFilter; + try + { + resourceFilter.Initialize( paths, TEST_DATA_BASE_PATH ); + 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( ResourceToolsTest, ResourceFilter_ConfirmFileLoadFailure_MultipleFilesOneThatDoesNotExist ) +{ + // This test validates that initializing a ResourceFilter with multiple ini files + // where one is valid and another does not exist, results in an exception being thrown. + 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, TEST_DATA_BASE_PATH ); + 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( ResourceToolsTest, ResourceFilter_ConfirmSuccessfulFileLoad_example1_ini ) +{ + // This test validates that initializing a ResourceFilter with a valid ini file (example1.ini) + // successfully loads without throwing any exceptions. + const std::filesystem::path iniPath1 = GetTestFileFileAbsolutePath( "ExampleIniFiles/example1.ini" ); + std::vector paths = { iniPath1 }; + ResourceTools::ResourceFilter resourceFilter; + try + { + resourceFilter.Initialize( paths, TEST_DATA_BASE_PATH ); + ASSERT_TRUE( true ); // If we got here, the file loaded successfully without any exceptions + } + 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( ResourceToolsTest, ResourceFilter_ValidateSuccessfulFileLoadUsingRelativePaths_validSimpleExample1_ini ) +{ + // This test validates that initializing a ResourceFilter with a valid ini file (validSimpleExample1.ini), + // using relative paths to .ini file, loads successfully and the expected paths and filters are present. + try + { + const std::filesystem::path iniPath1 = "ExampleIniFiles/validSimpleExample1.ini"; + std::vector paths = { iniPath1 }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( paths, TEST_DATA_BASE_PATH ); + + // Validate correct included paths via the resourceFilter + // Always compare those based on absolute paths: + std::set validResolvedRelativePaths = { + GetTestFileFileAbsolutePath( "resourcesOnBranch/introMovie.txt" ) , + GetTestFileFileAbsolutePath( "resourcesOnBranch/videoCardCategories.yaml" ) + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedRelativePath : validResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.FilePathMatchesIncludeFilterRules( resolvedRelativePath ), true ) << "Should have included absolute 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( const std::exception& e ) + { + FAIL() << "Test failed with: " << e.what(); + } + catch( ... ) + { + FAIL() << "Test failed when it should have passed."; + } +} + +TEST_F( ResourceToolsTest, ResourceFilter_ValidateSuccessfulFileLoadUsingAbsolutePaths_validSimpleExample1_ini ) +{ + // This test validates that initializing a ResourceFilter with a valid ini file (validSimpleExample1.ini), + // using absolute path to .ini file, loads successfully and the expected paths and filters are present. + try + { + const std::filesystem::path iniPath1 = GetTestFileFileAbsolutePath( "ExampleIniFiles/validSimpleExample1.ini" ); + std::vector paths = { iniPath1 }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( paths, TEST_DATA_BASE_PATH ); + + // Validate correct included paths via the resourceFilter (compare absolute paths): + std::set validResolvedAbsolutePaths = { + GetTestFileFileAbsolutePath( "resourcesOnBranch/introMovie.txt" ), + GetTestFileFileAbsolutePath( "resourcesOnBranch/videoCardCategories.yaml" ) + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedAbsPath : validResolvedAbsolutePaths ) + { + ASSERT_EQ( resourceFilter.FilePathMatchesIncludeFilterRules( 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( const std::exception& e ) + { + FAIL() << "Test failed with: " << e.what(); + } + catch( ... ) + { + FAIL() << "Test failed when it should have passed."; + } +} + +TEST_F( ResourceToolsTest, ResourceFilter_ValidateSuccessfulFileLoadUsingRelativePaths_validComplexExample1_ini ) +{ + // This test validates that initializing a ResourceFilter with a valid ini file (validComplexExample1.ini), + // using relative path to .ini file, loads successfully and the expected paths and filters are present. + try + { + const std::filesystem::path iniPath1 = "ExampleIniFiles/validComplexExample1.ini"; + std::vector paths = { iniPath1 }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( paths, TEST_DATA_BASE_PATH ); + + // Validate correct included paths via the resourceFilter (compare absolute paths): + std::set validResolvedRelativePaths = { + //"PatchWithInputChunk/NextBuildResources/introMovie.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resLocalCDN:/../NextBuildResources/introMoviePrefixed.txt + //"PatchWithInputChunk/NextBuildResources/introMovieSomewhatChanged.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/NextBuildResources/testResource2.txt" ) , + GetTestFileFileAbsolutePath( "PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/introMovie.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + //"PatchWithInputChunk/PreviousBuildResources/testResource.txt", // resRoot:/PatchWithInputChunk/PreviousBuildResources/* ![ testResource.txt ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/resFileIndexShort_build_next.txt" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/resFileIndexShort_build_previous.txt" ), + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedRelativePath : validResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.FilePathMatchesIncludeFilterRules( resolvedRelativePath ), true ) << "Should have included absolute 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 failed with: " << e.what(); + } + catch( ... ) + { + FAIL() << "Test failed when it should have passed."; + } +} + +TEST_F( ResourceToolsTest, ResourceFilter_ValidateSuccessfulFileLoadUsingAbsolutePath_validComplexExample1_ini ) +{ + // This test validates that initializing a ResourceFilter with a valid ini file (validComplexExample1.ini), + // using absolute path to .ini file, loads successfully and the expected paths and filters are present. + try + { + const std::filesystem::path iniPath1 = GetTestFileFileAbsolutePath( "ExampleIniFiles/validComplexExample1.ini" ); + std::vector pathsAbs = { iniPath1 }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( pathsAbs, TEST_DATA_BASE_PATH ); + + // Validate correct included paths via the resourceFilter: + std::set validResolvedRelativePaths = { + //"PatchWithInputChunk/NextBuildResources/introMovie.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resLocalCDN:/../NextBuildResources/introMoviePrefixed.txt + //"PatchWithInputChunk/NextBuildResources/introMovieSomewhatChanged.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/NextBuildResources/testResource2.txt" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/introMovie.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + //"PatchWithInputChunk/PreviousBuildResources/testResource.txt", // resRoot:/PatchWithInputChunk/PreviousBuildResources/* ![ testResource.txt ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/resFileIndexShort_build_next.txt" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/resFileIndexShort_build_previous.txt" ), + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedAbsPath : validResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.FilePathMatchesIncludeFilterRules( resolvedAbsPath ), true ) << "Should have included absolute path: " << resolvedAbsPath.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 failed with: " << e.what(); + } + catch( ... ) + { + FAIL() << "Test failed when it should have passed."; + } +} + +TEST_F( ResourceToolsTest, ResourceFilter_ValidateSuccessfulLoadOf2IniFiles_validComplexExample1_and_validSimpleExample1 ) +{ + // This test validates that initializing a ResourceFilter with two valid ini files + // (validComplexExample1.ini and validSimpleExample1.ini), using absolute paths, + // loads successfully and the expected paths and filters from both ini files are + // present in the resulting ResourceFilter. + try + { + std::vector paths = { + GetTestFileFileAbsolutePath( "ExampleIniFiles/validComplexExample1.ini" ), + GetTestFileFileAbsolutePath( "ExampleIniFiles/validSimpleExample1.ini" ) + }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( paths, TEST_DATA_BASE_PATH ); + + // Validate correct included paths via the resourceFilter: + std::set validResolvedRelativePaths = { + // From validComplexExample1: + //"PatchWithInputChunk/NextBuildResources/introMovie.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resLocalCDN:/../NextBuildResources/introMoviePrefixed.txt + //"PatchWithInputChunk/NextBuildResources/introMovieSomewhatChanged.txt", // resRoot:/PatchWithInputChunk/... ![ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/NextBuildResources/testResource2.txt" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/introMovie.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt" ), // resRoot:/PatchWithInputChunk/... ![ Movie ] + resPrevious:/* [ Movie ] + //"PatchWithInputChunk/PreviousBuildResources/testResource.txt", // resRoot:/PatchWithInputChunk/PreviousBuildResources/* ![ testResource.txt ] + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/resFileIndexShort_build_next.txt" ), + GetTestFileFileAbsolutePath( "PatchWithInputChunk/resFileIndexShort_build_previous.txt" ), + // From validSimpleExample1.ini: + GetTestFileFileAbsolutePath( "resourcesOnBranch/introMovie.txt" ), + GetTestFileFileAbsolutePath( "resourcesOnBranch/videoCardCategories.yaml" ) + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedRelativePath : validResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.FilePathMatchesIncludeFilterRules( resolvedRelativePath ), true ) << "Should have included absolute 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 failed with: " << e.what(); + } + catch( ... ) + { + FAIL() << "Test failed when it should have passed."; + } +} + +TEST_F( ResourceToolsTest, ResourceFilter_ValidateSuccess_FileLoadOverrideUsingDifferentRelativeButSameAbsolutePaths_validOverrideDifferentRelativeSameAbsolutePaths_ini ) +{ + // This test validates that initializing a ResourceFilter with a valid ini file (validOverrideDifferentRelativeSameAbsolutePaths.ini), + // using two distinct relative paths (which result in the same absolute path), loads successfully and the expected paths and filters are present. + // See notes in the validOverrideDifferentRelativeSameAbsolutePaths.ini file for further details. + try + { + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/validOverrideDifferentRelativeSameAbsolutePaths.ini" ); + std::vector paths = { iniPath }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( paths, TEST_DATA_BASE_PATH ); + + // Validate correct included and exclude paths via the resourceFilter: + // - Because of the two distinct relative paths (./resourcesOnBranch/* and /resourcesOnBranch/*) + // where one has both as include and the other both as exclude, then priority rules say that + // both of the files should be EXCLUDED (i.e. equal priority means that exclude wins) + std::set validIncludeResolvedRelativePaths = {}; + std::set validExcludeResolvedRelativePaths = { + GetTestFileFileAbsolutePath( "resourcesOnBranch/introMovie.txt" ), + GetTestFileFileAbsolutePath( "resourcesOnBranch/videoCardCategories.yaml" ) + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedRelativeIncludePath : validIncludeResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.FilePathMatchesIncludeFilterRules( resolvedRelativeIncludePath ), true ) << "Should have included absolute path: " << resolvedRelativeIncludePath.generic_string(); + } + for( const auto& resolvedRelativeExcludePath : validExcludeResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.FilePathMatchesIncludeFilterRules( resolvedRelativeExcludePath ), false ) << "Should have excluded absolute path: " << resolvedRelativeExcludePath.generic_string(); + } + + // Additional checks to make sure the FullResolvedPathMap contains correct include/exclude filter data + const auto& fullPathMap = resourceFilter.GetFullResolvedPathMap(); + ASSERT_EQ( fullPathMap.size(), 2 ); + + std::set expectedPaths = { + "./resourcesOnBranch/*", + "resourcesOnBranch/*" + }; + MapContainsPaths( expectedPaths, fullPathMap, "FullResolvedPathMap from validOverrideDifferentRelativeSameAbsolutePaths.ini" ); + + for( const auto& kv : fullPathMap ) + { + if( kv.first == "./resourcesOnBranch/*" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 0 ); + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 2 ); + EXPECT_TRUE( std::find( kv.second.GetExcludeFilter().begin(), kv.second.GetExcludeFilter().end(), ".yaml" ) != kv.second.GetExcludeFilter().end() ); + EXPECT_TRUE( std::find( kv.second.GetExcludeFilter().begin(), kv.second.GetExcludeFilter().end(), ".txt" ) != kv.second.GetExcludeFilter().end() ); + } + else if( kv.first == "resourcesOnBranch/*" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + 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() ); + } + else + { + FAIL() << "Unexpected path found in FullResolvedPathMap: " << kv.first; + } + } + } + catch( ... ) + { + FAIL() << "Test failed when it should have passed."; + } +} + +TEST_F( ResourceToolsTest, ResourceFilter_ValidateSuccess_FileLoadRespathInlineOverrides_validInlineFilterOverrideOnSameRelativePath_ini ) +{ + // This test validates that initializing a ResourceFilter with a valid ini file (validInlineFilterOverrideOnSameRelativePath.ini), using two + // identical "respath" path entries with distinct overriding include/exclude filters, loads successfully and the expected paths and filters are present. + // See notes in the validInlineFilterOverrideOnSameRelativePath.ini file for further details. + try + { + const std::filesystem::path iniPath = GetTestFileFileAbsolutePath( "ExampleIniFiles/validInlineFilterOverrideOnSameRelativePath.ini" ); + std::vector paths = { iniPath }; + ResourceTools::ResourceFilter resourceFilter; + resourceFilter.Initialize( paths, TEST_DATA_BASE_PATH ); + + // Validate correct included and exclude paths via the resourceFilter: + // - Because of the overriding rules for the identical "respath" path entries. + // We should end up with includes of BOTH .txt and .yaml files with no excludes. + std::set validIncludeResolvedRelativePaths = { + GetTestFileFileAbsolutePath( "resourcesOnBranch/introMovie.txt" ), + GetTestFileFileAbsolutePath( "resourcesOnBranch/videoCardCategories.yaml" ) + }; + std::set validExcludeResolvedRelativePaths = { + }; + + ASSERT_EQ( resourceFilter.HasFilters(), true ); + for( const auto& resolvedRelativeIncludePath : validIncludeResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.FilePathMatchesIncludeFilterRules( resolvedRelativeIncludePath ), true ) << "Should have included relative path: " << resolvedRelativeIncludePath.generic_string(); + } + for( const auto& resolvedRelativeExcludePath : validExcludeResolvedRelativePaths ) + { + ASSERT_EQ( resourceFilter.FilePathMatchesIncludeFilterRules( resolvedRelativeExcludePath ), false ) << "Should have excluded relative path: " << resolvedRelativeExcludePath.generic_string(); + } + + // Additional checks to make sure the FullResolvedPathMap contains correct include/exclude filter data + const auto& fullPathMap = resourceFilter.GetFullResolvedPathMap(); + ASSERT_EQ( fullPathMap.size(), 1 ); + + std::set expectedPaths = { + "resourcesOnBranch/*" + }; + MapContainsPaths( expectedPaths, fullPathMap, "FullResolvedPathMap from validInlineFilterOverrideOnSameRelativePath.ini" ); + + for( const auto& kv : fullPathMap ) + { + if( kv.first == "resourcesOnBranch/*" ) + { + EXPECT_EQ( kv.second.GetIncludeFilter().size(), 2 ); + EXPECT_EQ( kv.second.GetExcludeFilter().size(), 0 ); + 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(), ".yaml" ) != kv.second.GetIncludeFilter().end() ); + } + else + { + FAIL() << "Unexpected path found in FullResolvedPathMap: " << kv.first; + } + } + } + catch( ... ) + { + FAIL() << "Test failed when it should have passed."; + } +} diff --git a/tests/src/ResourceToolsLibraryTest.cpp b/tests/src/ResourceToolsLibraryTest.cpp index 85ff3ad..8c1e160 100644 --- a/tests/src/ResourceToolsLibraryTest.cpp +++ b/tests/src/ResourceToolsLibraryTest.cpp @@ -1,5 +1,7 @@ // Copyright © 2025 CCP ehf. +#include "ResourceToolsTest.h" + #include #include #include @@ -21,10 +23,6 @@ #include "Patching.h" #include "RollingChecksum.h" -struct ResourceToolsTest : public ResourcesTestFixture -{ -}; - TEST_F( ResourceToolsTest, Md5ChecksumGeneration ) { std::string input = "Dummy"; diff --git a/tests/src/ResourceToolsTest.h b/tests/src/ResourceToolsTest.h new file mode 100644 index 0000000..a2a9e07 --- /dev/null +++ b/tests/src/ResourceToolsTest.h @@ -0,0 +1,13 @@ +// Copyright © 2025 CCP ehf. + +#pragma once +#ifndef ResourceToolsTest_H +#define ResourceToolsTest_H + +#include "ResourcesTestFixture.h" + +class ResourceToolsTest : public ResourcesTestFixture +{ +}; + +#endif // ResourceToolsTest_H \ No newline at end of file diff --git a/tests/src/ResourcesCliTest.cpp b/tests/src/ResourcesCliTest.cpp index 0f3c847..cb100fc 100644 --- a/tests/src/ResourcesCliTest.cpp +++ b/tests/src/ResourcesCliTest.cpp @@ -8,11 +8,9 @@ struct ResourcesCliTest : public CliTestFixture TEST_F( ResourcesCliTest, RunWithoutArguments ) { - std::string output; - std::vector arguments; - int res = RunCli( arguments, output ); + int res = RunCli( arguments ); // Expect 4 which indicates failed with no command specified ASSERT_EQ( res, 4 ); @@ -20,13 +18,11 @@ TEST_F( ResourcesCliTest, RunWithoutArguments ) TEST_F( ResourcesCliTest, RunWithNonesenseArguments ) { - std::string output; - std::vector arguments; arguments.push_back( "Nonesense" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments ); // Expect 3 which indicates failed due to invalid operation ASSERT_EQ( res, 3 ); @@ -34,13 +30,11 @@ TEST_F( ResourcesCliTest, RunWithNonesenseArguments ) TEST_F( ResourcesCliTest, RunCreateGroupWithNoArguments ) { - std::string output; - std::vector arguments; arguments.push_back( "create-group" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments ); // Expect 2 which failed due to invalid operation arguments ASSERT_EQ( res, 2 ); @@ -48,13 +42,11 @@ TEST_F( ResourcesCliTest, RunCreateGroupWithNoArguments ) TEST_F( ResourcesCliTest, RunCreatePatchWithNoArguments ) { - std::string output; - std::vector arguments; arguments.push_back( "create-patch" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments ); // Expect 2 which failed due to invalid operation arguments ASSERT_EQ( res, 2 ); @@ -62,13 +54,11 @@ TEST_F( ResourcesCliTest, RunCreatePatchWithNoArguments ) TEST_F( ResourcesCliTest, RunCreateBundleWithNoArguments ) { - std::string output; - std::vector arguments; arguments.push_back( "create-bundle" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments ); // Expect 2 which failed due to invalid operation arguments ASSERT_EQ( res, 2 ); @@ -78,13 +68,11 @@ TEST_F( ResourcesCliTest, RunCreateBundleWithNoArguments ) TEST_F( ResourcesCliTest, RunApplyPatchWithNoArguments ) { - std::string output; - std::vector arguments; arguments.push_back( "apply-patch" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments ); // Expect 2 which failed due to invalid operation arguments ASSERT_EQ( res, 2 ); @@ -92,13 +80,11 @@ TEST_F( ResourcesCliTest, RunApplyPatchWithNoArguments ) TEST_F( ResourcesCliTest, RunUnpackBundleWithNoArguments ) { - std::string output; - std::vector arguments; arguments.push_back( "unpack-bundle" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments ); // Expect 2 which failed due to invalid operation arguments ASSERT_EQ( res, 2 ); @@ -106,9 +92,6 @@ TEST_F( ResourcesCliTest, RunUnpackBundleWithNoArguments ) TEST_F( ResourcesCliTest, CreateOperationWithInvalidInput ) { - - std::string output; - std::vector arguments; arguments.push_back( "create-group" ); @@ -119,7 +102,7 @@ TEST_F( ResourcesCliTest, CreateOperationWithInvalidInput ) std::filesystem::path inputDirectory = "INVALID_PATH"; arguments.push_back( inputDirectory.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments ); // Expect return 1 indicating failed during valid operation ASSERT_EQ( res, 1 ); @@ -128,8 +111,7 @@ TEST_F( ResourcesCliTest, CreateOperationWithInvalidInput ) #endif TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectory ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "create-group" ); @@ -144,9 +126,9 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectory ) std::filesystem::path outputFile = "GroupOut/ResourceGroup.yaml"; arguments.push_back( outputFile.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - ASSERT_EQ( res, 0 ); + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; #if _WIN64 std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "CreateResourceFiles/ResourceGroupWindows.yaml" ); @@ -160,8 +142,7 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectory ) TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryExportResources ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "create-group" ); @@ -169,12 +150,12 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryExportResources ) arguments.push_back( "--verbosity-level" ); arguments.push_back( "-1" ); - arguments.push_back( "--export-resources" ); + arguments.push_back( "--export-resources" ); - arguments.push_back( "--export-resources-destination-type" ); + arguments.push_back( "--export-resources-destination-type" ); arguments.push_back( "LOCAL_RELATIVE" ); - arguments.push_back( "--export-resources-destination-path" ); + arguments.push_back( "--export-resources-destination-path" ); std::string exportOutputPath = "ExportedResources"; arguments.push_back( exportOutputPath ); @@ -185,9 +166,9 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryExportResources ) std::filesystem::path outputFile = "GroupOut/ResourceGroup.yaml"; arguments.push_back( outputFile.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - ASSERT_EQ( res, 0 ); + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; #if _WIN64 std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "CreateResourceFiles/ResourceGroupWindows.yaml" ); @@ -198,13 +179,12 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryExportResources ) #endif EXPECT_TRUE( FilesMatch( goldFile, outputFile ) ); - EXPECT_TRUE( DirectoryIsSubset( exportOutputPath, inputDirectory ) ); + EXPECT_TRUE( DirectoryIsSubset( exportOutputPath, inputDirectory ) ); } TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryWithSkipCompression ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "create-group" ); @@ -212,7 +192,7 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryWithSkipCompression ) arguments.push_back( "--verbosity-level" ); arguments.push_back( "-1" ); - arguments.push_back( "--skip-compression" ); + arguments.push_back( "--skip-compression" ); std::filesystem::path inputDirectory = GetTestFileFileAbsolutePath( "CreateResourceFiles/ResourceFiles" ); arguments.push_back( inputDirectory.string() ); @@ -221,9 +201,9 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryWithSkipCompression ) std::filesystem::path outputFile = "GroupOut/ResourceGroup.yaml"; arguments.push_back( outputFile.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - ASSERT_EQ( res, 0 ); + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; #if _WIN64 std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "CreateResourceFiles/ResourceGroupSkipCompressionWindows.yaml" ); @@ -237,8 +217,7 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryWithSkipCompression ) TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryOldDocumentFormat ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "create-group" ); @@ -256,9 +235,9 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryOldDocumentFormat ) arguments.push_back( "--document-version" ); arguments.push_back( "0.0.0" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - ASSERT_EQ( res, 0 ); + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; #if _WIN64 std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "CreateResourceFiles/ResourceGroupWindows.csv" ); @@ -272,8 +251,7 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryOldDocumentFormat ) TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryOldDocumentFormatWithPrefix ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "create-group" ); @@ -294,9 +272,9 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryOldDocumentFormatWithP arguments.push_back( "--resource-prefix" ); arguments.push_back( "test" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - ASSERT_EQ( res, 0 ); + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; #if _WIN64 std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "CreateResourceFiles/ResourceGroupWindowsPrefixed.csv" ); @@ -308,10 +286,609 @@ TEST_F( ResourcesCliTest, CreateResourceGroupFromDirectoryOldDocumentFormatWithP EXPECT_TRUE( FilesMatch( goldFile, outputFile ) ); } -TEST_F( ResourcesCliTest, CreateBundle ) +//--------------------------------------- + +TEST_F( ResourcesCliTest, CreateGroup_UsingFilter_validSimpleExample1_YamlOutput ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_UsingFilter_validSimpleExample1.yaml" ); + std::filesystem::path filterIniFilePath = GetTestFileFileAbsolutePath( "ExampleIniFiles/validSimpleExample1.ini" ); + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( TEST_DATA_BASE_PATH ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.1.0" ); // This is default YAML document version (setting it explicitly for clarity in test) + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.yaml" ); +#elif __APPLE__ + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.yaml" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ) << " Output file does not match expected gold file."; +} + +TEST_F( ResourcesCliTest, CreateGroup_UsingFilter_validSimpleExample1_CsvTxtOutput ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_UsingFilter_validSimpleExample1.txt" ); + std::filesystem::path filterIniFilePath = GetTestFileFileAbsolutePath( "ExampleIniFiles/validSimpleExample1.ini" ); + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( TEST_DATA_BASE_PATH ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.0.0" ); // This is the "old style" csv (txt) document version + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.txt" ); +#elif __APPLE__ + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.txt" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ) << " Output file does not match expected gold file."; +} + +TEST_F( ResourcesCliTest, CreateGroup_ConfirmWorks_UsingFilter_validSimpleExample1_WithRelativeFilterFilePathAndBasePathSet ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_ConfirmWorks_UsingFilter_validSimpleExample1_WithRelativeFilterFilePathAndBasePathSet.txt" ); + std::filesystem::path filterIniFilePath = "ExampleIniFiles/validSimpleExample1.ini"; + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( TEST_DATA_BASE_PATH ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.0.0" ); // This is the "old style" csv (txt) document version + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.txt" ); +#elif __APPLE__ + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.txt" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ) << " Output file does not match expected gold file."; +} + +TEST_F( ResourcesCliTest, CreateGroup_ConfirmWorks_UsingFilter_validSimpleExample1_WithAbsoluteFilterFilePathAndBasePathSet ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_ConfirmWorks_UsingFilter_validSimpleExample1_WithAbsoluteFilterFilePathAndBasePathSet.txt" ); + std::filesystem::path filterIniFilePath = GetTestFileFileAbsolutePath( "ExampleIniFiles/validSimpleExample1.ini" ); + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( TEST_DATA_BASE_PATH ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.0.0" ); // This is the "old style" csv (txt) document version + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.txt" ); +#elif __APPLE__ + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.txt" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ) << " Output file does not match expected gold file."; +} + +TEST_F( ResourcesCliTest, CreateGroup_ConfirmFails_UsingFilter_validSimpleExample1_WithRelativeFilterFilePathAndWrongBasePathSet ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_ConfirmFails_UsingFilter_validSimpleExample1_WithRelativeFilterFilePathAndWrongBasePathSet.txt" ); + std::filesystem::path filterIniFilePath = "ExampleIniFiles/validSimpleExample1.ini"; + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( "Some/Incorrect/Base/Path" ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.0.0" ); // This is the "old style" csv (txt) document version + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "errorOutput: " << errorOutput << std::endl; + std::cout << "------------------------" << std::endl; + + // Should fail, expecting non-zero exit code + ASSERT_EQ( res, 1 ) << "CLI operation should fail for a reading relative filter .ini files with wrong --filter-file-prefixmap-basepath - with resultCode=1"; + // Check for expected error message + EXPECT_TRUE( errorOutput.find( "[ERROR: Failed to initialize ResourceFilter from .ini file]" ) != std::string::npos ) + << "Expected generic (top-level) error message about failure to initialize ResourceFilter from .ini file. Actual error: " << errorOutput; +} + +TEST_F( ResourcesCliTest, CreateGroup_ConfirmFails_UsingFilter_validSimpleExample1_WithAbsoluteFilterFilePathAndWrongBasePathSet ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_ConfirmFails_UsingFilter_validSimpleExample1_WithAbsoluteFilterFilePathAndWrongBasePathSet.txt" ); + std::filesystem::path filterIniFilePath = GetTestFileFileAbsolutePath( "ExampleIniFiles/validSimpleExample1.ini" ); + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( "Some/Incorrect/Base/Path" ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.0.0" ); // This is the "old style" csv (txt) document version + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "errorOutput: " << errorOutput << std::endl; + std::cout << "------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; + + // Because of the absolute path to the .ini file, the operation will be successful even though the + // basepath parameter is wrong (the .ini file can still be found and read successfully). + // However, the rules applied from the filter .ini file will resolve to absolute paths that do not exist. + // Therefore, no files will match the filter criteria and the output file will be empty. + EXPECT_TRUE( std::filesystem::exists( outputFilePath ) ) << " Empty output file [" << outputFilePath.generic_string() << "] was not created, when it should have been."; + EXPECT_TRUE( std::filesystem::is_empty( outputFilePath ) ) << " Output file should be empty due to filter .ini file rules not matching any files because of wrong basepath."; +} + +TEST_F( ResourcesCliTest, CreateGroup_ConfirmFails_UsingFilter_validSimpleExample1_WithRelativeFilterFilePathAndNoBasePath ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_ConfirmFails_UsingFilter_validSimpleExample1_WithRelativeFilterFilePathAndNoBasePath.txt" ); + std::filesystem::path filterIniFilePath = "ExampleIniFiles/validSimpleExample1.ini"; + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.0.0" ); // This is the "old style" csv (txt) document version + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "errorOutput: " << errorOutput << std::endl; + std::cout << "------------------------" << std::endl; + + // Should fail, expecting non-zero exit code + ASSERT_EQ( res, 1 ) << "CLI operation should fail for a reading relative filter .ini files with missing --filter-file-prefixmap-basepath - with resultCode=1"; + // Check for expected error message + EXPECT_TRUE( errorOutput.find( "[ERROR: Failed to initialize ResourceFilter from .ini file]" ) != std::string::npos ) + << "Expected generic (top-level) error message about failure to initialize ResourceFilter from .ini file. Actual error: " << errorOutput; +} + +TEST_F( ResourcesCliTest, CreateGroup_ConfirmFails_UsingFilter_validSimpleExample1_WithAbsoluteFilterFilePathAndNoBasePath ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_ConfirmFails_UsingFilter_validSimpleExample1_WithAbsoluteFilterFilePathAndNoBasePath.txt" ); + std::filesystem::path filterIniFilePath = GetTestFileFileAbsolutePath( "ExampleIniFiles/validSimpleExample1.ini" ); + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.0.0" ); // This is the "old style" csv (txt) document version + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "errorOutput: " << errorOutput << std::endl; + std::cout << "------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; + + // Because of the absolute path to the .ini file, the operation will be successful even though the + // basepath parameter is wrong (the .ini file can still be found and read successfully). + // However, the rules applied from the filter .ini file will resolve to absolute paths that do not exist. + // Therefore, no files will match the filter criteria and the output file will be empty. + EXPECT_TRUE( std::filesystem::exists( outputFilePath ) ) << " Empty output file [" << outputFilePath.generic_string() << "] was not created, when it should have been."; + EXPECT_TRUE( std::filesystem::is_empty( outputFilePath ) ) << " Output file should be empty due to filter .ini file rules not matching any files because of wrong basepath."; +} + +TEST_F( ResourcesCliTest, CreateGroup_UsingFilter_validComplexExample1_YamlOutput ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_UsingFilter_validComplexExample1.yaml" ); + std::filesystem::path filterIniFilePath = GetTestFileFileAbsolutePath( "ExampleIniFiles/validComplexExample1.ini" ); + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( TEST_DATA_BASE_PATH ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.1.0" ); // This is default YAML document version (setting it explicitly for clarity in test) + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_Windows.yaml" ); +#elif __APPLE__ + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_macOS.yaml" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ) << " Output file does not match expected gold file."; +} + +TEST_F( ResourcesCliTest, CreateGroup_UsingFilter_validComplexExample1_CsvTxtOutput ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_UsingFilter_validComplexExample1.txt" ); + std::filesystem::path filterIniFilePath = GetTestFileFileAbsolutePath( "ExampleIniFiles/validComplexExample1.ini" ); + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( TEST_DATA_BASE_PATH ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.0.0" ); // This is the "old style" csv (txt) document version + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_Windows.txt" ); +#elif __APPLE__ + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_macOS.txt" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ) << " Output file does not match expected gold file."; +} + +TEST_F( ResourcesCliTest, CreateGroup_UsingFilter_validSimpleAndComplexExample1_YamlOutput ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_UsingFilter_validSimpleAndComplexExample1.yaml" ); + std::vector filterIniFilePaths = { + GetTestFileFileAbsolutePath( "ExampleIniFiles/validSimpleExample1.ini" ), + GetTestFileFileAbsolutePath( "ExampleIniFiles/validComplexExample1.ini" ) + }; + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + for( auto filterFilePath : filterIniFilePaths ) + { + arguments.push_back( "--filter-file" ); + arguments.push_back( filterFilePath.lexically_normal().string() ); + } + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( TEST_DATA_BASE_PATH ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.1.0" ); // This is default YAML document version (setting it explicitly for clarity in test) + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_Windows.yaml" ); +#elif __APPLE__ + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_macOS.yaml" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ) << " Output file does not match expected gold file."; +} + +TEST_F( ResourcesCliTest, CreateGroup_UsingFilter_validSimpleAndComplexExample1_CsvTxtOutput ) +{ + // Setup test parameters + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_UsingFilter_validSimpleAndComplexExample1.txt" ); + std::vector filterIniFilePaths = { + GetTestFileFileAbsolutePath( "ExampleIniFiles/validSimpleExample1.ini" ), + GetTestFileFileAbsolutePath( "ExampleIniFiles/validComplexExample1.ini" ) + }; + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + for( auto filterFilePath : filterIniFilePaths ) + { + arguments.push_back( "--filter-file" ); + arguments.push_back( filterFilePath.lexically_normal().string() ); + } + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( TEST_DATA_BASE_PATH ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + arguments.push_back( "--document-version" ); + arguments.push_back( "0.0.0" ); // This is the "old style" csv (txt) document version + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "------------------------" << std::endl; + + ASSERT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; + + // Check expected outcome +#if _WIN64 + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_Windows.txt" ); +#elif __APPLE__ + std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_macOS.txt" ); +#else +#error Unsupported platform +#endif + EXPECT_TRUE( FilesMatch( goldFile, outputFilePath ) ) << " Output file does not match expected gold file."; +} + +TEST_F( ResourcesCliTest, CreateGroup_ConfirmFailureParsingWronglyFormattedIniFile_UsingFilter_invalidMissingNamedSection_ini ) +{ + // Test parameters: + std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_UsingFilter_invalidMissingNamedSection.yaml" ); + std::filesystem::path filterIniFilePath = GetTestFileFileAbsolutePath( "ExampleIniFiles/invalidMissingNamedSection.ini" ); + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( TEST_DATA_BASE_PATH ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "errorOutput: " << errorOutput << std::endl; + std::cout << "------------------------" << std::endl; + + // Should fail, expecting non-zero exit code + ASSERT_EQ( res, 1 ) << "CLI operation should fail for a filter .ini file with missing named section - with resultCode=1"; + // Check for expected error message + EXPECT_TRUE( errorOutput.find( "[ERROR: Failed to initialize ResourceFilter from .ini file]" ) != std::string::npos ) + << "Expected generic (top-level) error message about failure to initialize ResourceFilter from .ini file. Actual error: " << errorOutput; +} + +TEST_F( ResourcesCliTest, CreateGroup_ConfirmFailureUsingNoExistentFilterFile_iniFileDoesNotExist ) { + // Test parameters: std::string output; + std::string errorOutput; + std::vector arguments; + std::filesystem::path inputDirectoryPath = TEST_DATA_BASE_PATH; + std::filesystem::path outputFilePath = std::filesystem::absolute( "CliFilterCreateGroupOut/CreateGroup_UsingFilter_iniFileDoesNotExist.yaml" ); + std::filesystem::path filterIniFilePath = GetTestFileFileAbsolutePath( "ExampleIniFiles/iniFileNotFound.ini" ); + + // Ensure any previous test output files are removed + RemoveFiles( { outputFilePath } ); + + arguments.push_back( "create-group" ); + arguments.push_back( inputDirectoryPath.lexically_normal().string() ); + arguments.push_back( "--verbosity-level" ); + arguments.push_back( "3" ); + arguments.push_back( "--filter-file" ); + arguments.push_back( filterIniFilePath.lexically_normal().string() ); + arguments.push_back( "--filter-file-prefixmap-basepath" ); + arguments.push_back( TEST_DATA_BASE_PATH ); + arguments.push_back( "--output-file" ); + arguments.push_back( outputFilePath.lexically_normal().string() ); + + int res = RunCli( arguments, &output, &errorOutput ); + std::cout << "--- RunCli() output: ---" << std::endl; + std::cout << output << std::endl; + std::cout << "errorOutput: " << errorOutput << std::endl; + std::cout << "------------------------" << std::endl; + + // Should fail, expecting non-zero exit code + ASSERT_EQ( res, 1 ) << "CLI operation should fail for a non-existent filter .ini file - with resultCode=1"; + // Check for expected error message + EXPECT_TRUE( errorOutput.find( "[ERROR: Failed to initialize ResourceFilter from .ini file]" ) != std::string::npos ) + << "Expected generic (top-level) error message about failure to initialize ResourceFilter from .ini file. Actual error: " << errorOutput; +} +//--------------------------------------- + +TEST_F( ResourcesCliTest, CreateBundle ) +{ + std::string errorOutput; std::vector arguments; arguments.push_back( "create-bundle" ); @@ -343,9 +920,9 @@ TEST_F( ResourcesCliTest, CreateBundle ) arguments.push_back( "1000" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; // Check expected outcome std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "CreateBundle/BundleResourceGroup.yaml" ); @@ -357,8 +934,7 @@ TEST_F( ResourcesCliTest, CreateBundle ) TEST_F( ResourcesCliTest, RemoveResourcesWithUnknownResourceIgnoreOnResourceNotFound ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "remove-resources" ); @@ -382,15 +958,13 @@ TEST_F( ResourcesCliTest, RemoveResourcesWithUnknownResourceIgnoreOnResourceNotF arguments.push_back( "--ignore-missing-resources" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; } TEST_F( ResourcesCliTest, RemoveResourcesWithUnknownResourceWithInvalidPathToResourcesFile ) { - std::string output; - std::vector arguments; arguments.push_back( "remove-resources" ); @@ -412,15 +986,13 @@ TEST_F( ResourcesCliTest, RemoveResourcesWithUnknownResourceWithInvalidPathToRes arguments.push_back( resourceGroupAfterRemovePath.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments ); EXPECT_EQ( res, 1 ); } TEST_F( ResourcesCliTest, RemoveResourcesWithUnknownResource ) { - std::string output; - std::vector arguments; arguments.push_back( "remove-resources" ); @@ -442,15 +1014,14 @@ TEST_F( ResourcesCliTest, RemoveResourcesWithUnknownResource ) arguments.push_back( resourceGroupAfterRemovePath.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments ); EXPECT_EQ( res, 1 ); } TEST_F( ResourcesCliTest, RemoveResources ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "remove-resources" ); @@ -472,9 +1043,9 @@ TEST_F( ResourcesCliTest, RemoveResources ) arguments.push_back( resourceGroupAfterRemovePath.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; // Check output matches expected std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "RemoveResource/ResourceGroupAfterRemove.yaml" ); @@ -484,8 +1055,7 @@ TEST_F( ResourcesCliTest, RemoveResources ) TEST_F( ResourcesCliTest, DiffResourceGroupsWithTwoAdditions ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "diff-group" ); @@ -507,9 +1077,9 @@ TEST_F( ResourcesCliTest, DiffResourceGroupsWithTwoAdditions ) arguments.push_back( outputPath.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; // Check output matches expected std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "DiffGroups/ExpectedDiffWithAdditions.txt" ); @@ -523,8 +1093,7 @@ TEST_F( ResourcesCliTest, DiffResourceGroupsWithTwoAdditions ) TEST_F( ResourcesCliTest, DiffResourceGroupsWithTwoChanges ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "diff-group" ); @@ -546,9 +1115,9 @@ TEST_F( ResourcesCliTest, DiffResourceGroupsWithTwoChanges ) arguments.push_back( outputPath.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; // Check output matches expected std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "DiffGroups/ExpectedDiffWithChanges.txt" ); @@ -562,8 +1131,7 @@ TEST_F( ResourcesCliTest, DiffResourceGroupsWithTwoChanges ) TEST_F( ResourcesCliTest, DiffResourceGroupsWithTwoSubtractions ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "diff-group" ); @@ -585,9 +1153,9 @@ TEST_F( ResourcesCliTest, DiffResourceGroupsWithTwoSubtractions ) arguments.push_back( outputPath.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; // Check output matches expected std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "DiffGroups/ExpectedDiffWithSubtractions.txt" ); @@ -601,7 +1169,7 @@ TEST_F( ResourcesCliTest, DiffResourceGroupsWithTwoSubtractions ) TEST_F( ResourcesCliTest, MergeGroup ) { - std::string output; + std::string errorOutput; std::vector arguments; @@ -624,9 +1192,9 @@ TEST_F( ResourcesCliTest, MergeGroup ) arguments.push_back( mergedOutputPath.string() ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; // Check output matches expected std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "MergeGroups/YamlAdditive/ExpectedMergedResourceGroup.yaml" ); @@ -636,8 +1204,7 @@ TEST_F( ResourcesCliTest, MergeGroup ) TEST_F( ResourcesCliTest, CreatePatch ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "create-patch" ); @@ -678,9 +1245,9 @@ TEST_F( ResourcesCliTest, CreatePatch ) arguments.push_back( "--chunk-size" ); arguments.push_back( "50000000" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; // Check expected outcome std::filesystem::path goldFile = GetTestFileFileAbsolutePath( "Patch/PatchResourceGroup.yaml" ); @@ -692,8 +1259,7 @@ TEST_F( ResourcesCliTest, CreatePatch ) TEST_F( ResourcesCliTest, CreateGroup ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "create-group" ); @@ -711,9 +1277,9 @@ TEST_F( ResourcesCliTest, CreateGroup ) arguments.push_back( outputFilename ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; // Check expected outcome #if _WIN64 @@ -730,8 +1296,7 @@ TEST_F( ResourcesCliTest, CreateGroup ) TEST_F( ResourcesCliTest, ApplyPatch ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "apply-patch" ); @@ -774,9 +1339,9 @@ TEST_F( ResourcesCliTest, ApplyPatch ) std::filesystem::copy( resourcesToPatchBasePath, outputBasePath ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; // Check expected outcome std::filesystem::path goldDirectory = GetTestFileFileAbsolutePath( "Patch/NextBuildResources" ); @@ -785,8 +1350,7 @@ TEST_F( ResourcesCliTest, ApplyPatch ) TEST_F( ResourcesCliTest, UnpackBundle ) { - std::string output; - + std::string errorOutput; std::vector arguments; arguments.push_back( "unpack-bundle" ); @@ -808,9 +1372,9 @@ TEST_F( ResourcesCliTest, UnpackBundle ) arguments.push_back( "LOCAL_RELATIVE" ); - int res = RunCli( arguments, output ); + int res = RunCli( arguments, nullptr, &errorOutput ); - EXPECT_EQ( res, 0 ); + EXPECT_EQ( res, 0 ) << "CLI operation failed, errorOutput: " << errorOutput; // Check expected outcome EXPECT_TRUE( DirectoryIsSubset( GetTestFileFileAbsolutePath( "Bundle/Res" ), "UnpackBundleOut" ) ); 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/validInlineFilterOverrideOnSameRelativePath.ini b/tests/testData/ExampleIniFiles/validInlineFilterOverrideOnSameRelativePath.ini new file mode 100644 index 0000000..1de0a7d --- /dev/null +++ b/tests/testData/ExampleIniFiles/validInlineFilterOverrideOnSameRelativePath.ini @@ -0,0 +1,24 @@ +[DEFAULT] +prefixmap = resOnBranch:resourcesOnBranch + +#============================================================================= +# validInlineFilterOverrideOnSameRelativePath.ini - test file +# This test should: +# A. Have one prefixmap entry: +# * resOnBranch +# B. The respaths attribute should resolve as follows: +# - resOnBranch:/* ==> resourcesOnBranch/* with filter [ .yaml ] ![ .txt ] (parent) + ![ .yaml ] (inline #1) ==> 0 include & 2 exclude +# - resOnBranch:/* ==> resourcesOnBranch/* with filter [ .yaml ] ![ .txt ] + ![ .yaml ] (already combined filter, see line above) +# + [ .yaml ] ![ .txt ] (parent again) + [ .txt ] (inline #2) ==> 2 include & 0 exclude +# Because the first instance of the relative path has zero includes and excludes both extensions, +# and the second instance includes the .txt ON TOP of the first instance ALONG WITH the parent filter again. +# Then we should end up with a filter that will: +# - include both .txt and .yaml files +# - exclude nothing +#============================================================================= + +[testInlineFilterOverrideOnSameRelativePath] +filter = [ .yaml ] ![ .txt ] +respaths = resOnBranch:/* ![ .yaml ] + resOnBranch:/* [ .txt ] + diff --git a/tests/testData/ExampleIniFiles/validOverrideDifferentRelativeSameAbsolutePaths.ini b/tests/testData/ExampleIniFiles/validOverrideDifferentRelativeSameAbsolutePaths.ini new file mode 100644 index 0000000..ec6108a --- /dev/null +++ b/tests/testData/ExampleIniFiles/validOverrideDifferentRelativeSameAbsolutePaths.ini @@ -0,0 +1,28 @@ +[DEFAULT] +prefixmap = resRoot:. resOnBranch:resourcesOnBranch + +#============================================================================= +# validOverrideDifferentRelativeSameAbsolutePaths.ini - test file +# This test should: +# A. Have two entries in the prefixmap: +# * resRoot +# * resOnBranch +# B. The respaths attribute should resolve to the same actual location +# for both resRoot and resOnBranch (using the same type of wildcard, "*" in this case). + +# Resolution of respaths: +# - resRoot:/resourcesOnBranch/* ==> ./resourcesOnBranch/* with filter [ .yaml ] ![ .txt ] + ![ .yaml ] ==> 0 include & 2 exclude +# - resOnBranch:/* ==> resourcesOnBranch/* with filter [ .yaml ] ![ .txt ] + [ .txt ] ==> 2 include & 0 exclude +# Those are two different relative paths, that will resolve to the same actual path. +# Because one relative path includes both extensions and the other excludes both, +# all files will be EXCLUDED. +# This is because files at the same hierarchy priority (in this case, priority=0 +# because of wildcard search in the same directory), priority rules dictate that +# the files should be excluded. +#============================================================================= + +[testOverrideOfTwoRespathsUsingDifferentPrefixesThatResolveToDifferentRelativeButSameActualPaths] +filter = [ .yaml ] ![ .txt ] +respaths = resRoot:/resourcesOnBranch/* ![ .yaml ] + resOnBranch:/* [ .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.txt b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_Windows.txt new file mode 100644 index 0000000..afdda51 --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_Windows.txt @@ -0,0 +1,10 @@ +PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt,dc/dca8056adced9237_0fb2bed9d164ad014bcb060b95df7ba1,0fb2bed9d164ad014bcb060b95df7ba1,9425,3409,33206 +PatchWithInputChunk/NextBuildResources/testResource2.txt,82/8264e4640cf94b94_271c8036e4eae5515053e924a0a39e0f,271c8036e4eae5515053e924a0a39e0f,29,47,33206 +PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml,02/02b2b2627ca28b62_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33206 +PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml,44/446d5f683db0d467_b7431ad6d859bfd67a4c3fae337b0b6e,b7431ad6d859bfd67a4c3fae337b0b6e,3902,765,33206 +PatchWithInputChunk/PreviousBuildResources/introMovie.txt,fd/fd2a47b3d5faa494_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33206 +PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt,5d/5d2aadfa1344ac63_5e631fd37d3350e30095d1251b178f2c,5e631fd37d3350e30095d1251b178f2c,9117,3259,33206 +PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt,60/6054e5e0cf24763e_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33206 +PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml,fb/fbc1cb8895f6c396_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33206 +PatchWithInputChunk/resFileIndexShort_build_next.txt,1f/1f23be4b8bf6d2ae_4d398902b75611f7ae19903e6b461514,4d398902b75611f7ae19903e6b461514,612,324,33206 +PatchWithInputChunk/resFileIndexShort_build_previous.txt,32/326b0449af10b716_849821e0c98e37de4edc5f7156cf5887,849821e0c98e37de4edc5f7156cf5887,611,299,33206 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_validComplexExample1_macOS.txt b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_macOS.txt new file mode 100644 index 0000000..607c6f7 --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_macOS.txt @@ -0,0 +1,10 @@ +PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt,dc/dca8056adced9237_0fb2bed9d164ad014bcb060b95df7ba1,0fb2bed9d164ad014bcb060b95df7ba1,9425,3409,33188 +PatchWithInputChunk/NextBuildResources/testResource2.txt,82/8264e4640cf94b94_271c8036e4eae5515053e924a0a39e0f,271c8036e4eae5515053e924a0a39e0f,29,47,33188 +PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml,02/02b2b2627ca28b62_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33188 +PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml,44/446d5f683db0d467_b7431ad6d859bfd67a4c3fae337b0b6e,b7431ad6d859bfd67a4c3fae337b0b6e,3902,765,33188 +PatchWithInputChunk/PreviousBuildResources/introMovie.txt,fd/fd2a47b3d5faa494_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33188 +PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt,5d/5d2aadfa1344ac63_5e631fd37d3350e30095d1251b178f2c,5e631fd37d3350e30095d1251b178f2c,9117,3259,33188 +PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt,60/6054e5e0cf24763e_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33188 +PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml,fb/fbc1cb8895f6c396_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33188 +PatchWithInputChunk/resFileIndexShort_build_next.txt,1f/1f23be4b8bf6d2ae_4d398902b75611f7ae19903e6b461514,4d398902b75611f7ae19903e6b461514,612,324,33188 +PatchWithInputChunk/resFileIndexShort_build_previous.txt,32/326b0449af10b716_849821e0c98e37de4edc5f7156cf5887,849821e0c98e37de4edc5f7156cf5887,611,299,33188 diff --git a/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_macOS.yaml b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_macOS.yaml new file mode 100644 index 0000000..2a7e0a8 --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validComplexExample1_macOS.yaml @@ -0,0 +1,76 @@ +Version: 0.1.0 +Type: ResourceGroup +NumberOfResources: 10 +TotalResourcesSizeCompressed: 24573 +TotalResourcesSizeUnCompressed: 107508 +Resources: + - RelativePath: PatchWithInputChunk/resFileIndexShort_build_next.txt + Type: Resource + Location: 1f/1f23be4b8bf6d2ae_4d398902b75611f7ae19903e6b461514 + Checksum: 4d398902b75611f7ae19903e6b461514 + UncompressedSize: 612 + CompressedSize: 324 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/NextBuildResources/testResource2.txt + Type: Resource + Location: 82/8264e4640cf94b94_271c8036e4eae5515053e924a0a39e0f + Checksum: 271c8036e4eae5515053e924a0a39e0f + UncompressedSize: 29 + CompressedSize: 47 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml + Type: Resource + Location: 02/02b2b2627ca28b62_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt + Type: Resource + Location: dc/dca8056adced9237_0fb2bed9d164ad014bcb060b95df7ba1 + Checksum: 0fb2bed9d164ad014bcb060b95df7ba1 + UncompressedSize: 9425 + CompressedSize: 3409 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/resFileIndexShort_build_previous.txt + Type: Resource + Location: 32/326b0449af10b716_849821e0c98e37de4edc5f7156cf5887 + Checksum: 849821e0c98e37de4edc5f7156cf5887 + UncompressedSize: 611 + CompressedSize: 299 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml + Type: Resource + Location: 44/446d5f683db0d467_b7431ad6d859bfd67a4c3fae337b0b6e + Checksum: b7431ad6d859bfd67a4c3fae337b0b6e + UncompressedSize: 3902 + CompressedSize: 765 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMovie.txt + Type: Resource + Location: fd/fd2a47b3d5faa494_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml + Type: Resource + Location: fb/fbc1cb8895f6c396_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt + Type: Resource + Location: 5d/5d2aadfa1344ac63_5e631fd37d3350e30095d1251b178f2c + Checksum: 5e631fd37d3350e30095d1251b178f2c + UncompressedSize: 9117 + CompressedSize: 3259 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt + Type: Resource + Location: 60/6054e5e0cf24763e_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33188 \ No newline at end of file diff --git a/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_Windows.txt b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_Windows.txt new file mode 100644 index 0000000..163658a --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_Windows.txt @@ -0,0 +1,12 @@ +PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt,dc/dca8056adced9237_0fb2bed9d164ad014bcb060b95df7ba1,0fb2bed9d164ad014bcb060b95df7ba1,9425,3409,33206 +PatchWithInputChunk/NextBuildResources/testResource2.txt,82/8264e4640cf94b94_271c8036e4eae5515053e924a0a39e0f,271c8036e4eae5515053e924a0a39e0f,29,47,33206 +PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml,02/02b2b2627ca28b62_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33206 +PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml,44/446d5f683db0d467_b7431ad6d859bfd67a4c3fae337b0b6e,b7431ad6d859bfd67a4c3fae337b0b6e,3902,765,33206 +PatchWithInputChunk/PreviousBuildResources/introMovie.txt,fd/fd2a47b3d5faa494_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33206 +PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt,5d/5d2aadfa1344ac63_5e631fd37d3350e30095d1251b178f2c,5e631fd37d3350e30095d1251b178f2c,9117,3259,33206 +PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt,60/6054e5e0cf24763e_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33206 +PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml,fb/fbc1cb8895f6c396_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33206 +PatchWithInputChunk/resFileIndexShort_build_next.txt,1f/1f23be4b8bf6d2ae_4d398902b75611f7ae19903e6b461514,4d398902b75611f7ae19903e6b461514,612,324,33206 +PatchWithInputChunk/resFileIndexShort_build_previous.txt,32/326b0449af10b716_849821e0c98e37de4edc5f7156cf5887,849821e0c98e37de4edc5f7156cf5887,611,299,33206 +resourcesOnBranch/introMovie.txt,1c/1c6368ffb440041a_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33206 +resourcesOnBranch/videoCardCategories.yaml,ff/ffe4c396c9c935d4_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33206 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_validSimpleAndComplexExample1_macOS.txt b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_macOS.txt new file mode 100644 index 0000000..64c0010 --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_macOS.txt @@ -0,0 +1,12 @@ +PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt,dc/dca8056adced9237_0fb2bed9d164ad014bcb060b95df7ba1,0fb2bed9d164ad014bcb060b95df7ba1,9425,3409,33188 +PatchWithInputChunk/NextBuildResources/testResource2.txt,82/8264e4640cf94b94_271c8036e4eae5515053e924a0a39e0f,271c8036e4eae5515053e924a0a39e0f,29,47,33188 +PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml,02/02b2b2627ca28b62_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33188 +PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml,44/446d5f683db0d467_b7431ad6d859bfd67a4c3fae337b0b6e,b7431ad6d859bfd67a4c3fae337b0b6e,3902,765,33188 +PatchWithInputChunk/PreviousBuildResources/introMovie.txt,fd/fd2a47b3d5faa494_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33188 +PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt,5d/5d2aadfa1344ac63_5e631fd37d3350e30095d1251b178f2c,5e631fd37d3350e30095d1251b178f2c,9117,3259,33188 +PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt,60/6054e5e0cf24763e_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33188 +PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml,fb/fbc1cb8895f6c396_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33188 +PatchWithInputChunk/resFileIndexShort_build_next.txt,1f/1f23be4b8bf6d2ae_4d398902b75611f7ae19903e6b461514,4d398902b75611f7ae19903e6b461514,612,324,33188 +PatchWithInputChunk/resFileIndexShort_build_previous.txt,32/326b0449af10b716_849821e0c98e37de4edc5f7156cf5887,849821e0c98e37de4edc5f7156cf5887,611,299,33188 +resourcesOnBranch/introMovie.txt,1c/1c6368ffb440041a_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33188 +resourcesOnBranch/videoCardCategories.yaml,ff/ffe4c396c9c935d4_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33188 diff --git a/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_macOS.yaml b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_macOS.yaml new file mode 100644 index 0000000..c25018f --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleAndComplexExample1_macOS.yaml @@ -0,0 +1,90 @@ +Version: 0.1.0 +Type: ResourceGroup +NumberOfResources: 12 +TotalResourcesSizeCompressed: 32808 +TotalResourcesSizeUnCompressed: 149414 +Resources: + - RelativePath: resourcesOnBranch/introMovie.txt + Type: Resource + Location: 1c/1c6368ffb440041a_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33188 + - RelativePath: resourcesOnBranch/videoCardCategories.yaml + Type: Resource + Location: ff/ffe4c396c9c935d4_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/resFileIndexShort_build_next.txt + Type: Resource + Location: 1f/1f23be4b8bf6d2ae_4d398902b75611f7ae19903e6b461514 + Checksum: 4d398902b75611f7ae19903e6b461514 + UncompressedSize: 612 + CompressedSize: 324 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/NextBuildResources/testResource2.txt + Type: Resource + Location: 82/8264e4640cf94b94_271c8036e4eae5515053e924a0a39e0f + Checksum: 271c8036e4eae5515053e924a0a39e0f + UncompressedSize: 29 + CompressedSize: 47 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/NextBuildResources/videoCardCategories.yaml + Type: Resource + Location: 02/02b2b2627ca28b62_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/NextBuildResources/introMoviePrefixed.txt + Type: Resource + Location: dc/dca8056adced9237_0fb2bed9d164ad014bcb060b95df7ba1 + Checksum: 0fb2bed9d164ad014bcb060b95df7ba1 + UncompressedSize: 9425 + CompressedSize: 3409 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/resFileIndexShort_build_previous.txt + Type: Resource + Location: 32/326b0449af10b716_849821e0c98e37de4edc5f7156cf5887 + Checksum: 849821e0c98e37de4edc5f7156cf5887 + UncompressedSize: 611 + CompressedSize: 299 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/PatchResourceGroup_previousBuild_latestBuild.yaml + Type: Resource + Location: 44/446d5f683db0d467_b7431ad6d859bfd67a4c3fae337b0b6e + Checksum: b7431ad6d859bfd67a4c3fae337b0b6e + UncompressedSize: 3902 + CompressedSize: 765 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMovie.txt + Type: Resource + Location: fd/fd2a47b3d5faa494_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/videoCardCategories.yaml + Type: Resource + Location: fb/fbc1cb8895f6c396_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMoviePrefixed.txt + Type: Resource + Location: 5d/5d2aadfa1344ac63_5e631fd37d3350e30095d1251b178f2c + Checksum: 5e631fd37d3350e30095d1251b178f2c + UncompressedSize: 9117 + CompressedSize: 3259 + BinaryOperation: 33188 + - RelativePath: PatchWithInputChunk/PreviousBuildResources/introMovieSomewhatChanged.txt + Type: Resource + Location: 60/6054e5e0cf24763e_e9fadf6f2d386a0a0786bc863f20fa34 + Checksum: e9fadf6f2d386a0a0786bc863f20fa34 + UncompressedSize: 9091 + CompressedSize: 3232 + BinaryOperation: 33188 \ No newline at end of file diff --git a/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.txt b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.txt new file mode 100644 index 0000000..acf7b4d --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_Windows.txt @@ -0,0 +1,2 @@ +resourcesOnBranch/introMovie.txt,1c/1c6368ffb440041a_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33206 +resourcesOnBranch/videoCardCategories.yaml,ff/ffe4c396c9c935d4_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33206 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/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.txt b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.txt new file mode 100644 index 0000000..41dc43e --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.txt @@ -0,0 +1,2 @@ +resourcesOnBranch/introMovie.txt,1c/1c6368ffb440041a_e9fadf6f2d386a0a0786bc863f20fa34,e9fadf6f2d386a0a0786bc863f20fa34,9091,3232,33188 +resourcesOnBranch/videoCardCategories.yaml,ff/ffe4c396c9c935d4_90d25dac9f4ed3233c5fda72bee3dfe6,90d25dac9f4ed3233c5fda72bee3dfe6,32815,5003,33188 diff --git a/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.yaml b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.yaml new file mode 100644 index 0000000..9a68f0d --- /dev/null +++ b/tests/testData/ExpectedTestOutputFiles/CreateGroup_UsingFilter_validSimpleExample1_macOS.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: 33188 + - RelativePath: resourcesOnBranch/videoCardCategories.yaml + Type: Resource + Location: ff/ffe4c396c9c935d4_90d25dac9f4ed3233c5fda72bee3dfe6 + Checksum: 90d25dac9f4ed3233c5fda72bee3dfe6 + UncompressedSize: 32815 + CompressedSize: 5003 + BinaryOperation: 33188 \ No newline at end of file diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 7751ee3..89f7298 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -12,10 +12,19 @@ set(SRC_FILES include/Downloader.h include/FileDataStreamIn.h include/FileDataStreamOut.h + include/FilterDefaultSection.h + include/FilterNamedSection.h + include/FilterPrefixMap.h + include/FilterPrefixMapEntry.h + include/FilterResourceFile.h + include/FilterResourceFilter.h + include/FilterResourcePathFile.h + include/FilterResourcePathFileEntry.h include/GzipCompressionStream.h include/GzipDecompressionStream.h include/Md5ChecksumStream.h include/Patching.h + include/ResourceFilter.h include/ResourceTools.h include/RollingChecksum.h include/ScopedFile.h @@ -27,9 +36,17 @@ set(SRC_FILES src/Downloader.cpp src/FileDataStreamIn.cpp src/FileDataStreamOut.cpp + src/FilterNamedSection.cpp + src/FilterPrefixMap.cpp + src/FilterPrefixMapEntry.cpp + src/FilterResourceFile.cpp + src/FilterResourceFilter.cpp + src/FilterResourcePathFile.cpp + src/FilterResourcePathFileEntry.cpp src/GzipCompressionStream.cpp src/GzipDecompressionStream.cpp src/Md5ChecksumStream.cpp + src/ResourceFilter.cpp src/ResourceTools.cpp src/ScopedFile.cpp src/Patching.cpp diff --git a/tools/include/FilterDefaultSection.h b/tools/include/FilterDefaultSection.h new file mode 100644 index 0000000..e0c91ea --- /dev/null +++ b/tools/include/FilterDefaultSection.h @@ -0,0 +1,41 @@ +// Copyright © 2025 CCP ehf. + +#pragma once +#ifndef FilterDefaultSection_H +#define FilterDefaultSection_H + +#include "FilterPrefixmap.h" + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// FilterDefaultSection is a class that represents the [DEFAULT] +// section attribute of a .ini filter file. +// ------------------------------------------------------------- +class FilterDefaultSection +{ +public: + FilterDefaultSection() = default; + + // Constructs a FilterDefaultSection object + explicit FilterDefaultSection(const std::string& rawPrefixMap) + : m_prefixMap(rawPrefixMap) + { + } + + // Gets the prefix map of the [DEFAULT] section + const FilterPrefixMap& GetPrefixMap() const + { + return m_prefixMap; + } + +private: + // Container for the parsed prefixmap attribute. + FilterPrefixMap m_prefixMap; +}; + +} + +#endif // FilterDefaultSection_H diff --git a/tools/include/FilterNamedSection.h b/tools/include/FilterNamedSection.h new file mode 100644 index 0000000..c063040 --- /dev/null +++ b/tools/include/FilterNamedSection.h @@ -0,0 +1,70 @@ +// Copyright © 2025 CCP ehf. + +#pragma once +#ifndef FilterNamedSection_H +#define FilterNamedSection_H + +#include +#include +#include + +#include "FilterPrefixmap.h" +#include "FilterResourceFilter.h" +#include "FilterResourcePathFile.h" + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// FilterNamedSection is a class that represents all the contents +// of a named section (e.g: [SomeSectionName]) from a filter .ini file. +// ------------------------------------------------------------- +class FilterNamedSection +{ +public: + // Constructs a FilterNamedSection object for the given section. + // Using the raw string "filter", "respaths", optional "resfile", and + // parent "prefixmap" (from [DEFAULT] section) attributes as inputs. + explicit FilterNamedSection( const std::string& sectionName, + const std::string& rawFilter, + const std::string& rawRespaths, + const std::string& rawResfile, + const FilterPrefixMap& parentPrefixMap ); + + // Return the name of this section (e.g: [SomeSectionName]) from the .ini file. + const std::string& GetSectionName() const; + + // Return the combined resolved path map from both the "respaths" and optional "resfile" attributes. + // This is the main function to use to get the final resolved paths and their associated filters for this section. + const std::map& GetCombinedResolvedPathMap() const; + + // Return the resolved path map from the "respaths" attribute. Only used in tests to verify correctness of data. + const std::map& GetResolvedRespathsMap() const; + + // Return a pointer to the resolved path map from the optional "resfile" attribute. Only used in tests to verify correctness of data. + const std::map* GetResolvedResfileMap() const; + +private: + // Populate the combined resolved path map from both "respaths" and optional "resfile" attributes. + void PopulateCombinedResolvedPathMap(); + + // The name of this section (e.g: [SomeSectionName]) from the .ini file. + std::string m_sectionName; + + // The parsed "filter" attribute for this named section. + FilterResourceFilter m_filter; + + // The parsed "respaths" attribute. + FilterResourcePathFile m_respaths; + + // The optional parsed "resfile" attribute. + 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..30ecb6d --- /dev/null +++ b/tools/include/FilterPrefixMapEntry.h @@ -0,0 +1,45 @@ +// Copyright © 2025 CCP ehf. + +#pragma once +#ifndef FilterPrefixMapEntry_H +#define FilterPrefixMapEntry_H + +#include +#include + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// FilterPrefixMapEntry is a class that represents each +// "prefix1:pathA;pathB" (or "prefix2:pathC") entry, separated by +// a whitespace, from the "prefixmap" attribute. +// Located as part of the [DEFAULT] section of a filter .ini file. +// ------------------------------------------------------------- +class FilterPrefixMapEntry +{ +public: + // Constructs a FilterPrefixMapEntry object with the given prefix and raw paths (one or more ; separated). + explicit FilterPrefixMapEntry( const std::string& prefix, const std::string& rawPaths ); + + // Parses rawPaths and appends individual path entries to the m_paths set. + void AppendPaths( const std::string& prefix, const std::string& rawPaths ); + + // Gets the prefix identifier for this entry. + const std::string& GetPrefix() const; + + // Gets the ordered set of parsed paths for this entry. + const std::set& GetPaths() const; + +private: + // The prefix identifier for this entry. + std::string m_prefix; + + // Ordered set of parsed paths for this prefix. + 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..d80d64c --- /dev/null +++ b/tools/include/FilterPrefixmap.h @@ -0,0 +1,42 @@ +// Copyright © 2025 CCP ehf. + +#pragma once +#ifndef FilterPrefixmap_H +#define FilterPrefixmap_H + +#include +#include + +#include "FilterPrefixMapEntry.h" + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// FilterPrefixMap is a class that represents the "prefixmap" +// attribute (e.g: "prefix1:pathA;pathB prefix2:pathC") of a +// [DEFAULT] section inside a filter .ini file. +// ------------------------------------------------------------- +class FilterPrefixMap +{ +public: + FilterPrefixMap() = default; + + // Constructs a FilterPrefixMap object from a raw prefixmap string + explicit FilterPrefixMap( const std::string& rawPrefixMap ); + + // Gets the map of keyed prefixes to FilterPrefixMapEntry objects + const std::map& GetMapEntries() const; + +private: + // Called from constructor to parse the raw prefixmap string and populate m_prefixMapEntries. + void ParsePrefixMap( const std::string& rawPrefixMap ); + + // Map of prefixes to FilterPrefixMapEntry objects. + std::map m_prefixMapEntries; +}; + +} + +#endif // FilterPrefixmap_H diff --git a/tools/include/FilterResourceFile.h b/tools/include/FilterResourceFile.h new file mode 100644 index 0000000..ae89e93 --- /dev/null +++ b/tools/include/FilterResourceFile.h @@ -0,0 +1,51 @@ +// Copyright © 2025 CCP ehf. + +#pragma once +#ifndef FilterResourceFile_H +#define FilterResourceFile_H + +#include +#include + +#include "FilterDefaultSection.h" +#include "FilterNamedSection.h" + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// FilterResourceFile is a class that represents the fully +// parsed resource .ini file. +// ------------------------------------------------------------- +class FilterResourceFile +{ +public: + // Construct a FilterResourceFile object by parsing the supplied resource .ini file. + explicit FilterResourceFile( const std::filesystem::path& iniFilePath ); + + // Returns the fully resolved PathMaps for all named sections + // within this resource .ini file. + // Key = "resolved path", Value = associated include/exclude filters + const std::map& GetIniFileResolvedPathMap() const; + +private: + // Parses the resource .ini file and populates the m_defaultSection and m_namedSections members. + void ParseIniFile( const std::filesystem::path& iniFilePath ); + + // Populates the m_iniFileResolvedPathMap member by combining the resolved path maps from all named sections in this INI file. + void PopulateIniFileResolvedPathMap(); + + // The parsed [DEFAULT] section of the resource .ini file + FilterDefaultSection m_defaultSection; + + // Vector of all th parsed [NamedSection(s)] defined in the .ini file + std::vector m_namedSections; + + // Resolved PathMap for all named sections defined in a resource .ini file + std::map m_iniFileResolvedPathMap; +}; + +} + +#endif // FilterResourceFile_H diff --git a/tools/include/FilterResourceFilter.h b/tools/include/FilterResourceFilter.h new file mode 100644 index 0000000..d1fb42c --- /dev/null +++ b/tools/include/FilterResourceFilter.h @@ -0,0 +1,64 @@ +// Copyright © 2025 CCP ehf. + +#pragma once +#ifndef FilterResourceFilter_H +#define FilterResourceFilter_H + +#include +#include + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// FilterResourceFilter is a class that represents the parsed +// include and exclude filters for either a topLevel or inLine filter. +// ------------------------------------------------------------- +class FilterResourceFilter +{ +public: + FilterResourceFilter() = default; + + // Construct a FilterResourceFilter object by parsing the given raw filter string. + // isTopLevelFilter: + // - true = filter is from the "filter" attribute of a [NamedSection] + // - false = filter is an inline filter of a respaths/resfile line entry. + explicit FilterResourceFilter( const std::string& rawFilter, bool isToplevelFilter = false ); + + // Getters for the raw filter string. + // Needed to construct combined filters for respaths/resfile attribute line entries. + const std::string& GetRawFilter() const; + + // Getters for the parsed include filter vector. + const std::vector& GetIncludeFilter() const; + + // Getters for the parsed exclude filter vector. + const std::vector& GetExcludeFilter() const; + +private: + // Parse the incoming m_rawFilter string and populate the m_includeFilter and m_excludeFilter vectors accordingly. + void ParseFilters(); + + // Static helper function placing filter tokens in the correct include/exclude vector. + static void PlaceTokenInCorrectVector( const std::string& token, std::vector& fromVector, std::vector& toVector ); + + // Indicates whether this FilterResourceFilter is a top-level filter, + // i.e. from the "filter" attribute (true) of a [NamedSection] + // or an inline filter (false) i.e. from a respaths/resfile line "prefix1:pathA [ newInclude ]". + bool m_isToplevelFilter = false; + + // The raw filter string as read from the .ini file (e.g: "[ .yaml .txt ] ![ .exe ]"). + // Stored to enable easy concatenation of filters when constructing combined resolved filters. + std::string m_rawFilter; + + // Vector of the parsed include filter tokens, e.g: { ".yaml", ".txt" }. + std::vector m_includeFilter; + + // Vector of the parsed exclude filter tokens, e.g: { ".exe" }. + std::vector m_excludeFilter; +}; + +} + +#endif // FilterResourceFilter_H diff --git a/tools/include/FilterResourcePathFile.h b/tools/include/FilterResourcePathFile.h new file mode 100644 index 0000000..acde6e9 --- /dev/null +++ b/tools/include/FilterResourcePathFile.h @@ -0,0 +1,49 @@ +// Copyright © 2025 CCP ehf. + +#pragma once +#ifndef FilterResourcePathFile_H +#define FilterResourcePathFile_H + +#include + +#include "FilterPrefixmap.h" +#include "FilterResourceFilter.h" + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// FilterResourcePathFile is a class that represents a resfile/respaths +// attribute from a filter .ini file. +// Each "respaths" attribute may contain multiple line entries, +// stored in the FilterResourcePathFileEntry class. +// ------------------------------------------------------------- +class FilterResourcePathFile +{ +public: + // Construct a FilterResourcePathFile object for a resfile/respaths attribute. + explicit FilterResourcePathFile( const std::string& rawPathFileAttrib, + const FilterPrefixMap& parentPrefixMap, + const FilterResourceFilter& parentSectionFilter ); + + // Gets a map of fully resolved relative paths and the associated FilterResourceFilter include/exclude filters. + const std::map& GetResolvedPathMap() const; + +private: + // Parse the rawPathFileAttrib and populate the m_resolvedPathMap. + void ParseRawPathFileAttribute( const std::string& 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; +}; + +} + +#endif // FilterResourcePathFile_H diff --git a/tools/include/FilterResourcePathFileEntry.h b/tools/include/FilterResourcePathFileEntry.h new file mode 100644 index 0000000..9a6f7a7 --- /dev/null +++ b/tools/include/FilterResourcePathFileEntry.h @@ -0,0 +1,58 @@ +// Copyright © 2025 CCP ehf. + +#pragma once +#ifndef FilterResourcePathFileEntry_H +#define FilterResourcePathFileEntry_H + +#include +#include + +#include "FilterPrefixmap.h" +#include "FilterResourceFilter.h" + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// FilterResourcePathFileEntry is a class that represents a single line entry +// from a resfile/respaths attribute in a filter .ini file. +// Note: +// The raw representation of it from a filter .ini file can be: +// - "prefix:/pathPart/..." (sub-folder wildcard, without an inline filter) +// - "prefix:/pathPart/* [ .txt ] ![ .yaml] " (current folder wildcard, with an optional inline include and exclude filter) +// ------------------------------------------------------------- +class FilterResourcePathFileEntry +{ +public: + // Construct a FilterResourcePathFileEntry object for a single resfile/respaths attribute line entry. + explicit FilterResourcePathFileEntry( const std::string& rawPathLine, + const FilterPrefixMap& parentPrefixMap, + const FilterResourceFilter& parentSectionFilter ); + + // Gets the (possibly combined) filter for this resfile/respaths attribute line entry. + const FilterResourceFilter& GetEntryFilter() const; + + // Gets the resolved path set for this resfile/respaths attribute line entry. + const std::set& GetResolvedPaths() const; + +private: + // 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 entry. + FilterResourceFilter m_entryFilter; + + // The set of resolved paths (sorted). + std::set m_resolvedPaths; + + // Parse the rawPathLine and constructing the m_entryFilter and m_resolvedPaths + void ParseRawPathLine( const std::string& rawPathLine ); +}; + +} + +#endif //FilterResourcePathFileEntry_H diff --git a/tools/include/ResourceFilter.h b/tools/include/ResourceFilter.h new file mode 100644 index 0000000..4457a5a --- /dev/null +++ b/tools/include/ResourceFilter.h @@ -0,0 +1,80 @@ +// Copyright © 2026 CCP ehf. + +#pragma once +#ifndef ResourceFilter_H +#define ResourceFilter_H + +#include +#include + +#include "FilterResourceFile.h" +#include "FilterResourceFilter.h" + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// ResourceFilter is a class that wraps one or more filter .ini +// files and exposes a way (function FilePathMatchesIncludeFilterRules) +// to check if a given file path should be included or excluded based +// on all the filtering rules defined in those .ini file(s). +// ------------------------------------------------------------- +class ResourceFilter +{ +public: + ResourceFilter() = default; + + // Construct a ResourceFilter object by passing it a list of filter .ini file(s) and prefixmap base path. + explicit ResourceFilter( + const std::vector& iniFilePaths, + const std::filesystem::path& prefixmapAbsoluteBasePath ); + + // Initializes the ResourceFilter by passing in and parsing the supplied filter .ini file(s) and prefixmap base path. + void Initialize( + const std::vector& iniFilePaths, + const std::filesystem::path& prefixmapAbsoluteBasePath ); + + // Returns true if this ResourceFilter has any filter .ini files, false otherwise. + bool HasFilters() const; + + // Returns the full resolved relative PathMaps from all resource .ini file(s). + // Key = "resolved relative path", Value = FilterResourceFilter (include/exclude filters) + const std::map& GetFullResolvedPathMap() const; + + // Check if the inFilePath should be included or excluded based on + // filtering rules from all the filter .ini file(s) + bool FilePathMatchesIncludeFilterRules( const std::filesystem::path& inFilePath ); + +private: + // Populate the full resolved path map from all .ini file(s) in this ResourceFilter. + void PopulateFullResolvedPathMap(); + + // Static helper function for wildcard matching path strings (supports "*" and "...") + static bool WildcardMatch( std::string pattern, const std::string& checkStr ); + + // Static helper function to normalize paths (i.e. deal with \ / .. . etc) + static std::string NormalizePath( const std::string& path ); + + // Static helper function to get an absolute path by combining a relative path with an absolute base path (if not already absolute) + static std::filesystem::path GetAbsolutePathFromBase( + const std::filesystem::path& relativeOrAbsolutePath, + const std::filesystem::path& absoluteBasePath ); + + // A flag used to prevent multiple initializations of the ResourceFilter + bool m_initialized{ false }; + + // The filter .ini file(s) prefixmap attribute absolute base path + // Needed in order to resolve any relative paths from within those files + std::filesystem::path m_prefixmapAbsoluteBasePath; + + // Vector of all the filter .ini files (wrapped in FilterResourceFile objects) + std::vector> m_filterFiles; + + // Resolved PathMap from all the filter .ini files + std::map m_fullResolvedPathMap; +}; + +} + +#endif // ResourceFilter_H diff --git a/tools/src/FilterNamedSection.cpp b/tools/src/FilterNamedSection.cpp new file mode 100644 index 0000000..67a7fd7 --- /dev/null +++ b/tools/src/FilterNamedSection.cpp @@ -0,0 +1,146 @@ +// Copyright © 2025 CCP ehf. + +#include "FilterNamedSection.h" + +#include + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// Constructs a FilterNamedSection object for a named section +// within a filter .ini file. +// Arguments: +// sectionName - name of the section (e.g: [SomeSectionName]) +// rawFilter - the "filter" attribute for this section (e.g: "[ .yaml .txt ]") +// respaths - the "respaths" attribute of this section (e.g: "prefix:/someSubPath/*") +// resfile - the optional "resfile" attribute of this section (e.g: "prefix:/pathToSomeFile.txt") +// parentPrefixMap - the FilterPrefixMap from the [DEFAULT] section of the .ini file, +// used to resolve prefixes in "respaths" and "resfile" attributes to actual paths. +// Note: +// The combined map from both the "respaths" and "resfile" attributes +// is populated on construction of this object. +// ------------------------------------------------------------- +FilterNamedSection::FilterNamedSection( const std::string& sectionName, + const std::string& rawFilter, + const std::string& rawRespaths, + const std::string& rawResfile, + const FilterPrefixMap& parentPrefixMap ) : + m_sectionName( sectionName ), + m_filter( rawFilter, true ), // When constructing a [NamedSection], isToplevelFilter should always be true (as it's not an inline filter) + m_respaths( rawRespaths, parentPrefixMap, m_filter ), + m_resfile( rawResfile.empty() ? std::nullopt : std::make_optional( rawResfile, parentPrefixMap, m_filter ) ) +{ + if( rawRespaths.empty() ) + { + throw std::invalid_argument( "Respaths attribute is empty for section: " + m_sectionName ); + } + + // Populate the combined resolved path map from both "respaths" and optional "resfile" attributes. + PopulateCombinedResolvedPathMap(); +} + +// ------------------------------------------------------------- +// Description: +// Gets the name of this section (e.g: [SomeSectionName]) from the .ini file. +// Return Value: +// String representing the name of the section +// ------------------------------------------------------------- +const std::string& FilterNamedSection::GetSectionName() const +{ + return m_sectionName; +} + +// ------------------------------------------------------------- +// Description: +// Gets the combined resolved path map from both the "respaths" and optional "resfile" attributes. +// This is the main getter function to use for the full resolved paths +// and their associated filters from within this section. +// Return Value: +// Map of resolved paths to their associated FilterResourceFilter objects. +// ------------------------------------------------------------- +const std::map& FilterNamedSection::GetCombinedResolvedPathMap() const +{ + return m_resolvedCombinedPathMap; +} + +// ------------------------------------------------------------- +// Description: +// Gets the resolved path map from the "respaths" attribute only. +// Return Value: +// Map of resolved paths to their associated FilterResourceFilter objects. +// Note: +// Users of this class should use the GetCombinedResolvedPathMap() +// function instead of this one to get the "full combined" result. +// This function is exposed only to enable tests to verify correctness of data. +// ------------------------------------------------------------- +const std::map& FilterNamedSection::GetResolvedRespathsMap() const +{ + return m_respaths.GetResolvedPathMap(); +} + +// ------------------------------------------------------------- +// Description: +// Gets the resolved path map from the optional "resfile" attribute only. +// Return Value: +// Pointer to a map of resolved paths to their associated FilterResourceFilter objects, +// or nullptr if no resfile is present. This is a pointer (and not a reference) because of the optional m_resfile member. +// Note: +// Users of this class should use the GetCombinedResolvedPathMap() +// function instead of this one to get the "full combined" result. +// This function is exposed only to enable tests to verify correctness of data. +// ------------------------------------------------------------- +const std::map* FilterNamedSection::GetResolvedResfileMap() const +{ + if( m_resfile ) + { + return &m_resfile->GetResolvedPathMap(); + } + + return nullptr; +} + +// ------------------------------------------------------------- +// Description: +// Populates the combined resolved path map from both "respaths" and optional "resfile" attributes. +// Return Value: +// None (void) +// ------------------------------------------------------------- +void FilterNamedSection::PopulateCombinedResolvedPathMap() +{ + // Only populate the Combined map if not already done so. + if( m_resolvedCombinedPathMap.empty() ) + { + // Populate the combined map with "respaths" attribute entries first (as they are non-optional) + for( const auto& kv : m_respaths.GetResolvedPathMap() ) + { + m_resolvedCombinedPathMap.insert( {kv.first, kv.second } ); + } + + // Add the "resfile" attribute entries to the combined map (in case there are any). + // Make sure to combine any filters if the same key already exists from the "respaths" attribute. + if( m_resfile ) + { + // Allow "resfile" to contain multiple entries (future proofing it, not currently utilized as such in existing .ini filter files) + for( const auto& kv : m_resfile->GetResolvedPathMap() ) + { + // Combine filters of both if same key already exists (using the raw filter strings). + // Else just add the "resfile" attribue entry to the combined map as is. + auto it = m_resolvedCombinedPathMap.find( kv.first ); + if( it != m_resolvedCombinedPathMap.end() ) + { + std::string combinedRawFilter = it->second.GetRawFilter() + " " + kv.second.GetRawFilter(); + FilterResourceFilter combinedFilter( combinedRawFilter ); + m_resolvedCombinedPathMap.insert_or_assign( kv.first, combinedFilter ); + } + else + { + m_resolvedCombinedPathMap.insert( {kv.first, kv.second } ); + } + } + } + } +} + +} diff --git a/tools/src/FilterPrefixMapEntry.cpp b/tools/src/FilterPrefixMapEntry.cpp new file mode 100644 index 0000000..4b49ba8 --- /dev/null +++ b/tools/src/FilterPrefixMapEntry.cpp @@ -0,0 +1,90 @@ +// Copyright © 2025 CCP ehf. + +#include "FilterPrefixMapEntry.h" + +#include + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// Constructs a FilterPrefixMapEntry object for a given prefix +// and the ";" semicolon separated rawPaths string. +// Arguments: +// prefix - identifier of the prefix the paths belong to (e.g: "prefix1") +// rawPaths - string of one or more paths separated by ";" (e.g: pathA;pathB) +// ------------------------------------------------------------- +FilterPrefixMapEntry::FilterPrefixMapEntry( const std::string& prefix, const std::string& rawPaths ) : + m_prefix( prefix ) +{ + AppendPaths( prefix, rawPaths ); +} + +// ------------------------------------------------------------- +// Description: +// Parses rawPaths and appends individual path entries to the m_paths set +// for this prefix. +// Arguments: +// prefix - identifier of the prefix the paths belong to (e.g: "prefix1") +// rawPaths - string of one or more paths separated by ";" (e.g: pathA;pathB) +// ------------------------------------------------------------- +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; + // Loop through rawPaths and split by semicolons to extract individual paths (in case there are many) + 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 ); + } +} + +// ------------------------------------------------------------- +// Description: +// Gets the prefix identifier for this entry. +// Return Value: +// String representation of the prefix identifier (e.g: "prefix1") +// ------------------------------------------------------------- +const std::string& FilterPrefixMapEntry::GetPrefix() const +{ + return m_prefix; +} + +// ------------------------------------------------------------- +// Description: +// Gets the ordered set of parsed paths for this entry. +// Return Value: +// A set of strings representing the paths associated with +// this prefix (e.g: {"pathA", "pathB"}) +// ------------------------------------------------------------- +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..a8c25c4 --- /dev/null +++ b/tools/src/FilterPrefixmap.cpp @@ -0,0 +1,106 @@ +// Copyright © 2025 CCP ehf. + +#include "FilterPrefixmap.h" + +#include +#include + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// Constructs a FilterPrefixMap object from a raw prefixmap string +// by calling the ParsePrefixMap() private function. +// Arguments: +// rawPrefixMap - string representation of the raw prefixmap attribute +// ------------------------------------------------------------- +FilterPrefixMap::FilterPrefixMap( const std::string& rawPrefixMap ) +{ + ParsePrefixMap( rawPrefixMap ); +} + +// ------------------------------------------------------------- +// Description: +// Gets the map of keyed prefixes to FilterPrefixMapEntry objects. +// The FilterPrefixMapEntry objects contain a set of parsed paths for each prefix. +// Return Value: +// map of prefixes to FilterPrefixMapEntry objects +// ------------------------------------------------------------- +const std::map& FilterPrefixMap::GetMapEntries() const +{ + return m_prefixMapEntries; +} + +// ------------------------------------------------------------- +// Description: +// Parses the raw prefixmap string and populates m_prefixMapEntries +// with the corresponding FilterPrefixMapEntry objects. +// The raw prefixmap string is expected to be in the format: +// "prefix1:pathA;pathB prefix2:pathC" +// The function throws std::invalid_argument if format is invalid. +// Arguments: +// rawPrefixMap - string representation of the raw prefixmap attribute +// Return Value: +// None (void) +// ------------------------------------------------------------- +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..c547073 --- /dev/null +++ b/tools/src/FilterResourceFile.cpp @@ -0,0 +1,128 @@ +// Copyright © 2025 CCP ehf. + +#include "FilterResourceFile.h" + +#include + +#include "INIReader.h" + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// Construct a FilterResourceFile object by parsing the supplied resource .ini file. +// Arguments: +// iniFilePath - the file path to the resource .ini file to parse. +// ------------------------------------------------------------- +FilterResourceFile::FilterResourceFile( const std::filesystem::path& iniFilePath ) +{ + ParseIniFile( iniFilePath ); + PopulateIniFileResolvedPathMap(); +} + +// ------------------------------------------------------------- +// Description: +// Returns the fully resolved PathMaps for all named sections within this .ini file. +// Return Value: +// Map of resolved paths to their associated filters: +// - Key = "resolved path" +// - Value = associated include/exclude filters +// ------------------------------------------------------------- +const std::map& FilterResourceFile::GetIniFileResolvedPathMap() const +{ + return m_iniFileResolvedPathMap; +} + +// ------------------------------------------------------------- +// Description: +// Parses the resource .ini file and populates the m_defaultSection and +// m_namedSections members from the [DEFAULT] and [NamedSection(s)] respectively. +// Arguments: +// iniFilePath - the file path to the resource .ini file to parse. +// Return Value: +// None (void). +// ------------------------------------------------------------- +void FilterResourceFile::ParseIniFile( const std::filesystem::path& iniFilePath ) +{ + // Open, read and parse the resource INI file. + INIReader reader( iniFilePath.generic_string() ); + if( reader.ParseError() != 0 ) + { + throw std::runtime_error( "Failed to parse INI file: " + std::filesystem::absolute(iniFilePath ).generic_string() + " - " + reader.ParseErrorMessage() ); + } + + // Parse the [DEFAULT] section + if( !reader.HasSection( "DEFAULT" ) ) + { + throw std::invalid_argument( "Missing [DEFAULT] section in INI file: " + iniFilePath.generic_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 [namedSection] defined in INI file: " + iniFilePath.generic_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 ); + } +} + +// ------------------------------------------------------------- +// Description: +// Populate the fully resolved PathMaps for all [namedSections] +// in this .ini file. +// Return Value: +// None (void). +// ------------------------------------------------------------- +void FilterResourceFile::PopulateIniFileResolvedPathMap() +{ + 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( { kv.first, kv.second } ); + } + } + } + } +} + +} diff --git a/tools/src/FilterResourceFilter.cpp b/tools/src/FilterResourceFilter.cpp new file mode 100644 index 0000000..30e1a5c --- /dev/null +++ b/tools/src/FilterResourceFilter.cpp @@ -0,0 +1,190 @@ +// Copyright © 2025 CCP ehf. + +#include "FilterResourceFilter.h" + +#include +#include +#include + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// Construct a FilterResourceFilter object by parsing the given raw filter string. +// Arguments: +// rawFilter - the raw filter string to parse (e.g: "[ .yaml .txt ] ![ .exe ]") +// isTopLevelFilter - indicates whether the level of the filter: +// - true = filter is from the "filter" attribute of a [NamedSection] +// - false = (default value) filter is an inline filter of a respaths/resfile line entry. +// ------------------------------------------------------------- +FilterResourceFilter::FilterResourceFilter( const std::string& rawFilter, bool isToplevelFilter /* = false */ ) : + m_rawFilter( rawFilter ), + m_isToplevelFilter( isToplevelFilter ) +{ + ParseFilters(); +} + +// ------------------------------------------------------------- +// Description: +// Gets the raw filter string attribute from the .ini file, +// e.g: "[ .yaml .txt ] ![ .exe ]", either topLevel or inLine. +// Return Value: +// The raw string representation of the filter. +// Note: +// This function is needed as input to easily construct combined +// filters for "topLevel parent" and respaths/resfile attribute +// line filter entries, from both of their raw representation. +// ------------------------------------------------------------- +const std::string& FilterResourceFilter::GetRawFilter() const +{ + return m_rawFilter; +} + +// ------------------------------------------------------------- +// Description: +// Gets the parsed include filter vector. +// Return Value: +// Vector of strings representing the include filter tokens, e.g: { ".yaml", ".txt" }. +// ------------------------------------------------------------- +const std::vector& FilterResourceFilter::GetIncludeFilter() const +{ + return m_includeFilter; +} + +// ------------------------------------------------------------- +// Description: +// Gets the parsed exclude filter vector. +// Return Value: +// Vector of strings representing the exclude filter tokens, e.g: { ".exclude" }. +// ------------------------------------------------------------- +const std::vector& FilterResourceFilter::GetExcludeFilter() const +{ + return m_excludeFilter; +} + +// ------------------------------------------------------------- +// Description: +// Parses the raw filter string into the include and exclude +// filter vectors and places it in the correct vector. +// Return Value: +// None (void). +// ------------------------------------------------------------- +void FilterResourceFilter::ParseFilters() +{ + std::string s = m_rawFilter; + size_t pos = 0; + while( pos < s.size() ) + { + // Skip whitespaces + 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 += "[ * ]"; + } +} + +// ------------------------------------------------------------- +// Description: +// Static helper function placing filter tokens in the correct include/exclude vector. +// Arguments: +// token - the filter token to place in the correct vector (e.g: ".yaml") +// fromVector - the vector to remove the token from (if present) +// toVector - the vector to add the token to (if not already present in it) +// Return Value: +// None (void). +// ------------------------------------------------------------- +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 ); + } +} + +} diff --git a/tools/src/FilterResourcePathFile.cpp b/tools/src/FilterResourcePathFile.cpp new file mode 100644 index 0000000..451de65 --- /dev/null +++ b/tools/src/FilterResourcePathFile.cpp @@ -0,0 +1,120 @@ +// Copyright © 2025 CCP ehf. + +#include "FilterResourcePathFile.h" + +#include + +#include "FilterResourcePathFileEntry.h" + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// Constructs a FilterResourcePathFile object for a resfile/respaths attribute. +// Arguments: +// rawPathFileAttrib - the raw string for this resfile/respaths attribute from the filter .ini file. +// This can contain multiple lines of path entries, see FilterResourcePathFileEntry class. +// parentPrefixMap - the FilterPrefixMap from the [DEFAULT] section, used to +// resolve prefixes in the resfile/respaths attribute to actual paths. +// parentSectionFilter - the FilterResourceFilter from the same [namedSection] as this +// resfile/respaths attribute. Needed to create actual filters (some with optional inline) +// for each line entry (FilterResourcePathFileEntry) of the resfile/respaths attribute. +// Note: +// The parsing of the rawPathFileAttrib takes place in the ParseRawPathFileAttribute() function. +// ------------------------------------------------------------- +FilterResourcePathFile::FilterResourcePathFile( const std::string& rawPathFileAttrib, + const FilterPrefixMap& parentPrefixMap, + const FilterResourceFilter& parentSectionFilter ) : + m_parentPrefixMap( parentPrefixMap ), + m_parentSectionFilter( parentSectionFilter ) +{ + ParseRawPathFileAttribute( rawPathFileAttrib ); +} + +// ------------------------------------------------------------- +// Description: +// Gets a map of fully resolved relative paths and their +// associated FilterResourceFilter include/exclude filters. +// Return Value: +// Map with a key = resolved relative path and value of +// the associated include/exclude filters. +// ------------------------------------------------------------- +const std::map& FilterResourcePathFile::GetResolvedPathMap() const +{ + return m_resolvedPathMap; +} + +// ------------------------------------------------------------- +// Description: +// Parses the rawPathFileAttrib and populates the m_resolvedPathMap. +// Arguments: +// rawPathFileAttrib - the raw string for this resfile/respaths attribute +// from the filter .ini file. This can contain multiple lines of path +// entries that is represented by the FilterResourcePathFileEntry class. +// ------------------------------------------------------------- +void FilterResourcePathFile::ParseRawPathFileAttribute( const std::string& rawPathFileAttrib ) +{ + // Split rawPathFileAttrib into lines (in case of multiline attribute) + std::istringstream stream( 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 entryFilter = lineEntry.GetEntryFilter(); + const auto resolvedPaths = lineEntry.GetResolvedPaths(); + + for( const auto& path : resolvedPaths ) + { + // Check if the path already exists in the map, to determine if se should combine filters or not. + auto foundMapItem = m_resolvedPathMap.find( path ); + if( foundMapItem != m_resolvedPathMap.end() ) + { + // Combine the raw filters from the previous and current entry + const std::string& prevRawFilter = foundMapItem->second.GetRawFilter(); + const std::string& currRawFilter = entryFilter.GetRawFilter(); + std::string combinedRawFilter; + + if( !prevRawFilter.empty() && !currRawFilter.empty() ) + { + combinedRawFilter = prevRawFilter + " " + currRawFilter; + } + else if( !prevRawFilter.empty() ) + { + combinedRawFilter = prevRawFilter; + } + else + { + combinedRawFilter = currRawFilter; + } + // Update the map with the combined filter + m_resolvedPathMap.insert_or_assign( path, FilterResourceFilter( combinedRawFilter ) ); + } + else + { + // Path not present, just insert as is + m_resolvedPathMap.insert( { path, entryFilter } ); + } + } + } +} + +} diff --git a/tools/src/FilterResourcePathFileEntry.cpp b/tools/src/FilterResourcePathFileEntry.cpp new file mode 100644 index 0000000..2225f23 --- /dev/null +++ b/tools/src/FilterResourcePathFileEntry.cpp @@ -0,0 +1,138 @@ +// Copyright © 2025 CCP ehf. + +#include "FilterResourcePathFileEntry.h" + +#include +#include + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// Construct a FilterResourcePathFileEntry object for a single resfile/respaths attribute line entry. +// Arguments: +// rawPathLine - the raw string for this line entry from the .ini file +// e.g: "prefix1:/pathA/* [ .txt ] ![ .yaml] +// parentPrefixMap - the FilterPrefixMap from the [DEFAULT] section, +// used to resolve prefixes in this line entry to actual paths. +// parentSectionFilter - the FilterResourceFilter from the same +// [namedSection] as this resfile/respaths attribute. +// Needed to create actual filter (with optional inline one) for this line entry. +// ------------------------------------------------------------- +FilterResourcePathFileEntry::FilterResourcePathFileEntry( const std::string& rawPathLine, + const FilterPrefixMap& parentPrefixMap, + const FilterResourceFilter& parentSectionFilter ) : + m_parentPrefixMap( parentPrefixMap ), + m_parentSectionFilter( parentSectionFilter ) +{ + ParseRawPathLine( rawPathLine ); +} + +// ------------------------------------------------------------- +// Description: +// Gets the (possibly combined) filter for this resfile/respaths attribute line entry. +// Return Value: +// FilterResourceFilter object representing the combined +// include/exclude filters for this line entry. +// ------------------------------------------------------------- +const FilterResourceFilter& FilterResourcePathFileEntry::GetEntryFilter() const +{ + return m_entryFilter; +} + +// ------------------------------------------------------------- +// Description: +// Get resolved paths set for this resfile/respaths attribute line entry. +// Return Value: +// Set of strings representing the resolved relative paths for this line entry. +// ------------------------------------------------------------- +const std::set& FilterResourcePathFileEntry::GetResolvedPaths() const +{ + return m_resolvedPaths; +} + +// ------------------------------------------------------------- +// Description: +// Parses the rawPathLine and constructs/populates the +// m_entryFilter and m_resolvedPaths members. +// Arguments: +// rawPathLine - the raw string for this line entry +// e.g: "prefix1:/pathA/* [ .txt ] ![ .yaml]" +// ------------------------------------------------------------- +void FilterResourcePathFileEntry::ParseRawPathLine( const std::string& rawPathLine ) +{ + std::string rawPrefixPathToken; + std::string combinedRawFilter; + + // Split on whitespace: + // - first token is the prefix:pathPart, + // - rest would be the (optional) filterPart (to be read at later stage) + std::istringstream iss( rawPathLine ); + iss >> rawPrefixPathToken; + + // Validate that the rawPathToken is of the correct format "prefix:pathPart" + size_t colon = rawPrefixPathToken.find( ':' ); + if( colon == std::string::npos ) + { + throw std::invalid_argument( std::string( "Missing prefix in path for: " ) + rawPathLine ); + } + std::string prefixPart = rawPrefixPathToken.substr( 0, colon ); + std::string pathPart = rawPrefixPathToken.substr( colon + 1 ); + + // Now figure out which filter to use (inline or parent) + if( !iss.eof() ) + { + // There is more data, i.e. an optional filter exists. + // Construct it from the rest of the "line", will error out if filter format is wrong. + std::string rawOptionalFilterPart; + 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(); + } + + // Construct the parsed (potentially) combined filter for this line entry. + m_entryFilter = FilterResourceFilter( combinedRawFilter ); + + // Check that the prefix from the rawPathToken exists in the parent [DEFAULT] section prefix map. + const auto& prefixMapEntries = m_parentPrefixMap.GetMapEntries(); + auto foundPrefixMapEntry = prefixMapEntries.find( prefixPart ); + if( foundPrefixMapEntry == prefixMapEntries.end() ) + { + throw std::invalid_argument( std::string( "Prefix '" ) + prefixPart + "' not present in prefixMap for line: " + rawPathLine ); + } + + // Each FilterPrefixMapEntry may have multiple paths, combine/resolve for all of those paths + const auto& prefixEntry = foundPrefixMapEntry->second; + const auto& prefixPathsSet = prefixEntry.GetPaths(); + for( const auto& basePrefixMapPath : prefixPathsSet ) + { + // Ensure only one '/' at the join point + bool baseEndsWithSlash = !basePrefixMapPath.empty() && basePrefixMapPath.back() == '/'; + bool restStartsWithSlash = !pathPart.empty() && pathPart.front() == '/'; + + std::string resolvedPath = basePrefixMapPath; + if( baseEndsWithSlash && restStartsWithSlash ) + { + resolvedPath += pathPart.substr( 1 ); + } + else if( !baseEndsWithSlash && !restStartsWithSlash ) + { + resolvedPath += '/' + pathPart; + } + else + { + resolvedPath = basePrefixMapPath + pathPart; + } + 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..4d612e5 --- /dev/null +++ b/tools/src/ResourceFilter.cpp @@ -0,0 +1,359 @@ +// Copyright © 2026 CCP ehf. + +#include "ResourceFilter.h" + +#include + +namespace ResourceTools +{ + +// ------------------------------------------------------------- +// Description: +// Construct a ResourceFilter object by passing in filter .ini file(s). +// Arguments: +// iniFilePaths - vector of file paths to the filter .ini files +// prefixmapAbsoluteBasePath - absolute base path for resolving prefixmap attribute within filter .ini file(s) +// ------------------------------------------------------------- +ResourceFilter::ResourceFilter( + const std::vector& iniFilePaths, + const std::filesystem::path& prefixmapAbsoluteBasePath ) +{ + Initialize( iniFilePaths, prefixmapAbsoluteBasePath ); +} + +// ------------------------------------------------------------- +// Description: +// Initializes the ResourceFilter by passing in and creating +// FilterResourceFile objects for each of the supplied filter .ini file(s). +// Arguments: +// iniFilePaths - vector of file paths to the filter .ini files +// prefixmapAbsoluteBasePath - absolute base path for resolving prefixmap attribute within filter .ini file(s) +// Return Value: +// None (void). +// ------------------------------------------------------------- +void ResourceFilter::Initialize( + const std::vector& iniFilePaths, + const std::filesystem::path& prefixmapAbsoluteBasePath ) +{ + if( m_initialized ) + { + throw std::runtime_error( "ResourceFilter is already initialized." ); + } + + m_prefixmapAbsoluteBasePath = prefixmapAbsoluteBasePath; + + std::set uniquePaths( iniFilePaths.begin(), iniFilePaths.end() ); + for( const auto& path : uniquePaths ) + { + try + { + // Supplied filter .ini files should already be passed in with absolute path. + // In case they are not, fallback to resolving absolute path based on the + // m_prefixmapAbsoluteBasePath property insted of the current working directory. + // That property is otherwise intended for resolving relative paths within those filter .ini files. + m_filterFiles.emplace_back( std::make_unique( GetAbsolutePathFromBase( path, m_prefixmapAbsoluteBasePath ) ) ); + } + catch( const std::exception& e ) + { + // Optionally log or handle error + std::string errorMsg = "Unable to create ResourceFilter for: " + path.generic_string() + " - because of: " + e.what(); + throw std::runtime_error( errorMsg ); + } + } + + // Now we can populate the full resolved path map from all .ini file(s) in + // this ResourceFilter (combining filters for any overlapping paths). + PopulateFullResolvedPathMap(); + + m_initialized = true; +} + +// ------------------------------------------------------------- +// Description: +// Determine if this ResourceFilter has any filters +// Return Value: +// True = There are filter .ini files present +// False = There are no filter .ini files +// ------------------------------------------------------------- +bool ResourceFilter::HasFilters() const +{ + return !m_filterFiles.empty(); +} + +// ------------------------------------------------------------- +// Description: +// Returns the full resolved relative PathMaps from all FilterResourceFile(s) +// in this ResourceFilter. +// Return Value: +// Map of resolved paths to their associated filters from all +// FilterResourceFile(s) +// Key = "resolved relative path" +// Value = FilterResourceFilter (with include/exclude filters) +// ------------------------------------------------------------- +const std::map& ResourceFilter::GetFullResolvedPathMap() const +{ + return m_fullResolvedPathMap; +} + +// ------------------------------------------------------------- +// Description: +// Check if the inFilePath should be included or excluded based on +// the filtering rules from all .ini file(s) in this ResourceFilter. +// Arguments: +// inFilePath - the absolute file path to check against the filter rules +// Return Value: +// True = the file path matches the include filter rules and should be included +// False = the file path does not match the include filter rules and should be excluded +// ------------------------------------------------------------- +bool ResourceFilter::FilePathMatchesIncludeFilterRules( const std::filesystem::path& inFilePath ) +{ + // Make sure we work with the lexically normalized (already absolute) path representation of the input file. + std::string inFileNormalAbsPathStr = NormalizePath( inFilePath.generic_string() ); + std::filesystem::path inFilePathAbs = std::filesystem::absolute( inFileNormalAbsPathStr ); + + // 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 ) + { + // Convert the relative path from the filter rules .ini file(s) to a lexically normalized absolute path + // for comparison with the similarly lexically normalized absolute path of the input file (inFilePath). + std::string resolvedNormalAbsPathStr = NormalizePath( GetAbsolutePathFromBase( resolvedRelativePathStr, m_prefixmapAbsoluteBasePath ).generic_string() ); + std::filesystem::path resolvedPathAbs = std::filesystem::absolute( resolvedNormalAbsPathStr ); + + if( resolvedNormalAbsPathStr == inFileNormalAbsPathStr ) + { + // If there is an exact match on the full filename path, this means highest priority + // and the file path should be considered an "include". This is even though resolvedPath + // may have filters that indicate exclude (explicitly specifying a full filename path + // should override any wildcard exclusion filters on the same file path). + bestIncludePriority = -1; + continue; + } + + // Make sure the resolvedNormalAbsPathStr contains the "..." (recursive folder wildcard) if the + // original resolvedRelativePathStr had it. + // Only check the end of the string for any combination of ["/...", "...", ".../"]. + if( resolvedRelativePathStr.find( "...", resolvedRelativePathStr.size() - 4 ) != std::string::npos ) + { + if( resolvedNormalAbsPathStr.find( "...", resolvedNormalAbsPathStr.size() - 4 ) == std::string::npos ) + { + if( resolvedNormalAbsPathStr.back() != '/' ) + { + resolvedNormalAbsPathStr += '/'; + } + resolvedNormalAbsPathStr += "..."; + } + } + + // Perform Wildcard matching on the normalized absolute paths + if( !WildcardMatch( resolvedNormalAbsPathStr, inFileNormalAbsPathStr ) ) + { + // There was NO wildcard match on paths, ignore it + 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; +} + +// ------------------------------------------------------------- +// Description: +// Populates the full resolved path map from all .ini file(s) in this ResourceFilter. +// Return Value: +// None (void). +// ------------------------------------------------------------- +void ResourceFilter::PopulateFullResolvedPathMap() +{ + 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( { kv.first, kv.second } ); + } + } + } + } +} + +// ------------------------------------------------------------- +// Description: +// Static helper function for wildcard matching path strings. +// Supports the following wildcards: +// - "*" = matches any sequence of characters (at the same folder level) +// - "..." = matches any sequence of characters (as any recursive folder level) +// Arguments: +// pattern - The resolved path from the .ini file (can contain wildcards) +// checkStr - The input file path to check against the pattern +// Return Value: +// True = the checkStr matches the pattern (exact or with wildcards) +// False = the checkStr does not match the pattern (neither exact nor with wildcards) +// ------------------------------------------------------------- +bool ResourceFilter::WildcardMatch( std::string pattern, const std::string& checkStr ) +{ + // Replace any "..." with a unique token (RECURSIVE_FOLDER_ELLIPSES_WILDCARD) + constexpr char RECURSIVE_FOLDER_ELLIPSES_WILDCARD = '\x01'; + size_t pos; + while( ( pos = pattern.find( "..." ) ) != std::string::npos ) + { + pattern.replace( pos, 3, std::string( 1, RECURSIVE_FOLDER_ELLIPSES_WILDCARD ) ); + } + + // Escape special characters and deal with wildcards ("*" and "..." i.e. RECURSIVE_FOLDER_ELLIPSES_WILDCARD) + std::string regexPattern; + for( size_t i = 0; i < pattern.size(); ++i ) + { + if( pattern[i] == '*' ) + { + regexPattern += "[^/]*"; + } + else if( pattern[i] == RECURSIVE_FOLDER_ELLIPSES_WILDCARD ) + { + regexPattern += ".*"; + } + else if( std::string( ".^$|()[]{}+?\\" ).find( pattern[i] ) != std::string::npos ) + { + // Regex special characters that need escaping + regexPattern += '\\'; + regexPattern += pattern[i]; + } + else + { + regexPattern += pattern[i]; + } + } + + try + { + std::regex re( regexPattern, std::regex::ECMAScript | std::regex::icase ); + bool regexResult = std::regex_match( checkStr, re ); + return regexResult; + } + catch( const std::regex_error& e ) + { + std::string errorMsg = "Regex Exception during WildcardMatching - regexPattern: " + regexPattern + " checkString: " + checkStr + " - error details: " + e.what(); + throw std::runtime_error( errorMsg ); + } + catch( const std::exception& e ) + { + std::string errorMsg = "Standard Exception during WildcardMatching - regexPattern: " + regexPattern + " checkString: " + checkStr + " - error details: " + e.what(); + throw std::runtime_error( errorMsg ); + } +} + +// ------------------------------------------------------------- +// Description: +// Static helper function to normalize paths by removing redundant +// path components such as "." and ".." and converting to a generic format. +// Arguments: +// path - the file path to normalize +// Return Value: +// Normalized path string in generic format (using '/' as separator) +// ------------------------------------------------------------- +std::string ResourceFilter::NormalizePath( const std::string& path ) +{ + std::filesystem::path p( path ); + return p.lexically_normal().generic_string(); +} + +// ------------------------------------------------------------- +// Description: +// Static helper function to get an absolute path by combining a +// relative path with an absolute base path (if not already absolute). +// Arguments: +// relativeOrAbsolutePath - the input file path that is either relative or absolute +// absoluteBasePath - the absolute base path to use (if needed) +// Return Value: +// Guaranteed absolute file path +// ------------------------------------------------------------- +std::filesystem::path ResourceFilter::GetAbsolutePathFromBase( const std::filesystem::path& relativeOrAbsolutePath, + const std::filesystem::path& absoluteBasePath ) +{ + if( relativeOrAbsolutePath.is_absolute() ) + { + // Path is already absolute, just return it + return relativeOrAbsolutePath; + } + + // Path is relative, combine it with the absolute base path + return absoluteBasePath / relativeOrAbsolutePath; +} + +} // namespace ResourceTools diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 9c0fca4..7348714 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -1,7 +1,7 @@ { "default-registry": { "kind": "git", - "baseline": "bea476a80218a581bcb5318595849520217c825e", + "baseline": "4334d8b4c8916018600212ab4dd4bbdc343065d1", "repository": "https://github.com/microsoft/vcpkg.git" }, "registries": [ diff --git a/vcpkg.json b/vcpkg.json index 2578987..02c947d 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"