From 20b501cce2ca2e297e4cd4280216c6b73ae28dc1 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 20 Mar 2026 09:23:42 +0100 Subject: [PATCH 01/17] add functions to collect included XML files --- .../dataRepository/xmlWrapper.cpp | 83 +++++++++++++++++++ .../dataRepository/xmlWrapper.hpp | 44 ++++++++++ 2 files changed, 127 insertions(+) diff --git a/src/coreComponents/dataRepository/xmlWrapper.cpp b/src/coreComponents/dataRepository/xmlWrapper.cpp index 0c5a469a570..29be35414e7 100644 --- a/src/coreComponents/dataRepository/xmlWrapper.cpp +++ b/src/coreComponents/dataRepository/xmlWrapper.cpp @@ -24,6 +24,7 @@ #include "common/format/StringUtilities.hpp" #include "common/MpiWrapper.hpp" #include "dataRepository/KeyNames.hpp" +#include "common/Path.hpp" namespace geos { @@ -268,6 +269,88 @@ string buildMultipleInputXML( string_array const & inputFileList, return inputFileName; } +void collectIncluded( string const & filePath, + std::set< string > & collection ) +{ + xmlDocument doc; + xmlResult result = doc.loadFile( filePath ); + GEOS_THROW_IF( !result, + GEOS_FMT( "Could not load XML file '{}': {}", filePath, result.description() ), + InputError ); + xmlNode rootNode = doc.getFirstChild(); + + string const currentDir = splitPath( filePath ).first; + + for ( auto & includedNode : rootNode.children( includedListTag ) ) + { + for ( auto & fileNode : includedNode.children() ) + { + string const fileName = fileNode.attribute( "name" ).value(); + + GEOS_THROW_IF( fileName.empty(), + GEOS_FMT( "An included file entry in '{}' has an empty or missing 'name' attribute.", filePath ), + InputError ); + + string absolutePath = isAbsolutePath( fileName ) + ? getAbsolutePath( fileName ) + : getAbsolutePath( joinPath( currentDir, fileName ) ); + collection.insert( absolutePath ); + } + } +} + +std::set< string > collectIncluded( string const & filePath ) +{ + std::set< string > collection; + collectIncluded( filePath, collection ); + return collection; +} + +void collectIncludedRecursive( string const & filePath, + std::set< string > & collection ) +{ + // We want absolute paths + string const absFilePath = getAbsolutePath( filePath ); + + if ( collection.count( absFilePath ) > 0 ) + { + return; + } + collection.insert( absFilePath ); + + xmlDocument doc; + xmlResult result = doc.loadFile( absFilePath ); + GEOS_THROW_IF( !result, + GEOS_FMT( "Could not load XML file '{}': {}", filePath, result.description() ), + InputError ); + xmlNode rootNode = doc.getFirstChild(); + + string const currentDir = splitPath( filePath ).first; + + for ( auto & includedNode : rootNode.children( includedListTag ) ) + { + for ( auto & fileNode : includedNode.children( includedFileTag ) ) + { + string const includedFilePath = fileNode.attribute( "name" ).value(); + + if ( includedFilePath.empty() ) { continue; } + + string includedAbsPath = isAbsolutePath( includedFilePath ) + ? getAbsolutePath( includedFilePath ) + : getAbsolutePath( joinPath(currentDir, includedFilePath) ); + collectIncludedRecursive( includedAbsPath, + collection ); + } + } +} + +std::set< string > collectIncludedRecursive( string const & filePath ) +{ + std::set< string > collection; + collectIncludedRecursive( filePath, collection ); + return collection; +} + bool isFileMetadataAttribute( string const & name ) { static const std::set< string > fileMetadataAttributes { diff --git a/src/coreComponents/dataRepository/xmlWrapper.hpp b/src/coreComponents/dataRepository/xmlWrapper.hpp index 494eb2db119..998074a38ba 100644 --- a/src/coreComponents/dataRepository/xmlWrapper.hpp +++ b/src/coreComponents/dataRepository/xmlWrapper.hpp @@ -301,6 +301,50 @@ constexpr char const includedFileTag[] = "File"; string buildMultipleInputXML( string_array const & inputFileList, string const & outputDir = {} ); +/** + * @brief Collect the absolute paths of XML files directly included + * by a given xml file + * @param[in] filePath absolute path of the xml file to inspect + * @param[inout] collection collection to append with absolute file paths + * + * Only one level of inclusion is collected (files included by the included + * files are not added). See collectIncludedRecursive if you want this behavior. + * Duplicate entries are not inserted in @p collection + */ +void collectIncluded( string const & filePath, + std::set< string > & collection ); + +/** + * @brief Collect the absolute paths of XML files directly included + * by a given xml file + * @param[in] filePath absolute path of the xml file to inspect + * @return a collection of absolute paths + * + * Only one level of inclusion is collected (files included by the included + * files are not added). See collectIncludedRecursive if you want this behavior. + * Duplicate entries are not inserted in @p collection + */ +std::set< string > collectIncluded( string const & filePath ); + +/** + * @brief Recursively collect the absolute paths of an XML file and all XML + * files it includes + * @param[in] filePath absolute path of the root XML file + * @param[inout] collection collection to append with absolute file paths + * of every visisted file (including @p filePath itself) + */ +void collectIncludedRecursive( string const & filePath, + std::set< string > & collection ); + +/** + * @brief Recursively collect the absolute paths of an XML file and all XML + * files it includes + * @param[in] filePath absolute path of the root XML file + * @return a collection of absolute paths of every visited file (including + * @p filePath itself) + */ +std::set< string > collectIncludedRecursive( string const & filePath ); + /** * @return true if the attribute with the specified name declares metadata relative to the xml * @param name the name of an attribute From 3db8b1a1bb97076fc100cde2f97a3bfb50a61d0a Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 20 Mar 2026 09:41:38 +0100 Subject: [PATCH 02/17] add archiveInputDeck function --- src/coreComponents/fileIO/CMakeLists.txt | 2 + .../fileIO/Outputs/ArchiveInputDeck.cpp | 122 ++++++++++++++++++ .../fileIO/Outputs/ArchiveInputDeck.hpp | 53 ++++++++ 3 files changed, 177 insertions(+) create mode 100644 src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp create mode 100644 src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp diff --git a/src/coreComponents/fileIO/CMakeLists.txt b/src/coreComponents/fileIO/CMakeLists.txt index eedca99b65c..88177d8a2fe 100644 --- a/src/coreComponents/fileIO/CMakeLists.txt +++ b/src/coreComponents/fileIO/CMakeLists.txt @@ -24,6 +24,7 @@ Contains: # set( fileIO_headers LogLevelsInfo.hpp + Outputs/ArchiveInputDeck.hpp Outputs/BlueprintOutput.hpp Outputs/MemoryStatsOutput.hpp Outputs/OutputBase.hpp @@ -43,6 +44,7 @@ set( fileIO_headers # Specify all sources # set( fileIO_sources + Outputs/ArchiveInputDeck.cpp Outputs/BlueprintOutput.cpp Outputs/MemoryStatsOutput.cpp Outputs/OutputBase.cpp diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp new file mode 100644 index 00000000000..bff4742ef31 --- /dev/null +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp @@ -0,0 +1,122 @@ +/* + * ------------------------------------------------------------------------------------------------------------ + * SPDX-License-Identifier: LGPL-2.1-only + * + * Copyright (c) 2016-2024 Lawrence Livermore National Security LLC + * Copyright (c) 2018-2024 TotalEnergies + * Copyright (c) 2018-2024 The Board of Trustees of the Leland Stanford Junior University + * Copyright (c) 2023-2024 Chevron + * Copyright (c) 2019- GEOS/GEOSX Contributors + * All rights reserved + * + * See top level LICENSE, COPYRIGHT, CONTRIBUTORS, NOTICE, and ACKNOWLEDGEMENTS files for details. + * ------------------------------------------------------------------------------------------------------------ + */ + +/** + * @file ArchiveInputDeck.cpp + */ + +#include "ArchiveInputDeck.hpp" + +#include "common/Path.hpp" +#include "dataRepository/xmlWrapper.hpp" + +#include +#include + + +namespace geos +{ + +using namespace dataRepository; + +namespace archiveInputDeck +{ + +namespace +{ + +string makeTimestamp() +{ + auto const now = std::chrono::system_clock::now(); + auto const time_t_now = std::chrono::system_clock::to_time_t( now ); + std::ostringstream timestampStream; + timestampStream << std::put_time( std::localtime( &time_t_now ), "%Y%m%d_%H%M%S" ); + return timestampStream.str(); +} + +std::set< string > collectAbsFilePaths( string_array const & fileNames ) +{ + std::set< string > collection; + for ( string const & fileName : fileNames ) + { + xmlWrapper::collectIncludedRecursive( fileName, collection ); + } + return collection; +} + +/// @brief Prefixes a file path string if it is located "behind" the +/// specified directory +/// @param absFilePath The absolute path to the file +/// @param absDirPath The absolute path to the directory +/// @return A relative path of the file prefixed with "__" for every "../" +/// from the directory location +/// +/// Example: +/// @code +/// std::string foo = prefixBackwardPath( "/usr/foo/file.txt", "/usr/bar/buzz" ) +/// assert( foo == "____foo/file.txt" ) +/// @endcode +string prefixBackwardPath( string const & absFilePath, string const & absDirPath ) +{ + string relPath = std::filesystem::relative( std::filesystem::path( absFilePath ), + std::filesystem::path( absDirPath ) ); + + string prefix; + while( relPath.size() >= 3 && relPath.substr( 0, 3 ) == "../" ) + { + prefix += "__"; + relPath = relPath.substr( 3 ); + } + + return prefix + relPath; +} + +} + +void archiveInputDeck( string_array const & inputFileNames, + string const & outputDirectory ) +{ + if ( inputFileNames.empty() ) + { + return; + } + + string const timestamp = makeTimestamp(); + string const archiveDir = joinPath( outputDirectory, "inputFiles", timestamp ); + makeDirsForPath( archiveDir + "/" ); + + string const baseDir = splitPath( getAbsolutePath(inputFileNames[0]) ).first; + std::set< string > absFilePaths = collectAbsFilePaths( inputFileNames ); + + for ( string const & absFilePath : absFilePaths ) + { + string const destPath = joinPath( archiveDir, prefixBackwardPath( absFilePath, baseDir ) ); + makeDirsForPath( splitPath( destPath ).first + "/" ); + + std::error_code ec; + bool copied = std::filesystem::copy_file( absFilePath, + destPath, + std::filesystem::copy_options::overwrite_existing, + ec ); + GEOS_LOG_IF( !copied, + GEOS_FMT( "Failed to copy archive file '{}' into '{}': {}", + absFilePath, destPath, ec.message() ) ); + } + +} + +} /* namespace archiveInputDeck */ + +} /* namespace geos */ diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp new file mode 100644 index 00000000000..520a2ca410a --- /dev/null +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp @@ -0,0 +1,53 @@ +/* + * ------------------------------------------------------------------------------------------------------------ + * SPDX-License-Identifier: LGPL-2.1-only + * + * Copyright (c) 2016-2024 Lawrence Livermore National Security LLC + * Copyright (c) 2018-2024 TotalEnergies + * Copyright (c) 2018-2024 The Board of Trustees of the Leland Stanford Junior University + * Copyright (c) 2023-2024 Chevron + * Copyright (c) 2019- GEOS/GEOSX Contributors + * All rights reserved + * + * See top level LICENSE, COPYRIGHT, CONTRIBUTORS, NOTICE, and ACKNOWLEDGEMENTS files for details. + * ------------------------------------------------------------------------------------------------------------ + */ + +/** + * @file ArchiveInputDeck.hpp + */ + +#ifndef GEOS_FILEIO_OUTPUTS_ARCHIVEINPUTDECK_HPP_ +#define GEOS_FILEIO_OUTPUTS_ARCHIVEINPUTDECK_HPP_ + +#include "common/DataTypes.hpp" + +namespace geos +{ + +namespace archiveInputDeck +{ + +/** + * @brief Copy XML input files into the output directory, preserving the + * folder structure + * @param inputFileNames Container of XML file names to start the copy from + * @param outputDirectory The output directory to copy files into + * + * Copy XML input files and every included files they contain (specified in + * the tag. This function creates a somewhat similar folder + * structure to the actual structure in the disk. + * + * Note: XML files that are located "behind" the callpoint (the path to + * the first input file given as the -i paramater) will be prefixed + * with "__" for every "../" in the relative path from the callpoint. + */ +void archiveInputDeck( string_array const & inputFileNames, + string const & outputDirectory ); + +} /* namespace archiveInputDeck */ + +} /* namespace geos */ + + +#endif // GEOS_FILEIO_OUTPUTS_ARCHIVEINPUTDECK_HPP_ From 8afead32f162cc9e8b7ca0251c71e62a45fc0db4 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 20 Mar 2026 11:50:30 +0100 Subject: [PATCH 03/17] call archiveInputDeck() in ProblemManager --- src/coreComponents/mainInterface/ProblemManager.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/coreComponents/mainInterface/ProblemManager.cpp b/src/coreComponents/mainInterface/ProblemManager.cpp index 3c2718b1955..880ddfc63b5 100644 --- a/src/coreComponents/mainInterface/ProblemManager.cpp +++ b/src/coreComponents/mainInterface/ProblemManager.cpp @@ -37,6 +37,7 @@ #include "finiteVolume/FluxApproximationBase.hpp" #include "finiteVolume/HybridMimeticDiscretization.hpp" #include "fieldSpecification/FieldSpecificationManager.hpp" +#include "fileIO/Outputs/ArchiveInputDeck.hpp" #include "fileIO/Outputs/OutputBase.hpp" #include "fileIO/Outputs/OutputManager.hpp" #include "functions/FunctionManager.hpp" @@ -221,6 +222,8 @@ void ProblemManager::parseCommandLineInput() GEOS_LOG_RANK_0( "Opened XML file: " << absPath ); } + archiveInputDeck::archiveInputDeck( opts.inputFileNames, outputDirectory ); + inputFileName = xmlWrapper::buildMultipleInputXML( opts.inputFileNames, outputDirectory ); string & schemaName = commandLine.getReference< string >( viewKeys.schemaFileName ); From 25c325bb15f7c4a2fc034ee8611b014c1a867527 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 20 Mar 2026 11:55:19 +0100 Subject: [PATCH 04/17] modify archive directory name Rename the archive output directory "archive_inputFiles" instead of "inputFiles". This prevents the archived XML files to unintentionally overwrite the standard "inputFiles" in GEOS/ when running with `-o .` where '.' is GEOS/ location --- src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp index bff4742ef31..62ac7874206 100644 --- a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp @@ -94,7 +94,7 @@ void archiveInputDeck( string_array const & inputFileNames, } string const timestamp = makeTimestamp(); - string const archiveDir = joinPath( outputDirectory, "inputFiles", timestamp ); + string const archiveDir = joinPath( outputDirectory, "archive_inputFiles", timestamp ); makeDirsForPath( archiveDir + "/" ); string const baseDir = splitPath( getAbsolutePath(inputFileNames[0]) ).first; From 1cf33d5fb2ed1b42707abe4ec8b2e5bb6a25811f Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Tue, 24 Mar 2026 16:49:29 +0100 Subject: [PATCH 05/17] add tests for collectIncluded functions --- .../unitTests/testXmlWrapper.cpp | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) diff --git a/src/coreComponents/dataRepository/unitTests/testXmlWrapper.cpp b/src/coreComponents/dataRepository/unitTests/testXmlWrapper.cpp index 21c80c890a8..74a1bc06081 100644 --- a/src/coreComponents/dataRepository/unitTests/testXmlWrapper.cpp +++ b/src/coreComponents/dataRepository/unitTests/testXmlWrapper.cpp @@ -18,6 +18,9 @@ #include "dataRepository/xmlWrapper.hpp" #include "common/format/EnumStrings.hpp" +#include +#include + using namespace geos; TEST( testXmlWrapper, array3d_errors ) @@ -501,6 +504,214 @@ TEST( testXmlWrapper, testGroupNamesArrayFormats ) } } +class CollectIncludedTest : public ::testing::Test +{ +protected: + std::filesystem::path m_tempDir; + + void SetUp() override + { + m_tempDir = std::filesystem::temp_directory_path() / "geos_collectIncluded_test"; + std::filesystem::create_directories( m_tempDir ); + } + + void TearDown() override + { + std::filesystem::remove_all( m_tempDir ); + } + + string filePath( string const & filename ) + { + return ( m_tempDir / filename ).string(); + } + + void writeXML( string const & filename, string const & content ) + { + std::ofstream f( filePath( filename ) ); + f << content; + } +}; + + +TEST_F( CollectIncludedTest, collectIncluded_noIncludes ) +{ + writeXML( "base.xml", "" + "" ); + + auto result = xmlWrapper::collectIncluded( filePath( "base.xml" ) ); + + EXPECT_TRUE( result.empty() ); +} + +TEST_F( CollectIncludedTest, collectIncluded_singleInclude ) +{ + writeXML( "child.xml", "" + "" ); + writeXML( "base.xml", "" + " " + " " + " " + "" ); + + auto result = xmlWrapper::collectIncluded( filePath( "base.xml" ) ); + + EXPECT_NE( result.find( filePath( "child.xml" ) ), result.end() ); +} + +TEST_F( CollectIncludedTest, collectIncluded_multipleIncludes ) +{ + writeXML( "child1.xml", "" + "" ); + writeXML( "child2.xml", "" + "" ); + writeXML( "base.xml", "" + " " + " " + " " + " " + "" ); + + auto result = xmlWrapper::collectIncluded( filePath( "base.xml" ) ); + + EXPECT_NE( result.find( filePath( "child1.xml" ) ), result.end() ); + EXPECT_NE( result.find( filePath( "child2.xml" ) ), result.end() ); +} + +TEST_F( CollectIncludedTest, collectIncluded_emptyNameAttribute ) +{ + writeXML( "base.xml", "" + " " + " " + " " + "" ); + + std::set< string > result; + + EXPECT_ANY_THROW( xmlWrapper::collectIncluded( filePath( "base.xml" ), result ) ); + + EXPECT_TRUE( result.empty() ); +} + +TEST_F( CollectIncludedTest, collectIncluded_existingEntriesKept ) +{ + std::set< string > existingCollection; + existingCollection.insert( "/somewhere/thereisanalreadyexistingxmlfile.xml" ); + writeXML( "base.xml", "" + "" ); + + xmlWrapper::collectIncluded( filePath( "base.xml" ), existingCollection ); + + EXPECT_NE( existingCollection.find( "/somewhere/thereisanalreadyexistingxmlfile.xml" ), + existingCollection.end() ); +} + + +TEST_F( CollectIncludedTest, collectIncludedRecursive_noIncludes ) +{ + writeXML( "base.xml", "" + "" ); + + auto result = xmlWrapper::collectIncludedRecursive( filePath( "base.xml" ) ); + + EXPECT_EQ( result.size(), 1 ); // size 1 because collectIncludedRecursive collects the base file +} + +TEST_F( CollectIncludedTest, collectIncludedRecursive_singleInclude ) +{ + writeXML( "child.xml", "" + "" ); + writeXML( "base.xml", "" + " " + " " + " " + "" ); + + auto result = xmlWrapper::collectIncludedRecursive( filePath( "base.xml" ) ); + + EXPECT_NE( result.find( filePath( "child.xml" ) ), result.end() ); +} + +TEST_F( CollectIncludedTest, collectIncludedRecursive_multipleIncludes ) +{ + writeXML( "child1.xml", "" + "" ); + writeXML( "child2.xml", "" + "" ); + writeXML( "base.xml", "" + " " + " " + " " + " " + "" ); + + auto result = xmlWrapper::collectIncludedRecursive( filePath( "base.xml" ) ); + + EXPECT_NE( result.find( filePath( "child1.xml" ) ), result.end() ); + EXPECT_NE( result.find( filePath( "child2.xml" ) ), result.end() ); +} + +TEST_F( CollectIncludedTest, collectIncludedRecursive_simpleRecursive ) +{ + writeXML( "child.xml", "" + "" ); + + writeXML( "middle.xml", "" + " " + " " + " " + "" ); + + writeXML( "base.xml", "" + " " + " " + " " + "" ); + + auto result = xmlWrapper::collectIncludedRecursive( filePath( "base.xml" ) ); + + EXPECT_NE( result.find( filePath( "middle.xml" ) ), result.end() ); + EXPECT_NE( result.find( filePath( "child.xml" ) ), result.end() ); +} + +TEST_F( CollectIncludedTest, collectIncludedRecursive_cyclePrevention ) +{ + writeXML( "cycle.xml", "" + " " + " " + " " + "" ); + + auto result = xmlWrapper::collectIncludedRecursive( filePath( "cycle.xml" ) ); + + EXPECT_NE( result.find( filePath( "cycle.xml" ) ), result.end() ); +} + +TEST_F( CollectIncludedTest, collectIncludedRecursive_noDuplicates ) +{ + writeXML( "base.xml", "" + " " + " " + " " + "" ); + + auto result = xmlWrapper::collectIncludedRecursive( filePath( "base.xml" ) ); + + EXPECT_EQ( result.size(), 1 ); // collectIncludedRecursive collects the base file +} + +TEST_F( CollectIncludedTest, collectIncludedRecursive_existingEntriesKept ) +{ + std::set< string > existingCollection; + existingCollection.insert( "/somewhere/thereisanalreadyexistingxmlfile.xml" ); + writeXML( "base.xml", "" + "" ); + + xmlWrapper::collectIncludedRecursive( filePath( "base.xml" ), existingCollection ); + + EXPECT_NE( existingCollection.find( "/somewhere/thereisanalreadyexistingxmlfile.xml" ), + existingCollection.end() ); +} + int main( int argc, char * argv[] ) { From a4b35d032e63f4b95ba56694696bc57a93f939cc Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Tue, 24 Mar 2026 17:18:24 +0100 Subject: [PATCH 06/17] fix typo in archiveInputDeck documentation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp index 520a2ca410a..4d7a814c54a 100644 --- a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp @@ -39,7 +39,7 @@ namespace archiveInputDeck * structure to the actual structure in the disk. * * Note: XML files that are located "behind" the callpoint (the path to - * the first input file given as the -i paramater) will be prefixed + * the first input file given as the -i parameter) will be prefixed * with "__" for every "../" in the relative path from the callpoint. */ void archiveInputDeck( string_array const & inputFileNames, From c8739150b31c04384769547f53e90e079223cce4 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Tue, 24 Mar 2026 17:19:58 +0100 Subject: [PATCH 07/17] fix typo in collectIncludedRecursive documentation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/coreComponents/dataRepository/xmlWrapper.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreComponents/dataRepository/xmlWrapper.hpp b/src/coreComponents/dataRepository/xmlWrapper.hpp index 998074a38ba..0925f2b1c58 100644 --- a/src/coreComponents/dataRepository/xmlWrapper.hpp +++ b/src/coreComponents/dataRepository/xmlWrapper.hpp @@ -331,7 +331,7 @@ std::set< string > collectIncluded( string const & filePath ); * files it includes * @param[in] filePath absolute path of the root XML file * @param[inout] collection collection to append with absolute file paths - * of every visisted file (including @p filePath itself) + * of every visited file (including @p filePath itself) */ void collectIncludedRecursive( string const & filePath, std::set< string > & collection ); From 6efbe14b8c14509c23cbe59901a68260ea36ff31 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Tue, 24 Mar 2026 17:28:38 +0100 Subject: [PATCH 08/17] add missing header includes --- src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp index 62ac7874206..32dec80ab7d 100644 --- a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp @@ -20,6 +20,8 @@ #include "ArchiveInputDeck.hpp" #include "common/Path.hpp" +#include "common/format/Format.hpp" +#include "common/logger/Logger.hpp" #include "dataRepository/xmlWrapper.hpp" #include From 140aa51331da5daea6aa391347ff2b3f688e6083 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 25 Mar 2026 08:50:47 +0100 Subject: [PATCH 09/17] add filter on collectIncluded iteration --- src/coreComponents/dataRepository/xmlWrapper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreComponents/dataRepository/xmlWrapper.cpp b/src/coreComponents/dataRepository/xmlWrapper.cpp index 29be35414e7..06a939f6084 100644 --- a/src/coreComponents/dataRepository/xmlWrapper.cpp +++ b/src/coreComponents/dataRepository/xmlWrapper.cpp @@ -283,7 +283,7 @@ void collectIncluded( string const & filePath, for ( auto & includedNode : rootNode.children( includedListTag ) ) { - for ( auto & fileNode : includedNode.children() ) + for ( auto & fileNode : includedNode.children( includedFileTag ) ) { string const fileName = fileNode.attribute( "name" ).value(); From 9ed7c03892c4fc324821e952c45c5eeaa48eec1c Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 25 Mar 2026 11:26:00 +0100 Subject: [PATCH 10/17] add MPI rank 0 condition for archiveInputDeck call --- src/coreComponents/mainInterface/ProblemManager.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/coreComponents/mainInterface/ProblemManager.cpp b/src/coreComponents/mainInterface/ProblemManager.cpp index 880ddfc63b5..919651d36c0 100644 --- a/src/coreComponents/mainInterface/ProblemManager.cpp +++ b/src/coreComponents/mainInterface/ProblemManager.cpp @@ -222,7 +222,10 @@ void ProblemManager::parseCommandLineInput() GEOS_LOG_RANK_0( "Opened XML file: " << absPath ); } - archiveInputDeck::archiveInputDeck( opts.inputFileNames, outputDirectory ); + if ( MpiWrapper::commRank() == 0 ) + { + archiveInputDeck::archiveInputDeck( opts.inputFileNames, outputDirectory ); + } inputFileName = xmlWrapper::buildMultipleInputXML( opts.inputFileNames, outputDirectory ); From 5c306354ab30990f88200e0ad6507d19892499b4 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 25 Mar 2026 11:26:57 +0100 Subject: [PATCH 11/17] add output directory invariant for archiveInputDeck --- src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp index 32dec80ab7d..85f37ae91e9 100644 --- a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp @@ -95,6 +95,12 @@ void archiveInputDeck( string_array const & inputFileNames, return; } + if ( outputDirectory.empty() ) + { + return; + } + + string const timestamp = makeTimestamp(); string const archiveDir = joinPath( outputDirectory, "archive_inputFiles", timestamp ); makeDirsForPath( archiveDir + "/" ); From 0c5fb2df2d16a3fc8dceb43b79ed95f34d050cf7 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 25 Mar 2026 11:27:43 +0100 Subject: [PATCH 12/17] add tests for archiveInputDeck --- .../fileIO/Outputs/unitTests/CMakeLists.txt | 1 + .../unitTests/testArchiveInputDeck.cpp | 178 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 src/coreComponents/fileIO/Outputs/unitTests/testArchiveInputDeck.cpp diff --git a/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt b/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt index 04134af6c4a..cd7abf8d724 100644 --- a/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt +++ b/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt @@ -1,6 +1,7 @@ # Unit tests for fileIO/Outputs set( gtest_outputs_tests + testArchiveInputDeck.cpp testMemoryStats.cpp ) # Link against mainInterface to resolve basicSetup and ProblemManager symbols diff --git a/src/coreComponents/fileIO/Outputs/unitTests/testArchiveInputDeck.cpp b/src/coreComponents/fileIO/Outputs/unitTests/testArchiveInputDeck.cpp new file mode 100644 index 00000000000..3fce2caaf26 --- /dev/null +++ b/src/coreComponents/fileIO/Outputs/unitTests/testArchiveInputDeck.cpp @@ -0,0 +1,178 @@ +/* + * ------------------------------------------------------------------------------------------------------------ + * SPDX-License-Identifier: LGPL-2.1-only + * + * Copyright (c) 2016-2024 Lawrence Livermore National Security LLC + * Copyright (c) 2018-2024 TotalEnergies + * Copyright (c) 2018-2024 The Board of Trustees of the Leland Stanford Junior University + * Copyright (c) 2023-2024 Chevron + * Copyright (c) 2019- GEOS/GEOSX Contributors + * All rights reserved + * + * See top level LICENSE, COPYRIGHT, CONTRIBUTORS, NOTICE, and ACKNOWLEDGEMENTS files for details. + * ------------------------------------------------------------------------------------------------------------ + */ + +#include "fileIO/Outputs/ArchiveInputDeck.hpp" + +#include +#include +#include + +using namespace geos; + +namespace fs = std::filesystem; + +class ArchiveInputDeckTest : public ::testing::Test +{ +protected: + fs::path m_inputDir; + fs::path m_outputDir; + + void SetUp() override + { + m_inputDir = fs::temp_directory_path() / "geos_archiveInputDeck_test_input"; + m_outputDir = fs::temp_directory_path() / "geos_archiveInputDeck_test_output"; + fs::create_directories( m_inputDir ); + fs::create_directories( m_outputDir ); + } + + void TearDown() override + { + fs::remove_all( m_inputDir ); + fs::remove_all( m_outputDir ); + } + + string inputPath( string const & filename ) + { + return ( m_inputDir / filename ).string(); + } + + void writeXML( string const & filename, string const & content ) + { + fs::path const fullPath = m_inputDir / filename; + fs::create_directories( fullPath.parent_path() ); + std::ofstream f( fullPath ); + f << content; + } + + /// Return the single timestamped subdir in inputFiles/ + /// or an empty path + fs::path findArchiveDir() + { + fs::path const inputFilesDir = m_outputDir / "archive_inputFiles"; + if ( !fs::exists( inputFilesDir ) ) + { + return {}; + } + for ( auto const & entry : fs::directory_iterator( inputFilesDir ) ) + { + if ( entry.is_directory() ) + { + return entry.path(); + } + } + return {}; + } + + std::set< string > collectArchiveFiles( fs::path const & archiveDir ) + { + std::set< string > files; + + for ( auto const & entry : fs::recursive_directory_iterator( archiveDir ) ) + { + if ( entry.is_regular_file() ) + { + files.insert( fs::relative( entry.path(), archiveDir ).string() ); + } + } + + return files; + } +}; + + +TEST_F( ArchiveInputDeckTest, singleFile ) +{ + writeXML( "base.xml", "" + "" ); + string_array inputFiles { inputPath( "base.xml" ) }; + + archiveInputDeck::archiveInputDeck( inputFiles, m_outputDir.string() ); + + fs::path const archiveDir = findArchiveDir(); + EXPECT_FALSE( archiveDir.empty() ) << "Archive directory was not created"; + + auto const files = collectArchiveFiles( archiveDir ); + EXPECT_NE( files.find( "base.xml" ), files.end() ); +} + +TEST_F( ArchiveInputDeckTest, fileWithSubdirInclude ) +{ + writeXML( "subdir/child.xml", "" + "" ); + writeXML( "base.xml", "" + " " + " " + " " + "" ); + string_array inputFiles { inputPath( "base.xml" ) }; + + archiveInputDeck::archiveInputDeck( inputFiles, m_outputDir.string() ); + + fs::path const archiveDir = findArchiveDir(); + EXPECT_FALSE( archiveDir.empty() ) << "Archive directory was not created"; + + auto const files = collectArchiveFiles( archiveDir ); + EXPECT_NE( files.find( "subdir/child.xml" ), files.end() ); +} + +TEST_F( ArchiveInputDeckTest, fileOneDirectoryAbove ) +{ + writeXML( "other.xml", "" + "" ); + writeXML( "subdir/base.xml", "" + " " + " " + " " + "" ); + string_array inputFiles { inputPath( "subdir/base.xml" ) }; + + archiveInputDeck::archiveInputDeck( inputFiles, m_outputDir.string() ); + + fs::path const archiveDir = findArchiveDir(); + EXPECT_FALSE( archiveDir.empty() ) << "Archive directory was not created"; + + auto const files = collectArchiveFiles( archiveDir ); + EXPECT_NE( files.find( "__other.xml" ), files.end() ); +} + +TEST_F( ArchiveInputDeckTest, fileTwoDirectoriesAbove ) +{ + writeXML( "other.xml", "" + "" ); + writeXML( "subdir/subdir/base.xml", "" + " " + " " + " " + "" ); + string_array inputFiles { inputPath( "subdir/subdir/base.xml" ) }; + + archiveInputDeck::archiveInputDeck( inputFiles, m_outputDir.string() ); + + fs::path const archiveDir = findArchiveDir(); + EXPECT_FALSE( archiveDir.empty() ) << "Archive directory was not created"; + + auto const files = collectArchiveFiles( archiveDir ); + EXPECT_NE( files.find( "____other.xml" ), files.end() ); +} + + +int main( int argc, char * * argv ) +{ + ::testing::InitGoogleTest( &argc, argv ); + + int const result = RUN_ALL_TESTS(); + + return result; +} From bf1ca665331eaf49e88d8af7c3bdf32c36b99fcb Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 10 Apr 2026 16:41:48 +0200 Subject: [PATCH 13/17] modify archive's logic to flatten inputs --- .../fileIO/Outputs/ArchiveInputDeck.cpp | 112 ++++++----- .../fileIO/Outputs/ArchiveInputDeck.hpp | 16 +- .../fileIO/Outputs/unitTests/CMakeLists.txt | 1 - .../unitTests/testArchiveInputDeck.cpp | 178 ------------------ .../mainInterface/ProblemManager.cpp | 6 +- 5 files changed, 78 insertions(+), 235 deletions(-) delete mode 100644 src/coreComponents/fileIO/Outputs/unitTests/testArchiveInputDeck.cpp diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp index 85f37ae91e9..84625ebd61e 100644 --- a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp @@ -24,10 +24,10 @@ #include "common/logger/Logger.hpp" #include "dataRepository/xmlWrapper.hpp" +#include #include #include - namespace geos { @@ -48,38 +48,51 @@ string makeTimestamp() return timestampStream.str(); } -std::set< string > collectAbsFilePaths( string_array const & fileNames ) +void reorderTags( xmlWrapper::xmlNode rootNode, string_array const & tagOrder ) { - std::set< string > collection; - for ( string const & fileName : fileNames ) + xmlWrapper::xmlNode lastInserted; + for( string const & tagName : tagOrder ) { - xmlWrapper::collectIncludedRecursive( fileName, collection ); + xmlWrapper::xmlNode tag = rootNode.child( tagName.c_str() ); + if( !tag ) + { + continue; + } + + lastInserted ? rootNode.insert_move_after( tag, lastInserted ) + : rootNode.append_move( tag ); + + lastInserted = tag; } - return collection; -} -/// @brief Prefixes a file path string if it is located "behind" the -/// specified directory -/// @param absFilePath The absolute path to the file -/// @param absDirPath The absolute path to the directory -/// @return A relative path of the file prefixed with "__" for every "../" -/// from the directory location -/// -/// Example: -/// @code -/// std::string foo = prefixBackwardPath( "/usr/foo/file.txt", "/usr/bar/buzz" ) -/// assert( foo == "____foo/file.txt" ) -/// @endcode -string prefixBackwardPath( string const & absFilePath, string const & absDirPath ) -{ - string relPath = std::filesystem::relative( std::filesystem::path( absFilePath ), - std::filesystem::path( absDirPath ) ); + // ProblemManager's order list doesn't provide every XML tags available in GEOS + // so we put the missing ones below the ones it provides. + // And sort them alphabetically + stdVector< string > missingTags; + + for( xmlWrapper::xmlNode const & tag : rootNode.children() ) + { + string const & tagName = tag.name(); + + if( std::find( tagOrder.begin(), tagOrder.end(), tag.name() ) == tagOrder.end() ) + { + missingTags.push_back( tagName ); + } + } + + std::sort( missingTags.begin(), missingTags.end() ); + + for( string const & tagName : missingTags ) + { + xmlWrapper::xmlNode tag = rootNode.child( tagName.c_str() ); - string prefix; - while( relPath.size() >= 3 && relPath.substr( 0, 3 ) == "../" ) + if( tag ) { - prefix += "__"; - relPath = relPath.substr( 3 ); + rootNode.append_move( tag ); + } + } +} + } return prefix + relPath; @@ -95,34 +108,45 @@ void archiveInputDeck( string_array const & inputFileNames, return; } - if ( outputDirectory.empty() ) + +string archiveInputDeck( string_array const & inputFileNames, + string const & outputDirectory, + string_array const & xmlTagOrder ) +{ + if( inputFileNames.empty() || outputDirectory.empty() ) { - return; + return {}; } - string const timestamp = makeTimestamp(); string const archiveDir = joinPath( outputDirectory, "archive_inputFiles", timestamp ); makeDirsForPath( archiveDir + "/" ); - string const baseDir = splitPath( getAbsolutePath(inputFileNames[0]) ).first; - std::set< string > absFilePaths = collectAbsFilePaths( inputFileNames ); + xmlWrapper::xmlDocument flatDoc; + xmlWrapper::xmlNode root = flatDoc.appendChild( "Problem" ); - for ( string const & absFilePath : absFilePaths ) + for( string const & fileName : inputFileNames ) { - string const destPath = joinPath( archiveDir, prefixBackwardPath( absFilePath, baseDir ) ); - makeDirsForPath( splitPath( destPath ).first + "/" ); - - std::error_code ec; - bool copied = std::filesystem::copy_file( absFilePath, - destPath, - std::filesystem::copy_options::overwrite_existing, - ec ); - GEOS_LOG_IF( !copied, - GEOS_FMT( "Failed to copy archive file '{}' into '{}': {}", - absFilePath, destPath, ec.message() ) ); + xmlWrapper::xmlDocument doc; + xmlWrapper::xmlResult const result = doc.loadFile( fileName, true ); + GEOS_THROW_IF( !result, + GEOS_FMT( "Could not load XML file '{}': {}", fileName, result.description() ), + InputError ); + xmlWrapper::xmlNode docRoot = doc.getFirstChild(); + + doc.addIncludedXML( docRoot ); + + for( xmlWrapper::xmlNode & node : docRoot.children() ) + { + root.append_copy( node ); + } } + reorderTags( root, xmlTagOrder ); + + flatDoc.saveFile( joinPath( archiveDir, "input.xml" ) ); + + return archiveDir; } } /* namespace archiveInputDeck */ diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp index 4d7a814c54a..39a190428c0 100644 --- a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.hpp @@ -29,21 +29,17 @@ namespace archiveInputDeck { /** - * @brief Copy XML input files into the output directory, preserving the - * folder structure + * @brief Copy XML input files as a flat XML file into the output directory * @param inputFileNames Container of XML file names to start the copy from * @param outputDirectory The output directory to copy files into + * @param xmlTagOrder The order of the XML tags in the XML archive file * * Copy XML input files and every included files they contain (specified in - * the tag. This function creates a somewhat similar folder - * structure to the actual structure in the disk. - * - * Note: XML files that are located "behind" the callpoint (the path to - * the first input file given as the -i parameter) will be prefixed - * with "__" for every "../" in the relative path from the callpoint. + * the tag) into a single flat file. */ -void archiveInputDeck( string_array const & inputFileNames, - string const & outputDirectory ); +string archiveInputDeck( string_array const & inputFileNames, + string const & outputDirectory, + string_array const & xmlTagOrder ); } /* namespace archiveInputDeck */ diff --git a/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt b/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt index cd7abf8d724..04134af6c4a 100644 --- a/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt +++ b/src/coreComponents/fileIO/Outputs/unitTests/CMakeLists.txt @@ -1,7 +1,6 @@ # Unit tests for fileIO/Outputs set( gtest_outputs_tests - testArchiveInputDeck.cpp testMemoryStats.cpp ) # Link against mainInterface to resolve basicSetup and ProblemManager symbols diff --git a/src/coreComponents/fileIO/Outputs/unitTests/testArchiveInputDeck.cpp b/src/coreComponents/fileIO/Outputs/unitTests/testArchiveInputDeck.cpp deleted file mode 100644 index 3fce2caaf26..00000000000 --- a/src/coreComponents/fileIO/Outputs/unitTests/testArchiveInputDeck.cpp +++ /dev/null @@ -1,178 +0,0 @@ -/* - * ------------------------------------------------------------------------------------------------------------ - * SPDX-License-Identifier: LGPL-2.1-only - * - * Copyright (c) 2016-2024 Lawrence Livermore National Security LLC - * Copyright (c) 2018-2024 TotalEnergies - * Copyright (c) 2018-2024 The Board of Trustees of the Leland Stanford Junior University - * Copyright (c) 2023-2024 Chevron - * Copyright (c) 2019- GEOS/GEOSX Contributors - * All rights reserved - * - * See top level LICENSE, COPYRIGHT, CONTRIBUTORS, NOTICE, and ACKNOWLEDGEMENTS files for details. - * ------------------------------------------------------------------------------------------------------------ - */ - -#include "fileIO/Outputs/ArchiveInputDeck.hpp" - -#include -#include -#include - -using namespace geos; - -namespace fs = std::filesystem; - -class ArchiveInputDeckTest : public ::testing::Test -{ -protected: - fs::path m_inputDir; - fs::path m_outputDir; - - void SetUp() override - { - m_inputDir = fs::temp_directory_path() / "geos_archiveInputDeck_test_input"; - m_outputDir = fs::temp_directory_path() / "geos_archiveInputDeck_test_output"; - fs::create_directories( m_inputDir ); - fs::create_directories( m_outputDir ); - } - - void TearDown() override - { - fs::remove_all( m_inputDir ); - fs::remove_all( m_outputDir ); - } - - string inputPath( string const & filename ) - { - return ( m_inputDir / filename ).string(); - } - - void writeXML( string const & filename, string const & content ) - { - fs::path const fullPath = m_inputDir / filename; - fs::create_directories( fullPath.parent_path() ); - std::ofstream f( fullPath ); - f << content; - } - - /// Return the single timestamped subdir in inputFiles/ - /// or an empty path - fs::path findArchiveDir() - { - fs::path const inputFilesDir = m_outputDir / "archive_inputFiles"; - if ( !fs::exists( inputFilesDir ) ) - { - return {}; - } - for ( auto const & entry : fs::directory_iterator( inputFilesDir ) ) - { - if ( entry.is_directory() ) - { - return entry.path(); - } - } - return {}; - } - - std::set< string > collectArchiveFiles( fs::path const & archiveDir ) - { - std::set< string > files; - - for ( auto const & entry : fs::recursive_directory_iterator( archiveDir ) ) - { - if ( entry.is_regular_file() ) - { - files.insert( fs::relative( entry.path(), archiveDir ).string() ); - } - } - - return files; - } -}; - - -TEST_F( ArchiveInputDeckTest, singleFile ) -{ - writeXML( "base.xml", "" - "" ); - string_array inputFiles { inputPath( "base.xml" ) }; - - archiveInputDeck::archiveInputDeck( inputFiles, m_outputDir.string() ); - - fs::path const archiveDir = findArchiveDir(); - EXPECT_FALSE( archiveDir.empty() ) << "Archive directory was not created"; - - auto const files = collectArchiveFiles( archiveDir ); - EXPECT_NE( files.find( "base.xml" ), files.end() ); -} - -TEST_F( ArchiveInputDeckTest, fileWithSubdirInclude ) -{ - writeXML( "subdir/child.xml", "" - "" ); - writeXML( "base.xml", "" - " " - " " - " " - "" ); - string_array inputFiles { inputPath( "base.xml" ) }; - - archiveInputDeck::archiveInputDeck( inputFiles, m_outputDir.string() ); - - fs::path const archiveDir = findArchiveDir(); - EXPECT_FALSE( archiveDir.empty() ) << "Archive directory was not created"; - - auto const files = collectArchiveFiles( archiveDir ); - EXPECT_NE( files.find( "subdir/child.xml" ), files.end() ); -} - -TEST_F( ArchiveInputDeckTest, fileOneDirectoryAbove ) -{ - writeXML( "other.xml", "" - "" ); - writeXML( "subdir/base.xml", "" - " " - " " - " " - "" ); - string_array inputFiles { inputPath( "subdir/base.xml" ) }; - - archiveInputDeck::archiveInputDeck( inputFiles, m_outputDir.string() ); - - fs::path const archiveDir = findArchiveDir(); - EXPECT_FALSE( archiveDir.empty() ) << "Archive directory was not created"; - - auto const files = collectArchiveFiles( archiveDir ); - EXPECT_NE( files.find( "__other.xml" ), files.end() ); -} - -TEST_F( ArchiveInputDeckTest, fileTwoDirectoriesAbove ) -{ - writeXML( "other.xml", "" - "" ); - writeXML( "subdir/subdir/base.xml", "" - " " - " " - " " - "" ); - string_array inputFiles { inputPath( "subdir/subdir/base.xml" ) }; - - archiveInputDeck::archiveInputDeck( inputFiles, m_outputDir.string() ); - - fs::path const archiveDir = findArchiveDir(); - EXPECT_FALSE( archiveDir.empty() ) << "Archive directory was not created"; - - auto const files = collectArchiveFiles( archiveDir ); - EXPECT_NE( files.find( "____other.xml" ), files.end() ); -} - - -int main( int argc, char * * argv ) -{ - ::testing::InitGoogleTest( &argc, argv ); - - int const result = RUN_ALL_TESTS(); - - return result; -} diff --git a/src/coreComponents/mainInterface/ProblemManager.cpp b/src/coreComponents/mainInterface/ProblemManager.cpp index 919651d36c0..2c152a65fb1 100644 --- a/src/coreComponents/mainInterface/ProblemManager.cpp +++ b/src/coreComponents/mainInterface/ProblemManager.cpp @@ -222,9 +222,11 @@ void ProblemManager::parseCommandLineInput() GEOS_LOG_RANK_0( "Opened XML file: " << absPath ); } - if ( MpiWrapper::commRank() == 0 ) + if( MpiWrapper::commRank() == 0 ) { - archiveInputDeck::archiveInputDeck( opts.inputFileNames, outputDirectory ); + string_array xmlTagOrder; + initializationOrder( xmlTagOrder ); + archiveInputDeck::archiveInputDeck( opts.inputFileNames, outputDirectory, xmlTagOrder ); } inputFileName = xmlWrapper::buildMultipleInputXML( opts.inputFileNames, outputDirectory ); From 6b35d1b3c659e3f78e3f440407986e86eb26ff8c Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 10 Apr 2026 16:42:36 +0200 Subject: [PATCH 14/17] strip metadata attributes from the archived XML --- .../fileIO/Outputs/ArchiveInputDeck.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp index 84625ebd61e..ce475ce10d1 100644 --- a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp @@ -48,6 +48,17 @@ string makeTimestamp() return timestampStream.str(); } +void stripMetadataAttributes( xmlWrapper::xmlNode node ) +{ + node.remove_attribute( xmlWrapper::filePathString ); + node.remove_attribute( xmlWrapper::charOffsetString ); + + for( xmlWrapper::xmlNode child : node.children() ) + { + stripMetadataAttributes( child ); + } +} + void reorderTags( xmlWrapper::xmlNode rootNode, string_array const & tagOrder ) { xmlWrapper::xmlNode lastInserted; @@ -142,6 +153,7 @@ string archiveInputDeck( string_array const & inputFileNames, } } + stripMetadataAttributes( root ); reorderTags( root, xmlTagOrder ); flatDoc.saveFile( joinPath( archiveDir, "input.xml" ) ); From a61d8c7b3e909571e4f3125e42e8c7923ecae7c6 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 10 Apr 2026 16:43:56 +0200 Subject: [PATCH 15/17] sort XML attributes in the archived XML --- .../fileIO/Outputs/ArchiveInputDeck.cpp | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp index ce475ce10d1..1c94a8868bc 100644 --- a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp @@ -104,17 +104,48 @@ void reorderTags( xmlWrapper::xmlNode rootNode, string_array const & tagOrder ) } } +void sortAttributes( xmlWrapper::xmlNode node ) +{ + stdVector< std::pair< string, string > > attributes; + for( xmlWrapper::xmlAttribute attr = node.first_attribute(); + attr; + attr = attr.next_attribute() ) + { + attributes.emplace_back( attr.name(), attr.value() ); + } + + std::sort( attributes.begin(), + attributes.end(), + []( std::pair< string, string > const & a, + std::pair< string, string > const & b ) + { + // name attribute should be the first attribute, and not sorted alphabetically + bool const aIsName = ( a.first == "name" ); + bool const bIsName = ( b.first == "name" ); + if( aIsName != bIsName ) + { + return aIsName; } - return prefix + relPath; -} + // other attributes are sorted alphabetically + return a.first < b.first; + } ); + // pugi doesn't have any move_attribute method yet, so we have to + // copy and remove attributes + while( node.remove_attribute( node.first_attribute() ) ) + {} + for( auto const & attr : attributes ) + { + node.append_attribute( attr.first.c_str() ).set_value( attr.second.c_str() ); + } + + for( xmlWrapper::xmlNode child : node.children() ) + { + sortAttributes( child ); + } } -void archiveInputDeck( string_array const & inputFileNames, - string const & outputDirectory ) -{ - if ( inputFileNames.empty() ) { return; } @@ -155,6 +186,7 @@ string archiveInputDeck( string_array const & inputFileNames, stripMetadataAttributes( root ); reorderTags( root, xmlTagOrder ); + sortAttributes( root ); flatDoc.saveFile( joinPath( archiveDir, "input.xml" ) ); From 87380842d992c3037228e3eaf3b984c7f6112389 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 10 Apr 2026 16:44:24 +0200 Subject: [PATCH 16/17] copy schema.xsd to the archive --- .../fileIO/Outputs/ArchiveInputDeck.cpp | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp index 1c94a8868bc..c392c510b97 100644 --- a/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp +++ b/src/coreComponents/fileIO/Outputs/ArchiveInputDeck.cpp @@ -146,10 +146,37 @@ void sortAttributes( xmlWrapper::xmlNode node ) } } +void copySchemaToArchive( string const & archiveDir ) +{ + std::error_code ec; + std::filesystem::path const exeDir = std::filesystem::read_symlink( "/proc/self/exe", ec ).parent_path(); + if( ec ) { return; } + std::filesystem::path const candidates[] = { + exeDir / "../share/geosx/schema/schema.xsd", + exeDir / "schema.xsd" + }; + + for( auto const & schemaSource : candidates ) + { + if( std::filesystem::is_regular_file( schemaSource ) ) + { + std::filesystem::path const schemaDest = std::filesystem::path( archiveDir ) / "schema.xsd"; + std::filesystem::copy_file( schemaSource, + schemaDest, + std::filesystem::copy_options::overwrite_existing, + ec ); + GEOS_LOG_IF( ec, GEOS_FMT( "Failed to copy schema to archive: {}", ec.message() ) ); + break; + } + } +} + +} + string archiveInputDeck( string_array const & inputFileNames, string const & outputDirectory, @@ -190,6 +217,8 @@ string archiveInputDeck( string_array const & inputFileNames, flatDoc.saveFile( joinPath( archiveDir, "input.xml" ) ); + copySchemaToArchive( archiveDir ); + return archiveDir; } From 93e28067cbf052e2d14d6723c0bfa1e08ff2662e Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 10 Apr 2026 16:45:01 +0200 Subject: [PATCH 17/17] uncrustify --- .../unitTests/testXmlWrapper.cpp | 6 ++--- .../dataRepository/xmlWrapper.cpp | 23 +++++++++++-------- .../dataRepository/xmlWrapper.hpp | 10 ++++---- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/coreComponents/dataRepository/unitTests/testXmlWrapper.cpp b/src/coreComponents/dataRepository/unitTests/testXmlWrapper.cpp index 74a1bc06081..e0e6aef1b63 100644 --- a/src/coreComponents/dataRepository/unitTests/testXmlWrapper.cpp +++ b/src/coreComponents/dataRepository/unitTests/testXmlWrapper.cpp @@ -601,7 +601,7 @@ TEST_F( CollectIncludedTest, collectIncluded_existingEntriesKept ) xmlWrapper::collectIncluded( filePath( "base.xml" ), existingCollection ); - EXPECT_NE( existingCollection.find( "/somewhere/thereisanalreadyexistingxmlfile.xml" ), + EXPECT_NE( existingCollection.find( "/somewhere/thereisanalreadyexistingxmlfile.xml" ), existingCollection.end() ); } @@ -670,7 +670,7 @@ TEST_F( CollectIncludedTest, collectIncludedRecursive_simpleRecursive ) auto result = xmlWrapper::collectIncludedRecursive( filePath( "base.xml" ) ); EXPECT_NE( result.find( filePath( "middle.xml" ) ), result.end() ); - EXPECT_NE( result.find( filePath( "child.xml" ) ), result.end() ); + EXPECT_NE( result.find( filePath( "child.xml" ) ), result.end() ); } TEST_F( CollectIncludedTest, collectIncludedRecursive_cyclePrevention ) @@ -708,7 +708,7 @@ TEST_F( CollectIncludedTest, collectIncludedRecursive_existingEntriesKept ) xmlWrapper::collectIncludedRecursive( filePath( "base.xml" ), existingCollection ); - EXPECT_NE( existingCollection.find( "/somewhere/thereisanalreadyexistingxmlfile.xml" ), + EXPECT_NE( existingCollection.find( "/somewhere/thereisanalreadyexistingxmlfile.xml" ), existingCollection.end() ); } diff --git a/src/coreComponents/dataRepository/xmlWrapper.cpp b/src/coreComponents/dataRepository/xmlWrapper.cpp index 06a939f6084..46e502a90cf 100644 --- a/src/coreComponents/dataRepository/xmlWrapper.cpp +++ b/src/coreComponents/dataRepository/xmlWrapper.cpp @@ -281,9 +281,9 @@ void collectIncluded( string const & filePath, string const currentDir = splitPath( filePath ).first; - for ( auto & includedNode : rootNode.children( includedListTag ) ) + for( auto & includedNode : rootNode.children( includedListTag ) ) { - for ( auto & fileNode : includedNode.children( includedFileTag ) ) + for( auto & fileNode : includedNode.children( includedFileTag ) ) { string const fileName = fileNode.attribute( "name" ).value(); @@ -291,9 +291,9 @@ void collectIncluded( string const & filePath, GEOS_FMT( "An included file entry in '{}' has an empty or missing 'name' attribute.", filePath ), InputError ); - string absolutePath = isAbsolutePath( fileName ) + string absolutePath = isAbsolutePath( fileName ) ? getAbsolutePath( fileName ) - : getAbsolutePath( joinPath( currentDir, fileName ) ); + : getAbsolutePath( joinPath( currentDir, fileName ) ); collection.insert( absolutePath ); } } @@ -312,7 +312,7 @@ void collectIncludedRecursive( string const & filePath, // We want absolute paths string const absFilePath = getAbsolutePath( filePath ); - if ( collection.count( absFilePath ) > 0 ) + if( collection.count( absFilePath ) > 0 ) { return; } @@ -327,17 +327,20 @@ void collectIncludedRecursive( string const & filePath, string const currentDir = splitPath( filePath ).first; - for ( auto & includedNode : rootNode.children( includedListTag ) ) + for( auto & includedNode : rootNode.children( includedListTag ) ) { - for ( auto & fileNode : includedNode.children( includedFileTag ) ) + for( auto & fileNode : includedNode.children( includedFileTag ) ) { string const includedFilePath = fileNode.attribute( "name" ).value(); - if ( includedFilePath.empty() ) { continue; } + if( includedFilePath.empty() ) + { + continue; + } - string includedAbsPath = isAbsolutePath( includedFilePath ) + string includedAbsPath = isAbsolutePath( includedFilePath ) ? getAbsolutePath( includedFilePath ) - : getAbsolutePath( joinPath(currentDir, includedFilePath) ); + : getAbsolutePath( joinPath( currentDir, includedFilePath ) ); collectIncludedRecursive( includedAbsPath, collection ); } diff --git a/src/coreComponents/dataRepository/xmlWrapper.hpp b/src/coreComponents/dataRepository/xmlWrapper.hpp index 0925f2b1c58..cbd04536b5d 100644 --- a/src/coreComponents/dataRepository/xmlWrapper.hpp +++ b/src/coreComponents/dataRepository/xmlWrapper.hpp @@ -302,11 +302,11 @@ string buildMultipleInputXML( string_array const & inputFileList, string const & outputDir = {} ); /** - * @brief Collect the absolute paths of XML files directly included + * @brief Collect the absolute paths of XML files directly included * by a given xml file * @param[in] filePath absolute path of the xml file to inspect * @param[inout] collection collection to append with absolute file paths - * + * * Only one level of inclusion is collected (files included by the included * files are not added). See collectIncludedRecursive if you want this behavior. * Duplicate entries are not inserted in @p collection @@ -315,11 +315,11 @@ void collectIncluded( string const & filePath, std::set< string > & collection ); /** - * @brief Collect the absolute paths of XML files directly included + * @brief Collect the absolute paths of XML files directly included * by a given xml file * @param[in] filePath absolute path of the xml file to inspect * @return a collection of absolute paths - * + * * Only one level of inclusion is collected (files included by the included * files are not added). See collectIncludedRecursive if you want this behavior. * Duplicate entries are not inserted in @p collection @@ -340,7 +340,7 @@ void collectIncludedRecursive( string const & filePath, * @brief Recursively collect the absolute paths of an XML file and all XML * files it includes * @param[in] filePath absolute path of the root XML file - * @return a collection of absolute paths of every visited file (including + * @return a collection of absolute paths of every visited file (including * @p filePath itself) */ std::set< string > collectIncludedRecursive( string const & filePath );